mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 11:03:27 +00:00
solid wow
This commit is contained in:
parent
0543216eef
commit
292ae97b5f
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"name": "publish-artifacts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "ncc build index.ts --minify"
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -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..."
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
) : (
|
||||
<TruncatedText text={name} lines={lines} onTruncate={setIsTruncated} />
|
||||
)} */}
|
||||
{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 (
|
||||
<TruncatedText
|
||||
lines={props.lines ?? 2}
|
||||
postfix={ellipsis()}
|
||||
onTruncate={setIsTruncated}
|
||||
>
|
||||
{props.name}
|
||||
</TruncatedText>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
// </Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
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<Array<'left' | 'right'>>([]);
|
||||
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 (
|
||||
<TruncateMarkup lines={props.lines} ellipsis={ellipsis} onTruncate={props.onTruncate}>
|
||||
<div>{props.text}</div>
|
||||
</TruncateMarkup>
|
||||
<div
|
||||
style={{
|
||||
'word-break': 'break-word',
|
||||
...props.style
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Show when={props.prefix}>
|
||||
<div style={{ display: 'inline-block' }}>{props.prefix}</div>
|
||||
</Show>
|
||||
{cutoffChildren()}
|
||||
{/* <Show when={cutoff().length > 0}>{props.postfix}</Show> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<ComponentProps<'img'>, 'ref'> {
|
||||
interface FileThumbProps extends Pick<ComponentProps<'img'>, 'ref' | 'class'> {
|
||||
data: ExplorerItem;
|
||||
loadOriginal?: boolean;
|
||||
size?: number;
|
||||
|
@ -21,7 +30,6 @@ interface FileThumbProps extends Pick<ComponentProps<'img'>, '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<string | undefined>(() => {
|
||||
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 (
|
||||
<Show when={src()}>
|
||||
{(src) => (
|
||||
<Switch>
|
||||
<Match when={thumbType().variant === 'thumbnail'}>
|
||||
<Thumbnail
|
||||
{...props.childProps}
|
||||
ref={props.ref}
|
||||
src={src()}
|
||||
cover={props.cover}
|
||||
onLoad={() => 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
|
||||
}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={thumbType().variant === 'icon'}>
|
||||
<LayeredFileIcon
|
||||
{...props.childProps}
|
||||
ref={props.ref}
|
||||
src={src()}
|
||||
kind={itemData().kind}
|
||||
extension={itemData().extension}
|
||||
onLoad={() => onLoad('icon')}
|
||||
onError={(e) => onError('icon', e)}
|
||||
decoding={props.size ? 'async' : 'sync'}
|
||||
class={getClass()}
|
||||
draggable={false}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div
|
||||
style={{
|
||||
...(props.size
|
||||
? {
|
||||
maxWidth: props.size.toString(),
|
||||
width: props.size.toString(),
|
||||
height: props.size.toString()
|
||||
}
|
||||
: {})
|
||||
}}
|
||||
class={clsx(
|
||||
'relative flex shrink-0 items-center justify-center',
|
||||
// !loaded && 'invisible',
|
||||
!props.size && 'h-full w-full',
|
||||
props.cover && 'overflow-hidden',
|
||||
props.class
|
||||
)}
|
||||
</Show>
|
||||
>
|
||||
<Show when={src()}>
|
||||
{(src) => (
|
||||
<Switch>
|
||||
<Match when={thumbType().variant === 'thumbnail'}>
|
||||
<Thumbnail
|
||||
{...props.childProps}
|
||||
ref={props.ref}
|
||||
src={src()}
|
||||
cover={props.cover}
|
||||
onLoad={() => 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
|
||||
}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={thumbType().variant === 'icon'}>
|
||||
<LayeredFileIcon
|
||||
{...props.childProps}
|
||||
ref={props.ref}
|
||||
src={src()}
|
||||
kind={itemData().kind}
|
||||
extension={itemData().extension}
|
||||
onLoad={() => onLoad('icon')}
|
||||
onError={(e) => onError('icon', e)}
|
||||
decoding={props.size ? 'async' : 'sync'}
|
||||
class={getClass()}
|
||||
draggable={false}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -180,7 +210,14 @@ function Thumbnail(props: ThumbnailProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<img ref={setRef} draggable={false} />
|
||||
<img
|
||||
{...props}
|
||||
class={props.class}
|
||||
style={props.style}
|
||||
src={props.src}
|
||||
ref={setRef}
|
||||
draggable={false}
|
||||
/>
|
||||
{(props.cover || (size.width && size.width > 80)) && props.extension && (
|
||||
<div
|
||||
style={{
|
||||
|
|
|
@ -1,84 +1,145 @@
|
|||
// import getLineHeight from 'line-height';
|
||||
// import * as Solid from 'solid-js';
|
||||
import { createResizeObserver } from '@solid-primitives/resize-observer';
|
||||
import getLineHeight from 'line-height';
|
||||
import * as Solid from 'solid-js';
|
||||
|
||||
// export interface TruncateMarkupProps {
|
||||
// lines: number;
|
||||
// ellipsis?: Solid.JSX.Element | ((element: Solid.JSX.Element) => 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<TruncateMarkupProps>) {
|
||||
const [text, setText] = Solid.createSignal('');
|
||||
|
||||
// let el: HTMLDivElement;
|
||||
const children = Solid.children(() => props.children);
|
||||
|
||||
// Solid.onMount(() => {
|
||||
// // if (!isValid) return;
|
||||
const [ref, setRef] = Solid.createSignal<HTMLElement | null>(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<SplitDirection>, 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<SplitDirection>, 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<SplitDirection>,
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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 <TruncateMarkup.Atom /> 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 <div>foo {ellipsis}</div>
|
||||
// //
|
||||
// // Example:
|
||||
// // <TruncateMarkup lines={1}>
|
||||
// // <div>
|
||||
// // foo
|
||||
// // <div id="lvl2">bar</div>
|
||||
// // </div>
|
||||
// // </TruncateMarkup>
|
||||
// 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 <TruncateMarkup.Atom /> 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<TruncateMarkupProps> {
|
||||
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 <div>foo {ellipsis}</div>
|
||||
//
|
||||
// Example:
|
||||
// <TruncateMarkup lines={1}>
|
||||
// <div>
|
||||
// foo
|
||||
// <div id="lvl2">bar</div>
|
||||
// </div>
|
||||
// </TruncateMarkup>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
|
@ -121,8 +140,8 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
|
|||
style={{
|
||||
...props.style,
|
||||
position: 'relative',
|
||||
width: width.toString(),
|
||||
height: height.toString()
|
||||
width: `${width()}px`,
|
||||
height: `${height()}px`
|
||||
}}
|
||||
>
|
||||
<Solid.Show when={internalWidth() > 0 || internalHeight() > 0}>
|
||||
|
@ -142,47 +161,61 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
|
|||
{(virtualRow) => (
|
||||
<Solid.For each={columnVirtualizer.getVirtualItems()}>
|
||||
{(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 (
|
||||
<div
|
||||
data-index={index}
|
||||
style={{
|
||||
'position': 'absolute',
|
||||
'top': 0,
|
||||
'left': 0,
|
||||
'width': `${virtualColumn.size}px`,
|
||||
'height': `${virtualRow.size}px`,
|
||||
'transform': `translateX(${
|
||||
virtualColumn.start
|
||||
}px) translateY(${
|
||||
virtualRow.start -
|
||||
rowVirtualizer.options.scrollMargin
|
||||
}px)`,
|
||||
'padding-left':
|
||||
virtualColumn.index !== 0
|
||||
? grid().gap.x.toString()
|
||||
: 0,
|
||||
'padding-top':
|
||||
virtualRow.index !== 0 ? grid().gap.y.toString() : 0
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
margin: 'auto',
|
||||
width: grid().itemSize.width?.toString() ?? '100%',
|
||||
height: grid().itemSize.height?.toString() ?? '100%'
|
||||
}}
|
||||
>
|
||||
{children(index)}
|
||||
</div>
|
||||
</div>
|
||||
<Solid.Show when={index()}>
|
||||
{(index) => (
|
||||
<div
|
||||
data-index={index().value}
|
||||
style={{
|
||||
'position': 'absolute',
|
||||
'top': 0,
|
||||
'left': 0,
|
||||
'width': `${virtualColumn.size}px`,
|
||||
'height': `${virtualRow.size}px`,
|
||||
'transform': `translateX(${
|
||||
virtualColumn.start
|
||||
}px) translateY(${
|
||||
virtualRow.start -
|
||||
rowVirtualizer.options.scrollMargin
|
||||
}px)`,
|
||||
'padding-left':
|
||||
virtualColumn.index !== 0
|
||||
? grid().gap.x.toString()
|
||||
: 0,
|
||||
'padding-top':
|
||||
virtualRow.index !== 0
|
||||
? grid().gap.y.toString()
|
||||
: 0
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
margin: 'auto',
|
||||
width:
|
||||
`${grid().itemSize.width?.toString()}px` ??
|
||||
'100%',
|
||||
height:
|
||||
`${grid().itemSize.height?.toString()}px` ??
|
||||
'100%'
|
||||
}}
|
||||
>
|
||||
{children(index().value)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Solid.Show>
|
||||
);
|
||||
}}
|
||||
</Solid.For>
|
||||
|
|
|
@ -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<typeof createVirtualizer>[0];
|
||||
|
||||
|
@ -37,59 +38,83 @@ export type CreateGridProps<IdT extends Core.GridItemId, DataT extends Core.Grid
|
|||
overscan?: number;
|
||||
};
|
||||
|
||||
export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridItemData>({
|
||||
scrollRef,
|
||||
overscan,
|
||||
...props
|
||||
}: CreateGridProps<IdT, DataT>) {
|
||||
export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridItemData>(
|
||||
props: Accessor<CreateGridProps<IdT, DataT>>
|
||||
) {
|
||||
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<VirtualizerOptions>(() => ({
|
||||
...props.rowVirtualizer,
|
||||
const [rowVirtualizer, setRowVirtualizer] = createStore<VirtualizerOptions>({
|
||||
...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<VirtualizerOptions>(() => ({
|
||||
...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<VirtualizerOptions>({
|
||||
...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<IdT extends Core.GridItemId, DataT extends Core.GridI
|
|||
|
||||
return createMemo(() => ({
|
||||
...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
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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<Selecto>(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 (
|
||||
<GridContext.Provider
|
||||
value={{
|
||||
|
@ -346,278 +135,39 @@ export default ({ children }: { children: RenderItem }) => {
|
|||
getElementById
|
||||
}}
|
||||
>
|
||||
{/* {explorer.allowMultiSelect && (
|
||||
<Selecto
|
||||
ref={selecto}
|
||||
boundContainer={
|
||||
explorerView.ref.current
|
||||
? {
|
||||
element: explorerView.ref.current,
|
||||
top: false,
|
||||
bottom: false
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
selectableTargets={['[data-selectable]']}
|
||||
toggleContinueSelect="shift"
|
||||
hitRate={0}
|
||||
onDrag={(e) => {
|
||||
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<number>();
|
||||
|
||||
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<ReturnType<typeof getElementItem>>[]
|
||||
);
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
<VirtualGrid grid={grid}>
|
||||
{(index) => {
|
||||
const item = explorer.items()?.[index];
|
||||
if (!item) return null;
|
||||
const item = createMemo(() => explorer.items()?.[index]);
|
||||
|
||||
return (
|
||||
<GridItem
|
||||
index={index}
|
||||
item={item}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button !== 0 || !explorerView.selectable) return;
|
||||
<Show when={item()}>
|
||||
{(item) => (
|
||||
<GridItem
|
||||
index={index}
|
||||
item={item()}
|
||||
onMouseDown={(e) => {
|
||||
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}
|
||||
</GridItem>
|
||||
activeItem = item.data;
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</GridItem>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
}}
|
||||
</VirtualGrid>
|
||||
|
|
|
@ -4,8 +4,8 @@ import { GridViewItem } from './Item';
|
|||
export const GridView = () => {
|
||||
return (
|
||||
<Grid>
|
||||
{({ item, selected, cut }) => (
|
||||
<GridViewItem data={item} selected={selected} cut={cut} />
|
||||
{(props) => (
|
||||
<GridViewItem data={props.item} selected={props.selected} cut={props.cut} />
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<TOrder extends Ordering> {
|
||||
items: () => ExplorerItem[] | null;
|
||||
items: Accessor<ExplorerItem[] | null>;
|
||||
count?: number;
|
||||
parent?: ExplorerParent;
|
||||
loadMore?: () => void;
|
||||
|
@ -52,7 +51,7 @@ export interface UseExplorerProps<TOrder extends Ordering> {
|
|||
* @defaultValue `true`
|
||||
*/
|
||||
selectable?: boolean;
|
||||
settings: ReturnType<typeof useExplorerSettings<TOrder>>;
|
||||
settings: ReturnType<typeof createExplorerSettings<TOrder>>;
|
||||
/**
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
|
@ -64,38 +63,38 @@ export interface UseExplorerProps<TOrder extends Ordering> {
|
|||
* Controls top-level config and state for the explorer.
|
||||
* View- and inspector-specific state is not handled here.
|
||||
*/
|
||||
export function createExplorer<TOrder extends Ordering>({
|
||||
settings,
|
||||
layouts,
|
||||
...props
|
||||
}: UseExplorerProps<TOrder>) {
|
||||
export function createExplorer<TOrder extends Ordering>(props: UseExplorerProps<TOrder>) {
|
||||
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | null>(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<TOrder extends Ordering> = ReturnType<typeof createExplorer<TOrder>>;
|
||||
export type CreateExplorer<TOrder extends Ordering> = ReturnType<
|
||||
ReturnType<typeof createExplorer<TOrder>>
|
||||
>;
|
||||
|
||||
export function useExplorerSettings<TOrder extends Ordering>({
|
||||
export function createExplorerSettings<TOrder extends Ordering>({
|
||||
settings,
|
||||
onSettingsChanged,
|
||||
orderingKeys,
|
||||
|
@ -110,40 +109,40 @@ export function useExplorerSettings<TOrder extends Ordering>({
|
|||
}) {
|
||||
// const [store] = useState(() => proxy(settings));
|
||||
|
||||
const updateSettings = useDebouncedCallback(
|
||||
(settings: ExplorerSettings<TOrder>, location: Location) => {
|
||||
onSettingsChanged?.(settings, location);
|
||||
},
|
||||
500
|
||||
);
|
||||
// const updateSettings = useDebouncedCallback(
|
||||
// (settings: ExplorerSettings<TOrder>, 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<TOrder>, location);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [store, updateSettings, location, onSettingsChanged]);
|
||||
// (() => {
|
||||
// if (!onSettingsChanged || !location) return;
|
||||
// const unsubscribe = subscribe(store, () => {
|
||||
// updateSettings(snapshot(store) as ExplorerSettings<TOrder>, location);
|
||||
// });
|
||||
// return () => unsubscribe();
|
||||
// }, [store, updateSettings, location, onSettingsChanged]);
|
||||
|
||||
return {
|
||||
useSettingsSnapshot: () => useSnapshot(store),
|
||||
settingsStore: store,
|
||||
// useSettingsSnapshot: () => useSnapshot(store),
|
||||
// settingsStore: store,
|
||||
orderingKeys
|
||||
};
|
||||
}
|
||||
|
||||
export type UseExplorerSettings<TOrder extends Ordering> = ReturnType<
|
||||
typeof useExplorerSettings<TOrder>
|
||||
export type CreateExplorerSettings<TOrder extends Ordering> = ReturnType<
|
||||
typeof createExplorerSettings<TOrder>
|
||||
>;
|
||||
|
||||
function createSelectedItems(items: () => ExplorerItem[] | null) {
|
||||
function createSelectedItems(items: Accessor<ExplorerItem[]>) {
|
||||
// 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<ExplorerItem, string>();
|
||||
|
@ -153,7 +152,7 @@ function createSelectedItems(items: () => ExplorerItem[] | null) {
|
|||
const selectedItemHashes = new ReactiveSet<string>();
|
||||
|
||||
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);
|
|
@ -8,7 +8,7 @@ export function Explorer() {
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={explorer.scrollRef}
|
||||
ref={explorer.setScrollRef}
|
||||
class="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden"
|
||||
style={
|
||||
{
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { type CreateInfiniteQueryOptions } from '@tanstack/solid-query';
|
||||
import { type ExplorerItem, type SearchData } from '@sd/client';
|
||||
|
||||
import { type Ordering } from '../store';
|
||||
|
||||
export type CreateExplorerInfiniteQueryArgs<TArg> = {
|
||||
arg: TArg;
|
||||
// explorerSettings: CreateExplorerSettings<TOrder>;
|
||||
} & Pick<CreateInfiniteQueryOptions<SearchData<ExplorerItem>>, 'enabled' | 'suspense'>;
|
19
apps/web-astro/src/Explorer/queries/createExplorerQuery.ts
Normal file
19
apps/web-astro/src/Explorer/queries/createExplorerQuery.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { type CreateInfiniteQueryResult, type CreateQueryResult } from '@tanstack/solid-query';
|
||||
import { createMemo } from 'solid-js';
|
||||
|
||||
export function createExplorerQuery<Q>(
|
||||
query: CreateInfiniteQueryResult<Q[]>,
|
||||
count: CreateQueryResult<number>
|
||||
) {
|
||||
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<Q> = ReturnType<typeof createExplorerQuery<Q>>;
|
|
@ -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<FilePathOrder>;
|
||||
}) {
|
||||
const query = createPathsInfiniteQuery();
|
||||
|
||||
const count = createLibraryQuery(() => ['search.pathsCount', { filters: props.arg.filters }], {
|
||||
enabled: query.isSuccess
|
||||
});
|
||||
|
||||
return createExplorerQuery(query, count);
|
||||
}
|
|
@ -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<FilePathSearchArgs>
|
||||
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];
|
||||
// }
|
||||
});
|
||||
}
|
|
@ -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 (
|
||||
<RspcProvider queryClient={queryClient}>
|
||||
<PlatformProvider platform={platform}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<cacheCtx.Provider value={cache}>
|
||||
<ClientContextProvider currentLibraryId={LIBRARY_UUID}>
|
||||
<ClientInner />
|
||||
</ClientContextProvider>
|
||||
</cacheCtx.Provider>
|
||||
</QueryClientProvider>
|
||||
</PlatformProvider>
|
||||
</RspcProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientInner() {
|
||||
useTheme();
|
||||
|
||||
const clientCtx = useClientContext();
|
||||
|
||||
return (
|
||||
<Show when={clientCtx.library()}>
|
||||
{(library) => (
|
||||
<div class="bg-app">
|
||||
<LibraryContextProvider library={library()}>
|
||||
<Wrapper />
|
||||
</LibraryContextProvider>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
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 <Show when={location()}>{(location) => <Inner location={location()} />}</Show>;
|
||||
}
|
||||
|
||||
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<FilePathOrder>({
|
||||
...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 <Explorer />;
|
||||
return (
|
||||
<div class="flex h-screen w-screen">
|
||||
<ExplorerContextProvider explorer={explorer()}>
|
||||
<Explorer />
|
||||
</ExplorerContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
86
apps/web-astro/src/Platform.tsx
Normal file
86
apps/web-astro/src/Platform.tsx
Normal file
|
@ -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<OperatingSystem>;
|
||||
openDirectoryPickerDialog?(opts?: { title?: string; multiple: false }): Promise<null | string>;
|
||||
openDirectoryPickerDialog?(opts?: {
|
||||
title?: string;
|
||||
multiple?: boolean;
|
||||
}): Promise<null | string | string[]>;
|
||||
openFilePickerDialog?(): Promise<null | string | string[]>;
|
||||
saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise<string | null>;
|
||||
showDevtools?(): void;
|
||||
openPath?(path: string): void;
|
||||
openLogsDir?(): void;
|
||||
userHomeDir?(): Promise<string>;
|
||||
// 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<unknown>;
|
||||
requestFdaMacos?(): void;
|
||||
getFilePathOpenWithApps?(library: string, ids: number[]): Promise<unknown>;
|
||||
reloadWebview?(): Promise<unknown>;
|
||||
getEphemeralFilesOpenWithApps?(paths: string[]): Promise<unknown>;
|
||||
openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): Promise<unknown>;
|
||||
openEphemeralFileWith?(pathsAndUrls: [string, string][]): Promise<unknown>;
|
||||
refreshMenuBar?(): Promise<unknown>;
|
||||
setMenuBarItemState?(id: string, enabled: boolean): Promise<unknown>;
|
||||
lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any;
|
||||
updater?: {
|
||||
useSnapshot: () => UpdateStore;
|
||||
checkForUpdate(): Promise<Update | null>;
|
||||
installUpdate(): Promise<any>;
|
||||
runJustUpdatedCheck(onViewChangelog: () => void): Promise<void>;
|
||||
};
|
||||
// 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<Platform>(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 <context.Provider value={props.platform}>{props.children}</context.Provider>;
|
||||
}
|
4
apps/web-astro/src/Wrapper.tsx
Normal file
4
apps/web-astro/src/Wrapper.tsx
Normal file
|
@ -0,0 +1,4 @@
|
|||
import './patches';
|
||||
import '@sd/ui/style/style.scss';
|
||||
|
||||
export * from './Page';
|
86
apps/web-astro/src/cache.tsx
Normal file
86
apps/web-astro/src/cache.tsx
Normal file
|
@ -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<string, unknown>;
|
||||
|
||||
export const cacheCtx = createContext<Cache>();
|
||||
|
||||
export type UseCacheResult<T> = T extends (infer A)[]
|
||||
? UseCacheResult<A>[]
|
||||
: T extends object
|
||||
? T extends { '__type': any; '__id': string; '#type': infer U }
|
||||
? UseCacheResult<U>
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> }
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> };
|
||||
|
||||
function constructCache(nodes: Store<Nodes>, setNodes: SetStoreFunction<Nodes>) {
|
||||
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<T>(item: T) {
|
||||
return restore(nodes, item) as UseCacheResult<T>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type Cache = ReturnType<typeof constructCache>;
|
||||
export type Nodes = Record<string, Record<string, unknown>>;
|
||||
|
||||
export function createCache() {
|
||||
const [nodes, setNodes] = createStore({} as Nodes);
|
||||
const cache = constructCache(nodes, setNodes);
|
||||
|
||||
return {
|
||||
...cache,
|
||||
Provider: (props: ParentProps) => (
|
||||
<cacheCtx.Provider value={cache}>{props.children}</cacheCtx.Provider>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
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<Nodes>, 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;
|
||||
}
|
0
apps/web-astro/src/client.ts
Normal file
0
apps/web-astro/src/client.ts
Normal file
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import { Page } from "../Page";
|
||||
import { Page } from "../Wrapper";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
|
|
22
apps/web-astro/src/patches.ts
Normal file
22
apps/web-astro/src/patches.ts
Normal file
|
@ -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;
|
||||
})()
|
||||
})
|
||||
];
|
|
@ -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<T extends keyof Procedures> =
|
||||
| Exclude<Procedures[T], { input: LibraryArgs<any> }>
|
||||
|
@ -71,11 +73,14 @@ const libraryHooks = createReactQueryHooks<LibraryProceduresDef>(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 (
|
||||
<libraryHooks.Provider client={libraryClient as any} queryClient={queryClient}>
|
||||
<nonLibraryHooks.Provider client={nonLibraryClient as any} queryClient={queryClient}>
|
||||
{children as any}
|
||||
<libraryHooks.Provider client={libraryClient as any} queryClient={props.queryClient}>
|
||||
<nonLibraryHooks.Provider
|
||||
client={nonLibraryClient as any}
|
||||
queryClient={props.queryClient}
|
||||
>
|
||||
{props.children}
|
||||
</nonLibraryHooks.Provider>
|
||||
</libraryHooks.Provider>
|
||||
);
|
||||
|
|
384
apps/web-astro/src/style.scss
Normal file
384
apps/web-astro/src/style.scss
Normal file
|
@ -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);
|
||||
}
|
121
apps/web-astro/src/useClientContext.tsx
Normal file
121
apps/web-astro/src/useClientContext.tsx
Normal file
|
@ -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<typeof useCachedLibraries>;
|
||||
library: Accessor<LibraryConfigWrapped | null | undefined>;
|
||||
}
|
||||
|
||||
const ClientContext = createContext<ClientContext>(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 (
|
||||
<ClientContext.Provider
|
||||
value={{
|
||||
currentLibraryId: props.currentLibraryId,
|
||||
libraries,
|
||||
library
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
});
|
47
apps/web-astro/src/useLibraryContext.tsx
Normal file
47
apps/web-astro/src/useLibraryContext.tsx
Normal file
|
@ -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<LibraryContext>(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 (
|
||||
<LibraryContext.Provider value={{ library: props.library, libraries }}>
|
||||
{props.children}
|
||||
</LibraryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
// }
|
64
apps/web-astro/src/useTheme.ts
Normal file
64
apps/web-astro/src/useTheme.ts
Normal file
|
@ -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);
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
})
|
||||
});
|
|
@ -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",
|
||||
|
|
17
apps/web/src/index.tsx
Normal file
17
apps/web/src/index.tsx
Normal file
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<Suspense>
|
||||
<App />
|
||||
</Suspense>
|
||||
</React.StrictMode>
|
||||
);
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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==}
|
||||
|
|
Loading…
Reference in a new issue