From 6d798542f6f23841f2a277ca97830de0790decb0 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 27 Apr 2026 20:22:28 +0800 Subject: [PATCH] fix: restore main library window when going to library from reader, closes #3969 (#3973) When the main window has been destroyed (Windows/Linux default close), the reader's "go to library" button only closed the reader, leaving no library visible. Add ensureMainLibraryWindow() that shows an existing main window or recreates one with the 'main' label so the existing close-reader-window wiring keeps working. Also grant the cross-window show/unminimize permissions the call now needs. Co-authored-by: Claude Opus 4.7 (1M context) --- .../src-tauri/capabilities/default.json | 2 + .../src/__tests__/utils/nav.test.ts | 43 ++++++++++++++++--- .../app/reader/components/ReaderContent.tsx | 7 ++- apps/readest-app/src/utils/nav.ts | 33 ++++++++++++++ 4 files changed, 78 insertions(+), 7 deletions(-) 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[],