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) <noreply@anthropic.com>
This commit is contained in:
Huang Xin 2026-04-27 20:22:28 +08:00 committed by GitHub
parent ebbbf104b2
commit 6d798542f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 7 deletions

View file

@ -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",

View file

@ -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<string, unknown>) {
this['once'] = mockOnce;
}),
};
const ctor = vi.fn().mockImplementation(function (this: Record<string, unknown>) {
this['once'] = mockOnce;
}) as unknown as { getByLabel: ReturnType<typeof vi.fn> };
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<typeof vi.fn> };
// ── 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');
});
});

View file

@ -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 {

View file

@ -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<typeof useRouter>,
bookIds: string[],