mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 13:26:00 -04:00
valtio
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -27,7 +27,8 @@
|
||||
"trivago",
|
||||
"tsparticles",
|
||||
"unlisten",
|
||||
"upsert"
|
||||
"upsert",
|
||||
"valtio"
|
||||
],
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
|
||||
@@ -18,13 +18,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@rspc/client": "^0.0.5",
|
||||
"@sd/core": "workspace:*",
|
||||
"@sd/config": "workspace:*",
|
||||
"@sd/core": "workspace:*",
|
||||
"@sd/interface": "workspace:*",
|
||||
"@tanstack/react-query": "^4.0.10",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"immer": "^9.0.15",
|
||||
"lodash": "^4.17.21",
|
||||
"valtio": "^1.6.4",
|
||||
"valtio-persist": "^1.0.2",
|
||||
"zustand": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
48
packages/client/src/stores/explorerStore.ts
Normal file
48
packages/client/src/stores/explorerStore.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { proxy, ref } from 'valtio';
|
||||
|
||||
export type ExplorerLayoutMode = 'list' | 'grid';
|
||||
|
||||
export enum ExplorerKind {
|
||||
Location,
|
||||
Tag,
|
||||
Space
|
||||
}
|
||||
|
||||
const state = {
|
||||
locationId: null as number | null,
|
||||
layoutMode: 'grid' as ExplorerLayoutMode,
|
||||
gridItemSize: 100,
|
||||
listItemSize: 40,
|
||||
selectedRowIndex: 1,
|
||||
showInspector: true,
|
||||
multiSelectIndexes: [] as number[],
|
||||
contextMenuObjectId: null as number | null,
|
||||
newThumbnails: {} as Record<string, boolean>
|
||||
};
|
||||
|
||||
export const explorerStore = proxy({
|
||||
...state,
|
||||
reset: () => resetStore(explorerStore, state),
|
||||
addNewThumbnail: (cas_id: string) => {
|
||||
explorerStore.newThumbnails[cas_id] = true;
|
||||
},
|
||||
selectMore: (indexes: number[]) => {
|
||||
if (!explorerStore.multiSelectIndexes.length && indexes.length) {
|
||||
explorerStore.multiSelectIndexes = [explorerStore.selectedRowIndex, ...indexes];
|
||||
} else {
|
||||
explorerStore.multiSelectIndexes = [
|
||||
...new Set([...explorerStore.multiSelectIndexes, ...indexes])
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function resetStore<T extends Record<string, any>, E extends Record<string, any>>(
|
||||
store: T,
|
||||
defaults: E
|
||||
) {
|
||||
for (const key in defaults) {
|
||||
// @ts-ignore
|
||||
store[key] = defaults[key];
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './useLibraryStore';
|
||||
export * from './useExplorerStore';
|
||||
export * from './explorerStore';
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import produce from 'immer';
|
||||
import create from 'zustand';
|
||||
|
||||
export type ExplorerLayoutMode = 'list' | 'grid';
|
||||
|
||||
export enum ExplorerKind {
|
||||
Location,
|
||||
Tag,
|
||||
Space
|
||||
}
|
||||
|
||||
type ExplorerStore = {
|
||||
layoutMode: ExplorerLayoutMode;
|
||||
locationId: number | null; // used by top bar
|
||||
gridItemSize: number;
|
||||
listItemSize: number;
|
||||
showInspector: boolean;
|
||||
selectedRowIndex: number;
|
||||
multiSelectIndexes: number[];
|
||||
contextMenuObjectId: number | null;
|
||||
newThumbnails: Record<string, boolean>;
|
||||
addNewThumbnail: (cas_id: string) => void;
|
||||
selectMore: (indexes: number[]) => void;
|
||||
reset: () => void;
|
||||
set: (changes: Partial<ExplorerStore>) => void;
|
||||
};
|
||||
|
||||
export const useExplorerStore = create<ExplorerStore>((set) => ({
|
||||
layoutMode: 'grid',
|
||||
locationId: null,
|
||||
gridItemSize: 100,
|
||||
listItemSize: 40,
|
||||
showInspector: true,
|
||||
selectedRowIndex: 1,
|
||||
multiSelectIndexes: [],
|
||||
contextMenuObjectId: -1,
|
||||
newThumbnails: {},
|
||||
addNewThumbnail: (cas_id) =>
|
||||
set((state) =>
|
||||
produce(state, (draft) => {
|
||||
draft.newThumbnails[cas_id] = true;
|
||||
})
|
||||
),
|
||||
selectMore: (indexes) => {
|
||||
set((state) =>
|
||||
produce(state, (draft) => {
|
||||
if (!draft.multiSelectIndexes.length && indexes.length) {
|
||||
draft.multiSelectIndexes = [draft.selectedRowIndex, ...indexes];
|
||||
} else {
|
||||
draft.multiSelectIndexes = [...new Set([...draft.multiSelectIndexes, ...indexes])];
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
reset: () => set(() => ({})),
|
||||
set: (changes) => set((state) => ({ ...state, ...changes }))
|
||||
}));
|
||||
@@ -5,7 +5,7 @@ import create from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
|
||||
import { useBridgeQuery } from '../index';
|
||||
import { useExplorerStore } from './useExplorerStore';
|
||||
import { explorerStore } from './explorerStore';
|
||||
|
||||
type LibraryStore = {
|
||||
// the uuid of the currently active library
|
||||
@@ -28,7 +28,7 @@ export const useLibraryStore = create<LibraryStore>()(
|
||||
})
|
||||
);
|
||||
// reset other stores
|
||||
useExplorerStore().reset();
|
||||
explorerStore.reset();
|
||||
},
|
||||
init: async (libraries) => {
|
||||
set((state) =>
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
"tailwindcss": "^3.1.6",
|
||||
"use-count-up": "^3.0.1",
|
||||
"use-debounce": "^8.0.3",
|
||||
"valtio": "^1.6.4",
|
||||
"valtio-persist": "^1.0.2",
|
||||
"zod": "^3.18.0",
|
||||
"zustand": "4.0.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
explorerStore,
|
||||
rspc,
|
||||
useExplorerStore,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useLibraryStore
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
TrashSimple
|
||||
} from 'phosphor-react';
|
||||
import React, { memo, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { Inspector } from '../explorer/Inspector';
|
||||
import { WithContextMenu } from '../layout/MenuOverlay';
|
||||
@@ -29,7 +30,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function Explorer(props: Props) {
|
||||
const addNewThumbnail = useExplorerStore((store) => store.addNewThumbnail);
|
||||
const { addNewThumbnail, selectedRowIndex, showInspector } = useSnapshot(explorerStore);
|
||||
|
||||
const currentLibraryUuid = useLibraryStore((store) => store.currentLibraryUuid);
|
||||
|
||||
@@ -39,11 +40,6 @@ export default function Explorer(props: Props) {
|
||||
}
|
||||
});
|
||||
|
||||
const { selectedRowIndex, showInspector } = useExplorerStore((store) => ({
|
||||
selectedRowIndex: store.selectedRowIndex,
|
||||
showInspector: store.showInspector
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<ExplorerContextMenu>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useExplorerStore, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { explorerStore, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { ExplorerData } from '@sd/core';
|
||||
import {
|
||||
ArrowBendUpRight,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TrashSimple
|
||||
} from 'phosphor-react';
|
||||
import React from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { WithContextMenu } from '../layout/MenuOverlay';
|
||||
|
||||
@@ -19,7 +20,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function ExplorerContextMenu(props: Props) {
|
||||
const contextMenuObjectId = useExplorerStore((store) => store.contextMenuObjectId);
|
||||
const contextMenuObjectId = useSnapshot(explorerStore).contextMenuObjectId;
|
||||
|
||||
const { data: tags } = useLibraryQuery(['tags.getAll'], {});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useExplorerStore } from '@sd/client';
|
||||
import { explorerStore } from '@sd/client';
|
||||
import { ExplorerItem } from '@sd/core';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import FileThumb from './FileThumb';
|
||||
import { isObject } from './utils';
|
||||
@@ -13,17 +14,17 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
function FileItem(props: Props) {
|
||||
const set = useExplorerStore.getState().set;
|
||||
|
||||
const size = useExplorerStore((state) => state.gridItemSize) || 100;
|
||||
const { gridItemSize } = useSnapshot(explorerStore);
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={(e) => {
|
||||
const objectId = isObject(props.data) ? props.data.id : props.data.file?.id;
|
||||
if (objectId != undefined) {
|
||||
set({ contextMenuObjectId: objectId });
|
||||
if (props.index != undefined) set({ selectedRowIndex: props.index });
|
||||
explorerStore.contextMenuObjectId = objectId;
|
||||
if (props.index != undefined) {
|
||||
explorerStore.selectedRowIndex = props.index;
|
||||
}
|
||||
}
|
||||
}}
|
||||
draggable
|
||||
@@ -31,7 +32,7 @@ function FileItem(props: Props) {
|
||||
className={clsx('inline-block w-[100px] mb-3', props.className)}
|
||||
>
|
||||
<div
|
||||
style={{ width: size, height: size }}
|
||||
style={{ width: gridItemSize, height: gridItemSize }}
|
||||
className={clsx(
|
||||
'border-2 border-transparent rounded-lg text-center mb-1 active:translate-y-[1px]',
|
||||
{
|
||||
@@ -49,7 +50,7 @@ function FileItem(props: Props) {
|
||||
'border-4 border-gray-250 rounded-sm shadow-md shadow-gray-750 max-h-full max-w-full overflow-hidden'
|
||||
)}
|
||||
data={props.data}
|
||||
size={size}
|
||||
size={gridItemSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EllipsisHorizontalIcon } from '@heroicons/react/24/solid';
|
||||
import { LocationContext, useBridgeQuery, useExplorerStore, useLibraryQuery } from '@sd/client';
|
||||
import { LocationContext, explorerStore, useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { ExplorerContext, ExplorerItem, FilePath } from '@sd/core';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AppPropsContext, useExplorerStore } from '@sd/client';
|
||||
import { AppPropsContext, explorerStore } from '@sd/client';
|
||||
import { ExplorerItem } from '@sd/core';
|
||||
import clsx from 'clsx';
|
||||
import React, { useContext } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import icons from '../../assets/icons';
|
||||
import { Folder } from '../icons/Folder';
|
||||
@@ -16,7 +17,7 @@ interface Props {
|
||||
|
||||
export default function FileThumb({ data, ...props }: Props) {
|
||||
const appProps = useContext(AppPropsContext);
|
||||
const newThumbnails = useExplorerStore((store) => store.newThumbnails);
|
||||
const { newThumbnails } = useSnapshot(explorerStore);
|
||||
|
||||
if (isPath(data) && data.is_dir) return <Folder size={props.size * 0.7} />;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ExplorerLayoutMode, useExplorerStore } from '@sd/client';
|
||||
import { ExplorerLayoutMode, explorerStore } from '@sd/client';
|
||||
import { ExplorerContext, ExplorerItem, FilePath } from '@sd/core';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useKey, useWindowSize } from 'rooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import FileItem from './FileItem';
|
||||
import FileRow from './FileRow';
|
||||
@@ -23,16 +24,16 @@ export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
|
||||
const [goingUp, setGoingUp] = useState(false);
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
const { gridItemSize, layoutMode, listItemSize, selectedRowIndex } = useExplorerStore(
|
||||
(state) => ({
|
||||
selectedRowIndex: state.selectedRowIndex,
|
||||
gridItemSize: state.gridItemSize,
|
||||
layoutMode: state.layoutMode,
|
||||
listItemSize: state.listItemSize
|
||||
})
|
||||
);
|
||||
// const { gridItemSize, layoutMode, listItemSize, selectedRowIndex } = useExplorerStore(
|
||||
// (state) => ({
|
||||
// selectedRowIndex: state.selectedRowIndex,
|
||||
// gridItemSize: state.gridItemSize,
|
||||
// layoutMode: state.layoutMode,
|
||||
// listItemSize: state.listItemSize
|
||||
// })
|
||||
// );
|
||||
|
||||
const set = useExplorerStore.getState().set;
|
||||
const { gridItemSize, layoutMode, listItemSize, selectedRowIndex } = useSnapshot(explorerStore);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setWidth(innerRef.current?.offsetWidth || 0);
|
||||
@@ -61,14 +62,14 @@ export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
|
||||
e.preventDefault();
|
||||
setGoingUp(true);
|
||||
if (selectedRowIndex !== -1 && selectedRowIndex !== 0)
|
||||
set({ selectedRowIndex: selectedRowIndex - 1 });
|
||||
explorerStore.selectedRowIndex = selectedRowIndex - 1;
|
||||
});
|
||||
|
||||
useKey('ArrowDown', (e) => {
|
||||
e.preventDefault();
|
||||
setGoingUp(false);
|
||||
if (selectedRowIndex !== -1 && selectedRowIndex !== (data.length ?? 1) - 1)
|
||||
set({ selectedRowIndex: selectedRowIndex + 1 });
|
||||
explorerStore.selectedRowIndex = selectedRowIndex + 1;
|
||||
});
|
||||
|
||||
// const Header = () => (
|
||||
@@ -164,7 +165,7 @@ const WrappedItem: React.FC<WrappedItemProps> = memo(({ item, index, isSelected,
|
||||
}, [item, setSearchParams]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
useExplorerStore.getState().set({ selectedRowIndex: isSelected ? -1 : index });
|
||||
explorerStore.selectedRowIndex = isSelected ? -1 : index;
|
||||
}, [isSelected, index]);
|
||||
|
||||
if (kind === 'list') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
import { AppPropsContext, useExplorerStore, useLibraryMutation } from '@sd/client';
|
||||
import { AppPropsContext, explorerStore, useLibraryMutation } from '@sd/client';
|
||||
import { Dropdown } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from 'phosphor-react';
|
||||
import React, { DetailedHTMLProps, HTMLAttributes, RefAttributes, useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { Shortcut } from '../primitive/Shortcut';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
@@ -80,7 +81,7 @@ const SearchBar = React.forwardRef<HTMLInputElement, DefaultProps>((props, ref)
|
||||
});
|
||||
|
||||
export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||
const { layoutMode, set, locationId, showInspector } = useExplorerStore();
|
||||
const { layoutMode, locationId, showInspector } = useSnapshot(explorerStore);
|
||||
const { mutate: generateThumbsForLocation } = useLibraryMutation(
|
||||
'jobs.generateThumbsForLocation',
|
||||
{
|
||||
@@ -159,7 +160,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||
left
|
||||
active={layoutMode === 'list'}
|
||||
icon={Rows}
|
||||
onClick={() => set({ layoutMode: 'list' })}
|
||||
onClick={() => (explorerStore.layoutMode = 'list')}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Grid view">
|
||||
@@ -168,7 +169,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||
right
|
||||
active={layoutMode === 'grid'}
|
||||
icon={SquaresFour}
|
||||
onClick={() => set({ layoutMode: 'grid' })}
|
||||
onClick={() => (explorerStore.layoutMode = 'grid')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -194,7 +195,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||
<div className="flex mr-3 space-x-2">
|
||||
<TopBarButton
|
||||
active={showInspector}
|
||||
onClick={() => set({ showInspector: !showInspector })}
|
||||
onClick={() => (explorerStore.showInspector = !showInspector)}
|
||||
className="my-2"
|
||||
icon={SidebarSimple}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useExplorerStore, useLibraryQuery, useLibraryStore } from '@sd/client';
|
||||
import { explorerStore, useLibraryQuery, useLibraryStore } from '@sd/client';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import z from 'zod';
|
||||
@@ -20,10 +20,8 @@ export function useExplorerParams() {
|
||||
export const LocationExplorer: React.FC<unknown> = () => {
|
||||
const { location_id, path } = useExplorerParams();
|
||||
|
||||
// for top bar location context, could be replaced with react context as it is child component
|
||||
const set = useExplorerStore((state) => state.set);
|
||||
useEffect(() => {
|
||||
set({ locationId: location_id });
|
||||
explorerStore.locationId = location_id;
|
||||
}, [location_id]);
|
||||
|
||||
const library_id = useLibraryStore((state) => state.currentLibraryUuid);
|
||||
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user