From 779eeddf5264b031bedd389094967776ba7aad45 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:18:49 +0200 Subject: [PATCH] updated --- .../components/ui/finishedSelectionStore.ts | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 frontend/src/components/ui/finishedSelectionStore.ts diff --git a/frontend/src/components/ui/finishedSelectionStore.ts b/frontend/src/components/ui/finishedSelectionStore.ts new file mode 100644 index 0000000..df04f16 --- /dev/null +++ b/frontend/src/components/ui/finishedSelectionStore.ts @@ -0,0 +1,290 @@ +// frontend\src\components\ui\finishedSelectionStore.ts + +'use client' + +import { useSyncExternalStore } from 'react' + +export type FinishedSelectionItem = { + key: string + file: string +} + +type Listener = () => void + +export type FinishedSelectionStore = { + subscribe: (listener: Listener) => () => void + subscribeKey: (key: string, listener: Listener) => () => void + + getVersion: () => number + isSelected: (key: string) => boolean + hasAnySelection: () => boolean + getCount: () => number + getSelectedFiles: () => string[] + + select: (item: FinishedSelectionItem) => void + selectMany: (items: FinishedSelectionItem[]) => void + deselect: (key: string) => void + deselectMany: (keys: string[]) => void + toggle: (item: FinishedSelectionItem) => void + clear: () => void + + replaceFile: (oldFile: string, newFile: string) => void +} + +export function createFinishedSelectionStore(): FinishedSelectionStore { + let items: Record = {} + let version = 0 + + const listeners = new Set() + const keyListeners = new Map>() + + const emit = (changedKeys?: Iterable) => { + version += 1 + + for (const listener of listeners) { + listener() + } + + if (!changedKeys) return + + const notified = new Set() + + for (const key of changedKeys) { + const set = keyListeners.get(key) + if (!set) continue + + for (const listener of set) { + if (notified.has(listener)) continue + notified.add(listener) + listener() + } + } + } + + const subscribe = (listener: Listener) => { + listeners.add(listener) + return () => { + listeners.delete(listener) + } + } + + const subscribeKey = (key: string, listener: Listener) => { + const cleanKey = String(key ?? '') + let set = keyListeners.get(cleanKey) + + if (!set) { + set = new Set() + keyListeners.set(cleanKey, set) + } + + set.add(listener) + + return () => { + const current = keyListeners.get(cleanKey) + if (!current) return + + current.delete(listener) + if (current.size === 0) { + keyListeners.delete(cleanKey) + } + } + } + + const isSelected = (key: string) => { + const cleanKey = String(key ?? '') + return Boolean(cleanKey && items[cleanKey]) + } + + const hasAnySelection = () => { + for (const _key in items) { + return true + } + return false + } + + const getCount = () => Object.keys(items).length + + const getSelectedFiles = () => { + return Object.values(items) + .map((item) => String(item.file || '').trim()) + .filter(Boolean) + } + + const select = (item: FinishedSelectionItem) => { + const key = String(item?.key ?? '').trim() + const file = String(item?.file ?? '').trim() + + if (!key || !file) return + + const prev = items[key] + if (prev && prev.file === file) return + + items = { + ...items, + [key]: { key, file }, + } + + emit([key]) + } + + const selectMany = (nextItems: FinishedSelectionItem[]) => { + if (!Array.isArray(nextItems) || nextItems.length === 0) return + + let next = items + const changedKeys: string[] = [] + + for (const item of nextItems) { + const key = String(item?.key ?? '').trim() + const file = String(item?.file ?? '').trim() + + if (!key || !file) continue + + const prev = next[key] + if (prev && prev.file === file) continue + + if (next === items) { + next = { ...items } + } + + next[key] = { key, file } + changedKeys.push(key) + } + + if (changedKeys.length === 0) return + + items = next + emit(changedKeys) + } + + const deselect = (key: string) => { + const cleanKey = String(key ?? '').trim() + if (!cleanKey || !items[cleanKey]) return + + const next = { ...items } + delete next[cleanKey] + items = next + + emit([cleanKey]) + } + + const deselectMany = (keys: string[]) => { + if (!Array.isArray(keys) || keys.length === 0) return + + let next = items + const changedKeys: string[] = [] + + for (const rawKey of keys) { + const key = String(rawKey ?? '').trim() + if (!key || !next[key]) continue + + if (next === items) { + next = { ...items } + } + + delete next[key] + changedKeys.push(key) + } + + if (changedKeys.length === 0) return + + items = next + emit(changedKeys) + } + + const toggle = (item: FinishedSelectionItem) => { + const key = String(item?.key ?? '').trim() + const file = String(item?.file ?? '').trim() + + if (!key || !file) return + + if (items[key]) { + const next = { ...items } + delete next[key] + items = next + emit([key]) + return + } + + items = { + ...items, + [key]: { key, file }, + } + emit([key]) + } + + const clear = () => { + const keys = Object.keys(items) + if (keys.length === 0) return + + items = {} + emit(keys) + } + + const replaceFile = (oldFile: string, newFile: string) => { + const from = String(oldFile ?? '').trim() + const to = String(newFile ?? '').trim() + + if (!from || !to || from === to) return + + let next = items + const changedKeys: string[] = [] + + for (const [key, item] of Object.entries(items)) { + if (item.file !== from) continue + + if (next === items) { + next = { ...items } + } + + next[key] = { + ...item, + file: to, + } + + changedKeys.push(key) + } + + if (changedKeys.length === 0) return + + items = next + emit(changedKeys) + } + + return { + subscribe, + subscribeKey, + getVersion: () => version, + isSelected, + hasAnySelection, + getCount, + getSelectedFiles, + select, + selectMany, + deselect, + deselectMany, + toggle, + clear, + replaceFile, + } +} + +export function useSelectionVersion(store: FinishedSelectionStore): number { + return useSyncExternalStore( + store.subscribe, + store.getVersion, + () => 0 + ) +} + +export function useSelectionChecked( + store: FinishedSelectionStore, + key: string +): boolean { + const cleanKey = String(key ?? '') + + return useSyncExternalStore( + (listener) => store.subscribeKey(cleanKey, listener), + () => store.isSelected(cleanKey), + () => false + ) +} \ No newline at end of file