mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-07 04:23:29 +00:00
solid wow
This commit is contained in:
parent
0543216eef
commit
292ae97b5f
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"name": "publish-artifacts",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ncc build index.ts --minify"
|
"build": "ncc build index.ts --minify"
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "^1.14.0",
|
"@remix-run/router": "^1.14.0",
|
||||||
"@rspc/client": "0.0.0-main-45466c86",
|
"@rspc/client": "0.0.0-main-b8b35d28",
|
||||||
"@rspc/tauri": "0.0.0-main-45466c86",
|
"@rspc/tauri": "0.0.0-main-b8b35d28",
|
||||||
"@sd/client": "workspace:*",
|
"@sd/client": "workspace:*",
|
||||||
"@sd/interface": "workspace:*",
|
"@sd/interface": "workspace:*",
|
||||||
"@sd/ui": "workspace:*",
|
"@sd/ui": "workspace:*",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
"devPath": "http://localhost:8001",
|
"devPath": "http://localhost:4321",
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..."
|
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..."
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
"@react-navigation/drawer": "^6.6.6",
|
"@react-navigation/drawer": "^6.6.6",
|
||||||
"@react-navigation/native": "^6.1.9",
|
"@react-navigation/native": "^6.1.9",
|
||||||
"@react-navigation/stack": "^6.3.20",
|
"@react-navigation/stack": "^6.3.20",
|
||||||
"@rspc/client": "0.0.0-main-45466c86",
|
"@rspc/client": "0.0.0-main-b8b35d28",
|
||||||
"@rspc/react": "0.0.0-main-45466c86",
|
"@rspc/react": "0.0.0-main-b8b35d28",
|
||||||
"@sd/assets": "workspace:*",
|
"@sd/assets": "workspace:*",
|
||||||
"@sd/client": "workspace:*",
|
"@sd/client": "workspace:*",
|
||||||
"@shopify/flash-list": "1.4.3",
|
"@shopify/flash-list": "1.4.3",
|
||||||
|
|
|
@ -14,9 +14,10 @@
|
||||||
"@astrojs/react": "^3.0.7",
|
"@astrojs/react": "^3.0.7",
|
||||||
"@astrojs/solid-js": "^3.0.2",
|
"@astrojs/solid-js": "^3.0.2",
|
||||||
"@astrojs/tailwind": "^5.0.3",
|
"@astrojs/tailwind": "^5.0.3",
|
||||||
"@rspc/solid": "0.0.0-main-45466c86",
|
"@rspc/solid": "0.0.0-main-b8b35d28",
|
||||||
"@sd/assets": "workspace:^",
|
"@sd/assets": "workspace:^",
|
||||||
"@sd/client": "workspace:^",
|
"@sd/client": "workspace:^",
|
||||||
|
"@sd/ui": "workspace:^",
|
||||||
"@solid-primitives/event-listener": "^2.3.0",
|
"@solid-primitives/event-listener": "^2.3.0",
|
||||||
"@solid-primitives/intersection-observer": "^2.1.3",
|
"@solid-primitives/intersection-observer": "^2.1.3",
|
||||||
"@solid-primitives/resize-observer": "^2.0.22",
|
"@solid-primitives/resize-observer": "^2.0.22",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createContext, useContext, type ParentProps } from 'solid-js';
|
import { createContext, useContext, type ParentProps } from 'solid-js';
|
||||||
|
|
||||||
import { Ordering } from './store';
|
import { type CreateExplorer } from './createExplorer';
|
||||||
import { type CreateExplorer } from './useExplorer';
|
import { type Ordering } from './store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context that must wrap anything to do with the explorer.
|
* Context that must wrap anything to do with the explorer.
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import { createEventListener } from '@solid-primitives/event-listener';
|
import { createEventListener } from '@solid-primitives/event-listener';
|
||||||
|
import { createResizeObserver } from '@solid-primitives/resize-observer';
|
||||||
import clsx from 'clsx';
|
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';
|
// import { Tooltip } from '@sd/ui';
|
||||||
|
|
||||||
|
@ -159,8 +168,8 @@ export function RenameTextBox(props: RenameTextBoxProps) {
|
||||||
ref={ref!}
|
ref={ref!}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
contentEditable={allowRename()}
|
contentEditable={allowRename()}
|
||||||
className={clsx(
|
class={clsx(
|
||||||
'cursor-default overflow-hidden rounded-md px-1.5 py-px text-xs text-ink outline-none',
|
'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() && 'whitespace-normal bg-app !text-ink ring-2 ring-accent-deep',
|
||||||
!allowRename && props.idleClassName,
|
!allowRename && props.idleClassName,
|
||||||
props.class
|
props.class
|
||||||
|
@ -183,33 +192,116 @@ export function RenameTextBox(props: RenameTextBoxProps) {
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
{...wrapperProps}
|
{...wrapperProps}
|
||||||
>
|
>
|
||||||
{props.name}
|
{allowRename()
|
||||||
{/* {allowRename ? (
|
? props.name
|
||||||
name
|
: (() => {
|
||||||
) : (
|
const ellipsis = createMemo(() => {
|
||||||
<TruncatedText text={name} lines={lines} onTruncate={setIsTruncated} />
|
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>
|
</div>
|
||||||
// </Tooltip>
|
// </Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TruncatedTextProps {
|
const LINE_HEIGHT = 19;
|
||||||
text: string;
|
|
||||||
lines?: number;
|
|
||||||
onTruncate: (wasTruncated: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TruncatedText(props: TruncatedTextProps) {
|
function TruncatedText(props: {
|
||||||
const ellipsis = () => {
|
lines: number;
|
||||||
const extension = props.text.lastIndexOf('.');
|
prefix?: JSX.Element;
|
||||||
if (extension !== -1) return `...${props.text.slice(-(props.text.length - extension + 2))}`;
|
postfix?: JSX.Element;
|
||||||
return `...${props.text.slice(-8)}`;
|
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 (
|
return (
|
||||||
<TruncateMarkup lines={props.lines} ellipsis={ellipsis} onTruncate={props.onTruncate}>
|
<div
|
||||||
<div>{props.text}</div>
|
style={{
|
||||||
</TruncateMarkup>
|
'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 { getIcon, getIconByName } from '@sd/assets/util';
|
||||||
import { createElementSize } from '@solid-primitives/resize-observer';
|
import { createElementSize } from '@solid-primitives/resize-observer';
|
||||||
import clsx from 'clsx';
|
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 { createStore } from 'solid-js/store';
|
||||||
import { getExplorerItemData, type ExplorerItem } from '@sd/client';
|
import { getExplorerItemData, type ExplorerItem } from '@sd/client';
|
||||||
|
|
||||||
|
import { usePlatform } from '../../Platform';
|
||||||
import { LayeredFileIcon } from './LayeredFileIcon';
|
import { LayeredFileIcon } from './LayeredFileIcon';
|
||||||
import classes from './Thumb.module.scss';
|
import classes from './Thumb.module.scss';
|
||||||
|
|
||||||
interface FileThumbProps extends Pick<ComponentProps<'img'>, 'ref'> {
|
interface FileThumbProps extends Pick<ComponentProps<'img'>, 'ref' | 'class'> {
|
||||||
data: ExplorerItem;
|
data: ExplorerItem;
|
||||||
loadOriginal?: boolean;
|
loadOriginal?: boolean;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
@ -21,7 +30,6 @@ interface FileThumbProps extends Pick<ComponentProps<'img'>, 'ref'> {
|
||||||
extension?: boolean;
|
extension?: boolean;
|
||||||
mediaControls?: boolean;
|
mediaControls?: boolean;
|
||||||
pauseVideo?: boolean;
|
pauseVideo?: boolean;
|
||||||
className?: string;
|
|
||||||
frameClassName?: string;
|
frameClassName?: string;
|
||||||
childClassName?: string | ((type: ThumbType) => string | undefined);
|
childClassName?: string | ((type: ThumbType) => string | undefined);
|
||||||
isSidebarPreview?: boolean;
|
isSidebarPreview?: boolean;
|
||||||
|
@ -51,11 +59,13 @@ export function FileThumb(props: FileThumbProps) {
|
||||||
return { variant: 'icon' };
|
return { variant: 'icon' };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const platform = usePlatform();
|
||||||
|
|
||||||
const src = createMemo<string | undefined>(() => {
|
const src = createMemo<string | undefined>(() => {
|
||||||
switch (thumbType().variant) {
|
switch (thumbType().variant) {
|
||||||
case 'thumbnail':
|
case 'thumbnail':
|
||||||
// if (itemData().thumbnailKey.length > 0)
|
if (itemData().thumbnailKey.length > 0)
|
||||||
// return platform.getThumbnailUrlByThumbKey(itemData.thumbnailKey);
|
return platform.getThumbnailUrlByThumbKey(itemData().thumbnailKey);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'icon':
|
case 'icon':
|
||||||
|
@ -80,11 +90,12 @@ export function FileThumb(props: FileThumbProps) {
|
||||||
: props.childClassName;
|
: props.childClassName;
|
||||||
|
|
||||||
const childClassName = 'max-h-full max-w-full object-contain';
|
const childClassName = 'max-h-full max-w-full object-contain';
|
||||||
const frameClassName = clsx(
|
const frameClassName = () =>
|
||||||
'rounded-sm border-2 border-app-line bg-app-darkBox',
|
clsx(
|
||||||
props.frameClassName,
|
'rounded-sm border-2 border-app-line bg-app-darkBox',
|
||||||
true ? classes.checkers : classes.checkersLight
|
props.frameClassName,
|
||||||
);
|
true ? classes.checkers : classes.checkersLight
|
||||||
|
);
|
||||||
|
|
||||||
const getClass = () => clsx(childClassName, _childClassName());
|
const getClass = () => clsx(childClassName, _childClassName());
|
||||||
|
|
||||||
|
@ -109,60 +120,79 @@ export function FileThumb(props: FileThumbProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={src()}>
|
<div
|
||||||
{(src) => (
|
style={{
|
||||||
<Switch>
|
...(props.size
|
||||||
<Match when={thumbType().variant === 'thumbnail'}>
|
? {
|
||||||
<Thumbnail
|
maxWidth: props.size.toString(),
|
||||||
{...props.childProps}
|
width: props.size.toString(),
|
||||||
ref={props.ref}
|
height: props.size.toString()
|
||||||
src={src()}
|
}
|
||||||
cover={props.cover}
|
: {})
|
||||||
onLoad={() => onLoad('thumbnail')}
|
}}
|
||||||
onError={(e) => onError('thumbnail', e)}
|
class={clsx(
|
||||||
decoding={props.size ? 'async' : 'sync'}
|
'relative flex shrink-0 items-center justify-center',
|
||||||
class={clsx(
|
// !loaded && 'invisible',
|
||||||
props.cover
|
!props.size && 'h-full w-full',
|
||||||
? [
|
props.cover && 'overflow-hidden',
|
||||||
'min-h-full min-w-full object-cover object-center',
|
props.class
|
||||||
_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>
|
>
|
||||||
|
<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 (
|
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 && (
|
{(props.cover || (size.width && size.width > 80)) && props.extension && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -1,84 +1,145 @@
|
||||||
// import getLineHeight from 'line-height';
|
import { createResizeObserver } from '@solid-primitives/resize-observer';
|
||||||
// import * as Solid from 'solid-js';
|
import getLineHeight from 'line-height';
|
||||||
|
import * as Solid from 'solid-js';
|
||||||
|
|
||||||
// export interface TruncateMarkupProps {
|
export interface TruncateMarkupProps {
|
||||||
// lines: number;
|
lines: number;
|
||||||
// ellipsis?: Solid.JSX.Element | ((element: Solid.JSX.Element) => Solid.JSX.Element);
|
ellipsis?: Solid.JSX.Element | ((element: Solid.JSX.Element) => Solid.JSX.Element);
|
||||||
// children: (ref: HTMLDivElement, style: any) => Solid.JSX.Element;
|
children: (props: { ref: HTMLDivElement; style: any }) => Solid.JSX.Element;
|
||||||
// lineHeight?: number | string;
|
lineHeight?: number | string;
|
||||||
// tokenize?: string;
|
tokenize?: string;
|
||||||
// onTruncate?: (wasTruncated: boolean) => any;
|
onTruncate?: (wasTruncated: boolean) => any;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// export function TruncateMarkup(props: TruncateMarkupProps) {
|
export function TruncateMarkup(props: Solid.ParentProps<TruncateMarkupProps>) {
|
||||||
// let splitDirectionSeq = [];
|
const [text, setText] = Solid.createSignal('');
|
||||||
// let shouldTruncate = true;
|
|
||||||
// let wasLastCharTested = false;
|
|
||||||
// let endFound = false;
|
|
||||||
// let latestThatFits = null;
|
|
||||||
// let onTruncateCalled = false;
|
|
||||||
|
|
||||||
// let el: HTMLDivElement;
|
const children = Solid.children(() => props.children);
|
||||||
|
|
||||||
// Solid.onMount(() => {
|
const [ref, setRef] = Solid.createSignal<HTMLElement | null>(null);
|
||||||
// // if (!isValid) return;
|
|
||||||
|
|
||||||
// let splitDirectionSeq: Array<'left' | 'right'> = [];
|
let shouldTruncate = false;
|
||||||
// let shouldTruncate = true;
|
let latestThatFits = null;
|
||||||
// let wasLastCharTested = false;
|
let onTruncateCalled = false;
|
||||||
// let endFound = false;
|
let lineHeight: any = null;
|
||||||
// let latestThatFits = null;
|
let endFound = false;
|
||||||
// let onTruncateCalled = false;
|
let splitDirectionSeq = [];
|
||||||
|
let wasLastCharTested = false;
|
||||||
|
|
||||||
// const lineHeight = props.lineHeight || getLineHeight(el);
|
type SplitDirection = 'left' | 'right';
|
||||||
|
|
||||||
// const fits = Solid.createMemo(() => {
|
function splitString(string: string, splitDirections: Array<SplitDirection>, level: any) {
|
||||||
// const maxLines = props.lines;
|
if (!splitDirections.length) return string;
|
||||||
// const { height } = el!.getBoundingClientRect();
|
|
||||||
// const computedLines = Math.round(height / parseFloat(lineHeight));
|
|
||||||
|
|
||||||
// return maxLines >= computedLines;
|
if (splitDirections.length && policy.isAtomic(string)) {
|
||||||
// });
|
if (!wasLastCharTested) wasLastCharTested = true;
|
||||||
|
else endFound = true;
|
||||||
|
|
||||||
// function onTruncate(wasTruncated: boolean) {
|
return string;
|
||||||
// if (!onTruncateCalled) {
|
}
|
||||||
// onTruncateCalled = true;
|
|
||||||
// props.onTruncate?.(wasTruncated);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function truncateOriginalText() {
|
if (policy.tokenizeString) {
|
||||||
// endFound = false;
|
const wordsArray = splitArray(policy.tokenizeString(string), splitDirections, level);
|
||||||
// splitDirectionSeq = ['left'];
|
|
||||||
// wasLastCharTested = false;
|
|
||||||
|
|
||||||
// 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() {
|
const afterString = string.substring(pivotIndex);
|
||||||
// if (fits()) {
|
|
||||||
// shouldTruncate = false;
|
|
||||||
// onTruncate(false);
|
|
||||||
|
|
||||||
// return;
|
return beforeString + splitString(afterString, restSplitDirections, level);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// truncateOriginalText();
|
function splitArray(array: string[], splitDirections: Array<SplitDirection>, level) {
|
||||||
// }
|
if (!splitDirections.length) {
|
||||||
// });
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
// function childrenElementWithRef() {
|
if (array.length === 1) {
|
||||||
// const childrenArray = children.toArray();
|
return [split(array[0]!, splitDirections, /* isRoot */ false, level)];
|
||||||
// if (childrenArray.length > 1) {
|
}
|
||||||
// throw new Error('TruncateMarkup must have only one child element');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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 getLineHeight from 'line-height';
|
||||||
// import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
// import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
// import React from 'react';
|
import React from 'react';
|
||||||
// import ResizeObserver from 'resize-observer-polyfill';
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
|
||||||
// import { Atom, ATOM_STRING_ID, isAtomComponent } from './atom';
|
import { Atom, ATOM_STRING_ID, isAtomComponent } from './atom';
|
||||||
// import TOKENIZE_POLICY from './tokenize-rules';
|
|
||||||
|
const TOKENIZE_POLICY = {
|
||||||
// const SPLIT = {
|
characters: {
|
||||||
// LEFT: true,
|
tokenizeString: null,
|
||||||
// RIGHT: false
|
isAtomic: (str: string) => str.length <= 1
|
||||||
// };
|
},
|
||||||
|
words: {
|
||||||
// const toString = (node, string = '') => {
|
tokenizeString: (str: string) => str.match(/(\s*\S[\S\xA0]*)/g),
|
||||||
// if (!node) {
|
isAtomic: (str: string) => /^\s*[\S\xA0]*\s*$/.test(str)
|
||||||
// return string;
|
}
|
||||||
// } else if (typeof node === 'string') {
|
};
|
||||||
// return string + node;
|
|
||||||
// } else if (isAtomComponent(node)) {
|
const SPLIT = {
|
||||||
// return string + ATOM_STRING_ID;
|
LEFT: true,
|
||||||
// }
|
RIGHT: false
|
||||||
// const children = Array.isArray(node) ? node : node.props.children || '';
|
};
|
||||||
|
|
||||||
// return string + React.Children.map(children, (child) => toString(child)).join('');
|
const toString = (node, string = '') => {
|
||||||
// };
|
if (!node) {
|
||||||
|
return string;
|
||||||
// const cloneWithChildren = (node, children, isRootEl, level) => {
|
} else if (typeof node === 'string') {
|
||||||
// const getDisplayStyle = () => {
|
return string + node;
|
||||||
// if (isRootEl) {
|
} else if (isAtomComponent(node)) {
|
||||||
// return {
|
return string + ATOM_STRING_ID;
|
||||||
// // root element cannot be an inline element because of the line calculation
|
}
|
||||||
// display: (node.props.style || {}).display || 'block'
|
const children = Array.isArray(node) ? node : node.props.children || '';
|
||||||
// };
|
|
||||||
// } else if (level === 2) {
|
return string + React.Children.map(children, (child) => toString(child)).join('');
|
||||||
// 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
|
const cloneWithChildren = (node, children, isRootEl, level) => {
|
||||||
// display: (node.props.style || {}).display || 'inline-block'
|
const getDisplayStyle = () => {
|
||||||
// };
|
if (isRootEl) {
|
||||||
// } else return {};
|
return {
|
||||||
// };
|
// root element cannot be an inline element because of the line calculation
|
||||||
|
display: (node.props.style || {}).display || 'block'
|
||||||
// return {
|
};
|
||||||
// ...node,
|
} else if (level === 2) {
|
||||||
// props: {
|
return {
|
||||||
// ...node.props,
|
// level 2 elements (direct children of the root element) need to be inline because of the ellipsis.
|
||||||
// style: {
|
// if level 2 element was a block element, ellipsis would get rendered on a new line, breaking the max number of lines
|
||||||
// ...node.props.style,
|
display: (node.props.style || {}).display || 'inline-block'
|
||||||
// ...getDisplayStyle()
|
};
|
||||||
// },
|
} else return {};
|
||||||
// children
|
};
|
||||||
// }
|
|
||||||
// };
|
return {
|
||||||
// };
|
...node,
|
||||||
|
props: {
|
||||||
// const validateTree = (node) => {
|
...node.props,
|
||||||
// if (node == null || ['string', 'number'].includes(typeof node) || isAtomComponent(node)) {
|
style: {
|
||||||
// return true;
|
...node.props.style,
|
||||||
// } else if (typeof node.type === 'function') {
|
...getDisplayStyle()
|
||||||
// if (process.env.NODE_ENV !== 'production') {
|
},
|
||||||
// /* eslint-disable no-console */
|
children
|
||||||
// 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 */
|
|
||||||
// }
|
const validateTree = (node) => {
|
||||||
|
if (node == null || ['string', 'number'].includes(typeof node) || isAtomComponent(node)) {
|
||||||
// return false;
|
return true;
|
||||||
// }
|
} else if (typeof node.type === 'function') {
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
// if (node.props && node.props.children) {
|
/* eslint-disable no-console */
|
||||||
// return React.Children.toArray(node.props.children).reduce(
|
console.error(
|
||||||
// (isValid, child) => isValid && validateTree(child),
|
`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-).`
|
||||||
// true
|
);
|
||||||
// );
|
/* eslint-enable */
|
||||||
// }
|
}
|
||||||
|
|
||||||
// return true;
|
return false;
|
||||||
// };
|
}
|
||||||
|
|
||||||
// export default class TruncateMarkup extends React.Component {
|
if (node.props && node.props.children) {
|
||||||
// static Atom = Atom;
|
return React.Children.toArray(node.props.children).reduce(
|
||||||
|
(isValid, child) => isValid && validateTree(child),
|
||||||
// static propTypes = {
|
true
|
||||||
// children: PropTypes.element.isRequired,
|
);
|
||||||
// lines: PropTypes.number,
|
}
|
||||||
// ellipsis: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.func]),
|
|
||||||
// lineHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
return true;
|
||||||
// onTruncate: PropTypes.func,
|
};
|
||||||
// // eslint-disable-next-line
|
|
||||||
// onAfterTruncate: (props, propName, componentName) => {
|
export interface TruncateMarkupProps {
|
||||||
// if (props[propName]) {
|
lines: number;
|
||||||
// return new Error(
|
ellipsis?: JSX.Element | ((element: JSX.Element) => JSX.Element);
|
||||||
// `${componentName}: Setting \`onAfterTruncate\` prop is deprecated, use \`onTruncate\` instead.`
|
children: JSX.Element;
|
||||||
// );
|
lineHeight?: number | string;
|
||||||
// }
|
tokenize?: string;
|
||||||
// },
|
onTruncate?: (wasTruncated: boolean) => any;
|
||||||
// tokenize: (props, propName, componentName) => {
|
}
|
||||||
// const tokenizeValue = props[propName];
|
|
||||||
|
export default class TruncateMarkup extends React.Component<TruncateMarkupProps> {
|
||||||
// if (typeof tokenizeValue !== 'undefined') {
|
static Atom = Atom;
|
||||||
// if (!TOKENIZE_POLICY[tokenizeValue]) {
|
|
||||||
// /* eslint-disable no-console */
|
static propTypes = {
|
||||||
// return new Error(
|
children: PropTypes.element.isRequired,
|
||||||
// `${componentName}: Unknown option for prop 'tokenize': '${tokenizeValue}'. Option 'characters' will be used instead.`
|
lines: PropTypes.number,
|
||||||
// );
|
ellipsis: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.func]),
|
||||||
// /* eslint-enable */
|
lineHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
// }
|
onTruncate: PropTypes.func,
|
||||||
// }
|
// eslint-disable-next-line
|
||||||
// }
|
onAfterTruncate: (props, propName, componentName) => {
|
||||||
// };
|
if (props[propName]) {
|
||||||
|
return new Error(
|
||||||
// static defaultProps = {
|
`${componentName}: Setting \`onAfterTruncate\` prop is deprecated, use \`onTruncate\` instead.`
|
||||||
// lines: 1,
|
);
|
||||||
// ellipsis: '...',
|
}
|
||||||
// lineHeight: '',
|
},
|
||||||
// onTruncate: () => {},
|
tokenize: (props, propName, componentName) => {
|
||||||
// tokenize: 'characters'
|
const tokenizeValue = props[propName];
|
||||||
// };
|
|
||||||
|
if (typeof tokenizeValue !== 'undefined') {
|
||||||
// constructor(props) {
|
if (!TOKENIZE_POLICY[tokenizeValue]) {
|
||||||
// super(props);
|
/* eslint-disable no-console */
|
||||||
|
return new Error(
|
||||||
// this.state = {
|
`${componentName}: Unknown option for prop 'tokenize': '${tokenizeValue}'. Option 'characters' will be used instead.`
|
||||||
// text: this.childrenWithRefMemo(this.props.children)
|
);
|
||||||
// };
|
/* eslint-enable */
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
// lineHeight = null;
|
}
|
||||||
// splitDirectionSeq = [];
|
};
|
||||||
// shouldTruncate = true;
|
|
||||||
// wasLastCharTested = false;
|
static defaultProps = {
|
||||||
// endFound = false;
|
lines: 1,
|
||||||
// latestThatFits = null;
|
ellipsis: '...',
|
||||||
// onTruncateCalled = false;
|
lineHeight: '',
|
||||||
|
onTruncate: () => {},
|
||||||
// toStringMemo = memoizeOne(toString);
|
tokenize: 'characters'
|
||||||
// childrenWithRefMemo = memoizeOne(this.childrenElementWithRef);
|
};
|
||||||
// validateTreeMemo = memoizeOne(validateTree);
|
|
||||||
|
constructor(props) {
|
||||||
// get isValid() {
|
super(props);
|
||||||
// return this.validateTreeMemo(this.props.children);
|
|
||||||
// }
|
this.state = {
|
||||||
// get origText() {
|
text: this.childrenWithRefMemo(this.props.children)
|
||||||
// return this.childrenWithRefMemo(this.props.children);
|
};
|
||||||
// }
|
}
|
||||||
// get policy() {
|
|
||||||
// return TOKENIZE_POLICY[this.props.tokenize] || TOKENIZE_POLICY.characters;
|
lineHeight = null;
|
||||||
// }
|
splitDirectionSeq = [];
|
||||||
|
shouldTruncate = true;
|
||||||
// componentDidMount() {
|
wasLastCharTested = false;
|
||||||
// if (!this.isValid) {
|
endFound = false;
|
||||||
// return;
|
latestThatFits = null;
|
||||||
// }
|
onTruncateCalled = false;
|
||||||
|
|
||||||
// // get the computed line-height of the parent element
|
toStringMemo = memoizeOne(toString);
|
||||||
// // it'll be used for determining whether the text fits the container or not
|
childrenWithRefMemo = memoizeOne(this.childrenElementWithRef);
|
||||||
// this.lineHeight = this.props.lineHeight || getLineHeight(this.el);
|
validateTreeMemo = memoizeOne(validateTree);
|
||||||
// this.truncate();
|
|
||||||
// }
|
get isValid() {
|
||||||
|
return this.validateTreeMemo(this.props.children);
|
||||||
// UNSAFE_componentWillReceiveProps(nextProps) {
|
}
|
||||||
// this.shouldTruncate = false;
|
get origText() {
|
||||||
// this.latestThatFits = null;
|
return this.childrenWithRefMemo(this.props.children);
|
||||||
|
}
|
||||||
// this.setState(
|
get policy() {
|
||||||
// {
|
return TOKENIZE_POLICY[this.props.tokenize] || TOKENIZE_POLICY.characters;
|
||||||
// text: this.childrenWithRefMemo(nextProps.children)
|
}
|
||||||
// },
|
|
||||||
// () => {
|
componentDidMount() {
|
||||||
// if (!this.isValid) {
|
if (!this.isValid) {
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// this.lineHeight = nextProps.lineHeight || getLineHeight(this.el);
|
// get the computed line-height of the parent element
|
||||||
// this.shouldTruncate = true;
|
// it'll be used for determining whether the text fits the container or not
|
||||||
// this.truncate();
|
this.lineHeight = this.props.lineHeight || getLineHeight(this.el);
|
||||||
// }
|
this.truncate();
|
||||||
// );
|
}
|
||||||
// }
|
|
||||||
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
// componentDidUpdate() {
|
this.shouldTruncate = false;
|
||||||
// if (this.shouldTruncate === false || this.isValid === false) {
|
this.latestThatFits = null;
|
||||||
// return;
|
|
||||||
// }
|
this.setState(
|
||||||
|
{
|
||||||
// if (this.endFound) {
|
text: this.childrenWithRefMemo(nextProps.children)
|
||||||
// // 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.isValid) {
|
||||||
// if (this.latestThatFits !== null && this.state.text !== this.latestThatFits) {
|
return;
|
||||||
// /* eslint-disable react/no-did-update-set-state */
|
}
|
||||||
// this.setState({
|
|
||||||
// text: this.latestThatFits
|
this.lineHeight = nextProps.lineHeight || getLineHeight(this.el);
|
||||||
// });
|
this.shouldTruncate = true;
|
||||||
|
this.truncate();
|
||||||
// return;
|
}
|
||||||
// /* eslint-enable */
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// this.onTruncate(/* wasTruncated */ true);
|
componentDidUpdate() {
|
||||||
|
if (this.shouldTruncate === false || this.isValid === false) {
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (this.splitDirectionSeq.length) {
|
if (this.endFound) {
|
||||||
// if (this.fits()) {
|
// we've found the end where we cannot split the text further
|
||||||
// this.latestThatFits = this.state.text;
|
// that means we've already found the max subtree that fits the container
|
||||||
// // we've found a subtree that fits the container
|
// so we are rendering that
|
||||||
// // but we need to check if we didn't cut too much of it off
|
if (this.latestThatFits !== null && this.state.text !== this.latestThatFits) {
|
||||||
// // so we are changing the last splitting decision from splitting and going left
|
/* eslint-disable react/no-did-update-set-state */
|
||||||
// // to splitting and going right
|
this.setState({
|
||||||
// this.splitDirectionSeq.splice(
|
text: this.latestThatFits
|
||||||
// this.splitDirectionSeq.length - 1,
|
});
|
||||||
// 1,
|
|
||||||
// SPLIT.RIGHT,
|
return;
|
||||||
// SPLIT.LEFT
|
/* eslint-enable */
|
||||||
// );
|
}
|
||||||
// } else {
|
|
||||||
// this.splitDirectionSeq.push(SPLIT.LEFT);
|
this.onTruncate(/* wasTruncated */ true);
|
||||||
// }
|
|
||||||
|
return;
|
||||||
// this.tryToFit(this.origText, this.splitDirectionSeq);
|
}
|
||||||
// }
|
|
||||||
// }
|
if (this.splitDirectionSeq.length) {
|
||||||
|
if (this.fits()) {
|
||||||
// componentWillUnmount() {
|
this.latestThatFits = this.state.text;
|
||||||
// this.lineHeight = null;
|
// we've found a subtree that fits the container
|
||||||
// this.latestThatFits = null;
|
// but we need to check if we didn't cut too much of it off
|
||||||
// this.splitDirectionSeq = [];
|
// so we are changing the last splitting decision from splitting and going left
|
||||||
// }
|
// to splitting and going right
|
||||||
|
this.splitDirectionSeq.splice(
|
||||||
// onTruncate = (wasTruncated) => {
|
this.splitDirectionSeq.length - 1,
|
||||||
// if (!this.onTruncateCalled) {
|
1,
|
||||||
// this.onTruncateCalled = true;
|
SPLIT.RIGHT,
|
||||||
// this.props.onTruncate(wasTruncated);
|
SPLIT.LEFT
|
||||||
// }
|
);
|
||||||
// };
|
} else {
|
||||||
|
this.splitDirectionSeq.push(SPLIT.LEFT);
|
||||||
// handleResize = (el, prevResizeObserver) => {
|
}
|
||||||
// // clean up previous observer
|
|
||||||
// if (prevResizeObserver) {
|
this.tryToFit(this.origText, this.splitDirectionSeq);
|
||||||
// prevResizeObserver.disconnect();
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // unmounting or just unsetting the element to be replaced with a new one later
|
componentWillUnmount() {
|
||||||
// if (!el) return null;
|
this.lineHeight = null;
|
||||||
|
this.latestThatFits = null;
|
||||||
// /* Wrapper element resize handing */
|
this.splitDirectionSeq = [];
|
||||||
// let initialRender = true;
|
}
|
||||||
// const resizeCallback = () => {
|
|
||||||
// if (initialRender) {
|
onTruncate = (wasTruncated) => {
|
||||||
// // ResizeObserer cb is called on initial render too so we are skipping here
|
if (!this.onTruncateCalled) {
|
||||||
// initialRender = false;
|
this.onTruncateCalled = true;
|
||||||
// } else {
|
this.props.onTruncate(wasTruncated);
|
||||||
// // wrapper element has been resized, recalculating with the original text
|
}
|
||||||
// this.shouldTruncate = false;
|
};
|
||||||
// this.latestThatFits = null;
|
|
||||||
|
handleResize = (el, prevResizeObserver) => {
|
||||||
// this.setState(
|
// clean up previous observer
|
||||||
// {
|
if (prevResizeObserver) {
|
||||||
// text: this.origText
|
prevResizeObserver.disconnect();
|
||||||
// },
|
}
|
||||||
// () => {
|
|
||||||
// this.shouldTruncate = true;
|
// unmounting or just unsetting the element to be replaced with a new one later
|
||||||
// this.onTruncateCalled = false;
|
if (!el) return null;
|
||||||
// this.truncate();
|
|
||||||
// }
|
/* Wrapper element resize handing */
|
||||||
// );
|
let initialRender = true;
|
||||||
// }
|
const resizeCallback = () => {
|
||||||
// };
|
if (initialRender) {
|
||||||
|
// ResizeObserer cb is called on initial render too so we are skipping here
|
||||||
// const resizeObserver = prevResizeObserver || new ResizeObserver(resizeCallback);
|
initialRender = false;
|
||||||
|
} else {
|
||||||
// resizeObserver.observe(el);
|
// wrapper element has been resized, recalculating with the original text
|
||||||
|
this.shouldTruncate = false;
|
||||||
// return resizeObserver;
|
this.latestThatFits = null;
|
||||||
// };
|
|
||||||
|
this.setState(
|
||||||
// truncate() {
|
{
|
||||||
// if (this.fits()) {
|
text: this.origText
|
||||||
// // the whole text fits on the first try, no need to do anything else
|
},
|
||||||
// this.shouldTruncate = false;
|
() => {
|
||||||
// this.onTruncate(/* wasTruncated */ false);
|
this.shouldTruncate = true;
|
||||||
|
this.onTruncateCalled = false;
|
||||||
// return;
|
this.truncate();
|
||||||
// }
|
}
|
||||||
|
);
|
||||||
// this.truncateOriginalText();
|
}
|
||||||
// }
|
};
|
||||||
|
|
||||||
// setRef = (el) => {
|
const resizeObserver = prevResizeObserver || new ResizeObserver(resizeCallback);
|
||||||
// const isNewEl = this.el !== el;
|
|
||||||
// this.el = el;
|
resizeObserver.observe(el);
|
||||||
|
|
||||||
// // whenever we obtain a new element, attach resize handler
|
return resizeObserver;
|
||||||
// if (isNewEl) {
|
};
|
||||||
// this.resizeObserver = this.handleResize(el, this.resizeObserver);
|
|
||||||
// }
|
truncate() {
|
||||||
// };
|
if (this.fits()) {
|
||||||
|
// the whole text fits on the first try, no need to do anything else
|
||||||
// childrenElementWithRef(children) {
|
this.shouldTruncate = false;
|
||||||
// const child = React.Children.only(children);
|
this.onTruncate(/* wasTruncated */ false);
|
||||||
|
|
||||||
// return React.cloneElement(child, {
|
return;
|
||||||
// ref: this.setRef,
|
}
|
||||||
// style: {
|
|
||||||
// wordWrap: 'break-word',
|
this.truncateOriginalText();
|
||||||
// ...child.props.style
|
}
|
||||||
// }
|
|
||||||
// });
|
setRef = (el) => {
|
||||||
// }
|
const isNewEl = this.el !== el;
|
||||||
|
this.el = el;
|
||||||
// truncateOriginalText() {
|
|
||||||
// this.endFound = false;
|
// whenever we obtain a new element, attach resize handler
|
||||||
// this.splitDirectionSeq = [SPLIT.LEFT];
|
if (isNewEl) {
|
||||||
// this.wasLastCharTested = false;
|
this.resizeObserver = this.handleResize(el, this.resizeObserver);
|
||||||
|
}
|
||||||
// this.tryToFit(this.origText, this.splitDirectionSeq);
|
};
|
||||||
// }
|
|
||||||
|
childrenElementWithRef(children) {
|
||||||
// /**
|
const child = React.Children.only(children);
|
||||||
// * 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
|
return React.cloneElement(child, {
|
||||||
// * @param {ReactElement} rootEl - the original children element
|
ref: this.setRef,
|
||||||
// * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions
|
style: {
|
||||||
// */
|
wordWrap: 'break-word',
|
||||||
// tryToFit(rootEl, splitDirections) {
|
...child.props.style
|
||||||
// if (!rootEl.props.children) {
|
}
|
||||||
// // no markup in container
|
});
|
||||||
// return;
|
}
|
||||||
// }
|
|
||||||
|
truncateOriginalText() {
|
||||||
// const newRootEl = this.split(rootEl, splitDirections, /* isRootEl */ true);
|
this.endFound = false;
|
||||||
|
this.splitDirectionSeq = [SPLIT.LEFT];
|
||||||
// let ellipsis =
|
this.wasLastCharTested = false;
|
||||||
// typeof this.props.ellipsis === 'function'
|
|
||||||
// ? this.props.ellipsis(newRootEl)
|
this.tryToFit(this.origText, this.splitDirectionSeq);
|
||||||
// : this.props.ellipsis;
|
}
|
||||||
|
|
||||||
// ellipsis =
|
/**
|
||||||
// typeof ellipsis === 'object'
|
* Splits rootEl based on instructions and updates React's state with the returned element
|
||||||
// ? React.cloneElement(ellipsis, { key: 'ellipsis' })
|
* After React rerenders the new text, we'll check if the new text fits in componentDidUpdate
|
||||||
// : ellipsis;
|
* @param {ReactElement} rootEl - the original children element
|
||||||
|
* @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions
|
||||||
// const newChildren = newRootEl.props.children;
|
*/
|
||||||
// const newChildrenWithEllipsis = [].concat(newChildren, ellipsis);
|
tryToFit(rootEl, splitDirections) {
|
||||||
|
if (!rootEl.props.children) {
|
||||||
// // edge case tradeoff EC#1 - on initial render it doesn't fit in the requested number of lines (1) so it starts truncating
|
// no markup in container
|
||||||
// // - because of truncating and the ellipsis position, div#lvl2 will have display set to 'inline-block',
|
return;
|
||||||
// // 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>
|
const newRootEl = this.split(rootEl, splitDirections, /* isRootEl */ true);
|
||||||
// //
|
|
||||||
// // Example:
|
let ellipsis =
|
||||||
// // <TruncateMarkup lines={1}>
|
typeof this.props.ellipsis === 'function'
|
||||||
// // <div>
|
? this.props.ellipsis(newRootEl)
|
||||||
// // foo
|
: this.props.ellipsis;
|
||||||
// // <div id="lvl2">bar</div>
|
|
||||||
// // </div>
|
ellipsis =
|
||||||
// // </TruncateMarkup>
|
typeof ellipsis === 'object'
|
||||||
// const shouldRenderEllipsis =
|
? React.cloneElement(ellipsis, { key: 'ellipsis' })
|
||||||
// toString(newChildren) !== this.toStringMemo(this.props.children);
|
: ellipsis;
|
||||||
|
|
||||||
// this.setState({
|
const newChildren = newRootEl.props.children;
|
||||||
// text: {
|
const newChildrenWithEllipsis = [].concat(newChildren, ellipsis);
|
||||||
// ...newRootEl,
|
|
||||||
// props: {
|
// edge case tradeoff EC#1 - on initial render it doesn't fit in the requested number of lines (1) so it starts truncating
|
||||||
// ...newRootEl.props,
|
// - because of truncating and the ellipsis position, div#lvl2 will have display set to 'inline-block',
|
||||||
// children: shouldRenderEllipsis ? newChildrenWithEllipsis : newChildren
|
// 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>
|
||||||
// * Splits JSX node based on its type
|
// foo
|
||||||
// * @param {null|string|Array|Object} node - JSX node
|
// <div id="lvl2">bar</div>
|
||||||
// * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions
|
// </div>
|
||||||
// * @return {null|string|Array|Object} - split JSX node
|
// </TruncateMarkup>
|
||||||
// */
|
const shouldRenderEllipsis =
|
||||||
// split(node, splitDirections, isRoot = false, level = 1) {
|
toString(newChildren) !== this.toStringMemo(this.props.children);
|
||||||
// if (!node || isAtomComponent(node)) {
|
|
||||||
// this.endFound = true;
|
this.setState({
|
||||||
|
text: {
|
||||||
// return node;
|
...newRootEl,
|
||||||
// } else if (typeof node === 'string') {
|
props: {
|
||||||
// return this.splitString(node, splitDirections, level);
|
...newRootEl.props,
|
||||||
// } else if (Array.isArray(node)) {
|
children: shouldRenderEllipsis ? newChildrenWithEllipsis : newChildren
|
||||||
// return this.splitArray(node, splitDirections, level);
|
}
|
||||||
// }
|
}
|
||||||
|
});
|
||||||
// const newChildren = this.split(
|
}
|
||||||
// node.props.children,
|
|
||||||
// splitDirections,
|
/**
|
||||||
// /* isRoot */ false,
|
* Splits JSX node based on its type
|
||||||
// level + 1
|
* @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
|
||||||
// return cloneWithChildren(node, newChildren, isRoot, level);
|
*/
|
||||||
// }
|
split(node, splitDirections, isRoot = false, level = 1) {
|
||||||
|
if (!node || isAtomComponent(node)) {
|
||||||
// splitString(string, splitDirections = [], level) {
|
this.endFound = true;
|
||||||
// if (!splitDirections.length) {
|
|
||||||
// return string;
|
return node;
|
||||||
// }
|
} else if (typeof node === 'string') {
|
||||||
|
return this.splitString(node, splitDirections, level);
|
||||||
// if (splitDirections.length && this.policy.isAtomic(string)) {
|
} else if (Array.isArray(node)) {
|
||||||
// // allow for an extra render test with the current character included
|
return this.splitArray(node, splitDirections, level);
|
||||||
// // 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) {
|
const newChildren = this.split(
|
||||||
// this.wasLastCharTested = true;
|
node.props.children,
|
||||||
// } else {
|
splitDirections,
|
||||||
// // we are trying to split further but we have nowhere to go now
|
/* isRoot */ false,
|
||||||
// // that means we've already found the max subtree that fits the container
|
level + 1
|
||||||
// this.endFound = true;
|
);
|
||||||
// }
|
|
||||||
|
return cloneWithChildren(node, newChildren, isRoot, level);
|
||||||
// return string;
|
}
|
||||||
// }
|
|
||||||
|
splitString(string, splitDirections = [], level) {
|
||||||
// if (this.policy.tokenizeString) {
|
if (!splitDirections.length) {
|
||||||
// const wordsArray = this.splitArray(
|
return string;
|
||||||
// this.policy.tokenizeString(string),
|
}
|
||||||
// splitDirections,
|
|
||||||
// level
|
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
|
||||||
// // in order to preserve the input structure
|
// NOTE could be removed once EC#1 is taken care of
|
||||||
// return wordsArray.join('');
|
if (!this.wasLastCharTested) {
|
||||||
// }
|
this.wasLastCharTested = true;
|
||||||
|
} else {
|
||||||
// const [splitDirection, ...restSplitDirections] = splitDirections;
|
// we are trying to split further but we have nowhere to go now
|
||||||
// const pivotIndex = Math.ceil(string.length / 2);
|
// that means we've already found the max subtree that fits the container
|
||||||
// const beforeString = string.substring(0, pivotIndex);
|
this.endFound = true;
|
||||||
|
}
|
||||||
// if (splitDirection === SPLIT.LEFT) {
|
|
||||||
// return this.splitString(beforeString, restSplitDirections, level);
|
return string;
|
||||||
// }
|
}
|
||||||
// const afterString = string.substring(pivotIndex);
|
|
||||||
|
if (this.policy.tokenizeString) {
|
||||||
// return beforeString + this.splitString(afterString, restSplitDirections, level);
|
const wordsArray = this.splitArray(
|
||||||
// }
|
this.policy.tokenizeString(string),
|
||||||
|
splitDirections,
|
||||||
// splitArray(array, splitDirections = [], level) {
|
level
|
||||||
// if (!splitDirections.length) {
|
);
|
||||||
// return array;
|
|
||||||
// }
|
// in order to preserve the input structure
|
||||||
|
return wordsArray.join('');
|
||||||
// if (array.length === 1) {
|
}
|
||||||
// return [this.split(array[0], splitDirections, /* isRoot */ false, level)];
|
|
||||||
// }
|
const [splitDirection, ...restSplitDirections] = splitDirections;
|
||||||
|
const pivotIndex = Math.ceil(string.length / 2);
|
||||||
// const [splitDirection, ...restSplitDirections] = splitDirections;
|
const beforeString = string.substring(0, pivotIndex);
|
||||||
// const pivotIndex = Math.ceil(array.length / 2);
|
|
||||||
// const beforeArray = array.slice(0, pivotIndex);
|
if (splitDirection === SPLIT.LEFT) {
|
||||||
|
return this.splitString(beforeString, restSplitDirections, level);
|
||||||
// if (splitDirection === SPLIT.LEFT) {
|
}
|
||||||
// return this.splitArray(beforeArray, restSplitDirections, level);
|
const afterString = string.substring(pivotIndex);
|
||||||
// }
|
|
||||||
// const afterArray = array.slice(pivotIndex);
|
return beforeString + this.splitString(afterString, restSplitDirections, level);
|
||||||
|
}
|
||||||
// return beforeArray.concat(this.splitArray(afterArray, restSplitDirections, level));
|
|
||||||
// }
|
splitArray(array, splitDirections = [], level) {
|
||||||
|
if (!splitDirections.length) {
|
||||||
// fits() {
|
return array;
|
||||||
// const { lines: maxLines } = this.props;
|
}
|
||||||
// const { height } = this.el.getBoundingClientRect();
|
|
||||||
// const computedLines = Math.round(height / parseFloat(this.lineHeight));
|
if (array.length === 1) {
|
||||||
|
return [this.split(array[0], splitDirections, /* isRoot */ false, level)];
|
||||||
// return maxLines >= computedLines;
|
}
|
||||||
// }
|
|
||||||
|
const [splitDirection, ...restSplitDirections] = splitDirections;
|
||||||
// render() {
|
const pivotIndex = Math.ceil(array.length / 2);
|
||||||
// return this.state.text;
|
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"
|
class="h-full w-full"
|
||||||
data-selectable=""
|
data-selectable=""
|
||||||
data-selectable-index={props.index}
|
data-selectable-index={props.index}
|
||||||
data-selectable-id={itemId}
|
data-selectable-id={itemId()}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
if (explorerView.selectable && !explorer.selectedItems().has(props.item)) {
|
if (explorerView.selectable && !explorer.selectedItems().has(props.item)) {
|
||||||
explorer.resetSelectedItems([props.item]);
|
explorer.resetSelectedItems([props.item]);
|
||||||
|
|
|
@ -16,24 +16,35 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
|
||||||
|
|
||||||
const [offset, setOffset] = Solid.createSignal(0);
|
const [offset, setOffset] = Solid.createSignal(0);
|
||||||
|
|
||||||
const useVisibilityObserver = createVisibilityObserver();
|
// const useVisibilityObserver = createVisibilityObserver();
|
||||||
let loadMoreRef: HTMLDivElement | undefined;
|
let loadMoreRef: HTMLDivElement | undefined;
|
||||||
const inView = useVisibilityObserver(() => loadMoreRef);
|
// const inView = useVisibilityObserver(() => loadMoreRef);
|
||||||
|
|
||||||
const rowVirtualizer = createVirtualizer({
|
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() {
|
get scrollMargin() {
|
||||||
return offset();
|
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 width = Solid.createMemo(() => columnVirtualizer.getTotalSize());
|
||||||
const height = rowVirtualizer.getTotalSize();
|
const height = Solid.createMemo(() => rowVirtualizer.getTotalSize());
|
||||||
|
|
||||||
const internalWidth = () => width - (grid().padding.left + grid().padding.right);
|
const internalWidth = () => width() - (grid().padding.left + grid().padding.right);
|
||||||
const internalHeight = () => height - (grid().padding.top + grid().padding.bottom);
|
const internalHeight = () => height() - (grid().padding.top + grid().padding.bottom);
|
||||||
|
|
||||||
const loadMoreTriggerHeight = Solid.createMemo(() => {
|
const loadMoreTriggerHeight = Solid.createMemo(() => {
|
||||||
if (grid().horizontal || !grid().onLoadMore || !grid().rowCount || !grid().totalRowCount)
|
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);
|
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(() => {
|
const loadMoreTriggerWidth = Solid.createMemo(() => {
|
||||||
|
@ -76,9 +87,9 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
|
||||||
const loadMoreWidth =
|
const loadMoreWidth =
|
||||||
grid().loadMoreSize ?? columnVirtualizer.scrollElement?.clientWidth ?? 0;
|
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(
|
Solid.createEffect(
|
||||||
|
@ -95,24 +106,32 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
Solid.createEffect(() => {
|
// Solid.createEffect(() => {
|
||||||
if (inView()) grid().onLoadMore?.();
|
// if (inView()) grid().onLoadMore?.();
|
||||||
});
|
// });
|
||||||
|
|
||||||
Solid.createEffect(() => {
|
// Solid.createEffect(() => {
|
||||||
const element = grid().scrollRef();
|
// const element = grid().scrollRef();
|
||||||
if (!element) return;
|
// if (!element) return;
|
||||||
|
|
||||||
const observer = new MutationObserver(() => setOffset(ref?.offsetTop ?? 0));
|
// const observer = new MutationObserver(() => setOffset(ref?.offsetTop ?? 0));
|
||||||
|
|
||||||
observer.observe(element, {
|
// observer.observe(element, {
|
||||||
childList: true
|
// 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -121,8 +140,8 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
|
||||||
style={{
|
style={{
|
||||||
...props.style,
|
...props.style,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: width.toString(),
|
width: `${width()}px`,
|
||||||
height: height.toString()
|
height: `${height()}px`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Solid.Show when={internalWidth() > 0 || internalHeight() > 0}>
|
<Solid.Show when={internalWidth() > 0 || internalHeight() > 0}>
|
||||||
|
@ -142,47 +161,61 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
|
||||||
{(virtualRow) => (
|
{(virtualRow) => (
|
||||||
<Solid.For each={columnVirtualizer.getVirtualItems()}>
|
<Solid.For each={columnVirtualizer.getVirtualItems()}>
|
||||||
{(virtualColumn) => {
|
{(virtualColumn) => {
|
||||||
let index = grid().horizontal
|
const index = Solid.createMemo(() => {
|
||||||
? virtualColumn.index * grid().rowCount + virtualRow.index
|
let index = grid().horizontal
|
||||||
: virtualRow.index * grid().columnCount + virtualColumn.index;
|
? 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 (
|
return (
|
||||||
<div
|
<Solid.Show when={index()}>
|
||||||
data-index={index}
|
{(index) => (
|
||||||
style={{
|
<div
|
||||||
'position': 'absolute',
|
data-index={index().value}
|
||||||
'top': 0,
|
style={{
|
||||||
'left': 0,
|
'position': 'absolute',
|
||||||
'width': `${virtualColumn.size}px`,
|
'top': 0,
|
||||||
'height': `${virtualRow.size}px`,
|
'left': 0,
|
||||||
'transform': `translateX(${
|
'width': `${virtualColumn.size}px`,
|
||||||
virtualColumn.start
|
'height': `${virtualRow.size}px`,
|
||||||
}px) translateY(${
|
'transform': `translateX(${
|
||||||
virtualRow.start -
|
virtualColumn.start
|
||||||
rowVirtualizer.options.scrollMargin
|
}px) translateY(${
|
||||||
}px)`,
|
virtualRow.start -
|
||||||
'padding-left':
|
rowVirtualizer.options.scrollMargin
|
||||||
virtualColumn.index !== 0
|
}px)`,
|
||||||
? grid().gap.x.toString()
|
'padding-left':
|
||||||
: 0,
|
virtualColumn.index !== 0
|
||||||
'padding-top':
|
? grid().gap.x.toString()
|
||||||
virtualRow.index !== 0 ? grid().gap.y.toString() : 0
|
: 0,
|
||||||
}}
|
'padding-top':
|
||||||
>
|
virtualRow.index !== 0
|
||||||
<div
|
? grid().gap.y.toString()
|
||||||
style={{
|
: 0
|
||||||
margin: 'auto',
|
}}
|
||||||
width: grid().itemSize.width?.toString() ?? '100%',
|
>
|
||||||
height: grid().itemSize.height?.toString() ?? '100%'
|
<div
|
||||||
}}
|
style={{
|
||||||
>
|
margin: 'auto',
|
||||||
{children(index)}
|
width:
|
||||||
</div>
|
`${grid().itemSize.width?.toString()}px` ??
|
||||||
</div>
|
'100%',
|
||||||
|
height:
|
||||||
|
`${grid().itemSize.height?.toString()}px` ??
|
||||||
|
'100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children(index().value)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Solid.Show>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Solid.For>
|
</Solid.For>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { createElementSize, createResizeObserver } from '@solid-primitives/resize-observer';
|
import { createElementSize, createResizeObserver } from '@solid-primitives/resize-observer';
|
||||||
import { type createVirtualizer } from '@tanstack/solid-virtual';
|
import { type createVirtualizer } from '@tanstack/solid-virtual';
|
||||||
import * as Core from '@virtual-grid/core';
|
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];
|
type VirtualizerOptions = Parameters<typeof createVirtualizer>[0];
|
||||||
|
|
||||||
|
@ -37,59 +38,83 @@ export type CreateGridProps<IdT extends Core.GridItemId, DataT extends Core.Grid
|
||||||
overscan?: number;
|
overscan?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridItemData>({
|
export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridItemData>(
|
||||||
scrollRef,
|
props: Accessor<CreateGridProps<IdT, DataT>>
|
||||||
overscan,
|
) {
|
||||||
...props
|
|
||||||
}: CreateGridProps<IdT, DataT>) {
|
|
||||||
const [width, setWidth] = createSignal(0);
|
const [width, setWidth] = createSignal(0);
|
||||||
|
|
||||||
let staticWidth: number | null = null;
|
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>(() => ({
|
const [rowVirtualizer, setRowVirtualizer] = createStore<VirtualizerOptions>({
|
||||||
...props.rowVirtualizer,
|
...props().rowVirtualizer,
|
||||||
count: grid().totalRowCount,
|
count: grid().totalRowCount,
|
||||||
getScrollElement: scrollRef,
|
getScrollElement: props().scrollRef,
|
||||||
estimateSize: grid().getItemHeight,
|
estimateSize: grid().getItemHeight,
|
||||||
paddingStart: grid().padding.top,
|
paddingStart: grid().padding.top,
|
||||||
paddingEnd: grid().padding.bottom,
|
paddingEnd: grid().padding.bottom,
|
||||||
overscan: overscan ?? props.rowVirtualizer?.overscan
|
overscan: props().overscan ?? props().rowVirtualizer?.overscan
|
||||||
}));
|
});
|
||||||
|
|
||||||
const columnVirtualizer = createMemo<VirtualizerOptions>(() => ({
|
createEffect(() => {
|
||||||
...props.columnVirtualizer,
|
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,
|
horizontal: true,
|
||||||
count: grid().totalColumnCount,
|
count: grid().totalColumnCount,
|
||||||
getScrollElement: scrollRef,
|
getScrollElement: props().scrollRef,
|
||||||
estimateSize: grid().getItemWidth,
|
estimateSize: grid().getItemWidth,
|
||||||
paddingStart: grid().padding.left,
|
paddingStart: grid().padding.left,
|
||||||
paddingEnd: grid().padding.right,
|
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(
|
const isStatic = createMemo(
|
||||||
() =>
|
() =>
|
||||||
props.width !== undefined ||
|
props().width !== undefined ||
|
||||||
props.horizontal ||
|
props().horizontal ||
|
||||||
props.columns === 0 ||
|
props().columns === 0 ||
|
||||||
(props.columns === 'auto'
|
(props().columns === 'auto'
|
||||||
? !props.size || (typeof props.size === 'object' && !props.size.width)
|
? !props().size || (typeof props().size === 'object' && !props().size.width)
|
||||||
: (props.columns === undefined || props.columns) &&
|
: (props().columns === undefined || props().columns) &&
|
||||||
((typeof props.size === 'object' && props.size.width) ||
|
((typeof props().size === 'object' && props().size.width) ||
|
||||||
typeof props.size === 'number'))
|
typeof props().size === 'number'))
|
||||||
);
|
);
|
||||||
|
|
||||||
createElementSize;
|
createResizeObserver(
|
||||||
|
() => props().scrollRef(),
|
||||||
createResizeObserver(scrollRef, ({ width }) => {
|
({ width }) => {
|
||||||
if (width === undefined || isStatic()) {
|
if (width === undefined || isStatic()) {
|
||||||
if (width !== undefined) staticWidth = width;
|
if (width !== undefined) staticWidth = width;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
setWidth(width);
|
||||||
}
|
}
|
||||||
setWidth(width);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (staticWidth === null || width() === staticWidth || isStatic()) return;
|
if (staticWidth === null || width() === staticWidth || isStatic()) return;
|
||||||
|
@ -99,12 +124,12 @@ export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridI
|
||||||
|
|
||||||
return createMemo(() => ({
|
return createMemo(() => ({
|
||||||
...grid(),
|
...grid(),
|
||||||
scrollRef: scrollRef,
|
scrollRef: props().scrollRef,
|
||||||
onLoadMore: props.onLoadMore,
|
onLoadMore: props().onLoadMore,
|
||||||
loadMoreSize: props.loadMoreSize,
|
loadMoreSize: props().loadMoreSize,
|
||||||
virtualizer: {
|
virtualizer: {
|
||||||
rowVirtualizer: rowVirtualizer,
|
rowVirtualizer,
|
||||||
columnVirtualizer: columnVirtualizer
|
columnVirtualizer
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// import { Grid, useGrid } from '@virtual-grid/react';
|
// 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 { type ExplorerItem } from '@sd/client';
|
||||||
|
|
||||||
import { useExplorerContext } from '../../Context';
|
import { useExplorerContext } from '../../Context';
|
||||||
|
@ -18,16 +18,15 @@ export type RenderItem = (item: {
|
||||||
}) => JSX.Element;
|
}) => JSX.Element;
|
||||||
|
|
||||||
const CHROME_REGEX = /Chrome/;
|
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 os = useOperatingSystem();
|
||||||
// const realOS = useOperatingSystem(true);
|
// const realOS = useOperatingSystem(true);
|
||||||
|
|
||||||
const isChrome = CHROME_REGEX.test(navigator.userAgent);
|
|
||||||
|
|
||||||
const explorer = useExplorerContext();
|
const explorer = useExplorerContext();
|
||||||
const explorerView = useExplorerViewContext();
|
const explorerView = useExplorerViewContext();
|
||||||
const explorerSettings = explorer.useSettingsSnapshot();
|
// const explorerSettings = explorer.useSettingsSnapshot();
|
||||||
// const quickPreviewStore = useQuickPreviewStore();
|
// const quickPreviewStore = useQuickPreviewStore();
|
||||||
|
|
||||||
// const selecto = useRef<Selecto>(null);
|
// const selecto = useRef<Selecto>(null);
|
||||||
|
@ -43,34 +42,34 @@ export default ({ children }: { children: RenderItem }) => {
|
||||||
|
|
||||||
const [dragFromThumbnail, setDragFromThumbnail] = createSignal(false);
|
const [dragFromThumbnail, setDragFromThumbnail] = createSignal(false);
|
||||||
|
|
||||||
const itemDetailsHeight = 44 + (explorerSettings.showBytesInGridView ? 20 : 0);
|
const itemDetailsHeight = 44 + (false ? 20 : 0);
|
||||||
const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight;
|
// const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight;
|
||||||
const padding = explorerSettings.layoutMode === 'grid' ? 12 : 0;
|
const itemHeight = 100 + itemDetailsHeight;
|
||||||
|
// const padding = explorerSettings.layoutMode === 'grid' ? 12 : 0;
|
||||||
|
|
||||||
const grid = createGrid({
|
const grid = createGrid(() => ({
|
||||||
scrollRef: explorer.scrollRef,
|
scrollRef: explorer.scrollRef,
|
||||||
count: explorer.items?.length ?? 0,
|
count: explorer.items()?.length ?? 0,
|
||||||
totalCount: explorer.count,
|
totalCount: explorer.count,
|
||||||
...(explorerSettings.layoutMode === 'grid'
|
// ...(explorerSettings.layoutMode === 'grid'
|
||||||
? {
|
// ?
|
||||||
columns: 'auto',
|
columns: 'auto',
|
||||||
size: { width: explorerSettings.gridItemSize, height: itemHeight }
|
size: { width: 100, height: itemHeight },
|
||||||
}
|
// : { columns: explorerSettings.mediaColumns }),
|
||||||
: { columns: explorerSettings.mediaColumns }),
|
rowVirtualizer: { overscan: explorer.overscan ?? 10 },
|
||||||
rowVirtualizer: { overscan: explorer.overscan ?? 5 },
|
|
||||||
onLoadMore: explorer.loadMore,
|
onLoadMore: explorer.loadMore,
|
||||||
getItemId: (index: number) => {
|
getItemId: (index) => {
|
||||||
const item = explorer.items()?.[index];
|
const item = explorer.items()?.[index];
|
||||||
return item ? uniqueId(item) : undefined;
|
return item ? uniqueId(item) : undefined;
|
||||||
},
|
},
|
||||||
getItemData: (index: number) => explorer.items()?.[index],
|
getItemData: (index) => explorer.items()?.[index]
|
||||||
padding: {
|
// padding: {
|
||||||
bottom: explorerView.bottom ? padding + explorerView.bottom : undefined,
|
// bottom: explorerView.bottom ? padding + explorerView.bottom : undefined,
|
||||||
x: padding,
|
// x: padding,
|
||||||
y: padding
|
// y: padding
|
||||||
},
|
// },
|
||||||
gap: explorerSettings.layoutMode === 'grid' ? explorerSettings.gridGap : 1
|
// gap: explorerSettings.layoutMode === 'grid' ? explorerSettings.gridGap : 1
|
||||||
});
|
}));
|
||||||
|
|
||||||
const getElementById = (id: string) => {
|
const getElementById = (id: string) => {
|
||||||
if (!explorer.parent) return;
|
if (!explorer.parent) return;
|
||||||
|
@ -115,57 +114,6 @@ export default ({ children }: { children: RenderItem }) => {
|
||||||
return activeItem;
|
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(() => {
|
createEffect(() => {
|
||||||
if (explorer.selectedItems().size !== 0) return;
|
if (explorer.selectedItems().size !== 0) return;
|
||||||
|
|
||||||
|
@ -179,165 +127,6 @@ export default ({ children }: { children: RenderItem }) => {
|
||||||
// selecto.current?.setSelectedTargets([]);
|
// 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 (
|
return (
|
||||||
<GridContext.Provider
|
<GridContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -346,278 +135,39 @@ export default ({ children }: { children: RenderItem }) => {
|
||||||
getElementById
|
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}>
|
<VirtualGrid grid={grid}>
|
||||||
{(index) => {
|
{(index) => {
|
||||||
const item = explorer.items()?.[index];
|
const item = createMemo(() => explorer.items()?.[index]);
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridItem
|
<Show when={item()}>
|
||||||
index={index}
|
{(item) => (
|
||||||
item={item}
|
<GridItem
|
||||||
onMouseDown={(e) => {
|
index={index}
|
||||||
if (e.button !== 0 || !explorerView.selectable) return;
|
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) {
|
if (!explorer.allowMultiSelect) {
|
||||||
explorer.resetSelectedItems([item.data]);
|
explorer.resetSelectedItems([item.data]);
|
||||||
} else {
|
} else {
|
||||||
// selectoFirstColumn.current = item.column;
|
// selectoFirstColumn.current = item.column;
|
||||||
// selectoLastColumn.current = item.column;
|
// selectoLastColumn.current = item.column;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeItem = item.data;
|
activeItem = item.data;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{props.children}
|
||||||
</GridItem>
|
</GridItem>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</VirtualGrid>
|
</VirtualGrid>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { GridViewItem } from './Item';
|
||||||
export const GridView = () => {
|
export const GridView = () => {
|
||||||
return (
|
return (
|
||||||
<Grid>
|
<Grid>
|
||||||
{({ item, selected, cut }) => (
|
{(props) => (
|
||||||
<GridViewItem data={item} selected={selected} cut={cut} />
|
<GridViewItem data={props.item} selected={props.selected} cut={props.cut} />
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { JSX } from 'solid-js';
|
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 { useExplorerContext } from '../Context';
|
||||||
// import { toast } from '@sd/ui';
|
// import { toast } from '@sd/ui';
|
||||||
// import { useIsDark } from '~/hooks';
|
// import { useIsDark } from '~/hooks';
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { ReactiveSet } from '@solid-primitives/set';
|
import { ReactiveSet } from '@solid-primitives/set';
|
||||||
import { type InfiniteQueryObserverResult } from '@tanstack/solid-query';
|
import { type InfiniteQueryObserverResult } from '@tanstack/solid-query';
|
||||||
import { createMemo, createSignal, type Accessor, type ComponentProps } from 'solid-js';
|
import { createMemo, createSignal, type Accessor, type ComponentProps } from 'solid-js';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type {
|
import type {
|
||||||
ExplorerItem,
|
ExplorerItem,
|
||||||
|
@ -36,7 +35,7 @@ export type ExplorerParent =
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UseExplorerProps<TOrder extends Ordering> {
|
export interface UseExplorerProps<TOrder extends Ordering> {
|
||||||
items: () => ExplorerItem[] | null;
|
items: Accessor<ExplorerItem[] | null>;
|
||||||
count?: number;
|
count?: number;
|
||||||
parent?: ExplorerParent;
|
parent?: ExplorerParent;
|
||||||
loadMore?: () => void;
|
loadMore?: () => void;
|
||||||
|
@ -52,7 +51,7 @@ export interface UseExplorerProps<TOrder extends Ordering> {
|
||||||
* @defaultValue `true`
|
* @defaultValue `true`
|
||||||
*/
|
*/
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
settings: ReturnType<typeof useExplorerSettings<TOrder>>;
|
settings: ReturnType<typeof createExplorerSettings<TOrder>>;
|
||||||
/**
|
/**
|
||||||
* @defaultValue `true`
|
* @defaultValue `true`
|
||||||
*/
|
*/
|
||||||
|
@ -64,38 +63,38 @@ export interface UseExplorerProps<TOrder extends Ordering> {
|
||||||
* Controls top-level config and state for the explorer.
|
* Controls top-level config and state for the explorer.
|
||||||
* View- and inspector-specific state is not handled here.
|
* View- and inspector-specific state is not handled here.
|
||||||
*/
|
*/
|
||||||
export function createExplorer<TOrder extends Ordering>({
|
export function createExplorer<TOrder extends Ordering>(props: UseExplorerProps<TOrder>) {
|
||||||
settings,
|
|
||||||
layouts,
|
|
||||||
...props
|
|
||||||
}: UseExplorerProps<TOrder>) {
|
|
||||||
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | null>(null);
|
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
return {
|
return createMemo(() => ({
|
||||||
|
// Provided values
|
||||||
|
...props,
|
||||||
// Default values
|
// Default values
|
||||||
allowMultiSelect: true,
|
allowMultiSelect: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
setScrollRef,
|
setScrollRef,
|
||||||
count: props.items?.length,
|
get count() {
|
||||||
|
return props.items()?.length;
|
||||||
|
},
|
||||||
showPathBar: true,
|
showPathBar: true,
|
||||||
layouts: {
|
layouts: {
|
||||||
grid: true,
|
grid: true,
|
||||||
list: true,
|
list: true,
|
||||||
media: true,
|
media: true,
|
||||||
...layouts
|
...props.layouts
|
||||||
},
|
},
|
||||||
...settings,
|
...props.settings,
|
||||||
// Provided values
|
|
||||||
...props,
|
|
||||||
// Selected items
|
// 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,
|
settings,
|
||||||
onSettingsChanged,
|
onSettingsChanged,
|
||||||
orderingKeys,
|
orderingKeys,
|
||||||
|
@ -110,40 +109,40 @@ export function useExplorerSettings<TOrder extends Ordering>({
|
||||||
}) {
|
}) {
|
||||||
// const [store] = useState(() => proxy(settings));
|
// const [store] = useState(() => proxy(settings));
|
||||||
|
|
||||||
const updateSettings = useDebouncedCallback(
|
// const updateSettings = useDebouncedCallback(
|
||||||
(settings: ExplorerSettings<TOrder>, location: Location) => {
|
// (settings: ExplorerSettings<TOrder>, location: Location) => {
|
||||||
onSettingsChanged?.(settings, location);
|
// onSettingsChanged?.(settings, location);
|
||||||
},
|
// },
|
||||||
500
|
// 500
|
||||||
);
|
// );
|
||||||
|
|
||||||
useEffect(() => updateSettings.flush(), [location, updateSettings]);
|
// useEffect(() => updateSettings.flush(), [location, updateSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (updateSettings.isPending()) return;
|
// if (updateSettings.isPending()) return;
|
||||||
Object.assign(store, settings);
|
// Object.assign(store, settings);
|
||||||
}, [settings, store, updateSettings]);
|
// }, [settings, store, updateSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
// (() => {
|
||||||
if (!onSettingsChanged || !location) return;
|
// if (!onSettingsChanged || !location) return;
|
||||||
const unsubscribe = subscribe(store, () => {
|
// const unsubscribe = subscribe(store, () => {
|
||||||
updateSettings(snapshot(store) as ExplorerSettings<TOrder>, location);
|
// updateSettings(snapshot(store) as ExplorerSettings<TOrder>, location);
|
||||||
});
|
// });
|
||||||
return () => unsubscribe();
|
// return () => unsubscribe();
|
||||||
}, [store, updateSettings, location, onSettingsChanged]);
|
// }, [store, updateSettings, location, onSettingsChanged]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useSettingsSnapshot: () => useSnapshot(store),
|
// useSettingsSnapshot: () => useSnapshot(store),
|
||||||
settingsStore: store,
|
// settingsStore: store,
|
||||||
orderingKeys
|
orderingKeys
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseExplorerSettings<TOrder extends Ordering> = ReturnType<
|
export type CreateExplorerSettings<TOrder extends Ordering> = ReturnType<
|
||||||
typeof useExplorerSettings<TOrder>
|
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
|
// 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
|
// WeakMap ensures that ExplorerItems aren't held onto after they're evicted from cache
|
||||||
const itemHashesWeakMap = new WeakMap<ExplorerItem, string>();
|
const itemHashesWeakMap = new WeakMap<ExplorerItem, string>();
|
||||||
|
@ -153,7 +152,7 @@ function createSelectedItems(items: () => ExplorerItem[] | null) {
|
||||||
const selectedItemHashes = new ReactiveSet<string>();
|
const selectedItemHashes = new ReactiveSet<string>();
|
||||||
|
|
||||||
const itemsMap = createMemo(() =>
|
const itemsMap = createMemo(() =>
|
||||||
(items() ?? []).reduce((items, item) => {
|
items().reduce((items, item) => {
|
||||||
const hash = itemHashesWeakMap.get(item) ?? uniqueId(item);
|
const hash = itemHashesWeakMap.get(item) ?? uniqueId(item);
|
||||||
itemHashesWeakMap.set(item, hash);
|
itemHashesWeakMap.set(item, hash);
|
||||||
items.set(hash, item);
|
items.set(hash, item);
|
|
@ -8,7 +8,7 @@ export function Explorer() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={explorer.scrollRef}
|
ref={explorer.setScrollRef}
|
||||||
class="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden"
|
class="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden"
|
||||||
style={
|
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 { Explorer } from './Explorer';
|
||||||
import { createExplorer } from './Explorer/useExplorer';
|
import { ExplorerContextProvider } from './Explorer/Context';
|
||||||
import { createLibraryQuery, useRspcLibraryContext } from './rspc';
|
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() {
|
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 ctx = useRspcLibraryContext();
|
||||||
|
|
||||||
const query = createInfiniteQuery({
|
const paths = createPathsExplorerQuery({ arg: { take: 100 } });
|
||||||
queryKey: ['search.paths', { library_id: library.uuid, arg }] as const,
|
|
||||||
queryFn: async ({ queryKey: [_, { arg }] }) => {
|
const explorer = createExplorer<FilePathOrder>({
|
||||||
const result = await ctx!.client.query(['search.paths', arg]);
|
...paths,
|
||||||
return result;
|
settings: { orderingKeys: filePathOrderingKeysSchema },
|
||||||
},
|
isFetchingNextPage: paths.query.isFetchingNextPage,
|
||||||
getNextPageParam: (lastPage) => {
|
parent: {
|
||||||
if (arg.take === null || arg.take === undefined) return undefined;
|
type: 'Location',
|
||||||
if (lastPage.items.length < arg.take) return undefined;
|
get location() {
|
||||||
else return lastPage.nodes[arg.take - 1];
|
return props.location;
|
||||||
},
|
}
|
||||||
...args
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const count = createLibraryQuery(['search.pathsCount', { filters: props.arg.filters }], {
|
return (
|
||||||
enabled: query.isSuccess
|
<div class="flex h-screen w-screen">
|
||||||
});
|
<ExplorerContextProvider explorer={explorer()}>
|
||||||
|
<Explorer />
|
||||||
const explorer = createExplorer();
|
</ExplorerContextProvider>
|
||||||
|
</div>
|
||||||
return <Explorer />;
|
);
|
||||||
}
|
}
|
||||||
|
|
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>
|
<!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 { 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 { createReactQueryHooks, type Context } from '@rspc/solid';
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import { createContext, useContext, type ParentProps } from 'solid-js';
|
import { createContext, useContext, type ParentProps } from 'solid-js';
|
||||||
import { match, P } from 'ts-pattern';
|
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> =
|
type NonLibraryProcedure<T extends keyof Procedures> =
|
||||||
| Exclude<Procedures[T], { input: LibraryArgs<any> }>
|
| 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
|
// 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 (
|
return (
|
||||||
<libraryHooks.Provider client={libraryClient as any} queryClient={queryClient}>
|
<libraryHooks.Provider client={libraryClient as any} queryClient={props.queryClient}>
|
||||||
<nonLibraryHooks.Provider client={nonLibraryClient as any} queryClient={queryClient}>
|
<nonLibraryHooks.Provider
|
||||||
{children as any}
|
client={nonLibraryClient as any}
|
||||||
|
queryClient={props.queryClient}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
</nonLibraryHooks.Provider>
|
</nonLibraryHooks.Provider>
|
||||||
</libraryHooks.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} */
|
import tailwindFactory from '@sd/ui/tailwind';
|
||||||
export default {
|
|
||||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
export default tailwindFactory('web');
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "VITE_SD_DEMO_MODE=true playwright test",
|
"test": "VITE_SD_DEMO_MODE=true playwright test",
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
"@astrojs/solid-js": "^3.0.2",
|
"@astrojs/solid-js": "^3.0.2",
|
||||||
"@astrojs/tailwind": "^5.0.3",
|
"@astrojs/tailwind": "^5.0.3",
|
||||||
"@fontsource/inter": "^4.5.15",
|
"@fontsource/inter": "^4.5.15",
|
||||||
"@rspc/client": "0.0.0-main-45466c86",
|
"@rspc/client": "0.0.0-main-b8b35d28",
|
||||||
"@sd/client": "workspace:*",
|
"@sd/client": "workspace:*",
|
||||||
"@sd/interface": "workspace:*",
|
"@sd/interface": "workspace:*",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@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"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rspc/client": "0.0.0-main-45466c86",
|
"@rspc/client": "0.0.0-main-b8b35d28",
|
||||||
"@rspc/react": "0.0.0-main-45466c86",
|
"@rspc/react": "0.0.0-main-b8b35d28",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^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 { defineConfig } from 'vite';
|
||||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||||
// import solid from 'vite-plugin-solid';
|
// import solid from 'vite-plugin-solid';
|
||||||
|
@ -8,12 +8,9 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
// react({
|
react({
|
||||||
// exclude: ['**/*.solid.*']
|
exclude: ['**/*.solid.*']
|
||||||
// }),
|
}),
|
||||||
// solid({
|
|
||||||
// include: ['**/*.solid.*']
|
|
||||||
// }),
|
|
||||||
svg({ svgrOptions: { icon: true } }),
|
svg({ svgrOptions: { icon: true } }),
|
||||||
createHtmlPlugin({
|
createHtmlPlugin({
|
||||||
minify: true
|
minify: true
|
||||||
|
|
|
@ -81,11 +81,11 @@ importers:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.14.0
|
version: 1.14.0
|
||||||
'@rspc/client':
|
'@rspc/client':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86
|
version: 0.0.0-main-b8b35d28
|
||||||
'@rspc/tauri':
|
'@rspc/tauri':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86(@tauri-apps/api@1.5.1)
|
version: 0.0.0-main-b8b35d28(@tauri-apps/api@1.5.1)
|
||||||
'@sd/client':
|
'@sd/client':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/client
|
version: link:../../packages/client
|
||||||
|
@ -350,11 +350,11 @@ importers:
|
||||||
specifier: ^6.3.20
|
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)
|
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':
|
'@rspc/client':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86
|
version: 0.0.0-main-b8b35d28
|
||||||
'@rspc/react':
|
'@rspc/react':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0)
|
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':
|
'@sd/assets':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/assets
|
version: link:../../packages/assets
|
||||||
|
@ -576,8 +576,8 @@ importers:
|
||||||
specifier: ^4.5.15
|
specifier: ^4.5.15
|
||||||
version: 4.5.15
|
version: 4.5.15
|
||||||
'@rspc/client':
|
'@rspc/client':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86
|
version: 0.0.0-main-b8b35d28
|
||||||
'@sd/client':
|
'@sd/client':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/client
|
version: link:../../packages/client
|
||||||
|
@ -670,14 +670,17 @@ importers:
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3(astro@4.0.5)(tailwindcss@3.3.6)
|
version: 5.0.3(astro@4.0.5)(tailwindcss@3.3.6)
|
||||||
'@rspc/solid':
|
'@rspc/solid':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7)
|
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':
|
'@sd/assets':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/assets
|
version: link:../../packages/assets
|
||||||
'@sd/client':
|
'@sd/client':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/client
|
version: link:../../packages/client
|
||||||
|
'@sd/ui':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/ui
|
||||||
'@solid-primitives/event-listener':
|
'@solid-primitives/event-listener':
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.3.0(solid-js@1.8.7)
|
version: 2.3.0(solid-js@1.8.7)
|
||||||
|
@ -940,11 +943,11 @@ importers:
|
||||||
packages/client:
|
packages/client:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rspc/client':
|
'@rspc/client':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86
|
version: 0.0.0-main-b8b35d28
|
||||||
'@rspc/react':
|
'@rspc/react':
|
||||||
specifier: 0.0.0-main-45466c86
|
specifier: 0.0.0-main-b8b35d28
|
||||||
version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0)
|
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':
|
'@tanstack/react-query':
|
||||||
specifier: ^4.36.1
|
specifier: ^4.36.1
|
||||||
version: 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0)
|
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
|
magic-string: 0.27.0
|
||||||
react-docgen-typescript: 2.2.2(typescript@5.3.3)
|
react-docgen-typescript: 2.2.2(typescript@5.3.3)
|
||||||
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:
|
/@jridgewell/gen-mapping@0.3.3:
|
||||||
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
||||||
|
@ -7154,40 +7157,40 @@ packages:
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rspc/client@0.0.0-main-45466c86:
|
/@rspc/client@0.0.0-main-b8b35d28:
|
||||||
resolution: {integrity: sha512-1a3+jSJXXcHyoMYrqlb5DModVf5m7S4Y7a6BaUHmkhfXG4rttvthFHNAU1ODMpbI371egEePFhiVR8SnPsXe6Q==}
|
resolution: {integrity: sha512-wXBZ+KDBzBfXXKr2GWAe/UF+D5jLl1vM7mBTuFJsEV4ihquu2hzxAQBPuBE3j6JC7SIWzhQ+hodzGFUCL04Rsg==}
|
||||||
dev: false
|
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):
|
/@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-4b3SBm6KFS0fZ0JwdZPFZeb+ZG/FgXn3Qb4DJga5ioGr11YsDGMgeEyaxfC8dLSHtT5tj8vFqX/a6LoWk0Vqbw==}
|
resolution: {integrity: sha512-yjKtZkziLvUI3AyKAdN8l32O3yS2FBqKda24MWDcN/YWjFVw15Enfr9C2bGtuqrZZ/LKabrG67EnKWqGvTWkCQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@rspc/client': 0.0.0-main-45466c86
|
'@rspc/client': 0.0.0-main-b8b35d28
|
||||||
'@tanstack/react-query': ^4.26.0
|
'@tanstack/react-query': ^4.26.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
dependencies:
|
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)
|
'@tanstack/react-query': 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
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):
|
/@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-5rfeivH4I2LrMSx5Igfx2FfLn7F6s5OzVy9VtaJ6C4K/Yh9Y2AufR0yQwEApoUrfexGWbecmmoW+yPbDqaJvpA==}
|
resolution: {integrity: sha512-KjssarTWhoj8chAoESekTfucy/vg/eum+5Ghp1vMfUQWhGQ7f6SOXqeGQefOz0itIBAMCmQn+R03uFm59+GMCQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@rspc/client': 0.0.0-main-45466c86
|
'@rspc/client': 0.0.0-main-b8b35d28
|
||||||
'@tanstack/solid-query': ^4.6.0
|
'@tanstack/solid-query': ^4.6.0
|
||||||
solid-js: ^1.6.11
|
solid-js: ^1.6.11
|
||||||
dependencies:
|
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)
|
'@tanstack/solid-query': 4.36.1(solid-js@1.8.7)
|
||||||
solid-js: 1.8.7
|
solid-js: 1.8.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@rspc/tauri@0.0.0-main-45466c86(@tauri-apps/api@1.5.1):
|
/@rspc/tauri@0.0.0-main-b8b35d28(@tauri-apps/api@1.5.1):
|
||||||
resolution: {integrity: sha512-B+/PcZjxuVTQEtEgBV7UXYwf1uGNbN34+6aqbc5oxxifLDDn0MxVGmEju3Y518sYCfuYaIF8rYDl+oPM9a7NSg==}
|
resolution: {integrity: sha512-uzBBxsP8ENBUs16j5KXni72WKtVdi37uA/+GA1rf2M/qJvnc8W+slXrI4PXL9CBHnGRbwXk/gM1z9AdadqsFRw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tauri-apps/api': ^1.2.0
|
'@tauri-apps/api': ^1.2.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rspc/client': 0.0.0-main-45466c86
|
'@rspc/client': 0.0.0-main-b8b35d28
|
||||||
'@tauri-apps/api': 1.5.1
|
'@tauri-apps/api': 1.5.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
@ -7834,7 +7837,7 @@ packages:
|
||||||
magic-string: 0.30.5
|
magic-string: 0.30.5
|
||||||
rollup: 3.29.4
|
rollup: 3.29.4
|
||||||
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)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -8176,7 +8179,7 @@ packages:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-docgen: 7.0.1
|
react-docgen: 7.0.1
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
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:
|
transitivePeerDependencies:
|
||||||
- '@preact/preset-vite'
|
- '@preact/preset-vite'
|
||||||
- encoding
|
- encoding
|
||||||
|
@ -9304,7 +9307,7 @@ packages:
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6)
|
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6)
|
||||||
magic-string: 0.27.0
|
magic-string: 0.27.0
|
||||||
react-refresh: 0.14.0
|
react-refresh: 0.14.0
|
||||||
vite: 5.0.9(less@4.2.0)
|
vite: 5.0.9(@types/node@18.17.19)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -22537,6 +22540,7 @@ packages:
|
||||||
rollup: 4.9.0
|
rollup: 4.9.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/vite@5.0.9(sass@1.69.5):
|
/vite@5.0.9(sass@1.69.5):
|
||||||
resolution: {integrity: sha512-wVqMd5kp28QWGgfYPDfrj771VyHTJ4UDlCteLH7bJDGDEamaz5hV8IX6h1brSGgnnyf9lI2RnzXq/JmD0c2wwg==}
|
resolution: {integrity: sha512-wVqMd5kp28QWGgfYPDfrj771VyHTJ4UDlCteLH7bJDGDEamaz5hV8IX6h1brSGgnnyf9lI2RnzXq/JmD0c2wwg==}
|
||||||
|
|
Loading…
Reference in a new issue