refactor: the selection store using @tauri-store/zustand (#994)

* chore: add @tauri-store/zustand plugin

* refactor: the selection store using @tauri-store/zustand

* fix: data error

* fix: data error

* chore: remove config
This commit is contained in:
BiggerRain
2025-11-28 17:29:36 +08:00
committed by GitHub
parent ff7721d17f
commit 73ca224ad8
12 changed files with 174 additions and 73 deletions

View File

@@ -35,6 +35,7 @@
"@tauri-apps/plugin-shell": "^2.2.1", "@tauri-apps/plugin-shell": "^2.2.1",
"@tauri-apps/plugin-updater": "github:infinilabs/tauri-plugin-updater#v2", "@tauri-apps/plugin-updater": "github:infinilabs/tauri-plugin-updater#v2",
"@tauri-apps/plugin-window": "2.0.0-alpha.1", "@tauri-apps/plugin-window": "2.0.0-alpha.1",
"@tauri-store/zustand": "^1.1.0",
"@wavesurfer/react": "^1.0.11", "@wavesurfer/react": "^1.0.11",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.12.0", "axios": "^1.12.0",

30
pnpm-lock.yaml generated
View File

@@ -59,6 +59,9 @@ importers:
'@tauri-apps/plugin-window': '@tauri-apps/plugin-window':
specifier: 2.0.0-alpha.1 specifier: 2.0.0-alpha.1
version: 2.0.0-alpha.1 version: 2.0.0-alpha.1
'@tauri-store/zustand':
specifier: ^1.1.0
version: 1.1.0(@types/react@18.3.26)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1))
'@wavesurfer/react': '@wavesurfer/react':
specifier: ^1.0.11 specifier: ^1.0.11
version: 1.0.11(react@18.3.1)(wavesurfer.js@7.11.1) version: 1.0.11(react@18.3.1)(wavesurfer.js@7.11.1)
@@ -1364,6 +1367,12 @@ packages:
'@tauri-apps/plugin-window@2.0.0-alpha.1': '@tauri-apps/plugin-window@2.0.0-alpha.1':
resolution: {integrity: sha512-dFOAgal/3Txz3SQ+LNQq0AK1EPC+acdaFlwPVB/6KXUZYmaFleIlzgxDVoJCQ+/xOhxvYrdQaFLefh0I/Kldbg==} resolution: {integrity: sha512-dFOAgal/3Txz3SQ+LNQq0AK1EPC+acdaFlwPVB/6KXUZYmaFleIlzgxDVoJCQ+/xOhxvYrdQaFLefh0I/Kldbg==}
'@tauri-store/shared@0.10.2':
resolution: {integrity: sha512-hnEBbe/m9UG5ATSBp4yJOosLkX5QLblVsGIvz7mtBAqmzeWFfjP4+8X/T81L+bCH6+ref/0vsR9Ekex138kAfg==}
'@tauri-store/zustand@1.1.0':
resolution: {integrity: sha512-893TmbuiS+uX1KUNzALxnY9FRU9CUpOfZwl8tf7Wxoq4UU2ZabtZqWzLRVBGbs9rejuHGFMJeRE5U/U6tfzz7g==}
'@tootallnate/quickjs-emscripten@0.23.0': '@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
@@ -2161,6 +2170,9 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-toolkit@1.42.0:
resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==}
esbuild@0.21.5: esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -4863,6 +4875,22 @@ snapshots:
dependencies: dependencies:
'@tauri-apps/api': 2.0.0-alpha.6 '@tauri-apps/api': 2.0.0-alpha.6
'@tauri-store/shared@0.10.2':
dependencies:
'@tauri-apps/api': 2.9.0
es-toolkit: 1.42.0
'@tauri-store/zustand@1.1.0(@types/react@18.3.26)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1))':
dependencies:
'@tauri-apps/api': 2.9.0
'@tauri-store/shared': 0.10.2
zustand: 5.0.8(@types/react@18.3.26)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1))
transitivePeerDependencies:
- '@types/react'
- immer
- react
- use-sync-external-store
'@tootallnate/quickjs-emscripten@0.23.0': {} '@tootallnate/quickjs-emscripten@0.23.0': {}
'@types/babel__core@7.20.5': '@types/babel__core@7.20.5':
@@ -5683,6 +5711,8 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
es-toolkit@1.42.0: {}
esbuild@0.21.5: esbuild@0.21.5:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5 '@esbuild/aix-ppc64': 0.21.5

70
src-tauri/Cargo.lock generated
View File

