From 292ae97b5f9b32cc9e49798eb31f55ae0f9e2c71 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 19 Dec 2023 17:12:25 +0800 Subject: [PATCH] solid wow --- .../actions/publish-artifacts/package.json | 1 + apps/desktop/package.json | 4 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- apps/mobile/package.json | 4 +- apps/web-astro/package.json | 3 +- apps/web-astro/src/Explorer/Context.tsx | 4 +- .../src/Explorer/FilePath/RenameTextBox.tsx | 138 ++- .../web-astro/src/Explorer/FilePath/Thumb.tsx | 165 +-- .../src/Explorer/FilePath/TruncateMarkup.tsx | 191 ++-- .../Explorer/FilePath/TruncateMarkupReact.tsx | 977 +++++++++--------- .../web-astro/src/Explorer/View/Grid/Item.tsx | 2 +- .../src/Explorer/View/Grid/VirtualGrid.tsx | 161 +-- .../src/Explorer/View/Grid/createGrid.ts | 101 +- .../src/Explorer/View/Grid/index.tsx | 548 +--------- .../src/Explorer/View/GridView/index.tsx | 4 +- .../src/Explorer/View/RenameItemText.tsx | 3 +- .../{useExplorer.ts => createExplorer.ts} | 83 +- apps/web-astro/src/Explorer/index.tsx | 2 +- .../queries/createExplorerInfiniteQuery.ts | 9 + .../Explorer/queries/createExplorerQuery.ts | 19 + .../queries/createPathsExplorerQuery.ts | 18 + .../queries/createPathsInfiniteQuery.ts | 33 + apps/web-astro/src/Page.tsx | 157 ++- apps/web-astro/src/Platform.tsx | 86 ++ apps/web-astro/src/Wrapper.tsx | 4 + apps/web-astro/src/cache.tsx | 86 ++ apps/web-astro/src/client.ts | 0 apps/web-astro/src/pages/index.astro | 2 +- apps/web-astro/src/patches.ts | 22 + apps/web-astro/src/rspc.tsx | 17 +- apps/web-astro/src/style.scss | 384 +++++++ apps/web-astro/src/useClientContext.tsx | 121 +++ apps/web-astro/src/useLibraryContext.tsx | 47 + apps/web-astro/src/useTheme.ts | 64 ++ apps/web-astro/tailwind.config.mjs | 11 +- apps/web/astro.config.mjs | 30 - apps/web/package.json | 4 +- apps/web/src/index.tsx | 17 + packages/client/package.json | 4 +- packages/config/vite/index.ts | 11 +- pnpm-lock.yaml | 70 +- 41 files changed, 2210 insertions(+), 1399 deletions(-) rename apps/web-astro/src/Explorer/{useExplorer.ts => createExplorer.ts} (70%) create mode 100644 apps/web-astro/src/Explorer/queries/createExplorerInfiniteQuery.ts create mode 100644 apps/web-astro/src/Explorer/queries/createExplorerQuery.ts create mode 100644 apps/web-astro/src/Explorer/queries/createPathsExplorerQuery.ts create mode 100644 apps/web-astro/src/Explorer/queries/createPathsInfiniteQuery.ts create mode 100644 apps/web-astro/src/Platform.tsx create mode 100644 apps/web-astro/src/Wrapper.tsx create mode 100644 apps/web-astro/src/cache.tsx create mode 100644 apps/web-astro/src/client.ts create mode 100644 apps/web-astro/src/patches.ts create mode 100644 apps/web-astro/src/style.scss create mode 100644 apps/web-astro/src/useClientContext.tsx create mode 100644 apps/web-astro/src/useLibraryContext.tsx create mode 100644 apps/web-astro/src/useTheme.ts delete mode 100644 apps/web/astro.config.mjs create mode 100644 apps/web/src/index.tsx diff --git a/.github/actions/publish-artifacts/package.json b/.github/actions/publish-artifacts/package.json index 8404dd869..f56be26fe 100644 --- a/.github/actions/publish-artifacts/package.json +++ b/.github/actions/publish-artifacts/package.json @@ -1,4 +1,5 @@ { + "name": "publish-artifacts", "private": true, "scripts": { "build": "ncc build index.ts --minify" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 75d382ba9..5378144e2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -12,8 +12,8 @@ }, "dependencies": { "@remix-run/router": "^1.14.0", - "@rspc/client": "0.0.0-main-45466c86", - "@rspc/tauri": "0.0.0-main-45466c86", + "@rspc/client": "0.0.0-main-b8b35d28", + "@rspc/tauri": "0.0.0-main-b8b35d28", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 1aaed648c..0d79b7523 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -4,7 +4,7 @@ }, "build": { "distDir": "../dist", - "devPath": "http://localhost:8001", + "devPath": "http://localhost:4321", "beforeDevCommand": "pnpm dev", "beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..." }, diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 1e82a9925..7a91a5d82 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -24,8 +24,8 @@ "@react-navigation/drawer": "^6.6.6", "@react-navigation/native": "^6.1.9", "@react-navigation/stack": "^6.3.20", - "@rspc/client": "0.0.0-main-45466c86", - "@rspc/react": "0.0.0-main-45466c86", + "@rspc/client": "0.0.0-main-b8b35d28", + "@rspc/react": "0.0.0-main-b8b35d28", "@sd/assets": "workspace:*", "@sd/client": "workspace:*", "@shopify/flash-list": "1.4.3", diff --git a/apps/web-astro/package.json b/apps/web-astro/package.json index dbe0b264a..665af79a6 100644 --- a/apps/web-astro/package.json +++ b/apps/web-astro/package.json @@ -14,9 +14,10 @@ "@astrojs/react": "^3.0.7", "@astrojs/solid-js": "^3.0.2", "@astrojs/tailwind": "^5.0.3", - "@rspc/solid": "0.0.0-main-45466c86", + "@rspc/solid": "0.0.0-main-b8b35d28", "@sd/assets": "workspace:^", "@sd/client": "workspace:^", + "@sd/ui": "workspace:^", "@solid-primitives/event-listener": "^2.3.0", "@solid-primitives/intersection-observer": "^2.1.3", "@solid-primitives/resize-observer": "^2.0.22", diff --git a/apps/web-astro/src/Explorer/Context.tsx b/apps/web-astro/src/Explorer/Context.tsx index fda6d3e9d..59bafdb2d 100644 --- a/apps/web-astro/src/Explorer/Context.tsx +++ b/apps/web-astro/src/Explorer/Context.tsx @@ -1,7 +1,7 @@ import { createContext, useContext, type ParentProps } from 'solid-js'; -import { Ordering } from './store'; -import { type CreateExplorer } from './useExplorer'; +import { type CreateExplorer } from './createExplorer'; +import { type Ordering } from './store'; /** * Context that must wrap anything to do with the explorer. diff --git a/apps/web-astro/src/Explorer/FilePath/RenameTextBox.tsx b/apps/web-astro/src/Explorer/FilePath/RenameTextBox.tsx index f74538f30..b0a9d6661 100644 --- a/apps/web-astro/src/Explorer/FilePath/RenameTextBox.tsx +++ b/apps/web-astro/src/Explorer/FilePath/RenameTextBox.tsx @@ -1,6 +1,15 @@ import { createEventListener } from '@solid-primitives/event-listener'; +import { createResizeObserver } from '@solid-primitives/resize-observer'; import clsx from 'clsx'; -import { createEffect, createSignal, splitProps, type ComponentProps } from 'solid-js'; +import { + createEffect, + createMemo, + createSignal, + JSX, + Show, + splitProps, + type ComponentProps +} from 'solid-js'; // import { Tooltip } from '@sd/ui'; @@ -159,8 +168,8 @@ export function RenameTextBox(props: RenameTextBoxProps) { ref={ref!} role="textbox" contentEditable={allowRename()} - className={clsx( - 'cursor-default overflow-hidden rounded-md px-1.5 py-px text-xs text-ink outline-none', + class={clsx( + 'cursor-default overflow-hidden rounded-md px-1.5 py-px text-center text-xs text-ink outline-none', allowRename() && 'whitespace-normal bg-app !text-ink ring-2 ring-accent-deep', !allowRename && props.idleClassName, props.class @@ -183,33 +192,116 @@ export function RenameTextBox(props: RenameTextBoxProps) { onKeyDown={handleKeyDown} {...wrapperProps} > - {props.name} - {/* {allowRename ? ( - name - ) : ( - - )} */} + {allowRename() + ? props.name + : (() => { + const ellipsis = createMemo(() => { + const extension = props.name.lastIndexOf('.'); + if (extension !== -1) + return `...${props.name.slice( + -Math.min(props.name.length - extension + 2, 8) + )}`; + return `...${props.name.slice(-8)}`; + }); + + return ( + + {props.name} + + ); + })()} // ); } -interface TruncatedTextProps { - text: string; - lines?: number; - onTruncate: (wasTruncated: boolean) => void; -} +const LINE_HEIGHT = 19; -function TruncatedText(props: TruncatedTextProps) { - const ellipsis = () => { - const extension = props.text.lastIndexOf('.'); - if (extension !== -1) return `...${props.text.slice(-(props.text.length - extension + 2))}`; - return `...${props.text.slice(-8)}`; - }; +function TruncatedText(props: { + lines: number; + prefix?: JSX.Element; + postfix?: JSX.Element; + children: string; + style?: JSX.CSSProperties; + onTruncate?: (wasTruncated: boolean) => void; +}) { + const [cutoff, setCutoff] = createSignal>([]); + const cutoffChildren = createMemo(() => { + const length = props.children.length; + + let cursor = length; + + const cutoffsArray = cutoff(); + for (let i = 1; i <= cutoffsArray.length; i++) { + const delta = Math.ceil(length * Math.pow(0.5, i)); + const cutoff = cutoffsArray[i]!; + + cursor += (cutoff === 'left' ? -1 : 1) * delta; + } + + return props.children.slice(0, cursor); + }); + + let ref!: HTMLDivElement; + + let currentlyTruncating = false; + + const fits = createMemo( + () => ref?.getBoundingClientRect().height ?? 0 / LINE_HEIGHT <= props.lines + ); + + function truncate() { + if (fits()) { + setCutoff((c) => [...c, 'right' as const]); + return (currentlyTruncating = false); + } + + setCutoff((c) => [...c, 'left' as const]); + + if (fits()) { + return (currentlyTruncating = false); + } + + currentlyTruncating = true; + + truncate(); + } + + function reset() { + setCutoff([]); + + if (fits()) return; + + currentlyTruncating = true; + truncate(); + } + + createResizeObserver( + () => ref, + () => { + if (currentlyTruncating) return; + + reset(); + } + ); return ( - -
{props.text}
-
+
+ +
{props.prefix}
+
+ {cutoffChildren()} + {/* 0}>{props.postfix} */} +
); } diff --git a/apps/web-astro/src/Explorer/FilePath/Thumb.tsx b/apps/web-astro/src/Explorer/FilePath/Thumb.tsx index efb6c4521..e5d1b3320 100644 --- a/apps/web-astro/src/Explorer/FilePath/Thumb.tsx +++ b/apps/web-astro/src/Explorer/FilePath/Thumb.tsx @@ -1,14 +1,23 @@ import { getIcon, getIconByName } from '@sd/assets/util'; import { createElementSize } from '@solid-primitives/resize-observer'; import clsx from 'clsx'; -import { createMemo, createSignal, Match, Show, Switch, type ComponentProps } from 'solid-js'; +import { + createEffect, + createMemo, + createSignal, + Match, + Show, + Switch, + type ComponentProps +} from 'solid-js'; import { createStore } from 'solid-js/store'; import { getExplorerItemData, type ExplorerItem } from '@sd/client'; +import { usePlatform } from '../../Platform'; import { LayeredFileIcon } from './LayeredFileIcon'; import classes from './Thumb.module.scss'; -interface FileThumbProps extends Pick, 'ref'> { +interface FileThumbProps extends Pick, 'ref' | 'class'> { data: ExplorerItem; loadOriginal?: boolean; size?: number; @@ -21,7 +30,6 @@ interface FileThumbProps extends Pick, 'ref'> { extension?: boolean; mediaControls?: boolean; pauseVideo?: boolean; - className?: string; frameClassName?: string; childClassName?: string | ((type: ThumbType) => string | undefined); isSidebarPreview?: boolean; @@ -51,11 +59,13 @@ export function FileThumb(props: FileThumbProps) { return { variant: 'icon' }; }); + const platform = usePlatform(); + const src = createMemo(() => { switch (thumbType().variant) { case 'thumbnail': - // if (itemData().thumbnailKey.length > 0) - // return platform.getThumbnailUrlByThumbKey(itemData.thumbnailKey); + if (itemData().thumbnailKey.length > 0) + return platform.getThumbnailUrlByThumbKey(itemData().thumbnailKey); break; case 'icon': @@ -80,11 +90,12 @@ export function FileThumb(props: FileThumbProps) { : props.childClassName; const childClassName = 'max-h-full max-w-full object-contain'; - const frameClassName = clsx( - 'rounded-sm border-2 border-app-line bg-app-darkBox', - props.frameClassName, - true ? classes.checkers : classes.checkersLight - ); + const frameClassName = () => + clsx( + 'rounded-sm border-2 border-app-line bg-app-darkBox', + props.frameClassName, + true ? classes.checkers : classes.checkersLight + ); const getClass = () => clsx(childClassName, _childClassName()); @@ -109,60 +120,79 @@ export function FileThumb(props: FileThumbProps) { }; return ( - - {(src) => ( - - - onLoad('thumbnail')} - onError={(e) => onError('thumbnail', e)} - decoding={props.size ? 'async' : 'sync'} - class={clsx( - props.cover - ? [ - 'min-h-full min-w-full object-cover object-center', - _childClassName() - ] - : getClass(), - props.frame && - !(itemData().kind === 'Video' && props.blackBars) && - frameClassName - )} - crossOrigin="anonymous" // Here it is ok, because it is not a react attr - blackBars={ - props.blackBars && itemData().kind === 'Video' && !props.cover - } - blackBarsSize={props.blackBarsSize} - extension={ - props.extension && - itemData().extension && - itemData().kind === 'Video' - ? itemData().extension || undefined - : undefined - } - /> - - - onLoad('icon')} - onError={(e) => onError('icon', e)} - decoding={props.size ? 'async' : 'sync'} - class={getClass()} - draggable={false} - /> - - +
+ > + + {(src) => ( + + + onLoad('thumbnail')} + onError={(e) => onError('thumbnail', e)} + decoding={props.size ? 'async' : 'sync'} + class={clsx( + props.cover + ? [ + 'min-h-full min-w-full object-cover object-center', + _childClassName() + ] + : getClass(), + props.frame && + !(itemData().kind === 'Video' && props.blackBars) && + frameClassName() + )} + crossOrigin="anonymous" // Here it is ok, because it is not a react attr + blackBars={ + props.blackBars && itemData().kind === 'Video' && !props.cover + } + blackBarsSize={props.blackBarsSize} + extension={ + props.extension && + itemData().extension && + itemData().kind === 'Video' + ? itemData().extension || undefined + : undefined + } + /> + + + onLoad('icon')} + onError={(e) => onError('icon', e)} + decoding={props.size ? 'async' : 'sync'} + class={getClass()} + draggable={false} + /> + + + )} + +
); } @@ -180,7 +210,14 @@ function Thumbnail(props: ThumbnailProps) { return ( <> - + {(props.cover || (size.width && size.width > 80)) && props.extension && (
Solid.JSX.Element); -// children: (ref: HTMLDivElement, style: any) => Solid.JSX.Element; -// lineHeight?: number | string; -// tokenize?: string; -// onTruncate?: (wasTruncated: boolean) => any; -// } +export interface TruncateMarkupProps { + lines: number; + ellipsis?: Solid.JSX.Element | ((element: Solid.JSX.Element) => Solid.JSX.Element); + children: (props: { ref: HTMLDivElement; style: any }) => Solid.JSX.Element; + lineHeight?: number | string; + tokenize?: string; + onTruncate?: (wasTruncated: boolean) => any; +} -// export function TruncateMarkup(props: TruncateMarkupProps) { -// let splitDirectionSeq = []; -// let shouldTruncate = true; -// let wasLastCharTested = false; -// let endFound = false; -// let latestThatFits = null; -// let onTruncateCalled = false; +export function TruncateMarkup(props: Solid.ParentProps) { + const [text, setText] = Solid.createSignal(''); -// let el: HTMLDivElement; + const children = Solid.children(() => props.children); -// Solid.onMount(() => { -// // if (!isValid) return; + const [ref, setRef] = Solid.createSignal(null); -// let splitDirectionSeq: Array<'left' | 'right'> = []; -// let shouldTruncate = true; -// let wasLastCharTested = false; -// let endFound = false; -// let latestThatFits = null; -// let onTruncateCalled = false; + let shouldTruncate = false; + let latestThatFits = null; + let onTruncateCalled = false; + let lineHeight: any = null; + let endFound = false; + let splitDirectionSeq = []; + let wasLastCharTested = false; -// const lineHeight = props.lineHeight || getLineHeight(el); + type SplitDirection = 'left' | 'right'; -// const fits = Solid.createMemo(() => { -// const maxLines = props.lines; -// const { height } = el!.getBoundingClientRect(); -// const computedLines = Math.round(height / parseFloat(lineHeight)); + function splitString(string: string, splitDirections: Array, level: any) { + if (!splitDirections.length) return string; -// return maxLines >= computedLines; -// }); + if (splitDirections.length && policy.isAtomic(string)) { + if (!wasLastCharTested) wasLastCharTested = true; + else endFound = true; -// function onTruncate(wasTruncated: boolean) { -// if (!onTruncateCalled) { -// onTruncateCalled = true; -// props.onTruncate?.(wasTruncated); -// } -// } + return string; + } -// function truncateOriginalText() { -// endFound = false; -// splitDirectionSeq = ['left']; -// wasLastCharTested = false; + if (policy.tokenizeString) { + const wordsArray = splitArray(policy.tokenizeString(string), splitDirections, level); -// tryToFit(origText, splitDirectionSeq); -// } + return wordsArray.joing(''); + } + const [splitDirection, ...restSplitDirections] = splitDirections; + const pivotIndex = Math.ceil(string.length / 2); + const beforeString = string.substring(0, pivotIndex); -// function tryToFit() {} + if (splitDirection === 'left') return splitString(beforeString, restSplitDirections, level); -// function truncate() { -// if (fits()) { -// shouldTruncate = false; -// onTruncate(false); + const afterString = string.substring(pivotIndex); -// return; -// } + return beforeString + splitString(afterString, restSplitDirections, level); + } -// truncateOriginalText(); -// } -// }); + function splitArray(array: string[], splitDirections: Array, level) { + if (!splitDirections.length) { + return array; + } -// function childrenElementWithRef() { -// const childrenArray = children.toArray(); -// if (childrenArray.length > 1) { -// throw new Error('TruncateMarkup must have only one child element'); -// } + if (array.length === 1) { + return [split(array[0]!, splitDirections, /* isRoot */ false, level)]; + } -// const child = childrenArray[0]; -// } + const [splitDirection, ...restSplitDirections] = splitDirections; + const pivotIndex = Math.ceil(array.length / 2); + const beforeArray = array.slice(0, pivotIndex); -// const [text, setText] = Solid.createSignal(props.children); + if (splitDirection === 'left') return splitArray(beforeArray, restSplitDirections, level); -// return <>{text()}; -// } + const afterArray = array.slice(pivotIndex); + + return beforeArray.concat(splitArray(afterArray, restSplitDirections, level)); + } + + function split( + node: HTMLElement | string | null, + splitDirections: Array, + isRoot = false, + level = 1 + ): HTMLElement | string | null { + if (!node) { + endFound = true; + return node; + } else if (typeof node === 'string') { + return splitString(node, splitDirections, level); + } else if (node.nodeType === node.TEXT_NODE) { + return splitString(node.textContent ?? '', splitDirections, level); + } + + return node; + } + + function tryToFit(rootEl: HTMLElement, splitDirections: Array<'left' | 'right'>) { + if (!rootEl.firstChild) { + // no markup in container + return; + } + + const newRootEL = split(rootEl, splitDirections, true); + } + + function fits() { + const refValue = ref(); + if (!refValue) return false; + + const { height } = refValue.getBoundingClientRect(); + const computedLines = Math.round(height / parseFloat(lineHeight)); + + return props.lines >= computedLines; + } + + function truncateOriginalText() { + endFound = false; + splitDirectionSeq = ['left']; + wasLastCharTested = false; + + tryToFit(origText, splitDirectionSeq); + } + + function truncate() { + if (fits()) { + shouldTruncate = false; + onTruncate(false); + + return; + } + + truncateOriginalText(); + } + + createResizeObserver(ref, () => { + shouldTruncate = false; + latestThatFits = null; + + setText(origText); + + shouldTruncate = true; + onTruncateCalled = false; + truncate(); + }); + + return text; +} diff --git a/apps/web-astro/src/Explorer/FilePath/TruncateMarkupReact.tsx b/apps/web-astro/src/Explorer/FilePath/TruncateMarkupReact.tsx index 8ebd9accf..9b6d08289 100644 --- a/apps/web-astro/src/Explorer/FilePath/TruncateMarkupReact.tsx +++ b/apps/web-astro/src/Explorer/FilePath/TruncateMarkupReact.tsx @@ -1,479 +1,498 @@ -// import getLineHeight from 'line-height'; -// import memoizeOne from 'memoize-one'; -// import PropTypes from 'prop-types'; -// import React from 'react'; -// import ResizeObserver from 'resize-observer-polyfill'; - -// import { Atom, ATOM_STRING_ID, isAtomComponent } from './atom'; -// import TOKENIZE_POLICY from './tokenize-rules'; - -// const SPLIT = { -// LEFT: true, -// RIGHT: false -// }; - -// const toString = (node, string = '') => { -// if (!node) { -// return string; -// } else if (typeof node === 'string') { -// return string + node; -// } else if (isAtomComponent(node)) { -// return string + ATOM_STRING_ID; -// } -// const children = Array.isArray(node) ? node : node.props.children || ''; - -// return string + React.Children.map(children, (child) => toString(child)).join(''); -// }; - -// const cloneWithChildren = (node, children, isRootEl, level) => { -// const getDisplayStyle = () => { -// if (isRootEl) { -// return { -// // root element cannot be an inline element because of the line calculation -// display: (node.props.style || {}).display || 'block' -// }; -// } else if (level === 2) { -// return { -// // level 2 elements (direct children of the root element) need to be inline because of the ellipsis. -// // if level 2 element was a block element, ellipsis would get rendered on a new line, breaking the max number of lines -// display: (node.props.style || {}).display || 'inline-block' -// }; -// } else return {}; -// }; - -// return { -// ...node, -// props: { -// ...node.props, -// style: { -// ...node.props.style, -// ...getDisplayStyle() -// }, -// children -// } -// }; -// }; - -// const validateTree = (node) => { -// if (node == null || ['string', 'number'].includes(typeof node) || isAtomComponent(node)) { -// return true; -// } else if (typeof node.type === 'function') { -// if (process.env.NODE_ENV !== 'production') { -// /* eslint-disable no-console */ -// console.error( -// `ReactTruncateMarkup tried to render <${node.type.name} />, but truncating React components is not supported, the full content is rendered instead. Only DOM elements are supported. Alternatively, you can take advantage of the component (see more in the docs https://github.com/patrik-piskay/react-truncate-markup/blob/master/README.md#truncatemarkupatom-).` -// ); -// /* eslint-enable */ -// } - -// return false; -// } - -// if (node.props && node.props.children) { -// return React.Children.toArray(node.props.children).reduce( -// (isValid, child) => isValid && validateTree(child), -// true -// ); -// } - -// return true; -// }; - -// export default class TruncateMarkup extends React.Component { -// static Atom = Atom; - -// static propTypes = { -// children: PropTypes.element.isRequired, -// lines: PropTypes.number, -// ellipsis: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.func]), -// lineHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), -// onTruncate: PropTypes.func, -// // eslint-disable-next-line -// onAfterTruncate: (props, propName, componentName) => { -// if (props[propName]) { -// return new Error( -// `${componentName}: Setting \`onAfterTruncate\` prop is deprecated, use \`onTruncate\` instead.` -// ); -// } -// }, -// tokenize: (props, propName, componentName) => { -// const tokenizeValue = props[propName]; - -// if (typeof tokenizeValue !== 'undefined') { -// if (!TOKENIZE_POLICY[tokenizeValue]) { -// /* eslint-disable no-console */ -// return new Error( -// `${componentName}: Unknown option for prop 'tokenize': '${tokenizeValue}'. Option 'characters' will be used instead.` -// ); -// /* eslint-enable */ -// } -// } -// } -// }; - -// static defaultProps = { -// lines: 1, -// ellipsis: '...', -// lineHeight: '', -// onTruncate: () => {}, -// tokenize: 'characters' -// }; - -// constructor(props) { -// super(props); - -// this.state = { -// text: this.childrenWithRefMemo(this.props.children) -// }; -// } - -// lineHeight = null; -// splitDirectionSeq = []; -// shouldTruncate = true; -// wasLastCharTested = false; -// endFound = false; -// latestThatFits = null; -// onTruncateCalled = false; - -// toStringMemo = memoizeOne(toString); -// childrenWithRefMemo = memoizeOne(this.childrenElementWithRef); -// validateTreeMemo = memoizeOne(validateTree); - -// get isValid() { -// return this.validateTreeMemo(this.props.children); -// } -// get origText() { -// return this.childrenWithRefMemo(this.props.children); -// } -// get policy() { -// return TOKENIZE_POLICY[this.props.tokenize] || TOKENIZE_POLICY.characters; -// } - -// componentDidMount() { -// if (!this.isValid) { -// return; -// } - -// // get the computed line-height of the parent element -// // it'll be used for determining whether the text fits the container or not -// this.lineHeight = this.props.lineHeight || getLineHeight(this.el); -// this.truncate(); -// } - -// UNSAFE_componentWillReceiveProps(nextProps) { -// this.shouldTruncate = false; -// this.latestThatFits = null; - -// this.setState( -// { -// text: this.childrenWithRefMemo(nextProps.children) -// }, -// () => { -// if (!this.isValid) { -// return; -// } - -// this.lineHeight = nextProps.lineHeight || getLineHeight(this.el); -// this.shouldTruncate = true; -// this.truncate(); -// } -// ); -// } - -// componentDidUpdate() { -// if (this.shouldTruncate === false || this.isValid === false) { -// return; -// } - -// if (this.endFound) { -// // we've found the end where we cannot split the text further -// // that means we've already found the max subtree that fits the container -// // so we are rendering that -// if (this.latestThatFits !== null && this.state.text !== this.latestThatFits) { -// /* eslint-disable react/no-did-update-set-state */ -// this.setState({ -// text: this.latestThatFits -// }); - -// return; -// /* eslint-enable */ -// } - -// this.onTruncate(/* wasTruncated */ true); - -// return; -// } - -// if (this.splitDirectionSeq.length) { -// if (this.fits()) { -// this.latestThatFits = this.state.text; -// // we've found a subtree that fits the container -// // but we need to check if we didn't cut too much of it off -// // so we are changing the last splitting decision from splitting and going left -// // to splitting and going right -// this.splitDirectionSeq.splice( -// this.splitDirectionSeq.length - 1, -// 1, -// SPLIT.RIGHT, -// SPLIT.LEFT -// ); -// } else { -// this.splitDirectionSeq.push(SPLIT.LEFT); -// } - -// this.tryToFit(this.origText, this.splitDirectionSeq); -// } -// } - -// componentWillUnmount() { -// this.lineHeight = null; -// this.latestThatFits = null; -// this.splitDirectionSeq = []; -// } - -// onTruncate = (wasTruncated) => { -// if (!this.onTruncateCalled) { -// this.onTruncateCalled = true; -// this.props.onTruncate(wasTruncated); -// } -// }; - -// handleResize = (el, prevResizeObserver) => { -// // clean up previous observer -// if (prevResizeObserver) { -// prevResizeObserver.disconnect(); -// } - -// // unmounting or just unsetting the element to be replaced with a new one later -// if (!el) return null; - -// /* Wrapper element resize handing */ -// let initialRender = true; -// const resizeCallback = () => { -// if (initialRender) { -// // ResizeObserer cb is called on initial render too so we are skipping here -// initialRender = false; -// } else { -// // wrapper element has been resized, recalculating with the original text -// this.shouldTruncate = false; -// this.latestThatFits = null; - -// this.setState( -// { -// text: this.origText -// }, -// () => { -// this.shouldTruncate = true; -// this.onTruncateCalled = false; -// this.truncate(); -// } -// ); -// } -// }; - -// const resizeObserver = prevResizeObserver || new ResizeObserver(resizeCallback); - -// resizeObserver.observe(el); - -// return resizeObserver; -// }; - -// truncate() { -// if (this.fits()) { -// // the whole text fits on the first try, no need to do anything else -// this.shouldTruncate = false; -// this.onTruncate(/* wasTruncated */ false); - -// return; -// } - -// this.truncateOriginalText(); -// } - -// setRef = (el) => { -// const isNewEl = this.el !== el; -// this.el = el; - -// // whenever we obtain a new element, attach resize handler -// if (isNewEl) { -// this.resizeObserver = this.handleResize(el, this.resizeObserver); -// } -// }; - -// childrenElementWithRef(children) { -// const child = React.Children.only(children); - -// return React.cloneElement(child, { -// ref: this.setRef, -// style: { -// wordWrap: 'break-word', -// ...child.props.style -// } -// }); -// } - -// truncateOriginalText() { -// this.endFound = false; -// this.splitDirectionSeq = [SPLIT.LEFT]; -// this.wasLastCharTested = false; - -// this.tryToFit(this.origText, this.splitDirectionSeq); -// } - -// /** -// * Splits rootEl based on instructions and updates React's state with the returned element -// * After React rerenders the new text, we'll check if the new text fits in componentDidUpdate -// * @param {ReactElement} rootEl - the original children element -// * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions -// */ -// tryToFit(rootEl, splitDirections) { -// if (!rootEl.props.children) { -// // no markup in container -// return; -// } - -// const newRootEl = this.split(rootEl, splitDirections, /* isRootEl */ true); - -// let ellipsis = -// typeof this.props.ellipsis === 'function' -// ? this.props.ellipsis(newRootEl) -// : this.props.ellipsis; - -// ellipsis = -// typeof ellipsis === 'object' -// ? React.cloneElement(ellipsis, { key: 'ellipsis' }) -// : ellipsis; - -// const newChildren = newRootEl.props.children; -// const newChildrenWithEllipsis = [].concat(newChildren, ellipsis); - -// // edge case tradeoff EC#1 - on initial render it doesn't fit in the requested number of lines (1) so it starts truncating -// // - because of truncating and the ellipsis position, div#lvl2 will have display set to 'inline-block', -// // causing the whole body to fit in 1 line again -// // - if that happens, ellipsis is not needed anymore as the whole body is rendered -// // - NOTE this could be fixed by checking for this exact case and handling it separately so it renders
foo {ellipsis}
-// // -// // Example: -// // -// //
-// // foo -// //
bar
-// //
-// //
-// const shouldRenderEllipsis = -// toString(newChildren) !== this.toStringMemo(this.props.children); - -// this.setState({ -// text: { -// ...newRootEl, -// props: { -// ...newRootEl.props, -// children: shouldRenderEllipsis ? newChildrenWithEllipsis : newChildren -// } -// } -// }); -// } - -// /** -// * Splits JSX node based on its type -// * @param {null|string|Array|Object} node - JSX node -// * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions -// * @return {null|string|Array|Object} - split JSX node -// */ -// split(node, splitDirections, isRoot = false, level = 1) { -// if (!node || isAtomComponent(node)) { -// this.endFound = true; - -// return node; -// } else if (typeof node === 'string') { -// return this.splitString(node, splitDirections, level); -// } else if (Array.isArray(node)) { -// return this.splitArray(node, splitDirections, level); -// } - -// const newChildren = this.split( -// node.props.children, -// splitDirections, -// /* isRoot */ false, -// level + 1 -// ); - -// return cloneWithChildren(node, newChildren, isRoot, level); -// } - -// splitString(string, splitDirections = [], level) { -// if (!splitDirections.length) { -// return string; -// } - -// if (splitDirections.length && this.policy.isAtomic(string)) { -// // allow for an extra render test with the current character included -// // in most cases this variation was already tested, but some edge cases require this check -// // NOTE could be removed once EC#1 is taken care of -// if (!this.wasLastCharTested) { -// this.wasLastCharTested = true; -// } else { -// // we are trying to split further but we have nowhere to go now -// // that means we've already found the max subtree that fits the container -// this.endFound = true; -// } - -// return string; -// } - -// if (this.policy.tokenizeString) { -// const wordsArray = this.splitArray( -// this.policy.tokenizeString(string), -// splitDirections, -// level -// ); - -// // in order to preserve the input structure -// return wordsArray.join(''); -// } - -// const [splitDirection, ...restSplitDirections] = splitDirections; -// const pivotIndex = Math.ceil(string.length / 2); -// const beforeString = string.substring(0, pivotIndex); - -// if (splitDirection === SPLIT.LEFT) { -// return this.splitString(beforeString, restSplitDirections, level); -// } -// const afterString = string.substring(pivotIndex); - -// return beforeString + this.splitString(afterString, restSplitDirections, level); -// } - -// splitArray(array, splitDirections = [], level) { -// if (!splitDirections.length) { -// return array; -// } - -// if (array.length === 1) { -// return [this.split(array[0], splitDirections, /* isRoot */ false, level)]; -// } - -// const [splitDirection, ...restSplitDirections] = splitDirections; -// const pivotIndex = Math.ceil(array.length / 2); -// const beforeArray = array.slice(0, pivotIndex); - -// if (splitDirection === SPLIT.LEFT) { -// return this.splitArray(beforeArray, restSplitDirections, level); -// } -// const afterArray = array.slice(pivotIndex); - -// return beforeArray.concat(this.splitArray(afterArray, restSplitDirections, level)); -// } - -// fits() { -// const { lines: maxLines } = this.props; -// const { height } = this.el.getBoundingClientRect(); -// const computedLines = Math.round(height / parseFloat(this.lineHeight)); - -// return maxLines >= computedLines; -// } - -// render() { -// return this.state.text; -// } -// } +import getLineHeight from 'line-height'; +import memoizeOne from 'memoize-one'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; + +import { Atom, ATOM_STRING_ID, isAtomComponent } from './atom'; + +const TOKENIZE_POLICY = { + characters: { + tokenizeString: null, + isAtomic: (str: string) => str.length <= 1 + }, + words: { + tokenizeString: (str: string) => str.match(/(\s*\S[\S\xA0]*)/g), + isAtomic: (str: string) => /^\s*[\S\xA0]*\s*$/.test(str) + } +}; + +const SPLIT = { + LEFT: true, + RIGHT: false +}; + +const toString = (node, string = '') => { + if (!node) { + return string; + } else if (typeof node === 'string') { + return string + node; + } else if (isAtomComponent(node)) { + return string + ATOM_STRING_ID; + } + const children = Array.isArray(node) ? node : node.props.children || ''; + + return string + React.Children.map(children, (child) => toString(child)).join(''); +}; + +const cloneWithChildren = (node, children, isRootEl, level) => { + const getDisplayStyle = () => { + if (isRootEl) { + return { + // root element cannot be an inline element because of the line calculation + display: (node.props.style || {}).display || 'block' + }; + } else if (level === 2) { + return { + // level 2 elements (direct children of the root element) need to be inline because of the ellipsis. + // if level 2 element was a block element, ellipsis would get rendered on a new line, breaking the max number of lines + display: (node.props.style || {}).display || 'inline-block' + }; + } else return {}; + }; + + return { + ...node, + props: { + ...node.props, + style: { + ...node.props.style, + ...getDisplayStyle() + }, + children + } + }; +}; + +const validateTree = (node) => { + if (node == null || ['string', 'number'].includes(typeof node) || isAtomComponent(node)) { + return true; + } else if (typeof node.type === 'function') { + if (process.env.NODE_ENV !== 'production') { + /* eslint-disable no-console */ + console.error( + `ReactTruncateMarkup tried to render <${node.type.name} />, but truncating React components is not supported, the full content is rendered instead. Only DOM elements are supported. Alternatively, you can take advantage of the component (see more in the docs https://github.com/patrik-piskay/react-truncate-markup/blob/master/README.md#truncatemarkupatom-).` + ); + /* eslint-enable */ + } + + return false; + } + + if (node.props && node.props.children) { + return React.Children.toArray(node.props.children).reduce( + (isValid, child) => isValid && validateTree(child), + true + ); + } + + return true; +}; + +export interface TruncateMarkupProps { + lines: number; + ellipsis?: JSX.Element | ((element: JSX.Element) => JSX.Element); + children: JSX.Element; + lineHeight?: number | string; + tokenize?: string; + onTruncate?: (wasTruncated: boolean) => any; +} + +export default class TruncateMarkup extends React.Component { + static Atom = Atom; + + static propTypes = { + children: PropTypes.element.isRequired, + lines: PropTypes.number, + ellipsis: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.func]), + lineHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + onTruncate: PropTypes.func, + // eslint-disable-next-line + onAfterTruncate: (props, propName, componentName) => { + if (props[propName]) { + return new Error( + `${componentName}: Setting \`onAfterTruncate\` prop is deprecated, use \`onTruncate\` instead.` + ); + } + }, + tokenize: (props, propName, componentName) => { + const tokenizeValue = props[propName]; + + if (typeof tokenizeValue !== 'undefined') { + if (!TOKENIZE_POLICY[tokenizeValue]) { + /* eslint-disable no-console */ + return new Error( + `${componentName}: Unknown option for prop 'tokenize': '${tokenizeValue}'. Option 'characters' will be used instead.` + ); + /* eslint-enable */ + } + } + } + }; + + static defaultProps = { + lines: 1, + ellipsis: '...', + lineHeight: '', + onTruncate: () => {}, + tokenize: 'characters' + }; + + constructor(props) { + super(props); + + this.state = { + text: this.childrenWithRefMemo(this.props.children) + }; + } + + lineHeight = null; + splitDirectionSeq = []; + shouldTruncate = true; + wasLastCharTested = false; + endFound = false; + latestThatFits = null; + onTruncateCalled = false; + + toStringMemo = memoizeOne(toString); + childrenWithRefMemo = memoizeOne(this.childrenElementWithRef); + validateTreeMemo = memoizeOne(validateTree); + + get isValid() { + return this.validateTreeMemo(this.props.children); + } + get origText() { + return this.childrenWithRefMemo(this.props.children); + } + get policy() { + return TOKENIZE_POLICY[this.props.tokenize] || TOKENIZE_POLICY.characters; + } + + componentDidMount() { + if (!this.isValid) { + return; + } + + // get the computed line-height of the parent element + // it'll be used for determining whether the text fits the container or not + this.lineHeight = this.props.lineHeight || getLineHeight(this.el); + this.truncate(); + } + + UNSAFE_componentWillReceiveProps(nextProps) { + this.shouldTruncate = false; + this.latestThatFits = null; + + this.setState( + { + text: this.childrenWithRefMemo(nextProps.children) + }, + () => { + if (!this.isValid) { + return; + } + + this.lineHeight = nextProps.lineHeight || getLineHeight(this.el); + this.shouldTruncate = true; + this.truncate(); + } + ); + } + + componentDidUpdate() { + if (this.shouldTruncate === false || this.isValid === false) { + return; + } + + if (this.endFound) { + // we've found the end where we cannot split the text further + // that means we've already found the max subtree that fits the container + // so we are rendering that + if (this.latestThatFits !== null && this.state.text !== this.latestThatFits) { + /* eslint-disable react/no-did-update-set-state */ + this.setState({ + text: this.latestThatFits + }); + + return; + /* eslint-enable */ + } + + this.onTruncate(/* wasTruncated */ true); + + return; + } + + if (this.splitDirectionSeq.length) { + if (this.fits()) { + this.latestThatFits = this.state.text; + // we've found a subtree that fits the container + // but we need to check if we didn't cut too much of it off + // so we are changing the last splitting decision from splitting and going left + // to splitting and going right + this.splitDirectionSeq.splice( + this.splitDirectionSeq.length - 1, + 1, + SPLIT.RIGHT, + SPLIT.LEFT + ); + } else { + this.splitDirectionSeq.push(SPLIT.LEFT); + } + + this.tryToFit(this.origText, this.splitDirectionSeq); + } + } + + componentWillUnmount() { + this.lineHeight = null; + this.latestThatFits = null; + this.splitDirectionSeq = []; + } + + onTruncate = (wasTruncated) => { + if (!this.onTruncateCalled) { + this.onTruncateCalled = true; + this.props.onTruncate(wasTruncated); + } + }; + + handleResize = (el, prevResizeObserver) => { + // clean up previous observer + if (prevResizeObserver) { + prevResizeObserver.disconnect(); + } + + // unmounting or just unsetting the element to be replaced with a new one later + if (!el) return null; + + /* Wrapper element resize handing */ + let initialRender = true; + const resizeCallback = () => { + if (initialRender) { + // ResizeObserer cb is called on initial render too so we are skipping here + initialRender = false; + } else { + // wrapper element has been resized, recalculating with the original text + this.shouldTruncate = false; + this.latestThatFits = null; + + this.setState( + { + text: this.origText + }, + () => { + this.shouldTruncate = true; + this.onTruncateCalled = false; + this.truncate(); + } + ); + } + }; + + const resizeObserver = prevResizeObserver || new ResizeObserver(resizeCallback); + + resizeObserver.observe(el); + + return resizeObserver; + }; + + truncate() { + if (this.fits()) { + // the whole text fits on the first try, no need to do anything else + this.shouldTruncate = false; + this.onTruncate(/* wasTruncated */ false); + + return; + } + + this.truncateOriginalText(); + } + + setRef = (el) => { + const isNewEl = this.el !== el; + this.el = el; + + // whenever we obtain a new element, attach resize handler + if (isNewEl) { + this.resizeObserver = this.handleResize(el, this.resizeObserver); + } + }; + + childrenElementWithRef(children) { + const child = React.Children.only(children); + + return React.cloneElement(child, { + ref: this.setRef, + style: { + wordWrap: 'break-word', + ...child.props.style + } + }); + } + + truncateOriginalText() { + this.endFound = false; + this.splitDirectionSeq = [SPLIT.LEFT]; + this.wasLastCharTested = false; + + this.tryToFit(this.origText, this.splitDirectionSeq); + } + + /** + * Splits rootEl based on instructions and updates React's state with the returned element + * After React rerenders the new text, we'll check if the new text fits in componentDidUpdate + * @param {ReactElement} rootEl - the original children element + * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions + */ + tryToFit(rootEl, splitDirections) { + if (!rootEl.props.children) { + // no markup in container + return; + } + + const newRootEl = this.split(rootEl, splitDirections, /* isRootEl */ true); + + let ellipsis = + typeof this.props.ellipsis === 'function' + ? this.props.ellipsis(newRootEl) + : this.props.ellipsis; + + ellipsis = + typeof ellipsis === 'object' + ? React.cloneElement(ellipsis, { key: 'ellipsis' }) + : ellipsis; + + const newChildren = newRootEl.props.children; + const newChildrenWithEllipsis = [].concat(newChildren, ellipsis); + + // edge case tradeoff EC#1 - on initial render it doesn't fit in the requested number of lines (1) so it starts truncating + // - because of truncating and the ellipsis position, div#lvl2 will have display set to 'inline-block', + // causing the whole body to fit in 1 line again + // - if that happens, ellipsis is not needed anymore as the whole body is rendered + // - NOTE this could be fixed by checking for this exact case and handling it separately so it renders
foo {ellipsis}
+ // + // Example: + // + //
+ // foo + //
bar
+ //
+ //
+ const shouldRenderEllipsis = + toString(newChildren) !== this.toStringMemo(this.props.children); + + this.setState({ + text: { + ...newRootEl, + props: { + ...newRootEl.props, + children: shouldRenderEllipsis ? newChildrenWithEllipsis : newChildren + } + } + }); + } + + /** + * Splits JSX node based on its type + * @param {null|string|Array|Object} node - JSX node + * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions + * @return {null|string|Array|Object} - split JSX node + */ + split(node, splitDirections, isRoot = false, level = 1) { + if (!node || isAtomComponent(node)) { + this.endFound = true; + + return node; + } else if (typeof node === 'string') { + return this.splitString(node, splitDirections, level); + } else if (Array.isArray(node)) { + return this.splitArray(node, splitDirections, level); + } + + const newChildren = this.split( + node.props.children, + splitDirections, + /* isRoot */ false, + level + 1 + ); + + return cloneWithChildren(node, newChildren, isRoot, level); + } + + splitString(string, splitDirections = [], level) { + if (!splitDirections.length) { + return string; + } + + if (splitDirections.length && this.policy.isAtomic(string)) { + // allow for an extra render test with the current character included + // in most cases this variation was already tested, but some edge cases require this check + // NOTE could be removed once EC#1 is taken care of + if (!this.wasLastCharTested) { + this.wasLastCharTested = true; + } else { + // we are trying to split further but we have nowhere to go now + // that means we've already found the max subtree that fits the container + this.endFound = true; + } + + return string; + } + + if (this.policy.tokenizeString) { + const wordsArray = this.splitArray( + this.policy.tokenizeString(string), + splitDirections, + level + ); + + // in order to preserve the input structure + return wordsArray.join(''); + } + + const [splitDirection, ...restSplitDirections] = splitDirections; + const pivotIndex = Math.ceil(string.length / 2); + const beforeString = string.substring(0, pivotIndex); + + if (splitDirection === SPLIT.LEFT) { + return this.splitString(beforeString, restSplitDirections, level); + } + const afterString = string.substring(pivotIndex); + + return beforeString + this.splitString(afterString, restSplitDirections, level); + } + + splitArray(array, splitDirections = [], level) { + if (!splitDirections.length) { + return array; + } + + if (array.length === 1) { + return [this.split(array[0], splitDirections, /* isRoot */ false, level)]; + } + + const [splitDirection, ...restSplitDirections] = splitDirections; + const pivotIndex = Math.ceil(array.length / 2); + const beforeArray = array.slice(0, pivotIndex); + + if (splitDirection === SPLIT.LEFT) { + return this.splitArray(beforeArray, restSplitDirections, level); + } + const afterArray = array.slice(pivotIndex); + + return beforeArray.concat(this.splitArray(afterArray, restSplitDirections, level)); + } + + fits() { + const { lines: maxLines } = this.props; + const { height } = this.el.getBoundingClientRect(); + const computedLines = Math.round(height / parseFloat(this.lineHeight)); + + return maxLines >= computedLines; + } + + render() { + return this.state.text; + } +} diff --git a/apps/web-astro/src/Explorer/View/Grid/Item.tsx b/apps/web-astro/src/Explorer/View/Grid/Item.tsx index cfe15e05e..ac3b05309 100644 --- a/apps/web-astro/src/Explorer/View/Grid/Item.tsx +++ b/apps/web-astro/src/Explorer/View/Grid/Item.tsx @@ -67,7 +67,7 @@ export const GridItem = (props: Props) => { class="h-full w-full" data-selectable="" data-selectable-index={props.index} - data-selectable-id={itemId} + data-selectable-id={itemId()} onContextMenu={(e) => { if (explorerView.selectable && !explorer.selectedItems().has(props.item)) { explorer.resetSelectedItems([props.item]); diff --git a/apps/web-astro/src/Explorer/View/Grid/VirtualGrid.tsx b/apps/web-astro/src/Explorer/View/Grid/VirtualGrid.tsx index 028006b6e..77e323caf 100644 --- a/apps/web-astro/src/Explorer/View/Grid/VirtualGrid.tsx +++ b/apps/web-astro/src/Explorer/View/Grid/VirtualGrid.tsx @@ -16,24 +16,35 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) { const [offset, setOffset] = Solid.createSignal(0); - const useVisibilityObserver = createVisibilityObserver(); + // const useVisibilityObserver = createVisibilityObserver(); let loadMoreRef: HTMLDivElement | undefined; - const inView = useVisibilityObserver(() => loadMoreRef); + // const inView = useVisibilityObserver(() => loadMoreRef); const rowVirtualizer = createVirtualizer({ - ...grid().virtualizer.rowVirtualizer(), + get count() { + return grid().virtualizer.rowVirtualizer.count; + }, + getScrollElement: () => grid().virtualizer.rowVirtualizer.getScrollElement(), + estimateSize: (index) => grid().virtualizer.rowVirtualizer.estimateSize(index), get scrollMargin() { return offset(); } }); - const columnVirtualizer = createVirtualizer(grid().virtualizer.columnVirtualizer()); + const columnVirtualizer = createVirtualizer({ + horizontal: true, + get count() { + return grid().virtualizer.columnVirtualizer.count; + }, + getScrollElement: () => grid().virtualizer.columnVirtualizer.getScrollElement(), + estimateSize: (index) => grid().virtualizer.columnVirtualizer.estimateSize(index) + }); - const width = columnVirtualizer.getTotalSize(); - const height = rowVirtualizer.getTotalSize(); + const width = Solid.createMemo(() => columnVirtualizer.getTotalSize()); + const height = Solid.createMemo(() => rowVirtualizer.getTotalSize()); - const internalWidth = () => width - (grid().padding.left + grid().padding.right); - const internalHeight = () => height - (grid().padding.top + grid().padding.bottom); + const internalWidth = () => width() - (grid().padding.left + grid().padding.right); + const internalHeight = () => height() - (grid().padding.top + grid().padding.bottom); const loadMoreTriggerHeight = Solid.createMemo(() => { if (grid().horizontal || !grid().onLoadMore || !grid().rowCount || !grid().totalRowCount) @@ -54,9 +65,9 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) { loadMoreHeight = Math.max(0, rowVirtualizer.scrollElement.clientHeight - offset); } - const triggerHeight = height - lastRowTop + loadMoreHeight; + const triggerHeight = height() - lastRowTop + loadMoreHeight; - return Math.min(height, triggerHeight); + return Math.min(height(), triggerHeight); }); const loadMoreTriggerWidth = Solid.createMemo(() => { @@ -76,9 +87,9 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) { const loadMoreWidth = grid().loadMoreSize ?? columnVirtualizer.scrollElement?.clientWidth ?? 0; - const triggerWidth = width - lastColumnLeft + loadMoreWidth; + const triggerWidth = width() - lastColumnLeft + loadMoreWidth; - return Math.min(width, triggerWidth); + return Math.min(width(), triggerWidth); }); Solid.createEffect( @@ -95,24 +106,32 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) { ) ); - Solid.createEffect(() => { - if (inView()) grid().onLoadMore?.(); - }); + // Solid.createEffect(() => { + // if (inView()) grid().onLoadMore?.(); + // }); - Solid.createEffect(() => { - const element = grid().scrollRef(); - if (!element) return; + // Solid.createEffect(() => { + // const element = grid().scrollRef(); + // if (!element) return; - const observer = new MutationObserver(() => setOffset(ref?.offsetTop ?? 0)); + // const observer = new MutationObserver(() => setOffset(ref?.offsetTop ?? 0)); - observer.observe(element, { - childList: true - }); + // observer.observe(element, { + // childList: true + // }); - return () => observer.disconnect(); - }); + // return () => observer.disconnect(); + // }); - Solid.createEffect(() => setOffset(ref?.offsetTop ?? 0), []); + // Solid.createEffect(() => setOffset(ref?.offsetTop ?? 0), []); + + Solid.createEffect(() => + console.log({ + row: rowVirtualizer.getVirtualItems(), + column: columnVirtualizer.getVirtualItems(), + virtualizer: grid().virtualizer + }) + ); return (
0 || internalHeight() > 0}> @@ -142,47 +161,61 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) { {(virtualRow) => ( {(virtualColumn) => { - let index = grid().horizontal - ? virtualColumn.index * grid().rowCount + virtualRow.index - : virtualRow.index * grid().columnCount + virtualColumn.index; + const index = Solid.createMemo(() => { + let index = grid().horizontal + ? virtualColumn.index * grid().rowCount + virtualRow.index + : virtualRow.index * grid().columnCount + + virtualColumn.index; - if (grid().invert) index = grid().count - 1 - index; + if (grid().invert) index = grid().count - 1 - index; - if (index >= grid().count || index < 0) return null; + if (index >= grid().count || index < 0) return null; + return { value: index }; + }); return ( -
-
- {children(index)} -
-
+ + {(index) => ( +
+
+ {children(index().value)} +
+
+ )} +
); }}
diff --git a/apps/web-astro/src/Explorer/View/Grid/createGrid.ts b/apps/web-astro/src/Explorer/View/Grid/createGrid.ts index b22bf84a0..2dc7f8eef 100644 --- a/apps/web-astro/src/Explorer/View/Grid/createGrid.ts +++ b/apps/web-astro/src/Explorer/View/Grid/createGrid.ts @@ -1,7 +1,8 @@ import { createElementSize, createResizeObserver } from '@solid-primitives/resize-observer'; import { type createVirtualizer } from '@tanstack/solid-virtual'; import * as Core from '@virtual-grid/core'; -import { createEffect, createMemo, createSignal, type Accessor } from 'solid-js'; +import { createEffect, createMemo, createSignal, onMount, type Accessor } from 'solid-js'; +import { createStore } from 'solid-js/store'; type VirtualizerOptions = Parameters[0]; @@ -37,59 +38,83 @@ export type CreateGridProps({ - scrollRef, - overscan, - ...props -}: CreateGridProps) { +export function createGrid( + props: Accessor> +) { const [width, setWidth] = createSignal(0); let staticWidth: number | null = null; - const grid = createMemo(() => Core.grid({ width: width(), ...props })); + const grid = createMemo(() => Core.grid({ width: width(), ...props() })); - const rowVirtualizer = createMemo(() => ({ - ...props.rowVirtualizer, + const [rowVirtualizer, setRowVirtualizer] = createStore({ + ...props().rowVirtualizer, count: grid().totalRowCount, - getScrollElement: scrollRef, + getScrollElement: props().scrollRef, estimateSize: grid().getItemHeight, paddingStart: grid().padding.top, paddingEnd: grid().padding.bottom, - overscan: overscan ?? props.rowVirtualizer?.overscan - })); + overscan: props().overscan ?? props().rowVirtualizer?.overscan + }); - const columnVirtualizer = createMemo(() => ({ - ...props.columnVirtualizer, + createEffect(() => { + setRowVirtualizer({ + ...props().rowVirtualizer, + count: grid().totalRowCount, + getScrollElement: props().scrollRef, + estimateSize: grid().getItemHeight, + paddingStart: grid().padding.top, + paddingEnd: grid().padding.bottom, + overscan: props().overscan ?? props().rowVirtualizer?.overscan + }); + }); + + const [columnVirtualizer, setColumnVirtualizer] = createStore({ + ...props().columnVirtualizer, horizontal: true, count: grid().totalColumnCount, - getScrollElement: scrollRef, + getScrollElement: props().scrollRef, estimateSize: grid().getItemWidth, paddingStart: grid().padding.left, paddingEnd: grid().padding.right, - overscan: overscan ?? props.columnVirtualizer?.overscan - })); + overscan: props().overscan ?? props().columnVirtualizer?.overscan + }); + + createEffect(() => { + setColumnVirtualizer({ + ...props().columnVirtualizer, + horizontal: true, + count: grid().totalColumnCount, + getScrollElement: props().scrollRef, + estimateSize: grid().getItemWidth, + paddingStart: grid().padding.left, + paddingEnd: grid().padding.right, + overscan: props().overscan ?? props().columnVirtualizer?.overscan + }); + }); const isStatic = createMemo( () => - props.width !== undefined || - props.horizontal || - props.columns === 0 || - (props.columns === 'auto' - ? !props.size || (typeof props.size === 'object' && !props.size.width) - : (props.columns === undefined || props.columns) && - ((typeof props.size === 'object' && props.size.width) || - typeof props.size === 'number')) + props().width !== undefined || + props().horizontal || + props().columns === 0 || + (props().columns === 'auto' + ? !props().size || (typeof props().size === 'object' && !props().size.width) + : (props().columns === undefined || props().columns) && + ((typeof props().size === 'object' && props().size.width) || + typeof props().size === 'number')) ); - createElementSize; - - createResizeObserver(scrollRef, ({ width }) => { - if (width === undefined || isStatic()) { - if (width !== undefined) staticWidth = width; - return; + createResizeObserver( + () => props().scrollRef(), + ({ width }) => { + if (width === undefined || isStatic()) { + if (width !== undefined) staticWidth = width; + return; + } + setWidth(width); } - setWidth(width); - }); + ); createEffect(() => { if (staticWidth === null || width() === staticWidth || isStatic()) return; @@ -99,12 +124,12 @@ export function createGrid ({ ...grid(), - scrollRef: scrollRef, - onLoadMore: props.onLoadMore, - loadMoreSize: props.loadMoreSize, + scrollRef: props().scrollRef, + onLoadMore: props().onLoadMore, + loadMoreSize: props().loadMoreSize, virtualizer: { - rowVirtualizer: rowVirtualizer, - columnVirtualizer: columnVirtualizer + rowVirtualizer, + columnVirtualizer } })); } diff --git a/apps/web-astro/src/Explorer/View/Grid/index.tsx b/apps/web-astro/src/Explorer/View/Grid/index.tsx index cdd2e59bf..45ffac98a 100644 --- a/apps/web-astro/src/Explorer/View/Grid/index.tsx +++ b/apps/web-astro/src/Explorer/View/Grid/index.tsx @@ -1,5 +1,5 @@ // import { Grid, useGrid } from '@virtual-grid/react'; -import { createEffect, createSignal, type JSX } from 'solid-js'; +import { createEffect, createMemo, createSignal, Show, type JSX } from 'solid-js'; import { type ExplorerItem } from '@sd/client'; import { useExplorerContext } from '../../Context'; @@ -18,16 +18,15 @@ export type RenderItem = (item: { }) => JSX.Element; const CHROME_REGEX = /Chrome/; +const isChrome = CHROME_REGEX.test(navigator.userAgent); -export default ({ children }: { children: RenderItem }) => { +export default (props: { children: RenderItem }) => { // const os = useOperatingSystem(); // const realOS = useOperatingSystem(true); - const isChrome = CHROME_REGEX.test(navigator.userAgent); - const explorer = useExplorerContext(); const explorerView = useExplorerViewContext(); - const explorerSettings = explorer.useSettingsSnapshot(); + // const explorerSettings = explorer.useSettingsSnapshot(); // const quickPreviewStore = useQuickPreviewStore(); // const selecto = useRef(null); @@ -43,34 +42,34 @@ export default ({ children }: { children: RenderItem }) => { const [dragFromThumbnail, setDragFromThumbnail] = createSignal(false); - const itemDetailsHeight = 44 + (explorerSettings.showBytesInGridView ? 20 : 0); - const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight; - const padding = explorerSettings.layoutMode === 'grid' ? 12 : 0; + const itemDetailsHeight = 44 + (false ? 20 : 0); + // const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight; + const itemHeight = 100 + itemDetailsHeight; + // const padding = explorerSettings.layoutMode === 'grid' ? 12 : 0; - const grid = createGrid({ + const grid = createGrid(() => ({ scrollRef: explorer.scrollRef, - count: explorer.items?.length ?? 0, + count: explorer.items()?.length ?? 0, totalCount: explorer.count, - ...(explorerSettings.layoutMode === 'grid' - ? { - columns: 'auto', - size: { width: explorerSettings.gridItemSize, height: itemHeight } - } - : { columns: explorerSettings.mediaColumns }), - rowVirtualizer: { overscan: explorer.overscan ?? 5 }, + // ...(explorerSettings.layoutMode === 'grid' + // ? + columns: 'auto', + size: { width: 100, height: itemHeight }, + // : { columns: explorerSettings.mediaColumns }), + rowVirtualizer: { overscan: explorer.overscan ?? 10 }, onLoadMore: explorer.loadMore, - getItemId: (index: number) => { + getItemId: (index) => { const item = explorer.items()?.[index]; return item ? uniqueId(item) : undefined; }, - getItemData: (index: number) => explorer.items()?.[index], - padding: { - bottom: explorerView.bottom ? padding + explorerView.bottom : undefined, - x: padding, - y: padding - }, - gap: explorerSettings.layoutMode === 'grid' ? explorerSettings.gridGap : 1 - }); + getItemData: (index) => explorer.items()?.[index] + // padding: { + // bottom: explorerView.bottom ? padding + explorerView.bottom : undefined, + // x: padding, + // y: padding + // }, + // gap: explorerSettings.layoutMode === 'grid' ? explorerSettings.gridGap : 1 + })); const getElementById = (id: string) => { if (!explorer.parent) return; @@ -115,57 +114,6 @@ export default ({ children }: { children: RenderItem }) => { return activeItem; } - // function handleDragEnd() { - // getExplorerStore().isDragSelecting = false; - // selectoFirstColumn.current = undefined; - // selectoLastColumn.current = undefined; - // setDragFromThumbnail(false); - - // const allSelected = selecto.current?.getSelectedTargets() ?? []; - // activeItem.current = getActiveItem(allSelected); - // } - - // useEffect( - // () => { - // const element = explorer.scrollRef.current; - // if (!element) return; - - // const handleScroll = () => { - // selecto.current?.checkScroll(); - // selecto.current?.findSelectableTargets(); - // }; - - // element.addEventListener('scroll', handleScroll); - // return () => element.removeEventListener('scroll', handleScroll); - // }, - // // explorer.scrollRef is a stable reference so this only actually runs once - // [explorer.scrollRef] - // ); - - // useEffect(() => { - // if (!selecto.current) return; - - // const set = new Set(explorer.selectedItemHashes.value); - // if (set.size === 0) return; - - // const items = [...document.querySelectorAll('[data-selectable]')].filter((item) => { - // const id = getElementId(item); - // if (id === null) return; - - // const selected = set.has(id); - // if (selected) set.delete(id); - - // return selected; - // }); - - // selectoUnselected.current = set; - // selecto.current.setSelectedTargets(items as HTMLElement[]); - - // activeItem.current = getActiveItem(items); - - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [grid.columnCount, explorer.items]); - createEffect(() => { if (explorer.selectedItems().size !== 0) return; @@ -179,165 +127,6 @@ export default ({ children }: { children: RenderItem }) => { // selecto.current?.setSelectedTargets([]); // }); - const keyboardHandler = (e: KeyboardEvent, newIndex: number) => { - if (!explorerView.selectable) return; - - if (explorer.selectedItems().size > 0) { - e.preventDefault(); - e.stopPropagation(); - } - - const newSelectedItem = grid().getItem(newIndex); - if (!newSelectedItem?.data) return; - - if (!explorer.allowMultiSelect) explorer.resetSelectedItems([newSelectedItem.data]); - else { - const selectedItemElement = getElementById(uniqueId(newSelectedItem.data)); - if (!selectedItemElement) return; - - // if (e.shiftKey && !getQuickPreviewStore().open) { - // if (!explorer.selectedItems.has(newSelectedItem.data)) { - // explorer.addSelectedItem(newSelectedItem.data); - // // selecto.current?.setSelectedTargets([ - // // ...(selecto.current?.getSelectedTargets() || []), - // // selectedItemElement as HTMLElement - // // ]); - // } - // } else { - explorer.resetSelectedItems([newSelectedItem.data]); - // selecto.current?.setSelectedTargets([selectedItemElement as HTMLElement]); - // if (selectoUnselected.current.size > 0) selectoUnselected.current = new Set(); - // } - } - - activeItem = newSelectedItem.data; - - const scrollRef = explorer.scrollRef(), - viewRef = explorerView.ref(); - if (!scrollRef || !viewRef) return; - - const { top: viewTop } = viewRef.getBoundingClientRect(); - - const itemTop = newSelectedItem.rect.top + viewTop; - const itemBottom = newSelectedItem.rect.bottom + viewTop; - - const { height: scrollHeight } = scrollRef.getBoundingClientRect(); - - const scrollTop = - (explorerView.top ?? parseInt(getComputedStyle(scrollRef).paddingTop)) + 1; - - const scrollBottom = scrollHeight - 2; // (os !== 'windows' && os !== 'browser' ? 2 : 1); - - if (itemTop < scrollTop) { - scrollRef.scrollBy({ - top: - itemTop - - scrollTop - - (newSelectedItem.row === 0 ? grid().padding.top : grid().gap.y / 2) - }); - } else if (itemBottom > scrollBottom - (explorerView.bottom ?? 0)) { - scrollRef.scrollBy({ - top: - itemBottom - - scrollBottom + - (explorerView.bottom ?? 0) + - (newSelectedItem.row === grid().rowCount - 1 - ? grid().padding.bottom - : grid().gap.y / 2) - }); - } - }; - - const getGridItemHandler = (key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight') => { - const lastItem = activeItem; - if (!lastItem) return; - - const lastItemIndex = explorer.items()?.findIndex((item) => item === lastItem); - if (lastItemIndex === undefined || lastItemIndex === -1) return; - - const gridItem = grid().getItem(lastItemIndex); - if (!gridItem) return; - - let newIndex = gridItem.index; - - switch (key) { - case 'ArrowUp': - newIndex -= grid().columnCount; - break; - case 'ArrowDown': - newIndex += grid().columnCount; - break; - case 'ArrowLeft': - newIndex -= 1; - break; - case 'ArrowRight': - newIndex += 1; - break; - } - - return newIndex; - }; - - // useShortcut('explorerDown', (e) => { - // if (!explorerView.selectable) return; - - // if (explorer.selectedItems.size === 0) { - // const item = grid.getItem(0); - // if (!item?.data) return; - - // const selectedItemElement = getElementById(uniqueId(item.data)); - // if (!selectedItemElement) return; - - // explorer.resetSelectedItems([item.data]); - // selecto.current?.setSelectedTargets([selectedItemElement as HTMLElement]); - // activeItem.current = item.data; - // return; - // } - - // const newIndex = getGridItemHandler('ArrowDown'); - // if (newIndex === undefined) return; - // keyboardHandler(e, newIndex); - // }); - - // useShortcut('explorerUp', (e) => { - // if (!explorerView.selectable) return; - // const newIndex = getGridItemHandler('ArrowUp'); - // if (newIndex === undefined) return; - // keyboardHandler(e, newIndex); - // }); - - // useShortcut('explorerLeft', (e) => { - // if (!explorerView.selectable) return; - // const newIndex = getGridItemHandler('ArrowLeft'); - // if (newIndex === undefined) return; - // keyboardHandler(e, newIndex); - // }); - - // useShortcut('explorerRight', (e) => { - // if (!explorerView.selectable) return; - // const newIndex = getGridItemHandler('ArrowRight'); - // if (newIndex === undefined) return; - // keyboardHandler(e, newIndex); - // }); - - //everytime selected items change within quick preview we need to update selecto - // useEffect(() => { - // if (!selecto.current || !quickPreviewStore.open) return; - // if (explorer.selectedItems.size !== 1) return; - - // const [item] = Array.from(explorer.selectedItems); - // if (!item) return; - - // const itemId = uniqueId(item); - - // const element = getElementById(itemId); - - // if (!element) selectoUnselected.current = new Set(itemId); - // else selecto.current.setSelectedTargets([element as HTMLElement]); - - // activeItem.current = item; - // }, [explorer.items, explorer.selectedItems, quickPreviewStore.open, realOS, getElementById]); - return ( { getElementById }} > - {/* {explorer.allowMultiSelect && ( - { - if (!getExplorerStore().drag) return; - e.stop(); - handleDragEnd(); - }} - onDragStart={({ inputEvent }) => { - getExplorerStore().isDragSelecting = true; - - if ((inputEvent as MouseEvent).target instanceof HTMLImageElement) { - setDragFromThumbnail(true); - } - }} - onDragEnd={handleDragEnd} - onScroll={({ direction }) => { - selecto.current?.findSelectableTargets(); - explorer.scrollRef.current?.scrollBy( - (direction[0] || 0) * 10, - (direction[1] || 0) * 10 - ); - }} - scrollOptions={{ - container: { current: explorer.scrollRef.current }, - throttleTime: isChrome || dragFromThumbnail ? 30 : 10000 - }} - onSelect={(e) => { - const inputEvent = e.inputEvent as MouseEvent; - - if (inputEvent.type === 'mousedown') { - const el = inputEvent.shiftKey - ? e.added[0] || e.removed[0] - : e.selected[0]; - - if (!el) return; - - const item = getElementItem(el); - - if (!item?.data) return; - - if (!inputEvent.shiftKey) { - if (explorer.selectedItems.has(item.data)) { - selecto.current?.setSelectedTargets(e.beforeSelected); - } else { - selectoUnselected.current = new Set(); - explorer.resetSelectedItems([item.data]); - } - - return; - } - - if (e.added[0]) explorer.addSelectedItem(item.data); - else explorer.removeSelectedItem(item.data); - } else if (inputEvent.type === 'mousemove') { - const unselectedItems: string[] = []; - - e.added.forEach((el) => { - const item = getElementItem(el); - - if (!item?.data) return; - - explorer.addSelectedItem(item.data); - }); - - e.removed.forEach((el) => { - const item = getElementItem(el); - - if (!item?.data || typeof item.id === 'number') return; - - if (document.contains(el)) explorer.removeSelectedItem(item.data); - else unselectedItems.push(item.id); - }); - - const dragDirection = { - x: inputEvent.x === e.rect.left ? 'left' : 'right', - y: inputEvent.y === e.rect.bottom ? 'down' : 'up' - } as const; - - const dragStart = { - x: dragDirection.x === 'right' ? e.rect.left : e.rect.right, - y: dragDirection.y === 'down' ? e.rect.top : e.rect.bottom - }; - - const dragEnd = { x: inputEvent.x, y: inputEvent.y }; - - const columns = new Set(); - - const elements = [...e.added, ...e.removed]; - - const items = elements.reduce( - (items, el) => { - const item = getElementItem(el); - - if (!item) return items; - - columns.add(item.column); - return [...items, item]; - }, - [] as NonNullable>[] - ); - - if (columns.size > 1) { - items.sort((a, b) => a.column - b.column); - - const firstItem = - dragDirection.x === 'right' - ? items[0] - : items[items.length - 1]; - - const lastItem = - dragDirection.x === 'right' - ? items[items.length - 1] - : items[0]; - - if (firstItem && lastItem) { - selectoFirstColumn.current = firstItem.column; - selectoLastColumn.current = lastItem.column; - } - } else if (columns.size === 1) { - const column = [...columns.values()][0]!; - - items.sort((a, b) => a.row - b.row); - - const itemRect = elements[0]?.getBoundingClientRect(); - - const inDragArea = - itemRect && - (dragDirection.x === 'right' - ? dragEnd.x >= itemRect.left - : dragEnd.x <= itemRect.right); - - if ( - column !== selectoLastColumn.current || - (column === selectoLastColumn.current && !inDragArea) - ) { - const firstItem = - dragDirection.y === 'down' - ? items[0] - : items[items.length - 1]; - - if (firstItem) { - const viewRectTop = - explorerView.ref.current?.getBoundingClientRect().top ?? - 0; - - const itemTop = firstItem.rect.top + viewRectTop; - const itemBottom = firstItem.rect.bottom + viewRectTop; - - if ( - dragDirection.y === 'down' - ? dragStart.y < itemTop - : dragStart.y > itemBottom - ) { - const dragHeight = Math.abs( - dragStart.y - - (dragDirection.y === 'down' - ? itemTop - : itemBottom) - ); - - let itemsInDragCount = - (dragHeight - grid.gap.y) / - (grid.virtualItemSize.height + grid.gap.y); - - if (itemsInDragCount > 1) { - itemsInDragCount = Math.ceil(itemsInDragCount); - } else { - itemsInDragCount = Math.round(itemsInDragCount); - } - - [...Array(itemsInDragCount)].forEach((_, i) => { - const index = - dragDirection.y === 'down' - ? itemsInDragCount - i - : i + 1; - - const itemIndex = - firstItem.index + - (dragDirection.y === 'down' ? -index : index) * - grid.columnCount; - - const item = explorer.items?.[itemIndex]; - - if (item) { - if (inputEvent.shiftKey) { - if (explorer.selectedItems.has(item)) - explorer.removeSelectedItem(item); - else { - explorer.addSelectedItem(item); - if (inDragArea) - unselectedItems.push( - uniqueId(item) - ); - } - } else if (!inDragArea) - explorer.removeSelectedItem(item); - else { - explorer.addSelectedItem(item); - if (inDragArea) - unselectedItems.push(uniqueId(item)); - } - } - }); - } - } - - if (!inDragArea && column === selectoFirstColumn.current) { - selectoFirstColumn.current = undefined; - selectoLastColumn.current = undefined; - } else { - selectoLastColumn.current = column; - if (selectoFirstColumn.current === undefined) { - selectoFirstColumn.current = column; - } - } - } - } - - if (unselectedItems.length > 0) { - selectoUnselected.current = new Set([ - ...selectoUnselected.current, - ...unselectedItems - ]); - } - } - }} - /> - )} */} - {(index) => { - const item = explorer.items()?.[index]; - if (!item) return null; + const item = createMemo(() => explorer.items()?.[index]); return ( - { - if (e.button !== 0 || !explorerView.selectable) return; + + {(item) => ( + { + if (e.button !== 0 || !explorerView.selectable) return; - e.stopPropagation(); + e.stopPropagation(); - const item = grid().getItem(index); + const item = grid().getItem(index); - if (!item?.data) return; + if (!item?.data) return; - if (!explorer.allowMultiSelect) { - explorer.resetSelectedItems([item.data]); - } else { - // selectoFirstColumn.current = item.column; - // selectoLastColumn.current = item.column; - } + if (!explorer.allowMultiSelect) { + explorer.resetSelectedItems([item.data]); + } else { + // selectoFirstColumn.current = item.column; + // selectoLastColumn.current = item.column; + } - activeItem = item.data; - }} - > - {children} - + activeItem = item.data; + }} + > + {props.children} + + )} + ); }} diff --git a/apps/web-astro/src/Explorer/View/GridView/index.tsx b/apps/web-astro/src/Explorer/View/GridView/index.tsx index 8d4c24bf0..ee61eefe9 100644 --- a/apps/web-astro/src/Explorer/View/GridView/index.tsx +++ b/apps/web-astro/src/Explorer/View/GridView/index.tsx @@ -4,8 +4,8 @@ import { GridViewItem } from './Item'; export const GridView = () => { return ( - {({ item, selected, cut }) => ( - + {(props) => ( + )} ); diff --git a/apps/web-astro/src/Explorer/View/RenameItemText.tsx b/apps/web-astro/src/Explorer/View/RenameItemText.tsx index 080a4d4bb..ef6242d04 100644 --- a/apps/web-astro/src/Explorer/View/RenameItemText.tsx +++ b/apps/web-astro/src/Explorer/View/RenameItemText.tsx @@ -1,7 +1,8 @@ import clsx from 'clsx'; import { JSX } from 'solid-js'; -import { getExplorerItemData, useRspcLibraryContext, type ExplorerItem } from '@sd/client'; +import { getExplorerItemData, type ExplorerItem } from '@sd/client'; +import { useRspcLibraryContext } from '../../rspc'; import { useExplorerContext } from '../Context'; // import { toast } from '@sd/ui'; // import { useIsDark } from '~/hooks'; diff --git a/apps/web-astro/src/Explorer/useExplorer.ts b/apps/web-astro/src/Explorer/createExplorer.ts similarity index 70% rename from apps/web-astro/src/Explorer/useExplorer.ts rename to apps/web-astro/src/Explorer/createExplorer.ts index dac9cf7b1..b0543b91d 100644 --- a/apps/web-astro/src/Explorer/useExplorer.ts +++ b/apps/web-astro/src/Explorer/createExplorer.ts @@ -1,7 +1,6 @@ import { ReactiveSet } from '@solid-primitives/set'; import { type InfiniteQueryObserverResult } from '@tanstack/solid-query'; import { createMemo, createSignal, type Accessor, type ComponentProps } from 'solid-js'; -import { useDebouncedCallback } from 'use-debounce'; import { z } from 'zod'; import type { ExplorerItem, @@ -36,7 +35,7 @@ export type ExplorerParent = }; export interface UseExplorerProps { - items: () => ExplorerItem[] | null; + items: Accessor; count?: number; parent?: ExplorerParent; loadMore?: () => void; @@ -52,7 +51,7 @@ export interface UseExplorerProps { * @defaultValue `true` */ selectable?: boolean; - settings: ReturnType>; + settings: ReturnType>; /** * @defaultValue `true` */ @@ -64,38 +63,38 @@ export interface UseExplorerProps { * Controls top-level config and state for the explorer. * View- and inspector-specific state is not handled here. */ -export function createExplorer({ - settings, - layouts, - ...props -}: UseExplorerProps) { +export function createExplorer(props: UseExplorerProps) { const [scrollRef, setScrollRef] = createSignal(null); - return { + return createMemo(() => ({ + // Provided values + ...props, // Default values allowMultiSelect: true, selectable: true, scrollRef, setScrollRef, - count: props.items?.length, + get count() { + return props.items()?.length; + }, showPathBar: true, layouts: { grid: true, list: true, media: true, - ...layouts + ...props.layouts }, - ...settings, - // Provided values - ...props, + ...props.settings, // Selected items - ...createSelectedItems(() => props.items) - }; + ...createSelectedItems(() => props.items() ?? []) + })); } -export type CreateExplorer = ReturnType>; +export type CreateExplorer = ReturnType< + ReturnType> +>; -export function useExplorerSettings({ +export function createExplorerSettings({ settings, onSettingsChanged, orderingKeys, @@ -110,40 +109,40 @@ export function useExplorerSettings({ }) { // const [store] = useState(() => proxy(settings)); - const updateSettings = useDebouncedCallback( - (settings: ExplorerSettings, location: Location) => { - onSettingsChanged?.(settings, location); - }, - 500 - ); + // const updateSettings = useDebouncedCallback( + // (settings: ExplorerSettings, location: Location) => { + // onSettingsChanged?.(settings, location); + // }, + // 500 + // ); - useEffect(() => updateSettings.flush(), [location, updateSettings]); + // useEffect(() => updateSettings.flush(), [location, updateSettings]); - useEffect(() => { - if (updateSettings.isPending()) return; - Object.assign(store, settings); - }, [settings, store, updateSettings]); + // useEffect(() => { + // if (updateSettings.isPending()) return; + // Object.assign(store, settings); + // }, [settings, store, updateSettings]); - useEffect(() => { - if (!onSettingsChanged || !location) return; - const unsubscribe = subscribe(store, () => { - updateSettings(snapshot(store) as ExplorerSettings, location); - }); - return () => unsubscribe(); - }, [store, updateSettings, location, onSettingsChanged]); + // (() => { + // if (!onSettingsChanged || !location) return; + // const unsubscribe = subscribe(store, () => { + // updateSettings(snapshot(store) as ExplorerSettings, location); + // }); + // return () => unsubscribe(); + // }, [store, updateSettings, location, onSettingsChanged]); return { - useSettingsSnapshot: () => useSnapshot(store), - settingsStore: store, + // useSettingsSnapshot: () => useSnapshot(store), + // settingsStore: store, orderingKeys }; } -export type UseExplorerSettings = ReturnType< - typeof useExplorerSettings +export type CreateExplorerSettings = ReturnType< + typeof createExplorerSettings >; -function createSelectedItems(items: () => ExplorerItem[] | null) { +function createSelectedItems(items: Accessor) { // Doing pointer lookups for hashes is a bit faster than assembling a bunch of strings // WeakMap ensures that ExplorerItems aren't held onto after they're evicted from cache const itemHashesWeakMap = new WeakMap(); @@ -153,7 +152,7 @@ function createSelectedItems(items: () => ExplorerItem[] | null) { const selectedItemHashes = new ReactiveSet(); const itemsMap = createMemo(() => - (items() ?? []).reduce((items, item) => { + items().reduce((items, item) => { const hash = itemHashesWeakMap.get(item) ?? uniqueId(item); itemHashesWeakMap.set(item, hash); items.set(hash, item); diff --git a/apps/web-astro/src/Explorer/index.tsx b/apps/web-astro/src/Explorer/index.tsx index 5990aeb55..d97ca5e12 100644 --- a/apps/web-astro/src/Explorer/index.tsx +++ b/apps/web-astro/src/Explorer/index.tsx @@ -8,7 +8,7 @@ export function Explorer() { return (
= { + arg: TArg; + // explorerSettings: CreateExplorerSettings; +} & Pick>, 'enabled' | 'suspense'>; diff --git a/apps/web-astro/src/Explorer/queries/createExplorerQuery.ts b/apps/web-astro/src/Explorer/queries/createExplorerQuery.ts new file mode 100644 index 000000000..8af3337db --- /dev/null +++ b/apps/web-astro/src/Explorer/queries/createExplorerQuery.ts @@ -0,0 +1,19 @@ +import { type CreateInfiniteQueryResult, type CreateQueryResult } from '@tanstack/solid-query'; +import { createMemo } from 'solid-js'; + +export function createExplorerQuery( + query: CreateInfiniteQueryResult, + count: CreateQueryResult +) { + const items = createMemo(() => query.data?.pages.flatMap((d) => d) ?? null); + + const loadMore = () => { + if (query.hasNextPage && !query.isFetchingNextPage) { + query.fetchNextPage.call(undefined); + } + }; + + return { query, items, loadMore, count: count.data }; +} + +export type CreateExplorerQuery = ReturnType>; diff --git a/apps/web-astro/src/Explorer/queries/createPathsExplorerQuery.ts b/apps/web-astro/src/Explorer/queries/createPathsExplorerQuery.ts new file mode 100644 index 000000000..946ba74aa --- /dev/null +++ b/apps/web-astro/src/Explorer/queries/createPathsExplorerQuery.ts @@ -0,0 +1,18 @@ +import { type FilePathSearchArgs } from '@sd/client'; + +import { createLibraryQuery } from '../../rspc'; +import { createExplorerQuery } from './createExplorerQuery'; +import { createPathsInfiniteQuery } from './createPathsInfiniteQuery'; + +export function createPathsExplorerQuery(props: { + arg: FilePathSearchArgs; + // explorerSettings: CreateExplorerSettings; +}) { + const query = createPathsInfiniteQuery(); + + const count = createLibraryQuery(() => ['search.pathsCount', { filters: props.arg.filters }], { + enabled: query.isSuccess + }); + + return createExplorerQuery(query, count); +} diff --git a/apps/web-astro/src/Explorer/queries/createPathsInfiniteQuery.ts b/apps/web-astro/src/Explorer/queries/createPathsInfiniteQuery.ts new file mode 100644 index 000000000..ad57860ff --- /dev/null +++ b/apps/web-astro/src/Explorer/queries/createPathsInfiniteQuery.ts @@ -0,0 +1,33 @@ +import { createInfiniteQuery } from '@tanstack/solid-query'; +import type { FilePathSearchArgs } from '@sd/client'; + +import { useCache as useCacheContext } from '../../cache'; +import { useRspcLibraryContext } from '../../rspc'; +import { useLibraryContext } from '../../useLibraryContext'; + +export function createPathsInfiniteQuery() { + // props: CreateExplorerInfiniteQueryArgs + const ctx = useRspcLibraryContext(); + + const cache = useCacheContext(); + const library = useLibraryContext(); + + return createInfiniteQuery({ + queryKey: () => + [ + 'search.paths', + { library_id: library.library.uuid, arg: {} as FilePathSearchArgs } + ] as const, + queryFn: async ({ queryKey: [_, { arg }] }) => { + const result = await ctx!.client.query(['search.paths', arg]); + + cache.setNodes(result.nodes); + return cache.useCache(result.items); + } + // getNextPageParam: (lastPage) => { + // if (arg.take === null || arg.take === undefined) return undefined; + // if (lastPage.items.length < arg.take) return undefined; + // else return lastPage.nodes[arg.take - 1]; + // } + }); +} diff --git a/apps/web-astro/src/Page.tsx b/apps/web-astro/src/Page.tsx index 3b3013139..dcf005ca4 100644 --- a/apps/web-astro/src/Page.tsx +++ b/apps/web-astro/src/Page.tsx @@ -1,32 +1,143 @@ -import { createInfiniteQuery } from '@tanstack/solid-query'; +import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'; +import { createComputed, createMemo, Show } from 'solid-js'; +import { type FilePathOrder, type Location } from '@sd/client'; +import { cacheCtx, createCache, useCache } from './cache'; import { Explorer } from './Explorer'; -import { createExplorer } from './Explorer/useExplorer'; -import { createLibraryQuery, useRspcLibraryContext } from './rspc'; +import { ExplorerContextProvider } from './Explorer/Context'; +import { createExplorer } from './Explorer/createExplorer'; +import { createPathsExplorerQuery } from './Explorer/queries/createPathsExplorerQuery'; +import { filePathOrderingKeysSchema } from './Explorer/store'; +import { PlatformProvider, type Platform } from './Platform'; +import { createLibraryQuery, RspcProvider, useRspcLibraryContext } from './rspc'; +import { ClientContextProvider, useClientContext } from './useClientContext'; +import { LibraryContextProvider } from './useLibraryContext'; +import { useTheme } from './useTheme'; + +import './style.scss'; + +export const LIBRARY_UUID = 'f47c74cb-119d-42bf-b63d-87e2f9a2e3ba'; + +const spacedriveURL = (() => { + const currentURL = new URL(window.location.href); + if (import.meta.env.VITE_SDSERVER_ORIGIN) { + currentURL.host = import.meta.env.VITE_SDSERVER_ORIGIN; + } else if (import.meta.env.DEV) { + currentURL.host = 'localhost:8080'; + } + return `${currentURL.origin}/spacedrive`; +})(); + +const platform: Platform = { + platform: 'web', + getThumbnailUrlByThumbKey: (keyParts) => + `${spacedriveURL}/thumbnail/${keyParts.map((i) => encodeURIComponent(i)).join('/')}.webp`, + getFileUrl: (libraryId, locationLocalId, filePathId) => + `${spacedriveURL}/file/${encodeURIComponent(libraryId)}/${encodeURIComponent( + locationLocalId + )}/${encodeURIComponent(filePathId)}`, + getFileUrlByPath: (path) => `${spacedriveURL}/local-file-by-path/${encodeURIComponent(path)}`, + openLink: (url) => window.open(url, '_blank')?.focus(), + confirm: (message, cb) => cb(window.confirm(message)), + // auth: { + // start(url) { + // return window.open(url); + // }, + // finish(win: Window | null) { + // win?.close(); + // } + // }, + landingApiOrigin: 'https://spacedrive.com' +}; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + ...(import.meta.env.VITE_SD_DEMO_MODE && { + refetchOnWindowFocus: false, + staleTime: Infinity, + cacheTime: Infinity, + networkMode: 'offlineFirst', + enabled: false + }), + networkMode: 'always' + }, + mutations: { + networkMode: 'always' + } + // TODO: Mutations can't be globally disable which is annoying! + } +}); + +const cache = createCache(); export function Page() { - const { library } = useLibraryContext(); + return ( + + + + + + + + + + + + ); +} + +function ClientInner() { + useTheme(); + + const clientCtx = useClientContext(); + + return ( + + {(library) => ( +
+ + + +
+ )} +
+ ); +} + +function Wrapper() { + const cache = useCache(); + const locationQuery = createLibraryQuery(() => ['locations.get', 1]); + + createComputed(() => cache.setNodes(locationQuery.data?.nodes ?? [])); + const location = createMemo(() => cache.useCache(locationQuery.data?.item)); + + return {(location) => }; +} + +function Inner(props: { location: Location }) { + // const { library } = useLibraryContext(); const ctx = useRspcLibraryContext(); - const query = createInfiniteQuery({ - queryKey: ['search.paths', { library_id: library.uuid, arg }] as const, - queryFn: async ({ queryKey: [_, { arg }] }) => { - const result = await ctx!.client.query(['search.paths', arg]); - return result; - }, - getNextPageParam: (lastPage) => { - if (arg.take === null || arg.take === undefined) return undefined; - if (lastPage.items.length < arg.take) return undefined; - else return lastPage.nodes[arg.take - 1]; - }, - ...args + const paths = createPathsExplorerQuery({ arg: { take: 100 } }); + + const explorer = createExplorer({ + ...paths, + settings: { orderingKeys: filePathOrderingKeysSchema }, + isFetchingNextPage: paths.query.isFetchingNextPage, + parent: { + type: 'Location', + get location() { + return props.location; + } + } }); - const count = createLibraryQuery(['search.pathsCount', { filters: props.arg.filters }], { - enabled: query.isSuccess - }); - - const explorer = createExplorer(); - - return ; + return ( +
+ + + +
+ ); } diff --git a/apps/web-astro/src/Platform.tsx b/apps/web-astro/src/Platform.tsx new file mode 100644 index 000000000..594440df9 --- /dev/null +++ b/apps/web-astro/src/Platform.tsx @@ -0,0 +1,86 @@ +import { createContext, useContext, type ParentProps } from 'solid-js'; + +// import { auth } from '@sd/client'; + +export type OperatingSystem = 'browser' | 'linux' | 'macOS' | 'windows' | 'unknown'; + +// Platform represents the underlying native layer the app is running on. +// This could be Tauri or web. +export type Platform = { + platform: 'web' | 'tauri'; // This represents the specific platform implementation + getThumbnailUrlByThumbKey: (thumbKey: string[]) => string; + getFileUrl: (libraryId: string, locationLocalId: number, filePathId: number) => string; + getFileUrlByPath: (path: string) => string; + openLink: (url: string) => void; + // Tauri patches `window.confirm` to return `Promise` not `bool` + confirm(msg: string, cb: (result: boolean) => void): void; + getOs?(): Promise; + openDirectoryPickerDialog?(opts?: { title?: string; multiple: false }): Promise; + openDirectoryPickerDialog?(opts?: { + title?: string; + multiple?: boolean; + }): Promise; + openFilePickerDialog?(): Promise; + saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise; + showDevtools?(): void; + openPath?(path: string): void; + openLogsDir?(): void; + userHomeDir?(): Promise; + // Opens a file path with a given ID + openFilePaths?(library: string, ids: number[]): any; + openEphemeralFiles?(paths: string[]): any; + revealItems?( + library: string, + items: ( + | { Location: { id: number } } + | { FilePath: { id: number } } + | { Ephemeral: { path: string } } + )[] + ): Promise; + requestFdaMacos?(): void; + getFilePathOpenWithApps?(library: string, ids: number[]): Promise; + reloadWebview?(): Promise; + getEphemeralFilesOpenWithApps?(paths: string[]): Promise; + openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): Promise; + openEphemeralFileWith?(pathsAndUrls: [string, string][]): Promise; + refreshMenuBar?(): Promise; + setMenuBarItemState?(id: string, enabled: boolean): Promise; + lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any; + updater?: { + useSnapshot: () => UpdateStore; + checkForUpdate(): Promise; + installUpdate(): Promise; + runJustUpdatedCheck(onViewChangelog: () => void): Promise; + }; + // auth: auth.ProviderConfig; + landingApiOrigin: string; +}; + +export type Update = { version: string; body: string | null }; +export type UpdateStore = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'error' } + | { status: 'updateAvailable'; update: Update } + | { status: 'noUpdateAvailable' } + | { status: 'installing' }; + +// Keep this private and use through helpers below +const context = createContext(undefined!); + +// is a hook which allows you to fetch information about the current platform from the React context. +export function usePlatform(): Platform { + const ctx = useContext(context); + if (!ctx) + throw new Error( + "The 'PlatformProvider' has not been mounted above the current 'usePlatform' call." + ); + + return ctx; +} + +// provides the platform context to the rest of the app through React context. +// Mount it near the top of your component tree. +export function PlatformProvider(props: ParentProps<{ platform: Platform }>) { + return {props.children}; +} diff --git a/apps/web-astro/src/Wrapper.tsx b/apps/web-astro/src/Wrapper.tsx new file mode 100644 index 000000000..73a2b0d58 --- /dev/null +++ b/apps/web-astro/src/Wrapper.tsx @@ -0,0 +1,4 @@ +import './patches'; +import '@sd/ui/style/style.scss'; + +export * from './Page'; diff --git a/apps/web-astro/src/cache.tsx b/apps/web-astro/src/cache.tsx new file mode 100644 index 000000000..d0d1b0390 --- /dev/null +++ b/apps/web-astro/src/cache.tsx @@ -0,0 +1,86 @@ +import { createContext, useContext, type ParentProps } from 'solid-js'; +import { createStore, type SetStoreFunction, type Store } from 'solid-js/store'; + +export type CacheNode = { + __type: string; + __id: string; +} & Record; + +export const cacheCtx = createContext(); + +export type UseCacheResult = T extends (infer A)[] + ? UseCacheResult[] + : T extends object + ? T extends { '__type': any; '__id': string; '#type': infer U } + ? UseCacheResult + : { [K in keyof T]: UseCacheResult } + : { [K in keyof T]: UseCacheResult }; + +function constructCache(nodes: Store, setNodes: SetStoreFunction) { + return { + getNodes: () => nodes, + getNode: (type: string, id: string) => nodes?.[type]?.[id] as unknown | undefined, + setNodes: (newNodes: CacheNode | CacheNode[]) => { + if (!Array.isArray(newNodes)) newNodes = [newNodes]; + + for (const node of newNodes) { + if (!(typeof node === 'object' || '__type' in node || '__id' in node)) + throw new Error( + `Tried to 'setNodes' but encountered invalid node '${JSON.stringify(node)}'` + ); + + const { __type, __id, ...copy } = { ...node } as any; + + if (!nodes[node.__type]) setNodes(node.__type, {}); + setNodes(node.__type, node.__id, copy); // Be aware this is a merge, not a replace + } + }, + useCache(item: T) { + return restore(nodes, item) as UseCacheResult; + } + }; +} + +export type Cache = ReturnType; +export type Nodes = Record>; + +export function createCache() { + const [nodes, setNodes] = createStore({} as Nodes); + const cache = constructCache(nodes, setNodes); + + return { + ...cache, + Provider: (props: ParentProps) => ( + {props.children} + ) + }; +} + +export function useCache() { + const c = useContext(cacheCtx); + if (!c) throw new Error('Did you forget to mount `cache.Provider`?'); + return c; +} + +function restore(nodes: Store, item: unknown): unknown { + if (item === undefined || item === null) { + return item; + } else if (Array.isArray(item)) { + return item.map((v) => restore(nodes, v)); + } else if (typeof item === 'object') { + if ('__type' in item && '__id' in item) { + if (typeof item.__type !== 'string') throw new Error('Invalid `__type`'); + if (typeof item.__id !== 'string') throw new Error('Invalid `__id`'); + const result = nodes?.[item.__type]?.[item.__id]; + if (!result) + throw new Error(`Missing node for id '${item.__id}' of type '${item.__type}'`); + return result; + } + + return Object.fromEntries( + Object.entries(item).map(([key, value]) => [key, restore(nodes, value)]) + ); + } + + return item; +} diff --git a/apps/web-astro/src/client.ts b/apps/web-astro/src/client.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/web-astro/src/pages/index.astro b/apps/web-astro/src/pages/index.astro index b7260a606..1a11e9a35 100644 --- a/apps/web-astro/src/pages/index.astro +++ b/apps/web-astro/src/pages/index.astro @@ -1,5 +1,5 @@ --- -import { Page } from "../Page"; +import { Page } from "../Wrapper"; --- diff --git a/apps/web-astro/src/patches.ts b/apps/web-astro/src/patches.ts new file mode 100644 index 000000000..c8b0f4e5e --- /dev/null +++ b/apps/web-astro/src/patches.ts @@ -0,0 +1,22 @@ +import { AlphaRSPCError, initRspc, wsBatchLink } from '@rspc/client/v2'; + +globalThis.isDev = import.meta.env.DEV; +globalThis.rspcLinks = [ + // TODO + // loggerLink({ + // enabled: () => getDebugState().rspcLogger + // }), + wsBatchLink({ + url: (() => { + const currentURL = new URL(window.location.href); + currentURL.protocol = currentURL.protocol === 'https:' ? 'wss:' : 'ws:'; + if (import.meta.env.VITE_SDSERVER_ORIGIN) { + currentURL.host = import.meta.env.VITE_SDSERVER_ORIGIN; + } else if (import.meta.env.DEV) { + currentURL.host = 'localhost:8080'; + } + currentURL.pathname = 'rspc/ws'; + return currentURL.href; + })() + }) +]; diff --git a/apps/web-astro/src/rspc.tsx b/apps/web-astro/src/rspc.tsx index a4518937d..1ea069911 100644 --- a/apps/web-astro/src/rspc.tsx +++ b/apps/web-astro/src/rspc.tsx @@ -1,10 +1,12 @@ import { type ProcedureDef } from '@rspc/client'; -import { AlphaRSPCError, initRspc } from '@rspc/client/v2'; +import { AlphaRSPCError, initRspc, wsBatchLink } from '@rspc/client/v2'; import { createReactQueryHooks, type Context } from '@rspc/solid'; import { QueryClient } from '@tanstack/react-query'; import { createContext, useContext, type ParentProps } from 'solid-js'; import { match, P } from 'ts-pattern'; -import { currentLibraryCache, type LibraryArgs, type Procedures } from '@sd/client'; +import { type LibraryArgs, type Procedures } from '@sd/client'; + +import { currentLibraryCache } from './useClientContext'; type NonLibraryProcedure = | Exclude }> @@ -71,11 +73,14 @@ const libraryHooks = createReactQueryHooks(libraryClient, }); // TODO: Allow both hooks to use a unified context -> Right now they override each others local state -export function RspcProvider({ queryClient, children }: ParentProps<{ queryClient: QueryClient }>) { +export function RspcProvider(props: ParentProps<{ queryClient: QueryClient }>) { return ( - - - {children as any} + + + {props.children} ); diff --git a/apps/web-astro/src/style.scss b/apps/web-astro/src/style.scss new file mode 100644 index 000000000..c4245383e --- /dev/null +++ b/apps/web-astro/src/style.scss @@ -0,0 +1,384 @@ +a, +button { + cursor: default !important; +} + +body { + -webkit-user-select: none; + // font-family: 'InterVariable', sans-serif; +} + +.app-background { + @apply bg-app; +} + +.frame::before { + @apply bg-app-frame; + content: ''; + pointer-events: none; + user-select: none; + position: absolute; + inset: 0px; + border-radius: inherit; + padding: 1px; + mask: + linear-gradient(black, black) content-box content-box, + linear-gradient(black, black); + mask-composite: xor; + z-index: 9999; +} + +.has-blur-effects { + .app-background { + // adjust macOS blur intensity here + // @apply bg-app/[0.88]; + @apply bg-app; + } +} + +.no-scrollbar { + -ms-overflow-style: none; /* for Internet Explorer, Edge */ + scrollbar-width: none; /* for Firefox */ + overflow-y: scroll; +} + +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +/*Tooltip*/ + +.TooltipContent { + animation-duration: 0.6s; + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} +.TooltipContent[data-side='top'] { + animation-name: slideUp; +} +.TooltipContent[data-side='bottom'] { + animation-name: slideDown; +} + +.TooltipContent[data-side='right'] { + animation-name: slideRight; + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes slideRight { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.custom-scroll { + -ms-overflow-style: none; /* for Internet Explorer, Edge */ + scrollbar-width: none; /* for Firefox */ + overflow-y: scroll; +} + +::-webkit-scrollbar-corner { + background-color: transparent; +} + +.explorer-scroll { + &::-webkit-scrollbar { + height: 6px; + width: 6px; + } + &::-webkit-scrollbar-track { + @apply rounded-[6px] bg-transparent; + margin-top: var(--scrollbar-margin-top); + margin-bottom: var(--scrollbar-margin-bottom); + } + &::-webkit-scrollbar-thumb { + @apply rounded-[6px] bg-app-explorerScrollbar; + } +} + +.default-scroll { + &::-webkit-scrollbar { + height: 6px; + width: 8px; + } + &::-webkit-scrollbar-track { + @apply rounded-[6px] bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply rounded-[6px] bg-app-box; + } +} +.page-scroll { + &::-webkit-scrollbar { + height: 6px; + width: 8px; + } + &::-webkit-scrollbar-track { + @apply rounded-[6px] bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply rounded-[6px] bg-app-box; + } +} +.topbar-page-scroll { + &::-webkit-scrollbar { + height: 6px; + width: 8px; + } + &::-webkit-scrollbar-track { + @apply mt-[46px] rounded-[6px] bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply rounded-[6px] bg-app-box; + } +} +.quick-preview-images-scroll { + &::-webkit-scrollbar { + height: 6px; + width: 8px; + } + &::-webkit-scrollbar-track { + @apply rounded-[6px] bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply rounded-[6px] bg-white/20; + } +} +.job-manager-scroll { + &::-webkit-scrollbar { + height: 6px; + width: 8px; + } + &::-webkit-scrollbar-track { + @apply my-[2px] rounded-[6px] bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply rounded-[6px] bg-app-shade/20; + } +} +.inspector-scroll { + // overflow: overlay; + &::-webkit-scrollbar { + height: 6px; + width: 5px; + } + &::-webkit-scrollbar-track { + @apply my-[8px] bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply rounded-[6px] bg-app/70 opacity-0; + } + &:hover { + &::-webkit-scrollbar-thumb { + @apply opacity-100; + } + } +} + +.overlay-scroll { + // overflow: overlay; + &::-webkit-scrollbar { + height: 6px; + width: 5px; + } + &::-webkit-scrollbar-track { + @apply my-[5px] bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply w-[5px] rounded-[6px] bg-black/70 opacity-0; + } + &:hover { + &::-webkit-scrollbar-thumb { + @apply opacity-100; + } + } +} +.textviewer-scroll { + &::-webkit-scrollbar { + height: 6px; + width: 8px; + } + &::-webkit-scrollbar-track { + @apply bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply rounded-md bg-app-box; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@-webkit-keyframes slide-top { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + -webkit-transform: translateY(-50px); + transform: translateY(-50px); + } +} +@keyframes slide-top { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + -webkit-transform: translateY(-50px); + transform: translateY(-50px); + } +} + +.dialog-overlay[data-state='open'] { + animation: fadeIn 200ms ease-out forwards; +} +.dialog-overlay[data-state='closed'] { + animation: fadeIn 200ms ease-out forwards; +} +.dialog-content[data-state='open'] { + -webkit-animation: slide-top 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) both; + animation: slide-top 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) both; +} +.dialog-content[data-state='closed'] { + animation: bounceDown 100ms ease-in forwards; +} + +.picker { + position: relative; +} + +.swatch { + width: 28px; + height: 28px; + border-radius: 8px; + border: 3px solid #fff; + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.1), + inset 0 0 0 1px rgba(0, 0, 0, 0.1); + cursor: pointer; +} + +.popover { + position: absolute; + top: calc(100% + 2px); + left: 0; + border-radius: 9px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.react-colorful__saturation { + border-radius: 4px !important; +} + +.react-colorful__saturation-pointer { + width: 12px !important; + height: 12px !important; +} + +.react-colorful__hue, +.react-colorful__alpha { + margin-top: 12px !important; + height: 8px !important; + border-radius: 4px !important; +} + +.react-colorful__hue-pointer, +.react-colorful__alpha-pointer { + height: 18px !important; + width: 8px !important; + border-radius: 3px !important; +} + +.selecto-selection { + @apply rounded; + border-color: hsla(var(--color-accent)); + background-color: hsla(var(--color-accent), 0.2) !important; + z-index: 10 !important; +} + +.indeterminate-progress-bar { + /* Rounded border */ + border-radius: 9999px; + + /* Size */ + height: 4px; + + position: relative; + overflow: hidden; +} + +.indeterminate-progress-bar__progress { + /* Rounded border */ + border-radius: 9999px; + + /* Absolute position */ + position: absolute; + bottom: 0; + top: 0; + width: 50%; + + /* Move the bar infinitely */ + animation-duration: 2s; + animation-iteration-count: infinite; + animation-name: indeterminate-progress-bar; +} + +@keyframes indeterminate-progress-bar { + from { + left: -50%; + } + to { + left: 100%; + } +} + +.react-slidedown.search-options-slide { + transition-duration: 300ms; + transition-timing-function: cubic-bezier(0.85, 0, 0.15, 1); +} diff --git a/apps/web-astro/src/useClientContext.tsx b/apps/web-astro/src/useClientContext.tsx new file mode 100644 index 000000000..307ac86f3 --- /dev/null +++ b/apps/web-astro/src/useClientContext.tsx @@ -0,0 +1,121 @@ +import { + createContext, + createEffect, + createMemo, + useContext, + type Accessor, + type ParentProps +} from 'solid-js'; +import { valtioPersist, type LibraryConfigWrapped } from '@sd/client'; + +import { useCache } from './cache'; +import { createBridgeQuery, nonLibraryClient } from './rspc'; + +// The name of the localStorage key for caching library data +const libraryCacheLocalStorageKey = 'sd-library-list2'; // `2` is because the format of this underwent a breaking change when introducing normalised caching + +export const useCachedLibraries = () => { + const query = createBridgeQuery(() => ['library.list'] as const, { + keepPreviousData: true + // initialData: () => { + // const cachedData = localStorage.getItem(libraryCacheLocalStorageKey); + + // if (cachedData) { + // // If we fail to load cached data, it's fine + // try { + // return JSON.parse(cachedData); + // } catch (e) { + // console.error("Error loading cached 'sd-library-list' data", e); + // } + // } + + // return undefined; + // }, + // onSuccess: (data) => localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data)) + }); + const cache = useCache(); + createMemo(() => cache.setNodes(query.data?.nodes ?? [])); + const libraries = createMemo(() => cache.useCache(query.data?.items ?? [])); + + return { + ...query, + get data() { + return libraries(); + } + }; +}; + +// export async function getCachedLibraries(cache: NormalisedCache) { +// const cachedData = localStorage.getItem(libraryCacheLocalStorageKey); + +// if (cachedData) { +// // If we fail to load cached data, it's fine +// try { +// const data = JSON.parse(cachedData); +// cache.withNodes(data.nodes); +// return cache.withCache(data.items) as LibraryConfigWrapped[]; +// } catch (e) { +// console.error("Error loading cached 'sd-library-list' data", e); +// } +// } + +// const result = await nonLibraryClient.query(['library.list']); +// cache.withNodes(result.nodes); +// const libraries = cache.withCache(result.items); + +// localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(result)); + +// return libraries; +// } + +export interface ClientContext { + currentLibraryId: string | null; + libraries: ReturnType; + library: Accessor; +} + +const ClientContext = createContext(null!); + +interface ClientContextProviderProps extends ParentProps { + currentLibraryId: string | null; +} + +export const ClientContextProvider = (props: ClientContextProviderProps) => { + const libraries = useCachedLibraries(); + + createEffect(() => { + currentLibraryCache.id = props.currentLibraryId; + }); + + const library = createMemo( + () => + (libraries.data && libraries.data.find((l) => l.uuid === props.currentLibraryId)) || + null + ); + + return ( + + {props.children} + + ); +}; + +export const useClientContext = () => { + const ctx = useContext(ClientContext); + + if (ctx === undefined) throw new Error("'ClientContextProvider' not mounted"); + + return ctx; +}; + +export const useCurrentLibraryId = () => useClientContext().currentLibraryId; + +export const currentLibraryCache = valtioPersist('sd-current-library', { + id: null as string | null +}); diff --git a/apps/web-astro/src/useLibraryContext.tsx b/apps/web-astro/src/useLibraryContext.tsx new file mode 100644 index 000000000..3bcc91078 --- /dev/null +++ b/apps/web-astro/src/useLibraryContext.tsx @@ -0,0 +1,47 @@ +import { createContext, useContext, type ParentProps } from 'solid-js'; +import type { LibraryConfigWrapped } from '@sd/client'; + +import { createBridgeSubscription } from './rspc'; +import { useClientContext, type ClientContext } from './useClientContext'; + +export interface LibraryContext { + library: LibraryConfigWrapped; + libraries: ClientContext['libraries']; +} + +const LibraryContext = createContext(null!); + +interface LibraryContextProviderProps extends ParentProps { + library: LibraryConfigWrapped; +} + +export const LibraryContextProvider = (props: LibraryContextProviderProps) => { + const { libraries } = useClientContext(); + + // We put this into context because each hook creates a new subscription which means we get duplicate events from the backend if we don't do this + // TODO: This should probs be a library subscription - https://linear.app/spacedriveapp/issue/ENG-724/locationsonline-should-be-a-library-not-a-bridge-subscription + createBridgeSubscription(() => ['locations.online'] as const, { + onData: (d) => { + // getLibraryStore().onlineLocations = d; + } + }); + + return ( + + {props.children} + + ); +}; + +export const useLibraryContext = () => { + const ctx = useContext(LibraryContext); + + if (ctx === undefined) throw new Error("'LibraryContextProvider' not mounted"); + + return ctx; +}; + +// export function useOnlineLocations() { +// const { onlineLocations } = useLibraryStore(); +// return onlineLocations; +// } diff --git a/apps/web-astro/src/useTheme.ts b/apps/web-astro/src/useTheme.ts new file mode 100644 index 000000000..f6dd4e4d4 --- /dev/null +++ b/apps/web-astro/src/useTheme.ts @@ -0,0 +1,64 @@ +import { createEffect } from 'solid-js'; +import { createMutable } from 'solid-js/store'; +import { getThemeStore, useThemeStore } from '@sd/client'; + +import { usePlatform } from './Platform'; + +export const themeStore = createMutable({ + theme: 'dark', + syncThemeWithSystem: false, + hueValue: 235 +}); + +export function useTheme() { + // const themeStore = useThemeStore(); + const platform = usePlatform(); + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)'); + + createEffect(() => { + const handleThemeChange = () => { + if (themeStore.syncThemeWithSystem) { + platform.lockAppTheme?.('Auto'); + if (systemTheme.matches) { + document.documentElement.classList.remove('vanilla-theme'); + document.documentElement.style.setProperty( + '--dark-hue', + getThemeStore().hueValue.toString() + ); + getThemeStore().theme = 'dark'; + } else { + document.documentElement.classList.add('vanilla-theme'); + document.documentElement.style.setProperty( + '--light-hue', + getThemeStore().hueValue.toString() + ); + getThemeStore().theme = 'vanilla'; + } + } else { + if (themeStore.theme === 'dark') { + document.documentElement.classList.remove('vanilla-theme'); + document.documentElement.style.setProperty( + '--dark-hue', + getThemeStore().hueValue.toString() + ); + platform.lockAppTheme?.('Dark'); + } else if (themeStore.theme === 'vanilla') { + document.documentElement.classList.add('vanilla-theme'); + document.documentElement.style.setProperty( + '--light-hue', + getThemeStore().hueValue.toString() + ); + platform.lockAppTheme?.('Light'); + } + } + }; + + handleThemeChange(); + + systemTheme.addEventListener('change', handleThemeChange); + + return () => { + systemTheme.removeEventListener('change', handleThemeChange); + }; + }); +} diff --git a/apps/web-astro/tailwind.config.mjs b/apps/web-astro/tailwind.config.mjs index 19f0614dd..cf1f3e30c 100644 --- a/apps/web-astro/tailwind.config.mjs +++ b/apps/web-astro/tailwind.config.mjs @@ -1,8 +1,3 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], - theme: { - extend: {}, - }, - plugins: [], -} +import tailwindFactory from '@sd/ui/tailwind'; + +export default tailwindFactory('web'); diff --git a/apps/web/astro.config.mjs b/apps/web/astro.config.mjs deleted file mode 100644 index e22bfcbcf..000000000 --- a/apps/web/astro.config.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import react from '@astrojs/react'; -// import solid from '@astrojs/solid-js'; -import tailwind from '@astrojs/tailwind'; -import { defineConfig } from 'astro/config'; -import { mergeConfig } from 'vite'; - -import baseConfig from '../../packages/config/vite'; -import relativeAliasResolver from '../../packages/config/vite/relativeAliasResolver'; - -// https://astro.build/config -export default defineConfig({ - integrations: [ - react({ - // exclude: ['**/*.solid.*'] - }), - // solid({ - // include: ['**/*.solid.*'] - // }), - tailwind() - ], - // server: { - // port: 8002 - // }, - vite: mergeConfig(baseConfig, { - resolve: { - // BE REALLY DAMN CAREFUL MODIFYING THIS: https://github.com/spacedriveapp/spacedrive/pull/1353 - alias: [relativeAliasResolver] - } - }) -}); diff --git a/apps/web/package.json b/apps/web/package.json index b165d3a22..3d86a6b54 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "scripts": { - "dev": "astro dev", + "dev": "vite dev", "build": "vite build", "preview": "vite preview", "test": "VITE_SD_DEMO_MODE=true playwright test", @@ -14,7 +14,7 @@ "@astrojs/solid-js": "^3.0.2", "@astrojs/tailwind": "^5.0.3", "@fontsource/inter": "^4.5.15", - "@rspc/client": "0.0.0-main-45466c86", + "@rspc/client": "0.0.0-main-b8b35d28", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@tanstack/react-query": "^4.36.1", diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx new file mode 100644 index 000000000..2625cd29d --- /dev/null +++ b/apps/web/src/index.tsx @@ -0,0 +1,17 @@ +// WARNING: BE CAREFUL SAVING THIS FILE WITH A FORMATTER ENABLED. The import order is important and goes against prettier's recommendations. +import React, { Suspense } from 'react'; +import ReactDOM from 'react-dom/client'; + +import '@sd/ui/style/style.scss'; +import '~/patches'; + +import { App } from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( + + + + + +); diff --git a/packages/client/package.json b/packages/client/package.json index 5b2571f4a..ec070f24d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -11,8 +11,8 @@ "typecheck": "tsc -b" }, "dependencies": { - "@rspc/client": "0.0.0-main-45466c86", - "@rspc/react": "0.0.0-main-45466c86", + "@rspc/client": "0.0.0-main-b8b35d28", + "@rspc/react": "0.0.0-main-b8b35d28", "@tanstack/react-query": "^4.36.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/packages/config/vite/index.ts b/packages/config/vite/index.ts index 3eea4dcf1..16fc33fbc 100644 --- a/packages/config/vite/index.ts +++ b/packages/config/vite/index.ts @@ -1,4 +1,4 @@ -// import react from '@vitejs/plugin-react'; +import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import { createHtmlPlugin } from 'vite-plugin-html'; // import solid from 'vite-plugin-solid'; @@ -8,12 +8,9 @@ import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ plugins: [ tsconfigPaths(), - // react({ - // exclude: ['**/*.solid.*'] - // }), - // solid({ - // include: ['**/*.solid.*'] - // }), + react({ + exclude: ['**/*.solid.*'] + }), svg({ svgrOptions: { icon: true } }), createHtmlPlugin({ minify: true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b7ed3085..65e9e3d87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,11 +81,11 @@ importers: specifier: ^1.14.0 version: 1.14.0 '@rspc/client': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86 + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28 '@rspc/tauri': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86(@tauri-apps/api@1.5.1) + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28(@tauri-apps/api@1.5.1) '@sd/client': specifier: workspace:* version: link:../../packages/client @@ -350,11 +350,11 @@ importers: specifier: ^6.3.20 version: 6.3.20(@react-navigation/native@6.1.9)(react-native-gesture-handler@2.12.1)(react-native-safe-area-context@4.6.3)(react-native-screens@3.22.1)(react-native@0.72.6)(react@18.2.0) '@rspc/client': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86 + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28 '@rspc/react': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0) + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/react-query@4.36.1)(react@18.2.0) '@sd/assets': specifier: workspace:* version: link:../../packages/assets @@ -576,8 +576,8 @@ importers: specifier: ^4.5.15 version: 4.5.15 '@rspc/client': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86 + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28 '@sd/client': specifier: workspace:* version: link:../../packages/client @@ -670,14 +670,17 @@ importers: specifier: ^5.0.3 version: 5.0.3(astro@4.0.5)(tailwindcss@3.3.6) '@rspc/solid': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7) + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7) '@sd/assets': specifier: workspace:^ version: link:../../packages/assets '@sd/client': specifier: workspace:^ version: link:../../packages/client + '@sd/ui': + specifier: workspace:^ + version: link:../../packages/ui '@solid-primitives/event-listener': specifier: ^2.3.0 version: 2.3.0(solid-js@1.8.7) @@ -940,11 +943,11 @@ importers: packages/client: dependencies: '@rspc/client': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86 + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28 '@rspc/react': - specifier: 0.0.0-main-45466c86 - version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0) + specifier: 0.0.0-main-b8b35d28 + version: 0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/react-query@4.36.1)(react@18.2.0) '@tanstack/react-query': specifier: ^4.36.1 version: 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0) @@ -4681,7 +4684,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.3.3) typescript: 5.3.3 - vite: 5.0.9(less@4.2.0) + vite: 5.0.9(@types/node@18.17.19) /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} @@ -7154,40 +7157,40 @@ packages: requiresBuild: true optional: true - /@rspc/client@0.0.0-main-45466c86: - resolution: {integrity: sha512-1a3+jSJXXcHyoMYrqlb5DModVf5m7S4Y7a6BaUHmkhfXG4rttvthFHNAU1ODMpbI371egEePFhiVR8SnPsXe6Q==} + /@rspc/client@0.0.0-main-b8b35d28: + resolution: {integrity: sha512-wXBZ+KDBzBfXXKr2GWAe/UF+D5jLl1vM7mBTuFJsEV4ihquu2hzxAQBPuBE3j6JC7SIWzhQ+hodzGFUCL04Rsg==} dev: false - /@rspc/react@0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0): - resolution: {integrity: sha512-4b3SBm6KFS0fZ0JwdZPFZeb+ZG/FgXn3Qb4DJga5ioGr11YsDGMgeEyaxfC8dLSHtT5tj8vFqX/a6LoWk0Vqbw==} + /@rspc/react@0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/react-query@4.36.1)(react@18.2.0): + resolution: {integrity: sha512-yjKtZkziLvUI3AyKAdN8l32O3yS2FBqKda24MWDcN/YWjFVw15Enfr9C2bGtuqrZZ/LKabrG67EnKWqGvTWkCQ==} peerDependencies: - '@rspc/client': 0.0.0-main-45466c86 + '@rspc/client': 0.0.0-main-b8b35d28 '@tanstack/react-query': ^4.26.0 react: ^18.2.0 dependencies: - '@rspc/client': 0.0.0-main-45466c86 + '@rspc/client': 0.0.0-main-b8b35d28 '@tanstack/react-query': 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0) react: 18.2.0 dev: false - /@rspc/solid@0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7): - resolution: {integrity: sha512-5rfeivH4I2LrMSx5Igfx2FfLn7F6s5OzVy9VtaJ6C4K/Yh9Y2AufR0yQwEApoUrfexGWbecmmoW+yPbDqaJvpA==} + /@rspc/solid@0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7): + resolution: {integrity: sha512-KjssarTWhoj8chAoESekTfucy/vg/eum+5Ghp1vMfUQWhGQ7f6SOXqeGQefOz0itIBAMCmQn+R03uFm59+GMCQ==} peerDependencies: - '@rspc/client': 0.0.0-main-45466c86 + '@rspc/client': 0.0.0-main-b8b35d28 '@tanstack/solid-query': ^4.6.0 solid-js: ^1.6.11 dependencies: - '@rspc/client': 0.0.0-main-45466c86 + '@rspc/client': 0.0.0-main-b8b35d28 '@tanstack/solid-query': 4.36.1(solid-js@1.8.7) solid-js: 1.8.7 dev: false - /@rspc/tauri@0.0.0-main-45466c86(@tauri-apps/api@1.5.1): - resolution: {integrity: sha512-B+/PcZjxuVTQEtEgBV7UXYwf1uGNbN34+6aqbc5oxxifLDDn0MxVGmEju3Y518sYCfuYaIF8rYDl+oPM9a7NSg==} + /@rspc/tauri@0.0.0-main-b8b35d28(@tauri-apps/api@1.5.1): + resolution: {integrity: sha512-uzBBxsP8ENBUs16j5KXni72WKtVdi37uA/+GA1rf2M/qJvnc8W+slXrI4PXL9CBHnGRbwXk/gM1z9AdadqsFRw==} peerDependencies: '@tauri-apps/api': ^1.2.0 dependencies: - '@rspc/client': 0.0.0-main-45466c86 + '@rspc/client': 0.0.0-main-b8b35d28 '@tauri-apps/api': 1.5.1 dev: false @@ -7834,7 +7837,7 @@ packages: magic-string: 0.30.5 rollup: 3.29.4 typescript: 5.3.3 - vite: 5.0.9(less@4.2.0) + vite: 5.0.9(@types/node@18.17.19) transitivePeerDependencies: - encoding - supports-color @@ -8176,7 +8179,7 @@ packages: react: 18.2.0 react-docgen: 7.0.1 react-dom: 18.2.0(react@18.2.0) - vite: 5.0.9(less@4.2.0) + vite: 5.0.9(@types/node@18.17.19) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -9304,7 +9307,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 5.0.9(less@4.2.0) + vite: 5.0.9(@types/node@18.17.19) transitivePeerDependencies: - supports-color @@ -22537,6 +22540,7 @@ packages: rollup: 4.9.0 optionalDependencies: fsevents: 2.3.3 + dev: true /vite@5.0.9(sass@1.69.5): resolution: {integrity: sha512-wVqMd5kp28QWGgfYPDfrj771VyHTJ4UDlCteLH7bJDGDEamaz5hV8IX6h1brSGgnnyf9lI2RnzXq/JmD0c2wwg==}