Fix: apply selected theme in all windows

This commit is contained in:
aleksey-hoffman committed 2026-05-17 21:41:03 +03:00
1 parent f470369f03
commit 06dd790e27
2 files changed
+198

No files matched your search

@@ -0,0 +1,152 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
import { nextTick } from 'vue';
import {
beforeEach, describe, expect, it, vi,
} from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import { USER_SETTINGS_SCHEMA_VERSION } from '@/stores/schemas/user-settings';
import {
USER_SETTINGS_THEME_CHANGED_EVENT,
useUserSettingsStore,
} from '@/stores/storage/user-settings';
import type { StartupStorageFileBootstrap } from '@/stores/storage/utils/startup-storage-bootstrap';
import type { Theme } from '@/types/user-settings';
type ThemeEventCallback = (event: { payload: { theme: Theme } }) => void;
const {
emitMock,
lazyStoreSaveMock,
lazyStoreSetMock,
listenMock,
themeEventCallbacks,
webviewSetZoomMock,
} = vi.hoisted(() => ({
emitMock: vi.fn(),
lazyStoreSaveMock: vi.fn(),
lazyStoreSetMock: vi.fn(),
listenMock: vi.fn(),
themeEventCallbacks: new Map<string, ThemeEventCallback>(),
webviewSetZoomMock: vi.fn(),
}));
vi.mock('@tauri-apps/plugin-store', () => ({
LazyStore: class {
async save(): Promise<void> {
await lazyStoreSaveMock();
}
async set(key: string, value: unknown): Promise<void> {
await lazyStoreSetMock(key, value);
}
async entries(): Promise<[string, unknown][]> {
return [];
}
},
}));
vi.mock('@tauri-apps/api/event', () => ({
emit: emitMock,
listen: listenMock,
}));
vi.mock('@tauri-apps/api/webview', () => ({
getCurrentWebview: () => ({
setZoom: webviewSetZoomMock,
}),
}));
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));
vi.mock('@tauri-apps/api/path', () => ({
appDataDir: vi.fn(),
}));
vi.mock('@/stores/storage/user-paths', () => ({
useUserPathsStore: () => ({
customPaths: {
appUserDataSettingsPath: '/tmp/user-data/user-settings.json',
},
}),
}));
function createUserSettingsBootstrap(theme: Theme): StartupStorageFileBootstrap {
return {
path: '/tmp/user-data/user-settings.json',
status: 'ready',
data: {
__schemaVersion: USER_SETTINGS_SCHEMA_VERSION,
theme,
},
schemaVersion: USER_SETTINGS_SCHEMA_VERSION,
error: null,
};
}
describe('user settings theme sync', () => {
beforeEach(() => {
setActivePinia(createPinia());
document.documentElement.className = '';
document.documentElement.style.cssText = '';
emitMock.mockReset().mockResolvedValue(undefined);
lazyStoreSaveMock.mockReset();
lazyStoreSetMock.mockReset();
webviewSetZoomMock.mockReset();
themeEventCallbacks.clear();
listenMock.mockReset().mockImplementation(async (
eventName: string,
callback: ThemeEventCallback,
) => {
themeEventCallbacks.set(eventName, callback);
return vi.fn();
});
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: vi.fn(() => ({
matches: false,
addEventListener: vi.fn(),
})),
});
});
it('broadcasts theme changes to secondary windows', async () => {
const userSettingsStore = useUserSettingsStore();
await userSettingsStore.init(createUserSettingsBootstrap('dark'));
emitMock.mockClear();
await userSettingsStore.set('theme', 'light');
expect(emitMock).toHaveBeenCalledWith(USER_SETTINGS_THEME_CHANGED_EVENT, { theme: 'light' });
});
it('applies theme changes from other windows without writing them back', async () => {
const userSettingsStore = useUserSettingsStore();
await userSettingsStore.init(createUserSettingsBootstrap('dark'));
emitMock.mockClear();
lazyStoreSaveMock.mockClear();
lazyStoreSetMock.mockClear();
themeEventCallbacks.get(USER_SETTINGS_THEME_CHANGED_EVENT)?.({
payload: {
theme: 'light',
},
});
await nextTick();
expect(userSettingsStore.userSettings.theme).toBe('light');
expect(lazyStoreSetMock).not.toHaveBeenCalled();
expect(lazyStoreSaveMock).not.toHaveBeenCalled();
expect(emitMock).not.toHaveBeenCalled();
});
});
+46
View File
@@ -13,6 +13,7 @@ import type {
UserSettingsValue,
InfusionPageSettings,
VisualFiltersSettings,
Theme,
} from '@/types/user-settings';
import {
backgroundMedia,
@@ -26,6 +27,7 @@ import { useUserPathsStore } from './user-paths';
import { useExtensionsStorageStore } from './extensions';
import { i18n } from '@/localization';
import { getCurrentWebview } from '@tauri-apps/api/webview';
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event';
import {
buildAllowedUserSettingsStorageKeys,
migrateUserSettingsStorage,
@@ -41,6 +43,12 @@ import {
} from './utils/startup-storage-bootstrap';
import { BUILTIN_NAVIGATOR_ICON_THEME_IDS } from '@/types/icon-theme';
export const USER_SETTINGS_THEME_CHANGED_EVENT = 'user-settings:theme-changed';
type ThemeChangedEventPayload = {
theme: Theme;
};
export const useUserSettingsStore = defineStore('userSettings', () => {
const userPathsStore = useUserPathsStore();
const extensionsStorageStore = useExtensionsStorageStore();
@@ -49,6 +57,7 @@ export const useUserSettingsStore = defineStore('userSettings', () => {
const userSettingsDefault = ref<UserSettings | null>(null);
const themeTransitionOrigin = ref<ThemeTransitionOrigin | null>(null);
const themeTransitionsEnabled = ref(false);
const themeChangeEventUnlisten = ref<UnlistenFn | null>(null);
const allowedUserSettingsStorageKeys = ref<Set<string>>(new Set());
const userSettings = ref<UserSettings>({
language: {
@@ -377,6 +386,37 @@ export const useUserSettingsStore = defineStore('userSettings', () => {
}
}
async function broadcastThemeChange(theme: Theme) {
try {
await emit(USER_SETTINGS_THEME_CHANGED_EVENT, { theme } satisfies ThemeChangedEventPayload);
}
catch (error) {
console.error('Failed to broadcast theme change:', error);
}
}
async function ensureThemeChangeListener() {
if (themeChangeEventUnlisten.value) {
return;
}
try {
themeChangeEventUnlisten.value = await listen<ThemeChangedEventPayload>(
USER_SETTINGS_THEME_CHANGED_EVENT,
(event) => {
if (event.payload.theme === userSettings.value.theme) {
return;
}
userSettings.value.theme = event.payload.theme;
},
);
}
catch (error) {
console.error('Failed to listen for theme changes:', error);
}
}
async function setLanguage(newLanguage: LocalizationLanguage) {
userSettings.value.language = newLanguage;
await setUserSettingsStorage('language', newLanguage);
@@ -414,6 +454,11 @@ export const useUserSettingsStore = defineStore('userSettings', () => {
}
current[keys[keys.length - 1]] = value;
if (key === 'theme') {
await broadcastThemeChange(value as Theme);
}
await setUserSettingsStorage(key, value);
}
@@ -451,6 +496,7 @@ export const useUserSettingsStore = defineStore('userSettings', () => {
initTheme();
themeTransitionsEnabled.value = true;
await ensureThemeChangeListener();
initLanguage();
await initZoom();
}