Feature/improve restoring grid snapshots (#713)

* Update local storage functions

* Update local storage hooks

* Add session storage

* Improve storing virtuoso grid snapshot

* Use "useSyncExternalStore" for browser storage access

* Get rid of location replacements in "AppbarSearch"

Use session storage instead to prevent reloading the page all the time due to changing the location

* Get rid of location replacements in "SourceMangas"

Use session storage instead to prevent reloading the page all the time due to changing the location
This commit is contained in:
schroda
2024-04-07 15:22:03 +02:00
committed by GitHub
parent ea2f9dc5a4
commit aab3e25031
20 changed files with 220 additions and 190 deletions

51
src/util/AppStorage.ts Normal file
View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// eslint-disable-next-line max-classes-per-file
export class Storage {
constructor(private readonly storage: typeof window.localStorage) {}
parseValue<T>(value: string | null, defaultValue: T): T {
if (value === null) {
return defaultValue;
}
return JSON.parse(value);
}
getItem(key: string): string | null {
return this.storage.getItem(key);
}
getItemParsed<T>(key: string, defaultValue: T): T {
return this.parseValue(this.getItem(key), defaultValue);
}
setItem(key: string, value: unknown): void {
if (value === undefined) {
return;
}
const valueToStore = JSON.stringify(value);
this.storage.setItem(key, valueToStore);
window.dispatchEvent(
new StorageEvent('storage', {
key,
oldValue: this.getItem(key),
newValue: valueToStore,
}),
);
}
}
export class AppStorage {
static readonly local: Storage = new Storage(window.localStorage);
static readonly session: Storage = new Storage(window.sessionStorage);
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
export function getItem<T>(key: string, defaultValue: T): T {
const item = window.localStorage.getItem(key);
if (item !== null) {
return JSON.parse(item);
}
window.localStorage.setItem(key, JSON.stringify(defaultValue));
return defaultValue;
}
export function setItem<T>(key: string, value: T): void {
window.localStorage.setItem(key, JSON.stringify(value));
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useState, Dispatch, SetStateAction, useReducer, Reducer, useCallback } from 'react';
import * as storage from '@/util/localStorage';
export function useLocalStorage<T>(key: string, defaultValue: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, initialState));
const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(
(value) => {
setStoredValue((prevValue) => {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(prevValue) : value;
storage.setItem(key, valueToStore);
return valueToStore;
});
},
[key],
);
return [storedValue, setValue];
}
export function useReducerLocalStorage<S, A>(reducer: Reducer<S, A>, key: string, defaultState: S | (() => S)) {
const [storedValue, setValue] = useLocalStorage(key, defaultState);
return useReducer((state: S, action: A): S => {
const newState = reducer(state, action);
setValue(newState);
return newState;
}, storedValue);
}

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useLocalStorage } from '@/util/useLocalStorage.tsx';
import { useLocalStorage } from '@/util/useStorage.tsx';
export const getPersistedServerSetting = <T,>(serverValue: T | undefined, lastValue: T): T => {
const isDisabled = serverValue === 0;

88
src/util/useStorage.tsx Normal file
View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Dispatch, Reducer, SetStateAction, useCallback, useMemo, useReducer, useSyncExternalStore } from 'react';
import { AppStorage, Storage } from '@/util/AppStorage.ts';
const subscribeToStorageUpdates = (callback: () => void) => {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
};
function useStorage<T>(storage: Storage, key: string, defaultValue: T | (() => T)): [T, Dispatch<SetStateAction<T>>];
function useStorage<T = undefined>(
storage: Storage,
key: string,
): [T | undefined, Dispatch<SetStateAction<T | undefined>>];
function useStorage<T>(
storage: Storage,
key: string,
defaultValue?: T | (() => T) | undefined,
): [T | undefined, Dispatch<SetStateAction<T | undefined>>] {
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
const storedValueRaw = useSyncExternalStore(subscribeToStorageUpdates, () => storage.getItem(key));
const setValue = useCallback<React.Dispatch<React.SetStateAction<T | undefined>>>(
(value) => {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storage.getItemParsed(key, initialState)) : value;
storage.setItem(key, valueToStore);
},
[key],
);
const storedValue = useMemo(
() => (storedValueRaw !== null ? JSON.parse(storedValueRaw) : initialState),
[storedValueRaw, key],
);
return [storedValue, setValue];
}
const useReducerStorage = <S, A>(
storage: Storage,
reducer: Reducer<S, A>,
key: string,
defaultState: S | (() => S),
) => {
const [storedValue, setValue] = useStorage(storage, key, defaultState);
return useReducer((state: S, action: A): S => {
const newState = reducer(state, action);
setValue(newState);
return newState;
}, storedValue);
};
export function useLocalStorage<T>(key: string, defaultValue: T | (() => T)): [T, Dispatch<SetStateAction<T>>];
export function useLocalStorage<T = undefined>(key: string): [T | undefined, Dispatch<SetStateAction<T | undefined>>];
export function useLocalStorage<T>(
key: string,
defaultValue?: T | undefined | (() => T | undefined),
): [T | undefined, Dispatch<SetStateAction<T | undefined>>] {
return useStorage(AppStorage.local, key, defaultValue);
}
export function useReducerLocalStorage<S, A>(reducer: Reducer<S, A>, key: string, defaultState: S | (() => S)) {
return useReducerStorage(AppStorage.local, reducer, key, defaultState);
}
export function useSessionStorage<T>(key: string, defaultValue: T | (() => T)): [T, Dispatch<SetStateAction<T>>];
export function useSessionStorage<T = undefined>(key: string): [T | undefined, Dispatch<SetStateAction<T | undefined>>];
export function useSessionStorage<T>(
key: string,
defaultValue?: T | (() => T),
): [T | undefined, Dispatch<SetStateAction<T | undefined>>] {
return useStorage(AppStorage.session, key, defaultValue);
}
export function useReducerSessionStorage<S, A>(reducer: Reducer<S, A>, key: string, defaultState: S | (() => S)) {
return useReducerStorage(AppStorage.session, reducer, key, defaultState);
}