diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index e6b310ac..46ab50da 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -135,7 +135,9 @@ "core:window:default", "core:window:allow-close", "core:window:allow-center", + "core:window:allow-show", "core:window:allow-minimize", + "core:window:allow-unminimize", "core:window:allow-maximize", "core:window:allow-unmaximize", "core:window:allow-set-size", diff --git a/apps/readest-app/src/__tests__/utils/nav.test.ts b/apps/readest-app/src/__tests__/utils/nav.test.ts index fd7192bd..06c5748d 100644 --- a/apps/readest-app/src/__tests__/utils/nav.test.ts +++ b/apps/readest-app/src/__tests__/utils/nav.test.ts @@ -12,11 +12,11 @@ vi.mock('@tauri-apps/api/window', () => ({ vi.mock('@tauri-apps/api/webviewWindow', () => { const mockOnce = vi.fn(); - return { - WebviewWindow: vi.fn().mockImplementation(function (this: Record) { - this['once'] = mockOnce; - }), - }; + const ctor = vi.fn().mockImplementation(function (this: Record) { + this['once'] = mockOnce; + }) as unknown as { getByLabel: ReturnType }; + ctor.getByLabel = vi.fn(); + return { WebviewWindow: ctor }; }); vi.mock('@/services/environment', () => ({ @@ -41,8 +41,11 @@ import { redirectToLibrary, showReaderWindow, showLibraryWindow, + ensureMainLibraryWindow, } from '@/utils/nav'; +const WebviewWindowCtor = WebviewWindow as unknown as { getByLabel: ReturnType }; + // ── Helpers ────────────────────────────────────────────────────────── function mockRouter() { return { @@ -348,3 +351,33 @@ describe('showLibraryWindow', () => { expect(url).toContain('file=file2.epub'); }); }); + +describe('ensureMainLibraryWindow', () => { + test('shows and focuses the existing main window when present', async () => { + const main = { + show: vi.fn().mockResolvedValue(undefined), + unminimize: vi.fn().mockResolvedValue(undefined), + setFocus: vi.fn().mockResolvedValue(undefined), + }; + WebviewWindowCtor.getByLabel.mockResolvedValue(main); + + await ensureMainLibraryWindow(makeAppService() as never); + + expect(WebviewWindowCtor.getByLabel).toHaveBeenCalledWith('main'); + expect(main.show).toHaveBeenCalled(); + expect(main.unminimize).toHaveBeenCalled(); + expect(main.setFocus).toHaveBeenCalled(); + expect(WebviewWindow).not.toHaveBeenCalled(); + }); + + test('creates a new main-labelled window pointing at /library when missing', async () => { + WebviewWindowCtor.getByLabel.mockResolvedValue(null); + + await ensureMainLibraryWindow(makeAppService() as never); + + expect(WebviewWindow).toHaveBeenCalledTimes(1); + const [label, options] = vi.mocked(WebviewWindow).mock.calls[0]!; + expect(label).toBe('main'); + expect((options as { url: string }).url).toBe('/library'); + }); +}); diff --git a/apps/readest-app/src/app/reader/components/ReaderContent.tsx b/apps/readest-app/src/app/reader/components/ReaderContent.tsx index bcebc172..7da7df6c 100644 --- a/apps/readest-app/src/app/reader/components/ReaderContent.tsx +++ b/apps/readest-app/src/app/reader/components/ReaderContent.tsx @@ -20,7 +20,7 @@ import { isTauriAppPlatform } from '@/services/environment'; import { uniqueId } from '@/utils/misc'; import { throttle } from '@/utils/throttle'; import { eventDispatcher } from '@/utils/event'; -import { navigateToLibrary } from '@/utils/nav'; +import { ensureMainLibraryWindow, navigateToLibrary } from '@/utils/nav'; import { clearDiscordPresence } from '@/utils/discord'; import { BOOK_IDS_SEPARATOR } from '@/services/constants'; import { BookDetailModal } from '@/components/metadata'; @@ -171,13 +171,16 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i await saveSettings(envConfig, settings); }, 200); - const handleCloseBooksToLibrary = () => { + const handleCloseBooksToLibrary = async () => { handleCloseBooks(); if (isTauriAppPlatform()) { const currentWindow = getCurrentWindow(); if (currentWindow.label === 'main') { navigateBackToLibrary(); } else { + if (appService) { + await ensureMainLibraryWindow(appService); + } currentWindow.close(); } } else { diff --git a/apps/readest-app/src/utils/nav.ts b/apps/readest-app/src/utils/nav.ts index ec051f5b..b7ef24e4 100644 --- a/apps/readest-app/src/utils/nav.ts +++ b/apps/readest-app/src/utils/nav.ts @@ -53,6 +53,39 @@ export const showLibraryWindow = (appService: AppService, filenames: string[]) = createReaderWindow(appService, url); }; +// Bring the main library window back when a reader window asks to "go to library". +// If main was hidden (macOS close-to-hide) we re-show it. If it was destroyed +// (Windows/Linux default close), we recreate a window with the same 'main' +// label so the existing emitTo('main', 'close-reader-window', ...) wiring +// continues to work. +export const ensureMainLibraryWindow = async (appService: AppService) => { + const existing = await WebviewWindow.getByLabel('main'); + if (existing) { + await existing.show(); + await existing.unminimize(); + await existing.setFocus(); + return; + } + const win = new WebviewWindow('main', { + url: '/library', + width: 800, + height: 600, + center: true, + resizable: true, + title: appService.isMacOSApp ? '' : 'Readest', + decorations: !!appService.isMacOSApp, + transparent: !appService.isMacOSApp, + shadow: appService.isMacOSApp ? undefined : true, + titleBarStyle: appService.isMacOSApp ? 'overlay' : undefined, + scrollBarStyle: (appService.osPlatform === 'windows' + ? 'fluentOverlay' + : 'default') as unknown as ScrollBarStyle, + }); + win.once('tauri://error', (e) => { + console.error('error recreating main window', e); + }); +}; + export const navigateToReader = ( router: ReturnType, bookIds: string[],