diff --git a/src/Themes/component.tsx b/src/Themes/component.tsx index 25b459e6..e2aa9732 100644 --- a/src/Themes/component.tsx +++ b/src/Themes/component.tsx @@ -1,16 +1,11 @@ import './component.css' -import { FC, useEffect } from 'react' import { allColorSchemes, allThemes } from './model' -import { useSelectedColorScheme, useSelectedTheme } from './repository' +import { useSelectedColorScheme, useSelectedTheme } from './hook' +import { FC } from 'react' const ThemesComponent: FC = () => { - const [selectedTheme, setSelectedTheme] = useSelectedTheme('standard') - const [selectedColorScheme, setSelectedColorScheme] = useSelectedColorScheme('auto') - useEffect(() => { - document.documentElement.dataset['theme'] = selectedTheme - document.documentElement.dataset['colorScheme'] = selectedColorScheme - }, [selectedTheme, selectedColorScheme]) - + const [selectedTheme, setSelectedTheme] = useSelectedTheme() + const [selectedColorScheme, setSelectedColorScheme] = useSelectedColorScheme() return ( <>
diff --git a/src/Themes/hook.ts b/src/Themes/hook.ts new file mode 100644 index 00000000..c483d12c --- /dev/null +++ b/src/Themes/hook.ts @@ -0,0 +1,39 @@ +import { ColorScheme, Theme, isColorScheme, isTheme } from './model' +import { Dispatch, useEffect } from 'react' +import { Spec, useChromeStorageWithCache } from '../infrastructure/chromeStorage' +import { getOrInitialValue } from '../infrastructure/localStorageCache' + +const selectedThemeSpec: Spec = { + areaName: 'sync', + key: 'v3.selectedTheme', + initialValue: 'standard', + isType: isTheme, +} + +export const useSelectedTheme = (): [Theme, Dispatch] => { + const [theme, setTheme] = useChromeStorageWithCache(selectedThemeSpec) + useEffect(() => { + document.documentElement.dataset['theme'] = theme + }, [theme]) + return [theme, setTheme] +} + +const selectedColorSchemeSpec: Spec = { + areaName: 'sync', + key: 'v3.selectedColorScheme', + initialValue: 'auto', + isType: isColorScheme, +} + +export const useSelectedColorScheme = (): [ColorScheme, Dispatch] => { + const [colorScheme, setColorScheme] = useChromeStorageWithCache(selectedColorSchemeSpec) + useEffect(() => { + document.documentElement.dataset['colorScheme'] = colorScheme + }, [colorScheme]) + return [colorScheme, setColorScheme] +} + +export const preloadFromCache = () => { + document.documentElement.dataset['theme'] = getOrInitialValue(selectedThemeSpec) + document.documentElement.dataset['colorScheme'] = getOrInitialValue(selectedColorSchemeSpec) +} diff --git a/src/Themes/repository.ts b/src/Themes/repository.ts deleted file mode 100644 index b6a166af..00000000 --- a/src/Themes/repository.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ColorScheme, Theme, isColorScheme, isTheme } from './model' -import { useChromeStorage } from '../infrastructure/chromeStorage' - -export const useSelectedTheme = (initialValue: Theme) => - useChromeStorage({ - areaName: 'sync', - key: 'v3.selectedTheme', - initialValue, - isType: isTheme, - }) - -export const useSelectedColorScheme = (initialValue: ColorScheme) => - useChromeStorage({ - areaName: 'sync', - key: 'v3.selectedColorScheme', - initialValue, - isType: isColorScheme, - }) diff --git a/src/index.tsx b/src/index.tsx index 969a807a..bd347ffd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import App from './App/component' import React from 'react' import ReactDOM from 'react-dom/client' import { migratePreferencesFromV2ToV3 } from './migration' +import { preloadFromCache } from './Themes/hook' const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( @@ -11,6 +12,9 @@ root.render( ) +// preload the theme to prevent screen flicker +preloadFromCache() + // the default size of popup is too small, so explicitly set it // https://developer.chrome.com/docs/extensions/reference/action/#popup if (document.location.hash === '#popup') { diff --git a/src/infrastructure/chromeStorage.ts b/src/infrastructure/chromeStorage.ts index 431f4a11..565c76a8 100644 --- a/src/infrastructure/chromeStorage.ts +++ b/src/infrastructure/chromeStorage.ts @@ -1,13 +1,14 @@ -import { useEffect, useState } from 'react' +import { Dispatch, useEffect, useState } from 'react' +import { useLocalStorageCache } from './localStorageCache' -type Spec = { +export type Spec = { areaName: chrome.storage.AreaName key: string initialValue: T isType: (value: unknown) => value is T } -export const useChromeStorage = (spec: Spec): readonly [T, (newValue: T) => void] => { +export const useChromeStorage = (spec: Spec): readonly [T, Dispatch] => { const [storedValue, setStoredValue] = useState(spec.initialValue) useEffect( () => { @@ -27,7 +28,7 @@ export const useChromeStorage = (spec: Spec): readonly [T, (newValue: T) = ] } -const initialLoad = (spec: Spec, setStoredValue: (newValue: T) => void) => { +const initialLoad = (spec: Spec, setStoredValue: Dispatch) => { chrome.storage[spec.areaName] .get(spec.key) .then((items) => { @@ -48,7 +49,7 @@ const initialLoad = (spec: Spec, setStoredValue: (newValue: T) => void) => .catch((e) => console.error(e)) } -const subscribeChange = (spec: Spec, setStoredValue: (newValue: T) => void) => { +const subscribeChange = (spec: Spec, setStoredValue: Dispatch) => { const area = chrome.storage[spec.areaName] const listener = (changes: { [key: string]: chrome.storage.StorageChange }) => { if (!(spec.key in changes)) { @@ -68,3 +69,15 @@ const subscribeChange = (spec: Spec, setStoredValue: (newValue: T) => void area.onChanged.addListener(listener) return () => area.onChanged.removeListener(listener) } + +export const useChromeStorageWithCache = (spec: Spec): [T, Dispatch] => { + const [cache, setCache] = useLocalStorageCache(spec) + const [value, setValue] = useChromeStorage({ + ...spec, + initialValue: cache, + }) + useEffect(() => { + setCache(value) + }, [setCache, value]) + return [value, setValue] +} diff --git a/src/infrastructure/localStorageCache.ts b/src/infrastructure/localStorageCache.ts new file mode 100644 index 00000000..db71dc73 --- /dev/null +++ b/src/infrastructure/localStorageCache.ts @@ -0,0 +1,23 @@ +import { Dispatch, useEffect, useState } from 'react' + +type Spec = { + key: string + initialValue: T + isType: (value: unknown) => value is T +} + +export const useLocalStorageCache = (spec: Spec): [T, Dispatch] => { + const [value, setValue] = useState(getOrInitialValue(spec)) + useEffect(() => { + localStorage.setItem(spec.key, value) + }, [spec.key, value]) + return [value, setValue] +} + +export const getOrInitialValue = (spec: Spec): T => { + const cachedValue = localStorage.getItem(spec.key) + if (spec.isType(cachedValue)) { + return cachedValue + } + return spec.initialValue +} diff --git a/src/migration/folderItemPreferences.ts b/src/migration/folderItemPreferences.ts index 84be7f28..e9f5fadb 100644 --- a/src/migration/folderItemPreferences.ts +++ b/src/migration/folderItemPreferences.ts @@ -27,4 +27,5 @@ export const migrate = async () => { } const shortcutMap = upgrade(folderItemPreferences) await chrome.storage.sync.set({ [V3_KEY]: shortcutMap.serialize() }) + localStorage.removeItem(V2_KEY) } diff --git a/src/migration/folderPreferences.ts b/src/migration/folderPreferences.ts index d2500ee1..87dab905 100644 --- a/src/migration/folderPreferences.ts +++ b/src/migration/folderPreferences.ts @@ -26,4 +26,5 @@ export const migrate = async () => { } const folderCollapse = upgrade(folderPreferences) await chrome.storage.sync.set({ [V3_KEY]: folderCollapse.serialize() }) + localStorage.removeItem(V2_KEY) } diff --git a/src/migration/index.ts b/src/migration/index.ts index 1fc4b950..f6c63889 100644 --- a/src/migration/index.ts +++ b/src/migration/index.ts @@ -3,10 +3,6 @@ import * as folderPreferences from './folderPreferences' // Migrate preferences from v2 (Local Storage) to v3 (Chrome Storage) export const migratePreferencesFromV2ToV3 = async () => { - if (window.localStorage.length === 0) { - return - } await folderPreferences.migrate() await folderItemPreferences.migrate() - window.localStorage.clear() }