@@ -1214,6 +1214,7 @@ dependencies = [
"tauri-plugin-store", "tauri-plugin-store",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-windows-version", "tauri-plugin-windows-version",
"tauri-plugin-zustand",
"tempfile", "tempfile",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
@@ -1577,6 +1578,20 @@ dependencies = [
"syn 2.0.111", "syn 2.0.111",
] ]
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.9.0" version = "2.9.0"
@@ -7460,6 +7475,18 @@ dependencies = [
"windows-version", "windows-version",
] ]
[[package]]
name = "tauri-plugin-zustand"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aad14c7a1bf457a697c9b6805af27aff127928e59330ea7850d868381e5ef25"
dependencies = [
"serde",
"tauri",
"tauri-plugin",
"tauri-store",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.9.1" version = "2.9.1"
@@ -7512,6 +7539,49 @@ dependencies = [
"wry", "wry",
] ]
[[package]]
name = "tauri-store"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aee4e469de686b5ca47838ca6dd99c1b98cbf574848fb4f987eecd11d7c965f1"
dependencies = [
"dashmap",
"futures",
"itertools 0.14.0",
"semver",
"serde",
"serde_json",
"tauri",
"tauri-store-macros",
"tauri-store-utils",
"thiserror 2.0.17",
"tokio",
]
[[package]]
name = "tauri-store-macros"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90c61be15c39f0abf599c33946f97fdd736346719fc3bbdd4c8acf196f110e06"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "tauri-store-utils"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "855375aae57c8c81bb89c847716e4ec1130fab7e0628310cfd173136f4382080"
dependencies = [
"parking_lot",
"semver",
"tauri",
"thiserror 2.0.17",
"tokio",
]
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.8.0" version = "2.8.0"

View File

@@ -120,6 +120,7 @@ path-clean = "1.0.1"
actix-files = "0.6.8" actix-files = "0.6.8"
actix-web = "4.11.0" actix-web = "4.11.0"
tauri-plugin-clipboard-manager = "2" tauri-plugin-clipboard-manager = "2"
tauri-plugin-zustand = "1"
[dev-dependencies] [dev-dependencies]
tempfile = "3.23.0" tempfile = "3.23.0"

View File

@@ -0,0 +1,5 @@
{
"identifier": "zustand",
"windows": ["*"],
"permissions": ["zustand:default", "core:event:default"]
}

View File

@@ -110,6 +110,7 @@ pub fn run() {
) )
.plugin(tauri_plugin_windows_version::init()) .plugin(tauri_plugin_windows_version::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_zustand::init())
.plugin(prevent_default::init()); .plugin(prevent_default::init());
// Conditional compilation for macOS // Conditional compilation for macOS

View File

