@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:
Oscar Beaumont
2024-01-09 16:05:01 +08:00
committed by GitHub
parent 0d5f92667c
commit e2fc8d2423
50 changed files with 698 additions and 281 deletions

View File

@@ -21,7 +21,6 @@ export function createUpdater() {
listen<UpdateStore>('updater', (e) => {
Object.assign(updateStore, e.payload);
console.log(updateStore);
});
const onInstallCallbacks = new Set<() => void>();

View File

@@ -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();

View File

@@ -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' } });
}

View File

@@ -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>

View File

@@ -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;
}}
/>

View File

@@ -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]"

View File

@@ -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'], () => {

View File

@@ -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) => {

View File

@@ -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>
</>
);
}

View File

@@ -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>
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View 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;
}

View File

@@ -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>
);
}

View File

@@ -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} />

View File

@@ -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 = [

View File

@@ -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' } });
}

View File

@@ -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
});

View File

@@ -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');
}

View File

@@ -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": {

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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;
}
});

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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';
}

View File

@@ -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;
}

View File

@@ -32,3 +32,4 @@ export * from './lib';
export * from './form';
export * from './cache';
export * from './color';
export * from './solid';

View File

@@ -1,3 +1,3 @@
export * from './byte-size';
export * from './passwordStrength';
export * from './valito';
export * from './valtio';

View 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);
}

View 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';

View 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;
}
}

View 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;
}

View 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} />;
}

View 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

View 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} />;
}

View 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;
}

View File

@@ -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

View File

@@ -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);

View 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);
}

View File

@@ -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);
}
});
}

View File

@@ -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';

View 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);
}

View File

@@ -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];
}
}

View File

@@ -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);
}

View 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';
}

View 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
View File

Binary file not shown.