mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-06 06:13:22 -04:00
@sd/client SolidJS support - Part 1 (#1920)
* `createSolid` + `createPersistedMutable` * Move all Valtio stores to SolidJS * Remove Valtio from `@sd/client` * Missed auth store * wip * `useSolidStore` allow mutable setting * Restructure `@sd/client` a bit * Add `WithSolid` + custom `useObserver` * Parse props both ways * Universal Context * Solid to React context working * Working universal context * context inheritance * whoops * Make it clearer how the demo works * wip: `useUniversalQuery`
This commit is contained in:
@@ -21,7 +21,6 @@ export function createUpdater() {
|
||||
|
||||
listen<UpdateStore>('updater', (e) => {
|
||||
Object.assign(updateStore, e.payload);
|
||||
console.log(updateStore);
|
||||
});
|
||||
|
||||
const onInstallCallbacks = new Set<() => void>();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import { Appearance, NativeEventSubscription } from 'react-native';
|
||||
import { useDeviceContext } from 'twrnc';
|
||||
import { subscribe } from 'valtio';
|
||||
import { getThemeStore } from '@sd/client';
|
||||
import { themeStore, useSubscribeToThemeStore } from '@sd/client';
|
||||
import { changeTwTheme, tw } from '~/lib/tailwind';
|
||||
|
||||
export function useTheme() {
|
||||
@@ -11,20 +10,14 @@ export function useTheme() {
|
||||
|
||||
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribe(getThemeStore(), () => {
|
||||
changeTwTheme(getThemeStore().theme);
|
||||
forceUpdate();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
useSubscribeToThemeStore(() => {
|
||||
changeTwTheme(themeStore.theme);
|
||||
forceUpdate();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let systemThemeListener: NativeEventSubscription | undefined;
|
||||
if (getThemeStore().syncThemeWithSystem === true) {
|
||||
if (themeStore.syncThemeWithSystem === true) {
|
||||
systemThemeListener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
changeTwTheme(colorScheme === 'dark' ? 'dark' : 'vanilla');
|
||||
forceUpdate();
|
||||
|
||||
@@ -4,10 +4,10 @@ import { createContext, useContext } from 'react';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
currentLibraryCache,
|
||||
getOnboardingStore,
|
||||
insertLibrary,
|
||||
onboardingStore,
|
||||
resetOnboardingStore,
|
||||
telemetryStore,
|
||||
telemetryState,
|
||||
useBridgeMutation,
|
||||
useCachedLibraries,
|
||||
useMultiZodForm,
|
||||
@@ -61,7 +61,7 @@ const useFormState = () => {
|
||||
shareTelemetry: 'share-telemetry'
|
||||
}
|
||||
},
|
||||
onData: (data) => (getOnboardingStore().data = data)
|
||||
onData: (data) => (onboardingStore.data = data)
|
||||
});
|
||||
|
||||
const navigation = useNavigation<OnboardingStackScreenProps<any>['navigation']>();
|
||||
@@ -85,7 +85,7 @@ const useFormState = () => {
|
||||
|
||||
// opted to place this here as users could change their mind before library creation/onboarding finalization
|
||||
// it feels more fitting to configure it here (once)
|
||||
telemetryStore.shareFullTelemetry = data.Privacy.shareTelemetry === 'share-telemetry';
|
||||
telemetryState.shareFullTelemetry = data.Privacy.shareTelemetry === 'share-telemetry';
|
||||
|
||||
try {
|
||||
// show creation screen for a bit for smoothness
|
||||
@@ -99,7 +99,7 @@ const useFormState = () => {
|
||||
cache.withNodes(libraryRaw.nodes);
|
||||
const library = cache.withCache(libraryRaw.item);
|
||||
|
||||
if (telemetryStore.shareFullTelemetry) {
|
||||
if (telemetryState.shareFullTelemetry) {
|
||||
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { getDebugState, toggleFeatureFlag, useDebugState, useFeatureFlags } from '@sd/client';
|
||||
import { toggleFeatureFlag, useDebugState, useFeatureFlags } from '@sd/client';
|
||||
import Card from '~/components/layout/Card';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
@@ -17,7 +17,7 @@ const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
||||
<Button onPress={() => toggleFeatureFlag(['p2pPairing'])}>
|
||||
<Text style={tw`text-ink`}>Toggle P2P</Text>
|
||||
</Button>
|
||||
<Button onPress={() => (getDebugState().rspcLogger = !getDebugState().rspcLogger)}>
|
||||
<Button onPress={() => (debugState.rspcLogger = !debugState.rspcLogger)}>
|
||||
<Text style={tw`text-ink`}>Toggle rspc logger</Text>
|
||||
</Button>
|
||||
<Text style={tw`text-ink`}>{JSON.stringify(featureFlags)}</Text>
|
||||
@@ -26,7 +26,7 @@ const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
||||
onPress={() => {
|
||||
navigation.popToTop();
|
||||
navigation.replace('Settings');
|
||||
getDebugState().enabled = false;
|
||||
debugState.enabled = false;
|
||||
}}
|
||||
>
|
||||
<Text style={tw`text-ink`}>Disable Debug Mode</Text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RadixCheckbox, Select, SelectOption, Slider, tw, z } from '@sd/ui';
|
||||
import { getExplorerLayoutStore, useExplorerLayoutStore } from '~/../packages/client/src';
|
||||
import { explorerLayout, useExplorerLayoutStore } from '~/../packages/client/src';
|
||||
import i18n from '~/app/I18n';
|
||||
import { SortOrderSchema } from '~/app/route-schemas';
|
||||
import { useLocale } from '~/hooks';
|
||||
@@ -124,7 +124,7 @@ export default () => {
|
||||
name="showPathBar"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
getExplorerLayoutStore().showPathBar = value;
|
||||
explorerLayout.showPathBar = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
ExplorerItem,
|
||||
getEphemeralPath,
|
||||
getExplorerItemData,
|
||||
getExplorerLayoutStore,
|
||||
getIndexedItemFilePath,
|
||||
ObjectKindKey,
|
||||
useExplorerLayoutStore,
|
||||
@@ -163,7 +162,7 @@ export const QuickPreview = () => {
|
||||
}
|
||||
|
||||
if (!activeItem || !explorer.items) return;
|
||||
if (items.length > 1 && !getExplorerLayoutStore().showImageSlider) return;
|
||||
if (items.length > 1 && !explorerLayoutStore.showImageSlider) return;
|
||||
|
||||
const newSelectedItem =
|
||||
items.length > 1 &&
|
||||
@@ -465,7 +464,7 @@ export const QuickPreview = () => {
|
||||
<Tooltip label={t('show_slider')}>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
(getExplorerLayoutStore().showImageSlider =
|
||||
(explorerLayoutStore.showImageSlider =
|
||||
!explorerLayoutStore.showImageSlider)
|
||||
}
|
||||
className="w-fit px-2 text-[10px]"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { useKeys } from 'rooks';
|
||||
import {
|
||||
ExplorerLayout,
|
||||
getExplorerLayoutStore,
|
||||
explorerLayout,
|
||||
getItemObject,
|
||||
useSelector,
|
||||
type Object
|
||||
@@ -186,7 +186,7 @@ const useShortcuts = () => {
|
||||
useShortcut('showImageSlider', (e) => {
|
||||
if (isRenaming) return;
|
||||
e.stopPropagation();
|
||||
getExplorerLayoutStore().showImageSlider = !getExplorerLayoutStore().showImageSlider;
|
||||
explorerLayout.showImageSlider = !explorerLayout.showImageSlider;
|
||||
});
|
||||
|
||||
useKeys([meta.key, 'KeyN'], () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FolderNotchOpen } from '@phosphor-icons/react';
|
||||
import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react';
|
||||
import {
|
||||
getExplorerLayoutStore,
|
||||
explorerLayout,
|
||||
useExplorerLayoutStore,
|
||||
useLibrarySubscription,
|
||||
useSelector
|
||||
@@ -56,7 +56,7 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
|
||||
useShortcut('showPathBar', (e) => {
|
||||
e.stopPropagation();
|
||||
getExplorerLayoutStore().showPathBar = !layoutStore.showPathBar;
|
||||
explorerLayout.showPathBar = !layoutStore.showPathBar;
|
||||
});
|
||||
|
||||
useShortcut('showInspector', (e) => {
|
||||
|
||||
@@ -3,10 +3,7 @@ import { useNavigate } from 'react-router';
|
||||
import {
|
||||
backendFeatures,
|
||||
features,
|
||||
getDebugState,
|
||||
isEnabled,
|
||||
toggleFeatureFlag,
|
||||
useBridgeMutation,
|
||||
useBridgeQuery,
|
||||
useDebugState,
|
||||
useFeatureFlags,
|
||||
@@ -53,7 +50,7 @@ export default () => {
|
||||
>
|
||||
<Switch
|
||||
checked={debugState.rspcLogger}
|
||||
onClick={() => (getDebugState().rspcLogger = !debugState.rspcLogger)}
|
||||
onClick={() => (debugState.rspcLogger = !debugState.rspcLogger)}
|
||||
/>
|
||||
</Setting>
|
||||
<Setting
|
||||
@@ -70,8 +67,8 @@ export default () => {
|
||||
!debugState.shareFullTelemetry === false &&
|
||||
debugState.telemetryLogging
|
||||
)
|
||||
getDebugState().telemetryLogging = false;
|
||||
getDebugState().shareFullTelemetry = !debugState.shareFullTelemetry;
|
||||
debugState.telemetryLogging = false;
|
||||
debugState.shareFullTelemetry = !debugState.shareFullTelemetry;
|
||||
}}
|
||||
/>
|
||||
</Setting>
|
||||
@@ -89,8 +86,8 @@ export default () => {
|
||||
!debugState.telemetryLogging &&
|
||||
debugState.shareFullTelemetry === false
|
||||
)
|
||||
getDebugState().shareFullTelemetry = true;
|
||||
getDebugState().telemetryLogging = !debugState.telemetryLogging;
|
||||
debugState.shareFullTelemetry = true;
|
||||
debugState.telemetryLogging = !debugState.telemetryLogging;
|
||||
}}
|
||||
/>
|
||||
</Setting>
|
||||
@@ -137,7 +134,7 @@ export default () => {
|
||||
<Select
|
||||
value={debugState.reactQueryDevtools}
|
||||
size="sm"
|
||||
onChange={(value) => (getDebugState().reactQueryDevtools = value as any)}
|
||||
onChange={(value) => (debugState.reactQueryDevtools = value as any)}
|
||||
>
|
||||
<SelectOption value="disabled">Disabled</SelectOption>
|
||||
<SelectOption value="invisible">Invisible</SelectOption>
|
||||
@@ -192,29 +189,35 @@ function InvalidateDebugPanel() {
|
||||
}
|
||||
|
||||
function FeatureFlagSelector() {
|
||||
useFeatureFlags(); // Subscribe to changes
|
||||
const featureFlags = useFeatureFlags();
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root
|
||||
trigger={
|
||||
<Dropdown.Button variant="gray" className="w-full">
|
||||
<span className="truncate">Feature Flags</span>
|
||||
</Dropdown.Button>
|
||||
}
|
||||
className="mt-1 shadow-none data-[side=bottom]:slide-in-from-top-2 dark:divide-menu-selected/30 dark:border-sidebar-line dark:bg-sidebar-box"
|
||||
alignToTrigger
|
||||
>
|
||||
{[...features, ...backendFeatures].map((feat) => (
|
||||
<DropdownMenu.Item
|
||||
key={feat}
|
||||
label={feat}
|
||||
iconProps={{ weight: 'bold', size: 16 }}
|
||||
onClick={() => toggleFeatureFlag(feat)}
|
||||
className="font-medium text-white"
|
||||
icon={isEnabled(feat) ? CheckSquare : undefined}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenu.Root>
|
||||
<>
|
||||
<DropdownMenu.Root
|
||||
trigger={
|
||||
<Dropdown.Button variant="gray" className="w-full">
|
||||
<span className="truncate">Feature Flags</span>
|
||||
</Dropdown.Button>
|
||||
}
|
||||
className="mt-1 shadow-none data-[side=bottom]:slide-in-from-top-2 dark:divide-menu-selected/30 dark:border-sidebar-line dark:bg-sidebar-box"
|
||||
alignToTrigger
|
||||
>
|
||||
{[...features, ...backendFeatures].map((feat) => (
|
||||
<DropdownMenu.Item
|
||||
key={feat}
|
||||
label={feat}
|
||||
iconProps={{ weight: 'bold', size: 16 }}
|
||||
onClick={() => toggleFeatureFlag(feat)}
|
||||
className="font-medium text-white"
|
||||
icon={
|
||||
featureFlags.find((f) => feat === f) !== undefined
|
||||
? CheckSquare
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenu.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDiscoveredPeers } from '@sd/client';
|
||||
import { useDebugState, useDiscoveredPeers, useFeatureFlag, useFeatureFlags } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { useRouteTitle } from '~/hooks/useRouteTitle';
|
||||
@@ -64,7 +64,7 @@ export const Component = () => {
|
||||
<Icon name="Globe" size={128} />
|
||||
<h1 className="mt-4 text-lg font-bold">{t('your_local_network')}</h1>
|
||||
<p className="mt-1 max-w-sm text-center text-sm text-ink-dull">
|
||||
{t("network_page_description")}
|
||||
{t('network_page_description')}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,14 +2,7 @@ import { CheckCircle } from '@phosphor-icons/react';
|
||||
import clsx from 'clsx';
|
||||
import { useMotionValueEvent, useScroll } from 'framer-motion';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
getThemeStore,
|
||||
getUnitFormatStore,
|
||||
Themes,
|
||||
useThemeStore,
|
||||
useUnitFormatStore,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
import { Themes, unitFormatStore, useThemeStore, useUnitFormatStore, useZodForm } from '@sd/client';
|
||||
import { Button, Divider, Form, Select, SelectOption, SwitchField, z } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
@@ -98,20 +91,20 @@ export const Component = () => {
|
||||
setSelectedTheme(theme);
|
||||
if (theme === 'system') {
|
||||
lockAppTheme?.('Auto');
|
||||
getThemeStore().syncThemeWithSystem = true;
|
||||
themeStore.syncThemeWithSystem = true;
|
||||
} else if (theme === 'vanilla') {
|
||||
getThemeStore().syncThemeWithSystem = false;
|
||||
getThemeStore().theme = theme;
|
||||
themeStore.syncThemeWithSystem = false;
|
||||
themeStore.theme = theme;
|
||||
document.documentElement.classList.add('vanilla-theme');
|
||||
} else if (theme === 'dark') {
|
||||
getThemeStore().syncThemeWithSystem = false;
|
||||
getThemeStore().theme = theme;
|
||||
themeStore.syncThemeWithSystem = false;
|
||||
themeStore.theme = theme;
|
||||
document.documentElement.classList.remove('vanilla-theme');
|
||||
}
|
||||
};
|
||||
|
||||
const hueSliderHandler = (hue: number) => {
|
||||
getThemeStore().hueValue = hue;
|
||||
themeStore.hueValue = hue;
|
||||
if (themeStore.theme === 'vanilla') {
|
||||
document.documentElement.style.setProperty('--light-hue', hue.toString());
|
||||
} else if (themeStore.theme === 'dark') {
|
||||
@@ -230,7 +223,7 @@ export const Component = () => {
|
||||
|
||||
<Setting mini title={t('coordinates')}>
|
||||
<Select
|
||||
onChange={(e) => (getUnitFormatStore().coordinatesFormat = e)}
|
||||
onChange={(e) => (unitFormatStore.coordinatesFormat = e)}
|
||||
value={formatStore.coordinatesFormat}
|
||||
>
|
||||
<SelectOption value="dms">DMS</SelectOption>
|
||||
@@ -240,7 +233,7 @@ export const Component = () => {
|
||||
|
||||
<Setting mini title={t('distance')}>
|
||||
<Select
|
||||
onChange={(e) => (getUnitFormatStore().distanceFormat = e)}
|
||||
onChange={(e) => (unitFormatStore.distanceFormat = e)}
|
||||
value={formatStore.distanceFormat}
|
||||
>
|
||||
<SelectOption value="km">{t('kilometers')}</SelectOption>
|
||||
@@ -250,7 +243,7 @@ export const Component = () => {
|
||||
|
||||
<Setting mini title={t('temperature')}>
|
||||
<Select
|
||||
onChange={(e) => (getUnitFormatStore().temperatureFormat = e)}
|
||||
onChange={(e) => (unitFormatStore.temperatureFormat = e)}
|
||||
value={formatStore.temperatureFormat}
|
||||
>
|
||||
<SelectOption value="celsius">{t('celcius')}</SelectOption>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import { Controller, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
getDebugState,
|
||||
useBridgeMutation,
|
||||
useBridgeQuery,
|
||||
useConnectedPeers,
|
||||
@@ -189,7 +188,7 @@ export const Component = () => {
|
||||
<Switch
|
||||
size="md"
|
||||
checked={debugState.enabled}
|
||||
onClick={() => (getDebugState().enabled = !debugState.enabled)}
|
||||
onClick={() => (debugState.enabled = !debugState.enabled)}
|
||||
/>
|
||||
</Setting>
|
||||
<Setting
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { telemetryStore, useTelemetryState } from '@sd/client';
|
||||
import { telemetryState, useTelemetryState } from '@sd/client';
|
||||
import { Switch } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Component = () => {
|
||||
>
|
||||
<Switch
|
||||
checked={fullTelemetry}
|
||||
onClick={() => (telemetryStore.shareFullTelemetry = !fullTelemetry)}
|
||||
onClick={() => (telemetryState.shareFullTelemetry = !fullTelemetry)}
|
||||
size="md"
|
||||
/>
|
||||
</Setting>
|
||||
|
||||
37
interface/app/demo.react.tsx
Normal file
37
interface/app/demo.react.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
import { WithSolid } from '@sd/client';
|
||||
|
||||
import { Demo3, demoCtx } from './demo.solid';
|
||||
|
||||
export function Demo(props: { demo: string }) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const ctx = demoCtx.useContext();
|
||||
console.log('FROM REACT 1', ctx());
|
||||
|
||||
return (
|
||||
<div className="bg-green-500 p-2">
|
||||
<demoCtx.Provider value="set in react">
|
||||
<>
|
||||
<button onClick={() => setCount((count) => count + 1)}>Click me</button>
|
||||
<div>Hello from React: {count}</div>
|
||||
<div>{props.demo}</div>
|
||||
<div>CTX: {ctx()}</div>
|
||||
<Inner />
|
||||
<WithSolid root={Demo3} demo={count.toString()} />
|
||||
</>
|
||||
</demoCtx.Provider>
|
||||
<WithSolid root={Demo3} demo={count.toString()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
const ctx = demoCtx.useContext();
|
||||
console.log('FROM REACT 2', ctx());
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Demo2() {
|
||||
return null;
|
||||
}
|
||||
@@ -1,19 +1,52 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
|
||||
import { createSignal } from 'solid-js';
|
||||
import { render } from 'solid-js/web';
|
||||
import { createSharedContext, WithReact } from '@sd/client';
|
||||
|
||||
function Demo() {
|
||||
import { Demo as ReactDemo, Demo2 as ReactDemo2 } from './demo.react';
|
||||
|
||||
export const demoCtx = createSharedContext('the ctx was not set');
|
||||
|
||||
export function Demo(props: { demo: string }) {
|
||||
const [count, setCount] = createSignal(0);
|
||||
const [ctxValue, setCtxValue] = createSignal('set in solid');
|
||||
|
||||
return (
|
||||
<div class="absolute top-0 z-[99999] bg-red-500">
|
||||
<button onClick={() => setCount(count() + 1)}>Click me</button>
|
||||
<div>Hello from Solid: {count()}</div>
|
||||
<div class="absolute top-0 z-[99999] bg-red-500 p-2">
|
||||
<demoCtx.Provider value={ctxValue()}>
|
||||
<button onClick={() => setCount((count) => count + 1)} class="border p-1">
|
||||
Click me
|
||||
</button>
|
||||
<button onClick={() => setCtxValue((s) => `${s}1`)} class="ml-4 border p-1">
|
||||
Update ctx
|
||||
</button>
|
||||
<div>Hello from Solid: {count()}</div>
|
||||
<div>CTX: {props.demo}</div>
|
||||
<Inner />
|
||||
<WithReact root={ReactDemo} demo={count().toString()} />
|
||||
<WithReact root={ReactDemo2} />
|
||||
</demoCtx.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderDemo(element: HTMLDivElement): () => void {
|
||||
return render(Demo, element);
|
||||
function Inner() {
|
||||
const ctx = demoCtx.useContext();
|
||||
console.log('FROM SOLID', ctx());
|
||||
return <div>CTX: {ctx()}</div>;
|
||||
}
|
||||
|
||||
export function Demo2() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Demo3(props: { demo: string }) {
|
||||
const ctx = demoCtx.useContext();
|
||||
|
||||
return (
|
||||
<div class="m-2 bg-blue-500 p-2">
|
||||
<div>Hello from Solid again: {props.demo}</div>
|
||||
<div>CTX: {ctx()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Navigate, Outlet, redirect, useMatches, type RouteObject } from 'react-router-dom';
|
||||
import {
|
||||
currentLibraryCache,
|
||||
getCachedLibraries,
|
||||
NormalisedCache,
|
||||
useCachedLibraries,
|
||||
useFeatureFlag
|
||||
useFeatureFlag,
|
||||
WithSolid
|
||||
} from '@sd/client';
|
||||
import { Dialogs, Toaster } from '@sd/ui';
|
||||
import { RouterErrorBoundary } from '~/ErrorFallback';
|
||||
import { useRoutingContext } from '~/RoutingContext';
|
||||
|
||||
import { Platform, usePlatform } from '..';
|
||||
import { Platform } from '..';
|
||||
import libraryRoutes from './$libraryId';
|
||||
import { DragAndDropDebug } from './$libraryId/debug/dnd';
|
||||
import { renderDemo } from './demo.solid';
|
||||
import { Demo, Demo2 } from './demo.solid';
|
||||
import onboardingRoutes from './onboarding';
|
||||
import { RootContext } from './RootContext';
|
||||
|
||||
@@ -22,18 +23,6 @@ import './style.scss';
|
||||
// I18n needs to be bundled here.
|
||||
import './I18n';
|
||||
|
||||
function RenderSolid() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cleanup = () => {};
|
||||
if (ref.current) cleanup = renderDemo(ref.current);
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
// NOTE: all route `Layout`s below should contain
|
||||
// the `usePlausiblePageViewMonitor` hook, as early as possible (ideally within the layout itself).
|
||||
// the hook should only be included if there's a valid `ClientContext` (so not onboarding)
|
||||
@@ -47,7 +36,10 @@ export const createRoutes = (platform: Platform, cache: NormalisedCache) =>
|
||||
return (
|
||||
<RootContext.Provider value={{ rawPath }}>
|
||||
{useFeatureFlag('debugDragAndDrop') ? <DragAndDropDebug /> : null}
|
||||
{useFeatureFlag('solidJsDemo') ? <RenderSolid /> : null}
|
||||
{useFeatureFlag('solidJsDemo') ? (
|
||||
<WithSolid root={Demo} demo="123" />
|
||||
) : null}
|
||||
{useFeatureFlag('solidJsDemo') ? <WithSolid root={Demo2} /> : null}
|
||||
<Outlet />
|
||||
<Dialogs />
|
||||
<Toaster position="bottom-right" expand={true} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
import { useMatch, useNavigate } from 'react-router';
|
||||
import { getOnboardingStore, unlockOnboardingScreen, useOnboardingStore } from '@sd/client';
|
||||
import { onboardingStore, unlockOnboardingScreen, useOnboardingStore } from '@sd/client';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
|
||||
export default function OnboardingProgress() {
|
||||
@@ -16,7 +16,7 @@ export default function OnboardingProgress() {
|
||||
useEffect(() => {
|
||||
if (!currentScreen) return;
|
||||
|
||||
unlockOnboardingScreen(currentScreen, getOnboardingStore().unlockedScreens);
|
||||
unlockOnboardingScreen(currentScreen, onboardingStore.unlockedScreens);
|
||||
}, [currentScreen]);
|
||||
|
||||
const routes = [
|
||||
|
||||
@@ -3,11 +3,11 @@ import { createContext, useContext } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
currentLibraryCache,
|
||||
getOnboardingStore,
|
||||
getUnitFormatStore,
|
||||
insertLibrary,
|
||||
onboardingStore,
|
||||
resetOnboardingStore,
|
||||
telemetryStore,
|
||||
telemetryState,
|
||||
unitFormatStore,
|
||||
useBridgeMutation,
|
||||
useCachedLibraries,
|
||||
useMultiZodForm,
|
||||
@@ -83,7 +83,7 @@ const useFormState = () => {
|
||||
shareTelemetry: 'share-telemetry'
|
||||
}
|
||||
},
|
||||
onData: (data) => (getOnboardingStore().data = { ...obStore.data, ...data })
|
||||
onData: (data) => (onboardingStore.data = { ...obStore.data, ...data })
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -92,8 +92,8 @@ const useFormState = () => {
|
||||
|
||||
if (window.navigator.language === 'en-US') {
|
||||
// not perfect as some linux users use en-US by default, same w/ windows
|
||||
getUnitFormatStore().distanceFormat = 'miles';
|
||||
getUnitFormatStore().temperatureFormat = 'fahrenheit';
|
||||
unitFormatStore.distanceFormat = 'miles';
|
||||
unitFormatStore.temperatureFormat = 'fahrenheit';
|
||||
}
|
||||
|
||||
const createLibrary = useBridgeMutation('library.create');
|
||||
@@ -105,7 +105,7 @@ const useFormState = () => {
|
||||
|
||||
// opted to place this here as users could change their mind before library creation/onboarding finalization
|
||||
// it feels more fitting to configure it here (once)
|
||||
telemetryStore.shareFullTelemetry = data.privacy.shareTelemetry === 'share-telemetry';
|
||||
telemetryState.shareFullTelemetry = data.privacy.shareTelemetry === 'share-telemetry';
|
||||
|
||||
try {
|
||||
// show creation screen for a bit for smoothness
|
||||
@@ -122,7 +122,7 @@ const useFormState = () => {
|
||||
|
||||
platform.refreshMenuBar && platform.refreshMenuBar();
|
||||
|
||||
if (telemetryStore.shareFullTelemetry) {
|
||||
if (telemetryState.shareFullTelemetry) {
|
||||
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Navigate, redirect, RouteObject } from 'react-router';
|
||||
import { getOnboardingStore } from '@sd/client';
|
||||
import { onboardingStore } from '@sd/client';
|
||||
|
||||
import Alpha from './alpha';
|
||||
import { useOnboardingContext } from './context';
|
||||
@@ -11,11 +11,10 @@ import NewLibrary from './new-library';
|
||||
import Privacy from './privacy';
|
||||
|
||||
const Index = () => {
|
||||
const obStore = getOnboardingStore();
|
||||
const ctx = useOnboardingContext();
|
||||
|
||||
if (obStore.lastActiveScreen && !ctx.library)
|
||||
return <Navigate to={obStore.lastActiveScreen} replace />;
|
||||
if (onboardingStore.lastActiveScreen && !ctx.library)
|
||||
return <Navigate to={onboardingStore.lastActiveScreen} replace />;
|
||||
|
||||
return <Navigate to="alpha" replace />;
|
||||
};
|
||||
@@ -24,8 +23,8 @@ export default [
|
||||
{
|
||||
index: true,
|
||||
loader: () => {
|
||||
if (getOnboardingStore().lastActiveScreen)
|
||||
return redirect(`/onboarding/${getOnboardingStore().lastActiveScreen}`, {
|
||||
if (onboardingStore.lastActiveScreen)
|
||||
return redirect(`/onboarding/${onboardingStore.lastActiveScreen}`, {
|
||||
replace: true
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { getThemeStore, useThemeStore } from '@sd/client';
|
||||
import { useThemeStore } from '@sd/client';
|
||||
|
||||
import { usePlatform } from '..';
|
||||
|
||||
@@ -16,30 +16,30 @@ export function useTheme() {
|
||||
document.documentElement.classList.remove('vanilla-theme');
|
||||
document.documentElement.style.setProperty(
|
||||
'--dark-hue',
|
||||
getThemeStore().hueValue.toString()
|
||||
themeStore.hueValue.toString()
|
||||
);
|
||||
getThemeStore().theme = 'dark';
|
||||
themeStore.theme = 'dark';
|
||||
} else {
|
||||
document.documentElement.classList.add('vanilla-theme');
|
||||
document.documentElement.style.setProperty(
|
||||
'--light-hue',
|
||||
getThemeStore().hueValue.toString()
|
||||
themeStore.hueValue.toString()
|
||||
);
|
||||
getThemeStore().theme = 'vanilla';
|
||||
themeStore.theme = 'vanilla';
|
||||
}
|
||||
} else {
|
||||
if (themeStore.theme === 'dark') {
|
||||
document.documentElement.classList.remove('vanilla-theme');
|
||||
document.documentElement.style.setProperty(
|
||||
'--dark-hue',
|
||||
getThemeStore().hueValue.toString()
|
||||
themeStore.hueValue.toString()
|
||||
);
|
||||
lockAppTheme?.('Dark');
|
||||
} else if (themeStore.theme === 'vanilla') {
|
||||
document.documentElement.classList.add('vanilla-theme');
|
||||
document.documentElement.style.setProperty(
|
||||
'--light-hue',
|
||||
getThemeStore().hueValue.toString()
|
||||
themeStore.hueValue.toString()
|
||||
);
|
||||
lockAppTheme?.('Light');
|
||||
}
|
||||
|
||||
@@ -13,14 +13,16 @@
|
||||
"dependencies": {
|
||||
"@oscartbeaumont-sd/rspc-client": "=0.0.0-main-dc31e5b2",
|
||||
"@oscartbeaumont-sd/rspc-react": "=0.0.0-main-dc31e5b2",
|
||||
"@solid-primitives/deep": "^0.2.4",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/solid-query": "^5.17.9",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
"fast-equals": "^5.0.1",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"valtio": "^1.11.2",
|
||||
"solid-js": "^1.8.8",
|
||||
"zod": "~3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
export * from './useClientContext';
|
||||
export * from './useDebugState';
|
||||
export * from './useExplorerLayoutStore';
|
||||
export * from './useFeatureFlag';
|
||||
export * from './useForceUpdate';
|
||||
export * from './useLibraryContext';
|
||||
export * from './useLibraryStore';
|
||||
export * from './useOnboardingStore';
|
||||
export * from './useP2PEvents';
|
||||
export * from './usePlausible';
|
||||
export * from './useTelemetryState';
|
||||
export * from './useThemeStore';
|
||||
export * from './useUnitFormatStore';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { valtioPersist } from '../lib';
|
||||
|
||||
const explorerLayoutStore = valtioPersist('sd-explorer-layout', {
|
||||
showPathBar: true,
|
||||
showImageSlider: true
|
||||
});
|
||||
|
||||
export function useExplorerLayoutStore() {
|
||||
return useSnapshot(explorerLayoutStore);
|
||||
}
|
||||
|
||||
export function getExplorerLayoutStore() {
|
||||
return explorerLayoutStore;
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
|
||||
import { LibraryConfigWrapped } from '../core';
|
||||
import { useBridgeSubscription } from '../rspc';
|
||||
import { libraryStore, useLibraryStore } from '../stores/libraryStore';
|
||||
import { ClientContext, useClientContext } from './useClientContext';
|
||||
import { getLibraryStore, useLibraryStore } from './useLibraryStore';
|
||||
|
||||
export interface LibraryContext {
|
||||
library: LibraryConfigWrapped;
|
||||
@@ -23,7 +23,7 @@ export const LibraryContextProvider = ({ children, library }: LibraryContextProv
|
||||
// TODO: This should probs be a library subscription - https://linear.app/spacedriveapp/issue/ENG-724/locationsonline-should-be-a-library-not-a-bridge-subscription
|
||||
useBridgeSubscription(['locations.online'], {
|
||||
onData: (d) => {
|
||||
getLibraryStore().onlineLocations = d;
|
||||
libraryStore.onlineLocations = d;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
|
||||
const state = {
|
||||
onlineLocations: [] as number[][]
|
||||
};
|
||||
|
||||
const libraryStore = proxy(state);
|
||||
|
||||
export function useLibraryStore() {
|
||||
return useSnapshot(libraryStore);
|
||||
}
|
||||
|
||||
export function getLibraryStore() {
|
||||
return libraryStore;
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import Plausible, { PlausibleOptions as PlausibleTrackerOptions } from 'plausibl
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { BuildInfo } from '../core';
|
||||
import { useDebugState } from './useDebugState';
|
||||
import { PlausiblePlatformType, telemetryStore, useTelemetryState } from './useTelemetryState';
|
||||
import { useDebugState } from '../stores/debugState';
|
||||
import { PlausiblePlatformType, telemetryState, useTelemetryState } from '../stores/telemetryState';
|
||||
|
||||
/**
|
||||
* This should be in sync with the Core's version.
|
||||
@@ -373,7 +373,7 @@ export const initPlausible = ({
|
||||
platformType: PlausiblePlatformType;
|
||||
buildInfo: BuildInfo | undefined;
|
||||
}) => {
|
||||
telemetryStore.platform = platformType;
|
||||
telemetryStore.buildInfo = buildInfo;
|
||||
telemetryState.platform = platformType;
|
||||
telemetryState.buildInfo = buildInfo;
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { valtioPersist } from '../lib';
|
||||
|
||||
export type Themes = 'vanilla' | 'dark';
|
||||
|
||||
const themeStore = valtioPersist('sd-theme', {
|
||||
theme: 'dark' as Themes,
|
||||
syncThemeWithSystem: false,
|
||||
hueValue: 235
|
||||
});
|
||||
|
||||
export function useThemeStore() {
|
||||
return useSnapshot(themeStore);
|
||||
}
|
||||
|
||||
export function getThemeStore() {
|
||||
return themeStore;
|
||||
}
|
||||
|
||||
export function isDarkTheme() {
|
||||
return themeStore.theme === 'dark';
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { valtioPersist } from '../lib';
|
||||
|
||||
export type CoordinatesFormat = 'dms' | 'dd';
|
||||
export type DistanceFormat = 'km' | 'miles';
|
||||
export type TemperatureFormat = 'celsius' | 'fahrenheit';
|
||||
|
||||
const unitFormatStore = valtioPersist('sd-display-units', {
|
||||
// these are the defaults as 99% of users would want to see them this way
|
||||
// if the `en-US` locale is detected during onboarding, the distance/temp are changed to freedom units
|
||||
coordinatesFormat: 'dms' as CoordinatesFormat,
|
||||
distanceFormat: 'km' as DistanceFormat,
|
||||
temperatureFormat: 'celsius' as TemperatureFormat
|
||||
});
|
||||
|
||||
export function useUnitFormatStore() {
|
||||
return useSnapshot(unitFormatStore);
|
||||
}
|
||||
|
||||
export function getUnitFormatStore() {
|
||||
return unitFormatStore;
|
||||
}
|
||||
@@ -32,3 +32,4 @@ export * from './lib';
|
||||
export * from './form';
|
||||
export * from './cache';
|
||||
export * from './color';
|
||||
export * from './solid';
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './byte-size';
|
||||
export * from './passwordStrength';
|
||||
export * from './valito';
|
||||
export * from './valtio';
|
||||
|
||||
124
packages/client/src/solid/context.tsx
Normal file
124
packages/client/src/solid/context.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
createElement,
|
||||
createContext as createReactContext,
|
||||
isValidElement,
|
||||
PropsWithChildren,
|
||||
JSX as ReactJSX,
|
||||
useEffect,
|
||||
useContext as useReactContext,
|
||||
useRef
|
||||
} from 'react';
|
||||
import {
|
||||
children,
|
||||
createContext as createSolidContext,
|
||||
getOwner,
|
||||
Owner,
|
||||
JSX as SolidJSX,
|
||||
useContext as useSolidContext
|
||||
} from 'solid-js';
|
||||
import { createStore, type Store } from 'solid-js/store';
|
||||
|
||||
import { insideReactRender } from './internal';
|
||||
import { useObserver, useObserverWithOwner } from './useObserver';
|
||||
|
||||
type RegisteredContext = {
|
||||
id: symbol;
|
||||
store: Store<any>;
|
||||
};
|
||||
|
||||
const reactGlobalContext = createReactContext([] as RegisteredContext[]);
|
||||
const solidGlobalContext = createSolidContext(() => [] as RegisteredContext[]);
|
||||
|
||||
export function createSharedContext<T>(initialValue: T) {
|
||||
const id = Symbol('shared-context');
|
||||
|
||||
function Provider<C>(props: { value: T; children: C }): C {
|
||||
const isSolid =
|
||||
'get' in Object.getOwnPropertyDescriptor(props, 'children')! ||
|
||||
!isValidElement(props.children);
|
||||
|
||||
const ctxEntry: RegisteredContext = {
|
||||
id,
|
||||
store: () => props.value
|
||||
};
|
||||
|
||||
if (isSolid) {
|
||||
const globalCtx = useSolidContext(solidGlobalContext);
|
||||
|
||||
return solidGlobalContext.Provider({
|
||||
value: () => [...globalCtx().filter((c) => c.id !== id), ctxEntry],
|
||||
get children() {
|
||||
return props.children as SolidJSX.Element;
|
||||
}
|
||||
}) as any;
|
||||
} else {
|
||||
const globalCtx = useReactContext(reactGlobalContext);
|
||||
|
||||
return createElement(
|
||||
reactGlobalContext.Provider as any,
|
||||
{
|
||||
value: [...globalCtx.filter((c) => c.id !== id), ctxEntry]
|
||||
},
|
||||
props.children as any
|
||||
) as any;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Provider,
|
||||
useContext: () => {
|
||||
const isInsideReact = insideReactRender();
|
||||
let globalCtx: any;
|
||||
if (isInsideReact) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
globalCtx = useReactContext(reactGlobalContext);
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
globalCtx = useSolidContext(solidGlobalContext);
|
||||
}
|
||||
|
||||
let reactObserver: T | undefined = undefined;
|
||||
|
||||
return () => {
|
||||
const ctx = ((isInsideReact ? globalCtx : globalCtx()) as RegisteredContext[]).find(
|
||||
(ctx) => ctx.id === id
|
||||
);
|
||||
if (!ctx) return initialValue;
|
||||
|
||||
if (isInsideReact) {
|
||||
if (!reactObserver) reactObserver = useObserver(() => ctx.store() as T);
|
||||
return reactObserver as T; // This function doesn't do anything other than make the API consistent
|
||||
} else {
|
||||
return ctx.store() as T;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useWithContextReact(): (elem: () => SolidJSX.Element) => SolidJSX.Element {
|
||||
const globalCtx = useReactContext(reactGlobalContext);
|
||||
const ref = useRef(createStore<RegisteredContext[]>([]));
|
||||
|
||||
useEffect(() => ref.current[1](globalCtx), [globalCtx, ref]);
|
||||
|
||||
return (elem) =>
|
||||
solidGlobalContext.Provider({
|
||||
value: () => ref.current[0],
|
||||
children: elem as any
|
||||
});
|
||||
}
|
||||
|
||||
export function useWithContextSolid(): (elem: ReactJSX.Element) => ReactJSX.Element {
|
||||
const owner = getOwner()!;
|
||||
return (elem) => createElement(WithContext, { owner }, elem);
|
||||
}
|
||||
|
||||
function WithContext(props: PropsWithChildren<{ owner: Owner }>) {
|
||||
const globalCtx = useObserverWithOwner(props.owner, () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useSolidContext(solidGlobalContext)();
|
||||
});
|
||||
|
||||
return createElement(reactGlobalContext.Provider, { value: globalCtx }, props.children);
|
||||
}
|
||||
6
packages/client/src/solid/index.ts
Normal file
6
packages/client/src/solid/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './interop';
|
||||
export * from './react';
|
||||
export * from './solid.solid';
|
||||
export * from './useObserver';
|
||||
export * from './rq';
|
||||
export { createSharedContext } from './context';
|
||||
11
packages/client/src/solid/internal.ts
Normal file
11
packages/client/src/solid/internal.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export function insideReactRender() {
|
||||
try {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useState();
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
55
packages/client/src/solid/interop.ts
Normal file
55
packages/client/src/solid/interop.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { trackDeep } from '@solid-primitives/deep';
|
||||
import { createEffect, createRoot } from 'solid-js';
|
||||
import { type Store, type StoreNode } from 'solid-js/store';
|
||||
|
||||
import { useObserver } from './useObserver';
|
||||
|
||||
export function useSolidStore<T extends object = {}>(store: Store<T>) {
|
||||
const state = useObserver(() => ({ ...store }));
|
||||
return new Proxy(state, {
|
||||
get: (target, prop) => Reflect.get(target, prop),
|
||||
set: (_, prop, value) => Reflect.set(store, prop, value)
|
||||
});
|
||||
}
|
||||
|
||||
type CreatePersistedMutableOpts<T> = {
|
||||
onSave?: (value: T) => T;
|
||||
};
|
||||
|
||||
// `@solid-primitives/storage`'s `makePersisted` doesn't support `solid-js/store`'s `createMutable` so we roll our own.
|
||||
export function createPersistedMutable<T extends StoreNode>(
|
||||
key: string,
|
||||
mutable: T,
|
||||
opts?: CreatePersistedMutableOpts<T>
|
||||
) {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value) {
|
||||
const persisted = JSON.parse(value);
|
||||
Object.assign(mutable, persisted);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error loading persisted state from localStorage key '${key}': ${err}`);
|
||||
}
|
||||
|
||||
// I tried using a `Proxy` here but I couldn't get it working with arrays.
|
||||
// https://codepen.io/oscartbeaumont/pen/BabzazE
|
||||
const dispose = createRoot((dispose) => {
|
||||
createEffect(() => {
|
||||
// Subscribe to store
|
||||
trackDeep(mutable);
|
||||
|
||||
let item: string;
|
||||
if (opts?.onSave) {
|
||||
item = JSON.stringify(opts.onSave(mutable));
|
||||
} else {
|
||||
item = JSON.stringify(mutable);
|
||||
}
|
||||
localStorage.setItem(key, item);
|
||||
});
|
||||
return dispose;
|
||||
});
|
||||
if ('onHotReload' in globalThis) globalThis?.onHotReload(dispose);
|
||||
|
||||
return mutable;
|
||||
}
|
||||
30
packages/client/src/solid/react.tsx
Normal file
30
packages/client/src/solid/react.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { JSX as SolidJSX } from 'solid-js';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import { useWithContextReact } from './context';
|
||||
|
||||
type Props<T> =
|
||||
| ({
|
||||
root: (props: T) => SolidJSX.Element;
|
||||
} & T)
|
||||
| {
|
||||
root: () => SolidJSX.Element;
|
||||
};
|
||||
|
||||
export function WithSolid<T>(props: Props<T>) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const applyCtx = useWithContextReact();
|
||||
|
||||
useEffect(() => {
|
||||
let cleanup = () => {};
|
||||
if (ref.current)
|
||||
cleanup = render(() => {
|
||||
const { root, ...childProps } = props;
|
||||
return applyCtx(() => root(childProps as any));
|
||||
}, ref.current);
|
||||
return cleanup;
|
||||
}, [props, applyCtx]);
|
||||
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
14
packages/client/src/solid/rq.ts
Normal file
14
packages/client/src/solid/rq.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// import { useQuery } from '@tanstack/react-query';
|
||||
// import { createQuery } from '@tanstack/solid-query';
|
||||
|
||||
// import { insideReactRender } from './internal';
|
||||
|
||||
// export function useUniversalQuery() {
|
||||
// if (insideReactRender()) {
|
||||
// useQuery();
|
||||
// } else {
|
||||
// createQuery();
|
||||
// }
|
||||
// }
|
||||
|
||||
export {}; // TODO
|
||||
59
packages/client/src/solid/solid.solid.tsx
Normal file
59
packages/client/src/solid/solid.solid.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
|
||||
import { trackDeep } from '@solid-primitives/deep';
|
||||
import { createElement, StrictMode, type FunctionComponent } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createEffect, onCleanup, splitProps } from 'solid-js';
|
||||
|
||||
import { useWithContextSolid } from './context';
|
||||
|
||||
type Props<T> =
|
||||
| {
|
||||
root: FunctionComponent<{}>;
|
||||
}
|
||||
| ({
|
||||
root: FunctionComponent<T>;
|
||||
} & T);
|
||||
|
||||
export function WithReact<T extends object>(props: Props<T>) {
|
||||
let ref: HTMLDivElement | undefined;
|
||||
let root: ReactDOM.Root | undefined;
|
||||
let cleanup: (() => void) | undefined = undefined;
|
||||
|
||||
const applyCtx = useWithContextSolid();
|
||||
const [_, childProps] = splitProps(props, ['root']);
|
||||
|
||||
// TODO: Inject all context's
|
||||
const render = (childProps: any) => {
|
||||
if (!ref) return;
|
||||
if (!root) {
|
||||
root = ReactDOM.createRoot(ref);
|
||||
// The `setTimeout` is to ensure React has time to do the intial render.
|
||||
// React doesn't like when you unmount it while it's rendering.
|
||||
cleanup = () => {
|
||||
setTimeout(() => root?.unmount());
|
||||
root = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
root.render(
|
||||
createElement(
|
||||
StrictMode,
|
||||
null,
|
||||
applyCtx(createElement(props.root as any, childProps, null))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const trackedProps = trackDeep(childProps);
|
||||
render({ ...trackedProps });
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
cleanup?.();
|
||||
cleanup = undefined;
|
||||
});
|
||||
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
64
packages/client/src/solid/useObserver.ts
Normal file
64
packages/client/src/solid/useObserver.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createReaction, createRoot, Owner, runWithOwner } from 'solid-js';
|
||||
|
||||
// TODO: Can we unify these hooks???
|
||||
|
||||
// A version of `react-solid-state`'s method that works with newer React versions.
|
||||
// https://github.com/solidjs/react-solid-state/issues/4
|
||||
export function useObserver<T>(fn: () => T) {
|
||||
const [_, setTick] = useState(0);
|
||||
const state = useRef({
|
||||
onUpdate: () => {},
|
||||
// An really ugly workaround for React `StrictMode`'s double firing of `useEffect`.
|
||||
doneFirstFire: false
|
||||
});
|
||||
const reaction = useRef<{ dispose: () => void; track: (fn: () => void) => void }>();
|
||||
if (!reaction.current) {
|
||||
reaction.current = createRoot((dispose) => ({
|
||||
dispose,
|
||||
track: createReaction(() => state.current.onUpdate())
|
||||
}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// We set this after a `useEffect` to ensure we don't trigger an update prior to mount
|
||||
// cause that makes React madge.
|
||||
state.current.onUpdate = () => setTick((t) => t + 1);
|
||||
state.current.doneFirstFire = true;
|
||||
|
||||
return () => {
|
||||
if (!state.current.doneFirstFire) {
|
||||
reaction.current?.dispose();
|
||||
reaction.current = undefined;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
let rendering!: T;
|
||||
reaction.current.track(() => (rendering = fn()));
|
||||
return rendering;
|
||||
}
|
||||
|
||||
// This is a very low-level primitive. Be careful with it!
|
||||
export function useObserverWithOwner<T>(owner: Owner, fn: () => T) {
|
||||
const [_, setTick] = useState(0);
|
||||
const state = useRef({
|
||||
onUpdate: () => {}
|
||||
});
|
||||
const reaction = useRef<{ track: (fn: () => void) => void }>();
|
||||
if (!reaction.current) {
|
||||
reaction.current = runWithOwner(owner, () => ({
|
||||
track: createReaction(() => state.current.onUpdate())
|
||||
}))!;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// We set this after a `useEffect` to ensure we don't trigger an update prior to mount
|
||||
// cause that makes React madge.
|
||||
state.current.onUpdate = () => setTick((t) => t + 1);
|
||||
}, []);
|
||||
|
||||
let rendering!: T;
|
||||
reaction.current.track(() => (rendering = fn()));
|
||||
return rendering;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { RSPCError } from '@oscartbeaumont-sd/rspc-client';
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { nonLibraryClient } from '../rspc';
|
||||
import { useSolidStore } from '../solid';
|
||||
|
||||
interface Store {
|
||||
state: { status: 'loading' | 'notLoggedIn' | 'loggingIn' | 'loggedIn' | 'loggingOut' };
|
||||
@@ -13,14 +14,14 @@ export interface ProviderConfig {
|
||||
}
|
||||
|
||||
// inner object so we can overwrite it in one assignment
|
||||
const store = proxy<Store>({
|
||||
const store = createMutable<Store>({
|
||||
state: {
|
||||
status: 'loading'
|
||||
}
|
||||
});
|
||||
|
||||
export function useStateSnapshot() {
|
||||
return useSnapshot(store).state;
|
||||
return useSolidStore(store).state;
|
||||
}
|
||||
|
||||
nonLibraryClient
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { valtioPersist } from '../lib/valito';
|
||||
import { createPersistedMutable, useSolidStore } from '../solid';
|
||||
|
||||
export interface DebugState {
|
||||
enabled: boolean;
|
||||
@@ -11,20 +11,19 @@ export interface DebugState {
|
||||
telemetryLogging: boolean;
|
||||
}
|
||||
|
||||
const debugState: DebugState = valtioPersist('sd-debugState', {
|
||||
enabled: globalThis.isDev,
|
||||
rspcLogger: false,
|
||||
reactQueryDevtools: globalThis.isDev ? 'invisible' : 'enabled',
|
||||
shareFullTelemetry: false,
|
||||
telemetryLogging: false
|
||||
});
|
||||
export const debugState = createPersistedMutable(
|
||||
'sd-debugState',
|
||||
createMutable<DebugState>({
|
||||
enabled: globalThis.isDev,
|
||||
rspcLogger: false,
|
||||
reactQueryDevtools: globalThis.isDev ? 'invisible' : 'enabled',
|
||||
shareFullTelemetry: false,
|
||||
telemetryLogging: false
|
||||
})
|
||||
);
|
||||
|
||||
export function useDebugState() {
|
||||
return useSnapshot(debugState);
|
||||
}
|
||||
|
||||
export function getDebugState() {
|
||||
return debugState;
|
||||
return useSolidStore(debugState);
|
||||
}
|
||||
|
||||
export function useDebugStateEnabler(): () => void {
|
||||
@@ -32,7 +31,7 @@ export function useDebugStateEnabler(): () => void {
|
||||
|
||||
useEffect(() => {
|
||||
if (clicked >= 5) {
|
||||
getDebugState().enabled = true;
|
||||
debugState.enabled = true;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => setClicked(0), 1000);
|
||||
15
packages/client/src/stores/explorerLayout.ts
Normal file
15
packages/client/src/stores/explorerLayout.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { createPersistedMutable, useSolidStore } from '../solid';
|
||||
|
||||
export const explorerLayout = createPersistedMutable(
|
||||
'sd-explorer-layout',
|
||||
createMutable({
|
||||
showPathBar: true,
|
||||
showImageSlider: true
|
||||
})
|
||||
);
|
||||
|
||||
export function useExplorerLayoutStore() {
|
||||
return useSolidStore(explorerLayout);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { subscribe, useSnapshot } from 'valtio';
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import type { BackendFeature } from '../core';
|
||||
import { valtioPersist } from '../lib/valito';
|
||||
import { nonLibraryClient, useBridgeQuery } from '../rspc';
|
||||
import { createPersistedMutable, useObserver, useSolidStore } from '../solid';
|
||||
|
||||
export const features = [
|
||||
'p2pPairing',
|
||||
@@ -20,11 +20,13 @@ export const backendFeatures: BackendFeature[] = ['syncEmitMessages', 'filesOver
|
||||
|
||||
export type FeatureFlag = (typeof features)[number] | BackendFeature;
|
||||
|
||||
const featureFlagState = valtioPersist(
|
||||
export const featureFlagsStore = createPersistedMutable(
|
||||
'sd-featureFlags',
|
||||
{ enabled: [] as FeatureFlag[] },
|
||||
createMutable({
|
||||
enabled: [] as FeatureFlag[]
|
||||
}),
|
||||
{
|
||||
saveFn(data) {
|
||||
onSave: (data) => {
|
||||
// Clone so we don't mess with the original data
|
||||
const data2: typeof data = JSON.parse(JSON.stringify(data));
|
||||
// Only save frontend flags (backend flags are saved in the backend)
|
||||
@@ -38,9 +40,9 @@ export function useLoadBackendFeatureFlags() {
|
||||
const nodeConfig = useBridgeQuery(['nodeState']);
|
||||
|
||||
useEffect(() => {
|
||||
featureFlagState.enabled = [
|
||||
featureFlagsStore.enabled = [
|
||||
// Remove all backend features.
|
||||
...featureFlagState.enabled.filter((f) => features.includes(f as any)),
|
||||
...featureFlagsStore.enabled.filter((f) => features.includes(f as any)),
|
||||
// Add back in current state of backend features
|
||||
|
||||
...(nodeConfig.data?.features ?? [])
|
||||
@@ -49,20 +51,18 @@ export function useLoadBackendFeatureFlags() {
|
||||
}
|
||||
|
||||
export function useFeatureFlags() {
|
||||
return useSnapshot(featureFlagState);
|
||||
// We have to be special here.
|
||||
// `useSolidStore` would not work as it "subscribes" to the array, not the items in the array.
|
||||
return useObserver(() => [...featureFlagsStore.enabled]);
|
||||
}
|
||||
|
||||
export function useFeatureFlag(flag: FeatureFlag | FeatureFlag[]) {
|
||||
useSnapshot(featureFlagState); // Rerender on change
|
||||
useFeatureFlags(); // Rerender on change
|
||||
return Array.isArray(flag) ? flag.every((f) => isEnabled(f)) : isEnabled(flag);
|
||||
}
|
||||
|
||||
export function useOnFeatureFlagsChange(callback: (flags: FeatureFlag[]) => void) {
|
||||
useEffect(() => subscribe(featureFlagState, () => callback(featureFlagState.enabled)));
|
||||
}
|
||||
|
||||
export const isEnabled = (flag: FeatureFlag) =>
|
||||
featureFlagState.enabled.find((ff) => flag === ff) !== undefined;
|
||||
featureFlagsStore.enabled.find((ff) => flag === ff) !== undefined;
|
||||
|
||||
export function toggleFeatureFlag(flags: FeatureFlag | FeatureFlag[]) {
|
||||
if (!Array.isArray(flags)) {
|
||||
@@ -74,7 +74,7 @@ export function toggleFeatureFlag(flags: FeatureFlag | FeatureFlag[]) {
|
||||
void (async () => {
|
||||
// Tauri's `confirm` returns a Promise
|
||||
// Only prompt when enabling the feature
|
||||
const result = featureFlagState.enabled.find((ff) => f === ff)
|
||||
const result = featureFlagsStore.enabled.find((ff) => f === ff)
|
||||
? true
|
||||
: await confirm(
|
||||
'This feature will render your database broken and it WILL need to be reset! Use at your own risk!'
|
||||
@@ -88,7 +88,7 @@ export function toggleFeatureFlag(flags: FeatureFlag | FeatureFlag[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!featureFlagState.enabled.find((ff) => f === ff)) {
|
||||
if (!featureFlagsStore.enabled.find((ff) => f === ff)) {
|
||||
let message: string | undefined;
|
||||
if (f === 'p2pPairing') {
|
||||
message =
|
||||
@@ -104,14 +104,14 @@ export function toggleFeatureFlag(flags: FeatureFlag | FeatureFlag[]) {
|
||||
const result = await confirm(message);
|
||||
|
||||
if (result) {
|
||||
featureFlagState.enabled.push(f);
|
||||
featureFlagsStore.enabled.push(f);
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
featureFlagState.enabled.push(f);
|
||||
featureFlagsStore.enabled.push(f);
|
||||
}
|
||||
} else {
|
||||
featureFlagState.enabled = featureFlagState.enabled.filter((ff) => f !== ff);
|
||||
featureFlagsStore.enabled = featureFlagsStore.enabled.filter((ff) => f !== ff);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
export * as auth from './auth';
|
||||
export * from './debugState';
|
||||
export * from './explorerLayout';
|
||||
export * from './featureFlags';
|
||||
export * from './libraryStore';
|
||||
export * from './onboardingStore';
|
||||
export * from './telemetryState';
|
||||
export * from './themeStore';
|
||||
export * from './unitFormatStore';
|
||||
|
||||
11
packages/client/src/stores/libraryStore.ts
Normal file
11
packages/client/src/stores/libraryStore.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { useSolidStore } from '../solid';
|
||||
|
||||
export const libraryStore = createMutable({
|
||||
onlineLocations: [] as number[][]
|
||||
});
|
||||
|
||||
export function useLibraryStore() {
|
||||
return useSolidStore(libraryStore);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { valtioPersist } from '../lib';
|
||||
import { createPersistedMutable, useSolidStore } from '../solid';
|
||||
|
||||
export enum UseCase {
|
||||
CameraRoll = 'cameraRoll',
|
||||
@@ -18,25 +18,24 @@ const onboardingStoreDefaults = () => ({
|
||||
data: {} as Record<string, any> | undefined
|
||||
});
|
||||
|
||||
const appOnboardingStore = valtioPersist('onboarding', onboardingStoreDefaults());
|
||||
export const onboardingStore = createPersistedMutable(
|
||||
'onboarding',
|
||||
createMutable(onboardingStoreDefaults())
|
||||
);
|
||||
|
||||
export function useOnboardingStore() {
|
||||
return useSnapshot(appOnboardingStore);
|
||||
}
|
||||
|
||||
export function getOnboardingStore() {
|
||||
return appOnboardingStore;
|
||||
return useSolidStore(onboardingStore);
|
||||
}
|
||||
|
||||
export function resetOnboardingStore() {
|
||||
Object.assign(appOnboardingStore, onboardingStoreDefaults());
|
||||
Object.assign(onboardingStore, onboardingStoreDefaults());
|
||||
}
|
||||
|
||||
export function unlockOnboardingScreen(key: string, unlockedScreens: string[] = []) {
|
||||
appOnboardingStore.lastActiveScreen = key;
|
||||
onboardingStore.lastActiveScreen = key;
|
||||
if (unlockedScreens.includes(key)) {
|
||||
appOnboardingStore.unlockedScreens = unlockedScreens;
|
||||
onboardingStore.unlockedScreens = unlockedScreens;
|
||||
} else {
|
||||
appOnboardingStore.unlockedScreens = [...unlockedScreens, key];
|
||||
onboardingStore.unlockedScreens = [...unlockedScreens, key];
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { BuildInfo } from '../core';
|
||||
import { valtioPersist } from '../lib';
|
||||
import { createPersistedMutable, useSolidStore } from '../solid';
|
||||
|
||||
/**
|
||||
* Possible Platform types that can be sourced from `usePlatform().platform` or even hardcoded.
|
||||
@@ -17,12 +17,15 @@ type TelemetryState = {
|
||||
buildInfo: BuildInfo | undefined;
|
||||
};
|
||||
|
||||
export const telemetryStore = valtioPersist<TelemetryState>('sd-telemetryStore', {
|
||||
shareFullTelemetry: false, // false by default
|
||||
platform: 'unknown',
|
||||
buildInfo: undefined
|
||||
});
|
||||
export const telemetryState = createPersistedMutable(
|
||||
'sd-explorer-layout',
|
||||
createMutable<TelemetryState>({
|
||||
shareFullTelemetry: false, // false by default
|
||||
platform: 'unknown',
|
||||
buildInfo: undefined
|
||||
})
|
||||
);
|
||||
|
||||
export function useTelemetryState() {
|
||||
return useSnapshot(telemetryStore);
|
||||
return useSolidStore(telemetryState);
|
||||
}
|
||||
31
packages/client/src/stores/themeStore.ts
Normal file
31
packages/client/src/stores/themeStore.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { createPersistedMutable, useObserver, useSolidStore } from '../solid';
|
||||
|
||||
export type Themes = 'vanilla' | 'dark';
|
||||
|
||||
export const themeStore = createPersistedMutable(
|
||||
'sd-theme',
|
||||
createMutable({
|
||||
theme: 'dark' as Themes,
|
||||
syncThemeWithSystem: false,
|
||||
hueValue: 235
|
||||
})
|
||||
);
|
||||
|
||||
export function useThemeStore() {
|
||||
return useSolidStore(themeStore);
|
||||
}
|
||||
|
||||
export function useSubscribeToThemeStore(callback: () => void) {
|
||||
useObserver(() => {
|
||||
// Subscribe to store
|
||||
const _ = { ...themeStore };
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
export function isDarkTheme() {
|
||||
return themeStore.theme === 'dark';
|
||||
}
|
||||
22
packages/client/src/stores/unitFormatStore.tsx
Normal file
22
packages/client/src/stores/unitFormatStore.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createMutable } from 'solid-js/store';
|
||||
|
||||
import { createPersistedMutable, useSolidStore } from '../solid';
|
||||
|
||||
export type CoordinatesFormat = 'dms' | 'dd';
|
||||
export type DistanceFormat = 'km' | 'miles';
|
||||
export type TemperatureFormat = 'celsius' | 'fahrenheit';
|
||||
|
||||
export const unitFormatStore = createPersistedMutable(
|
||||
'sd-display-units',
|
||||
createMutable({
|
||||
// these are the defaults as 99% of users would want to see them this way
|
||||
// if the `en-US` locale is detected during onboarding, the distance/temp are changed to freedom units
|
||||
coordinatesFormat: 'dms' as CoordinatesFormat,
|
||||
distanceFormat: 'km' as DistanceFormat,
|
||||
temperatureFormat: 'celsius' as TemperatureFormat
|
||||
})
|
||||
);
|
||||
|
||||
export function useUnitFormatStore() {
|
||||
return useSolidStore(unitFormatStore);
|
||||
}
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user