@@ -67,7 +67,7 @@ export const AssistantFetcher = ({
lastServerId.current = currentService?.id; lastServerId.current = currentService?.id;
return { return {
total: response.hits.total.value, total: response?.hits?.total?.value ?? 0,
list: assistantList, list: assistantList,
}; };
} catch (error) { } catch (error) {

View File

@@ -126,10 +126,6 @@ const SelectionSettings = () => {
const iconsOnly = useSelectionStore((state) => state.iconsOnly); const iconsOnly = useSelectionStore((state) => state.iconsOnly);
const setIconsOnly = useSelectionStore((state) => state.setIconsOnly); const setIconsOnly = useSelectionStore((state) => state.setIconsOnly);
useEffect(() => {
useSelectionStore.getState().initSync();
}, []);
// Initialize from global store; write back on change for multi-window sync // Initialize from global store; write back on change for multi-window sync
const toolbarConfig = useSelectionStore((s) => s.toolbarConfig); const toolbarConfig = useSelectionStore((s) => s.toolbarConfig);
const setToolbarConfig = useSelectionStore((s) => s.setToolbarConfig); const setToolbarConfig = useSelectionStore((s) => s.setToolbarConfig);

View File

@@ -119,10 +119,6 @@ export default function SelectionWindow() {
}; };
}, [autoHideMs]); }, [autoHideMs]);
useEffect(() => {
useSelectionStore.getState().initSync();
}, []);
const close = async () => { const close = async () => {
if (timerRef.current) { if (timerRef.current) {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);

View File

@@ -15,10 +15,10 @@ import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
import { useIconfontScript } from "@/hooks/useScript"; import { useIconfontScript } from "@/hooks/useScript";
import { Extension } from "@/components/Settings/Extensions"; import { Extension } from "@/components/Settings/Extensions";
import { useExtensionsStore } from "@/stores/extensionsStore"; import { useExtensionsStore } from "@/stores/extensionsStore";
import { useSelectionStore } from "@/stores/selectionStore";
import { useServers } from "@/hooks/useServers"; import { useServers } from "@/hooks/useServers";
import { useDeepLinkManager } from "@/hooks/useDeepLinkManager"; import { useDeepLinkManager } from "@/hooks/useDeepLinkManager";
import { useSelectionWindow } from "../hooks/useSelectionWindow"; import { useSelectionWindow } from "../hooks/useSelectionWindow";
import { useSelectionStore } from "@/stores/selectionStore";
export default function LayoutOutlet() { export default function LayoutOutlet() {
const location = useLocation(); const location = useLocation();
@@ -27,6 +27,9 @@ export default function LayoutOutlet() {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const { activeTheme, isDark, setIsDark, setTheme } = useThemeStore(); const { activeTheme, isDark, setIsDark, setTheme } = useThemeStore();
// Initialize selection store synchronization
useSelectionStore();
// init servers isTauri // init servers isTauri
useServers(); useServers();
// init deep link manager // init deep link manager

View File

@@ -1,65 +1,63 @@
import { create } from "zustand"; import { create } from 'zustand';
import { persist, subscribeWithSelector } from "zustand/middleware"; import { createTauriStore } from '@tauri-store/zustand';
import platformAdapter from "@/utils/platformAdapter";
export type ISelectionStore = { // Types adapted from Selection/index.tsx to ensure compatibility
// whether selection is enabled export type LucideIconName =
selectionEnabled: boolean; | "Search"
setSelectionEnabled: (selectionEnabled: boolean) => void; | "Bot"
// toolbar buttons configuration for selection window | "Languages"
toolbarConfig: any[]; | "FileText"
setToolbarConfig: (toolbarConfig: any[]) => void; | "Copy"
// whether to show icons only (hide labels) in selection window | "Volume2";
iconsOnly: boolean;
setIconsOnly: (iconsOnly: boolean) => void; type IconConfig =
// initialize cross-window sync listeners once | { type: "lucide"; name: LucideIconName; color?: string }
initSync: () => Promise<void>; | { type: "custom"; dataUrl: string; color?: string };
type ActionType =
| "search"
| "ask_ai"
| "translate"
| "summary"
| "copy"
| "speak"
| "custom";
export type ButtonConfig = {
id: string;
label: string;
icon: IconConfig;
action: {
type: ActionType;
assistantId?: string;
assistantServerId?: string;
eventName?: string;
};
labelKey?: string;
}; };
export const useSelectionStore = create<ISelectionStore>()( type SelectionStore = {
subscribeWithSelector( iconsOnly: boolean;
persist( setIconsOnly: (iconsOnly: boolean) => void;
(set) => ({ toolbarConfig: ButtonConfig[];
selectionEnabled: false, setToolbarConfig: (config: ButtonConfig[]) => void;
setSelectionEnabled(selectionEnabled) { selectionEnabled: boolean;
set({ selectionEnabled }); setSelectionEnabled: (enabled: boolean) => void;
}, }
toolbarConfig: [],
setToolbarConfig(toolbarConfig) { // A Zustand store, like any other.
return set({ toolbarConfig }); export const useSelectionStore = create<SelectionStore>((set) => ({
}, iconsOnly: false,
iconsOnly: false, setIconsOnly: (iconsOnly) => set({ iconsOnly }),
setIconsOnly(iconsOnly) { toolbarConfig: [],
set({ iconsOnly }); setToolbarConfig: (toolbarConfig) => set({ toolbarConfig }),
// broadcast to other windows selectionEnabled: true,
try { setSelectionEnabled: (selectionEnabled) => set({ selectionEnabled }),
platformAdapter.emitEvent("selection-icons-only", { value: iconsOnly }); }));
} catch {}
}, // A handle to the Tauri plugin.
initSync: async () => { // We will need this to start the store.
// ensure listener only initialized once per window context export const tauriHandler = createTauriStore('selection-store', useSelectionStore, {
const hasInit = (window as any).__selectionIconsOnlyInit__; saveOnChange: true,
if (hasInit) return; autoStart: true,
(window as any).__selectionIconsOnlyInit__ = true; });
try {
await platformAdapter.listenEvent(
"selection-icons-only",
({ payload }: any) => {
const next = Boolean(payload?.value);
// apply without re-broadcast to avoid echo
set({ iconsOnly: next });
}
);
} catch {}
},
}),
{
name: "selection-store",
partialize: (state) => ({
toolbarConfig: state.toolbarConfig,
iconsOnly: state.iconsOnly,
}),
}
)
)
);