From b6d7f6da5d29a9533703466359698fb0cc6dcf92 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 22 Sep 2025 00:50:34 +0200 Subject: [PATCH] Add zustand dev tools --- package.json | 1 + .../auto-scroll/ReaderAutoScrollStore.ts | 117 ++++++++++----- .../reader/overlay/ReaderOverlayStore.ts | 14 +- .../progress-bar/ReaderProgressBarStore.tsx | 24 ++-- .../reader/stores/ReaderChaptersStore.ts | 30 ++-- .../reader/stores/ReaderPagesStore.ts | 92 ++++++++---- src/features/reader/stores/ReaderStore.ts | 135 ++++++++++++------ .../reader/tap-zones/ReaderTapZoneStore.tsx | 14 +- src/lib/zustand/Zustand.types.ts | 9 +- src/lib/zustand/ZustandUtil.ts | 23 +++ yarn.lock | 13 ++ 11 files changed, 325 insertions(+), 147 deletions(-) create mode 100644 src/lib/zustand/ZustandUtil.ts diff --git a/package.json b/package.json index e42dce57..3ef215f7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@mui/system": "7.3.1", "@mui/utils": "7.3.1", "@mui/x-date-pickers": "8.10.0", + "@redux-devtools/extension": "3.3.0", "@vibrant/color": "4.0.0", "apollo-upload-client": "18.0.1", "csstype": "3.1.3", diff --git a/src/features/reader/auto-scroll/ReaderAutoScrollStore.ts b/src/features/reader/auto-scroll/ReaderAutoScrollStore.ts index 048ada79..60c3a7cd 100644 --- a/src/features/reader/auto-scroll/ReaderAutoScrollStore.ts +++ b/src/features/reader/auto-scroll/ReaderAutoScrollStore.ts @@ -7,7 +7,7 @@ */ import { createRef, MutableRefObject } from 'react'; -import { ImmerStateCreator } from '@/lib/zustand/Zustand.types.ts'; +import { SliceCreator } from '@/lib/zustand/Zustand.types.ts'; import { ScrollOffset } from '@/base/Base.types.ts'; import { useAutomaticScrolling } from '@/base/hooks/useAutomaticScrolling.ts'; import { noOp } from '@/lib/HelperFunctions.ts'; @@ -58,50 +58,86 @@ const DEFAULT_STATE = { >; export const createReaderAutoScrollStoreSlice = ( - ...[set, get]: Parameters> -): ReaderAutoScrollStoreSlice => - ({ - autoScroll: { - ...DEFAULT_STATE, - reset: () => ({ autoScroll: { ...get().autoScroll, ...DEFAULT_STATE } }), - setIsActive: (active) => - set((draft) => { + ...[createActionName, set, get]: Parameters> +): ReaderAutoScrollStoreSlice => ({ + autoScroll: { + ...DEFAULT_STATE, + reset: () => ({ autoScroll: { ...get().autoScroll, ...DEFAULT_STATE } }), + setIsActive: (active) => + set( + (draft) => { draft.autoScroll.isActive = active; - }), - setIsPaused: (paused) => - set((draft) => { + }, + undefined, + createActionName('setIsActive'), + ), + setIsPaused: (paused) => + set( + (draft) => { draft.autoScroll.isPaused = paused; - }), - setStart: (start) => - set((draft) => { + }, + undefined, + createActionName('setIsPaused'), + ), + setStart: (start) => + set( + (draft) => { draft.autoScroll.start = start; - }), - setCancel: (cancel) => - set((draft) => { + }, + undefined, + createActionName('setStart'), + ), + setCancel: (cancel) => + set( + (draft) => { draft.autoScroll.cancel = cancel; - }), - setToggleActive: (toggleActive) => - set((draft) => { + }, + undefined, + createActionName('setCancel'), + ), + setToggleActive: (toggleActive) => + set( + (draft) => { draft.autoScroll.toggleActive = toggleActive; - }), - setPause: (pause) => - set((draft) => { + }, + undefined, + createActionName('setToggleActive'), + ), + setPause: (pause) => + set( + (draft) => { draft.autoScroll.pause = pause; - }), - setResume: (resume) => - set((draft) => { + }, + undefined, + createActionName('setPause'), + ), + setResume: (resume) => + set( + (draft) => { draft.autoScroll.resume = resume; - }), - setInvert: (invert) => - set((draft) => { + }, + undefined, + createActionName('setResume'), + ), + setInvert: (invert) => + set( + (draft) => { draft.autoScroll.invert = invert; - }), - setDirection: (direction) => - set((draft) => { + }, + undefined, + createActionName('setInvert'), + ), + setDirection: (direction) => + set( + (draft) => { draft.autoScroll.direction = direction; - }), - setScrollRef: (scrollElement) => - set((draft) => { + }, + undefined, + createActionName('setDirection'), + ), + setScrollRef: (scrollElement) => + set( + (draft) => { if (!scrollElement) { draft.autoScroll.scrollRef = undefined; return; @@ -111,6 +147,9 @@ export const createReaderAutoScrollStoreSlice = is not assignable to type WritableDraft> draft.autoScroll.scrollRef = ref; - }), - }, - }) satisfies ReaderAutoScrollStoreSlice; + }, + undefined, + createActionName('setScrollRef'), + ), + }, +}); diff --git a/src/features/reader/overlay/ReaderOverlayStore.ts b/src/features/reader/overlay/ReaderOverlayStore.ts index 0dec878a..060257bb 100644 --- a/src/features/reader/overlay/ReaderOverlayStore.ts +++ b/src/features/reader/overlay/ReaderOverlayStore.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ImmerStateCreator } from '@/lib/zustand/Zustand.types.ts'; +import { SliceCreator } from '@/lib/zustand/Zustand.types.ts'; export interface ReaderOverlayStoreSlice { overlay: { @@ -21,14 +21,18 @@ const DEFAULT_STATE = { } satisfies Pick; export const createReaderOverlayStoreSlice = ( - ...[set, get]: Parameters> + ...[createActionName, set, get]: Parameters> ): ReaderOverlayStoreSlice => ({ overlay: { isVisible: DEFAULT_STATE.isVisible, setIsVisible: (visible) => - set((draft) => { - draft.overlay.isVisible = visible; - }), + set( + (draft) => { + draft.overlay.isVisible = visible; + }, + undefined, + createActionName('setIsVisible'), + ), reset: () => ({ overlay: { ...get().overlay, ...DEFAULT_STATE } }), }, }); diff --git a/src/features/reader/overlay/progress-bar/ReaderProgressBarStore.tsx b/src/features/reader/overlay/progress-bar/ReaderProgressBarStore.tsx index 6a1b99fb..8552332f 100644 --- a/src/features/reader/overlay/progress-bar/ReaderProgressBarStore.tsx +++ b/src/features/reader/overlay/progress-bar/ReaderProgressBarStore.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ImmerStateCreator } from '@/lib/zustand/Zustand.types.ts'; +import { SliceCreator } from '@/lib/zustand/Zustand.types.ts'; import { TReaderProgressBarContext } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts'; export interface ReaderProgressBarStoreSlice { @@ -21,18 +21,26 @@ const DEFAULT_STATE = { } satisfies Pick; export const createReaderProgressBarStoreSlice = ( - ...[set, get]: Parameters> + ...[createActionName, set, get]: Parameters> ): ReaderProgressBarStoreSlice => ({ progressBar: { ...DEFAULT_STATE, setIsMaximized: (maximized) => - set((draft) => { - draft.progressBar.isMaximized = maximized; - }), + set( + (draft) => { + draft.progressBar.isMaximized = maximized; + }, + undefined, + createActionName('setIsMaximized'), + ), setIsDragging: (dragging) => - set((draft) => { - draft.progressBar.isDragging = dragging; - }), + set( + (draft) => { + draft.progressBar.isDragging = dragging; + }, + undefined, + createActionName('setIsDragging'), + ), reset: () => ({ progressBar: { ...get().progressBar, ...DEFAULT_STATE } }), }, }); diff --git a/src/features/reader/stores/ReaderChaptersStore.ts b/src/features/reader/stores/ReaderChaptersStore.ts index 3796e802..484d807b 100644 --- a/src/features/reader/stores/ReaderChaptersStore.ts +++ b/src/features/reader/stores/ReaderChaptersStore.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ImmerStateCreator } from '@/lib/zustand/Zustand.types.ts'; +import { SliceCreator } from '@/lib/zustand/Zustand.types.ts'; import { ReaderStateChapters } from '@/features/reader/Reader.types.ts'; export interface ReaderChaptersStoreSlice { @@ -44,25 +44,29 @@ export const READER_DEFAULT_CHAPTERS_STATE = { }; export const createReaderChaptersStoreSlice = ( - ...[set, get]: Parameters> + ...[createActionName, set, get]: Parameters> ): ReaderChaptersStoreSlice => ({ chapters: { ...READER_DEFAULT_CHAPTERS_STATE.chapters, reset: () => ({ chapters: { ...get().chapters, ...READER_DEFAULT_CHAPTERS_STATE.chapters } }), setReaderStateChapters: (state) => - set((draft) => { - if (typeof state === 'function') { + set( + (draft) => { + if (typeof state === 'function') { + draft.chapters = { + ...get().chapters, + ...state(get().chapters), + }; + return; + } + draft.chapters = { ...get().chapters, - ...state(get().chapters), + ...state, }; - return; - } - - draft.chapters = { - ...get().chapters, - ...state, - }; - }), + }, + undefined, + createActionName('setReaderStateChapters'), + ), }, }); diff --git a/src/features/reader/stores/ReaderPagesStore.ts b/src/features/reader/stores/ReaderPagesStore.ts index dcecd4be..6acc4290 100644 --- a/src/features/reader/stores/ReaderPagesStore.ts +++ b/src/features/reader/stores/ReaderPagesStore.ts @@ -7,7 +7,7 @@ */ import { ReaderStatePages, ReaderTransitionPageMode } from '@/features/reader/Reader.types.ts'; -import { ImmerStateCreator } from '@/lib/zustand/Zustand.types.ts'; +import { SliceCreator } from '@/lib/zustand/Zustand.types.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; export interface ReaderPagesStoreSlice { @@ -48,47 +48,79 @@ export const READER_DEFAULT_PAGES_STATE: Omit< }; export const createReaderPagesStoreSlice = ( - ...[set, get]: Parameters> + ...[createActionName, set, get]: Parameters> ): ReaderPagesStoreSlice => ({ pages: { ...READER_DEFAULT_PAGES_STATE, reset: () => ({ pages: { ...get().pages, ...READER_DEFAULT_PAGES_STATE } }), setCurrentPageIndex: (index) => - set((draft) => { - draft.pages.currentPageIndex = index; - }), + set( + (draft) => { + draft.pages.currentPageIndex = index; + }, + undefined, + createActionName('setCurrentPageIndex'), + ), setPageToScrollToIndex: (index) => - set((draft) => { - draft.pages.pageToScrollToIndex = index; - }), + set( + (draft) => { + draft.pages.pageToScrollToIndex = index; + }, + undefined, + createActionName('setPageToScrollToIndex'), + ), setTotalPages: (total) => - set((draft) => { - draft.pages.totalPages = total; - }), + set( + (draft) => { + draft.pages.totalPages = total; + }, + undefined, + createActionName('setTotalPages'), + ), setPageUrls: (urls) => - set((draft) => { - draft.pages.pageUrls = urls; - }), + set( + (draft) => { + draft.pages.pageUrls = urls; + }, + undefined, + createActionName('setPageUrls'), + ), setPageLoadStates: (loadStates) => - set((draft) => { - if (typeof loadStates === 'function') { - draft.pages.pageLoadStates = loadStates(get().pages.pageLoadStates); - return; - } + set( + (draft) => { + if (typeof loadStates === 'function') { + draft.pages.pageLoadStates = loadStates(get().pages.pageLoadStates); + return; + } - draft.pages.pageLoadStates = loadStates; - }), + draft.pages.pageLoadStates = loadStates; + }, + undefined, + createActionName('setPageLoadStates'), + ), setPages: (pages) => - set((draft) => { - draft.pages.pages = pages; - }), + set( + (draft) => { + draft.pages.pages = pages; + }, + undefined, + createActionName('setPages'), + ), setTransitionPageMode: (mode) => - set((draft) => { - draft.pages.transitionPageMode = mode; - }), + set( + (draft) => { + draft.pages.transitionPageMode = mode; + }, + undefined, + createActionName('setTransitionPageMode'), + ), setRetryFailedPagesKeyPrefix: (prefix) => - set((draft) => { - draft.pages.retryFailedPagesKeyPrefix = prefix; - }), + set( + (draft) => { + draft.pages.retryFailedPagesKeyPrefix = prefix; + }, + undefined, + createActionName('setRetryFailedPagesKeyPrefix'), + ), }, }); diff --git a/src/features/reader/stores/ReaderStore.ts b/src/features/reader/stores/ReaderStore.ts index 94f4d52d..3e750cc4 100644 --- a/src/features/reader/stores/ReaderStore.ts +++ b/src/features/reader/stores/ReaderStore.ts @@ -7,6 +7,7 @@ */ import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { useShallow } from 'zustand/react/shallow'; import { TMangaReader } from '@/features/manga/Manga.types.ts'; @@ -33,6 +34,9 @@ import { createReaderChaptersStoreSlice, ReaderChaptersStoreSlice, } from '@/features/reader/stores/ReaderChaptersStore.ts'; +import { ZustandUtil } from '@/lib/zustand/ZustandUtil.ts'; + +const UNSERIALIZABLE_KEYS = ['scrollRef']; interface ReaderStore extends ReaderOverlayStoreSlice, @@ -64,59 +68,100 @@ const DEFAULT_STATE = { } satisfies Pick & { scrollbar: Pick }; const readerStore = create()( - immer((set, get, store) => ({ - ...DEFAULT_STATE, - reset: () => - set(() => ({ - ...get(), + devtools( + immer((set, get, store) => { + const createActionName = ZustandUtil.createActionNameCreator('reader'); + const createActionNameCreator = (name: string) => + ZustandUtil.createActionNameCreator(name, createActionName); + + return { ...DEFAULT_STATE, + reset: () => + set( + () => ({ + ...get(), + ...DEFAULT_STATE, + scrollbar: { + ...get().scrollbar, + ...DEFAULT_STATE.scrollbar, + }, + settings: { + ...get().settings, + ...DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG, + }, + ...get().overlay.reset(), + ...get().autoScroll.reset(), + ...get().pages.reset(), + ...get().chapters.reset(), + ...get().progressBar.reset(), + ...get().tapZone.reset(), + }), + undefined, + createActionName('reset'), + ), + setManga: (manga) => + set( + (draft) => { + draft.manga = manga; + }, + undefined, + createActionName('setManga'), + ), scrollbar: { - ...get().scrollbar, ...DEFAULT_STATE.scrollbar, + setXSize: (size) => + set( + (draft) => { + draft.scrollbar.xSize = size; + }, + undefined, + createActionName('scrollbar', 'setXSize'), + ), + setYSize: (size) => + set( + (draft) => { + draft.scrollbar.ySize = size; + }, + undefined, + createActionName('scrollbar', 'setYSize'), + ), }, settings: { - ...get().settings, ...DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG, + setSettings: (settings) => + set( + (draft) => { + draft.settings = { + ...get().settings, + ...settings, + }; + }, + undefined, + createActionName('settings', 'setSettings'), + ), }, - ...get().overlay.reset(), - ...get().autoScroll.reset(), - ...get().pages.reset(), - ...get().chapters.reset(), - ...get().progressBar.reset(), - ...get().tapZone.reset(), - })), - setManga: (manga) => - set((draft) => { - draft.manga = manga; - }), - scrollbar: { - ...DEFAULT_STATE.scrollbar, - setXSize: (size) => - set((draft) => { - draft.scrollbar.xSize = size; - }), - setYSize: (size) => - set((draft) => { - draft.scrollbar.ySize = size; - }), + ...createReaderOverlayStoreSlice(createActionNameCreator('overlay'), set, get, store), + ...createReaderAutoScrollStoreSlice(createActionNameCreator('autoScroll'), set, get, store), + ...createReaderPagesStoreSlice(createActionNameCreator('pages'), set, get, store), + ...createReaderChaptersStoreSlice(createActionNameCreator('chapters'), set, get, store), + ...createReaderProgressBarStoreSlice(createActionNameCreator('progressBar'), set, get, store), + ...createReaderTapZoneStoreSlice(createActionNameCreator('tapZone'), set, get, store), + }; + }), + { + name: 'ReaderStore', + anonymousActionType: 'reader/action', + serialize: { + replacer: (key: string, value: any) => { + if (UNSERIALIZABLE_KEYS.includes(key)) { + return null; + } + + return value; + }, + }, }, - settings: { - ...DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG, - setSettings: (settings) => - set((draft) => { - draft.settings = { - ...get().settings, - ...settings, - }; - }), - }, - ...createReaderOverlayStoreSlice(set, get, store), - ...createReaderAutoScrollStoreSlice(set, get, store), - ...createReaderPagesStoreSlice(set, get, store), - ...createReaderChaptersStoreSlice(set, get, store), - ...createReaderProgressBarStoreSlice(set, get, store), - ...createReaderTapZoneStoreSlice(set, get, store), - })), + ), ); export const useReaderStore = (selector: (state: ReaderStore) => T): T => readerStore(useShallow(selector)); export const getReaderStore = () => readerStore.getState(); diff --git a/src/features/reader/tap-zones/ReaderTapZoneStore.tsx b/src/features/reader/tap-zones/ReaderTapZoneStore.tsx index 6d8e3dbf..6820e609 100644 --- a/src/features/reader/tap-zones/ReaderTapZoneStore.tsx +++ b/src/features/reader/tap-zones/ReaderTapZoneStore.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ImmerStateCreator } from '@/lib/zustand/Zustand.types.ts'; +import { SliceCreator } from '@/lib/zustand/Zustand.types.ts'; import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts'; export interface ReaderTapZoneStoreSlice { @@ -20,14 +20,18 @@ const DEFAULT_STATE = { } satisfies Pick; export const createReaderTapZoneStoreSlice = ( - ...[set, get]: Parameters> + ...[createActionName, set, get]: Parameters> ): ReaderTapZoneStoreSlice => ({ tapZone: { ...DEFAULT_STATE, setShowPreview: (showPreview) => - set((draft) => { - draft.tapZone.showPreview = showPreview; - }), + set( + (draft) => { + draft.tapZone.showPreview = showPreview; + }, + undefined, + createActionName('setShowPreview'), + ), reset: () => ({ tapZone: { ...get().tapZone, ...DEFAULT_STATE } }), }, }); diff --git a/src/lib/zustand/Zustand.types.ts b/src/lib/zustand/Zustand.types.ts index b66efb49..6592b6c2 100644 --- a/src/lib/zustand/Zustand.types.ts +++ b/src/lib/zustand/Zustand.types.ts @@ -6,6 +6,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { StateCreator } from 'zustand'; +import { StateCreator as ZustandStateCreator } from 'zustand'; -export type ImmerStateCreator = StateCreator; +export type StateCreator = ZustandStateCreator; + +export type SliceCreator = ( + actionNameCreator: (...names: string[]) => string, + ...args: Parameters> +) => T; diff --git a/src/lib/zustand/ZustandUtil.ts b/src/lib/zustand/ZustandUtil.ts new file mode 100644 index 00000000..35abb236 --- /dev/null +++ b/src/lib/zustand/ZustandUtil.ts @@ -0,0 +1,23 @@ +/* + * 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 class ZustandUtil { + static createActionName(...names: string[]) { + const storeAndSlices = names.slice(0, -1); + const action = names.slice(-1)[0]; + + return `${storeAndSlices.join(':')}/${action}`; + } + + static createActionNameCreator( + name: string, + parentCreator: typeof ZustandUtil.createActionName = ZustandUtil.createActionName, + ): typeof ZustandUtil.createActionName { + return (...names: string[]) => parentCreator(name, ...names); + } +} diff --git a/yarn.lock b/yarn.lock index 19db9ea2..113c03fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2634,6 +2634,14 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@redux-devtools/extension@3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@redux-devtools/extension/-/extension-3.3.0.tgz#bc775d289f15604c472112920beac2cf4dbb7907" + integrity sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g== + dependencies: + "@babel/runtime" "^7.23.2" + immutable "^4.3.4" + "@remix-run/router@1.19.1": version "1.19.1" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.1.tgz#984771bfd1de2715f42394c87fb716c1349e014f" @@ -5981,6 +5989,11 @@ immer@10.1.1: resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== +immutable@^4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== + immutable@~3.7.6: version "3.7.6" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"