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:
51
src/util/AppStorage.ts
Normal file
51
src/util/AppStorage.ts
Normal 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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
88
src/util/useStorage.tsx
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user