Skip to content

Commit

Permalink
Preload theme to prevent screen flicker (#183)
Browse files Browse the repository at this point in the history
* Preload theme to prevent screen flicker

* refactor: extract localStorageCache
  • Loading branch information
int128 authored Aug 28, 2022
1 parent 386b78c commit eaa29cb
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 36 deletions.
13 changes: 4 additions & 9 deletions src/Themes/component.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div>
Expand Down
39 changes: 39 additions & 0 deletions src/Themes/hook.ts
Original file line number Diff line number Diff line change
@@ -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<Theme> = {
areaName: 'sync',
key: 'v3.selectedTheme',
initialValue: 'standard',
isType: isTheme,
}

export const useSelectedTheme = (): [Theme, Dispatch<Theme>] => {
const [theme, setTheme] = useChromeStorageWithCache(selectedThemeSpec)
useEffect(() => {
document.documentElement.dataset['theme'] = theme
}, [theme])
return [theme, setTheme]
}

const selectedColorSchemeSpec: Spec<ColorScheme> = {
areaName: 'sync',
key: 'v3.selectedColorScheme',
initialValue: 'auto',
isType: isColorScheme,
}

export const useSelectedColorScheme = (): [ColorScheme, Dispatch<ColorScheme>] => {
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)
}
18 changes: 0 additions & 18 deletions src/Themes/repository.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -11,6 +12,9 @@ root.render(
</React.StrictMode>
)

// 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') {
Expand Down
23 changes: 18 additions & 5 deletions src/infrastructure/chromeStorage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useEffect, useState } from 'react'
import { Dispatch, useEffect, useState } from 'react'
import { useLocalStorageCache } from './localStorageCache'

type Spec<T> = {
export type Spec<T> = {
areaName: chrome.storage.AreaName
key: string
initialValue: T
isType: (value: unknown) => value is T
}

export const useChromeStorage = <T>(spec: Spec<T>): readonly [T, (newValue: T) => void] => {
export const useChromeStorage = <T>(spec: Spec<T>): readonly [T, Dispatch<T>] => {
const [storedValue, setStoredValue] = useState<T>(spec.initialValue)
useEffect(
() => {
Expand All @@ -27,7 +28,7 @@ export const useChromeStorage = <T>(spec: Spec<T>): readonly [T, (newValue: T) =
]
}

const initialLoad = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void) => {
const initialLoad = <T>(spec: Spec<T>, setStoredValue: Dispatch<T>) => {
chrome.storage[spec.areaName]
.get(spec.key)
.then((items) => {
Expand All @@ -48,7 +49,7 @@ const initialLoad = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void) =>
.catch((e) => console.error(e))
}

const subscribeChange = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void) => {
const subscribeChange = <T>(spec: Spec<T>, setStoredValue: Dispatch<T>) => {
const area = chrome.storage[spec.areaName]
const listener = (changes: { [key: string]: chrome.storage.StorageChange }) => {
if (!(spec.key in changes)) {
Expand All @@ -68,3 +69,15 @@ const subscribeChange = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void
area.onChanged.addListener(listener)
return () => area.onChanged.removeListener(listener)
}

export const useChromeStorageWithCache = <T extends string>(spec: Spec<T>): [T, Dispatch<T>] => {
const [cache, setCache] = useLocalStorageCache(spec)
const [value, setValue] = useChromeStorage<T>({
...spec,
initialValue: cache,
})
useEffect(() => {
setCache(value)
}, [setCache, value])
return [value, setValue]
}
23 changes: 23 additions & 0 deletions src/infrastructure/localStorageCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Dispatch, useEffect, useState } from 'react'

type Spec<T extends string> = {
key: string
initialValue: T
isType: (value: unknown) => value is T
}

export const useLocalStorageCache = <T extends string>(spec: Spec<T>): [T, Dispatch<T>] => {
const [value, setValue] = useState<T>(getOrInitialValue(spec))
useEffect(() => {
localStorage.setItem(spec.key, value)
}, [spec.key, value])
return [value, setValue]
}

export const getOrInitialValue = <T extends string>(spec: Spec<T>): T => {
const cachedValue = localStorage.getItem(spec.key)
if (spec.isType(cachedValue)) {
return cachedValue
}
return spec.initialValue
}
1 change: 1 addition & 0 deletions src/migration/folderItemPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions src/migration/folderPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 0 additions & 4 deletions src/migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

0 comments on commit eaa29cb

Please sign in to comment.