diff --git a/package.json b/package.json index 070fdb20..08c5a900 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "platform": "^1.3.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.54.2", + "react-hook-form": "^7.58.1", "react-textarea-autosize": "^8.5.3", "react-transition-group": "^4.4.5", "sonner": "^2.0.5", diff --git a/src/components/option/SettingForm.tsx b/src/components/option/SettingForm.tsx index 50244b0c..052f7eb2 100644 --- a/src/components/option/SettingForm.tsx +++ b/src/components/option/SettingForm.tsx @@ -49,7 +49,7 @@ import { LINK_COMMAND_STARTUP_METHOD, STYLE_VARIABLE, } from '@/const' -import type { SettingsType } from '@/types' +import type { UserSettings } from '@/types' import { PopupPlacementSchema } from '@/types/schema' import { isMenuCommand, @@ -112,14 +112,12 @@ const formSchema = z .strict() type FormValues = z.infer -type SettingsFormType = Omit +type SettingsFormType = Omit export function SettingForm({ className }: { className?: string }) { - const [settingData, setSettingData] = useState() const [isSaving, setIsSaving] = useState(false) - const initializedRef = useRef(false) const saveToRef = useRef() - const iconToRef = useRef() + const isLoadingRef = useRef() const loadingRef = useRef(null) const os = isMac() ? 'mac' : 'windows' @@ -127,7 +125,7 @@ export function SettingForm({ className }: { className?: string }) { resolver: zodResolver(formSchema), mode: 'onChange', }) - const { reset, getValues, setValue, register, watch } = form + const { reset, getValues, setValue, register, subscribe } = form const startupMethod = useWatch({ control: form.control, @@ -159,16 +157,17 @@ export function SettingForm({ className }: { className?: string }) { // Update form with latest settings const updateFormSettings = async () => { + isLoadingRef.current = true const settings = await loadSettingsData() reset(settings) + await sleep(100) + isLoadingRef.current = false } // Initial settings load useEffect(() => { const initializeSettings = async () => { await updateFormSettings() - // Set initialized after 100ms to avoid flickering - setTimeout(() => (initializedRef.current = true), 100) } initializeSettings() }, []) @@ -212,9 +211,8 @@ export function SettingForm({ className }: { className?: string }) { settings.commands = [...commands, ...linkCommands] await Settings.set({ + ...current, ...settings, - settingVersion: current.settingVersion, - stars: current.stars, }) await sleep(1000) } catch (e) { @@ -236,28 +234,6 @@ export function SettingForm({ className }: { className?: string }) { setValue('popupPlacement', popupPlacement) } - // Save after 500 ms to storage. - useEffect(() => { - let unmounted = false - - // Skip saving if the settingData is not initialized. - if (!initializedRef.current) { - return - } - - clearTimeout(saveToRef.current) - saveToRef.current = window.setTimeout(() => { - if (unmounted || settingData == null) return - updateSettings(settingData) - }, 1 * 500 /* ms */) - - return () => { - unmounted = true - clearTimeout(saveToRef.current) - clearTimeout(iconToRef.current) - } - }, [settingData]) - // Set default value for startupMethod. useEffect(() => { if (startupMethod === STARTUP_METHOD.KEYBOARD) { @@ -317,11 +293,25 @@ export function SettingForm({ className }: { className?: string }) { }, [linkCommandMethod]) useEffect(() => { - const subscription = watch((value) => { - setSettingData(value as SettingsFormType) + // Save after 500 ms to storage. + const subscription = subscribe({ + formState: { values: true }, + callback: ({ values }) => { + // Skip saving if the settingData is loaded. + if (isLoadingRef.current) return + + clearTimeout(saveToRef.current) + saveToRef.current = window.setTimeout(() => { + if (values == null) return + updateSettings(values as SettingsFormType) + }, 1 * 500 /* ms */) + }, }) - return () => subscription.unsubscribe() - }, [watch]) + return () => { + clearTimeout(saveToRef.current) + subscription() + } + }, [subscribe]) return (
diff --git a/src/services/settings.ts b/src/services/settings.ts index 21e18cce..717e2b9b 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -138,19 +138,17 @@ export const Settings = { // Stars await Storage.set(LOCAL_STORAGE_KEY.STARS, data.stars) - // UserStats + // Remove UserStats, stars and shortcuts from data + const { commandExecutionCount, hasShownReviewRequest, stars, ...restData } = + data + const userStats: UserStats = { - commandExecutionCount: data.commandExecutionCount, - hasShownReviewRequest: data.hasShownReviewRequest, + commandExecutionCount, + hasShownReviewRequest, } - await Storage.set(STORAGE_KEY.USER_STATS, userStats) - // Shortcuts + await Storage.set(STORAGE_KEY.USER_STATS, userStats) await Storage.set(STORAGE_KEY.SHORTCUTS, data.shortcuts) - - // Remove UserStats, stars and shortcuts from data - const { commandExecutionCount, hasShownReviewRequest, stars, ...restData } = - data await Storage.set(STORAGE_KEY.USER, restData) await Storage.set(LOCAL_STORAGE_KEY.CACHES, caches) return true diff --git a/src/services/storage.ts b/src/services/storage.ts index d9b1393b..6f949855 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -1,6 +1,11 @@ import DefaultSettings, { DefaultCommands } from './defaultSettings' import { Command, CaptureDataStorage } from '@/types' +const SYNC_DEBOUNCE_DELAY = 10 + +let syncSetTimeout: NodeJS.Timeout | null +const syncSetData = new Map() + export enum STORAGE_KEY { USER = 0, COMMAND_COUNT = 2, @@ -130,11 +135,24 @@ export const Storage = { */ set: async (key: KEY, value: T): Promise => { const area = detectStorageArea(key) - await area.set({ [key]: value }) - if (chrome.runtime.lastError != null) { - throw chrome.runtime.lastError + + if (area === chrome.storage.sync) { + if (syncSetTimeout != null) { + clearTimeout(syncSetTimeout) + } + syncSetData.set(key, value) + + syncSetTimeout = setTimeout(async () => { + const dataToSet = Object.fromEntries(syncSetData) + await area.set(dataToSet) + syncSetData.clear() + syncSetTimeout = null + }, SYNC_DEBOUNCE_DELAY) + return true + } else { + await area.set({ [key]: value }) + return true } - return true }, /** @@ -228,7 +246,10 @@ export const Storage = { setCommands: async ( commands: Command[], ): Promise => { - // Update commands. + const count = commands.length + const preCount = await Storage.get(STORAGE_KEY.COMMAND_COUNT) + + // Update commands and count. const data = commands.reduce( (acc, cmd, i) => { acc[`${CMD_PREFIX}${i}`] = cmd @@ -236,23 +257,17 @@ export const Storage = { }, {} as { [key: string]: Command }, ) - await chrome.storage.sync.set(data) - if (chrome.runtime.lastError != null) { - throw chrome.runtime.lastError - } + await chrome.storage.sync.set({ + ...data, + [STORAGE_KEY.COMMAND_COUNT]: commands.length, + }) // Remove surplus commands - const count = commands.length - const preCount = await Storage.get(STORAGE_KEY.COMMAND_COUNT) - const removeKeys = getIndicesToRemove(preCount, count).map( - (i) => `${CMD_PREFIX}${i}`, - ) - await chrome.storage.sync.remove(removeKeys) - - // Update command count. - await chrome.storage.sync.set({ [STORAGE_KEY.COMMAND_COUNT]: count }) - if (chrome.runtime.lastError != null) { - throw chrome.runtime.lastError + if (preCount > count) { + const removeKeys = getIndicesToRemove(preCount, count).map( + (i) => `${CMD_PREFIX}${i}`, + ) + await chrome.storage.sync.remove(removeKeys) } return true }, diff --git a/yarn.lock b/yarn.lock index 2f26a4fd..6e87bb8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3933,10 +3933,10 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" -react-hook-form@^7.54.2: - version "7.54.2" - resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz" - integrity sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg== +react-hook-form@^7.58.1: + version "7.58.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.58.1.tgz#b755acc1b42a19e4253c878b766e4c5ebd070fe8" + integrity sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA== react-is@^16.13.1: version "16.13.1"