diff --git a/apps/desktop/src/updater.tsx b/apps/desktop/src/updater.tsx index 2bc2c757a..059df428d 100644 --- a/apps/desktop/src/updater.tsx +++ b/apps/desktop/src/updater.tsx @@ -21,7 +21,6 @@ export function createUpdater() { listen('updater', (e) => { Object.assign(updateStore, e.payload); - console.log(updateStore); }); const onInstallCallbacks = new Set<() => void>(); diff --git a/apps/mobile/src/hooks/useTheme.ts b/apps/mobile/src/hooks/useTheme.ts index c14adb9ab..47aded1f1 100644 --- a/apps/mobile/src/hooks/useTheme.ts +++ b/apps/mobile/src/hooks/useTheme.ts @@ -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(); diff --git a/apps/mobile/src/screens/onboarding/context.tsx b/apps/mobile/src/screens/onboarding/context.tsx index 869fb90ea..ebd97ddfd 100644 --- a/apps/mobile/src/screens/onboarding/context.tsx +++ b/apps/mobile/src/screens/onboarding/context.tsx @@ -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['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' } }); } diff --git a/apps/mobile/src/screens/settings/info/Debug.tsx b/apps/mobile/src/screens/settings/info/Debug.tsx index f88ebeef1..376e8b63b 100644 --- a/apps/mobile/src/screens/settings/info/Debug.tsx +++ b/apps/mobile/src/screens/settings/info/Debug.tsx @@ -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'>) => { - {JSON.stringify(featureFlags)} @@ -26,7 +26,7 @@ const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => { onPress={() => { navigation.popToTop(); navigation.replace('Settings'); - getDebugState().enabled = false; + debugState.enabled = false; }} > Disable Debug Mode diff --git a/interface/app/$libraryId/Explorer/OptionsPanel.tsx b/interface/app/$libraryId/Explorer/OptionsPanel.tsx index 881d580bf..4b06a9016 100644 --- a/interface/app/$libraryId/Explorer/OptionsPanel.tsx +++ b/interface/app/$libraryId/Explorer/OptionsPanel.tsx @@ -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; }} /> diff --git a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx index 3cd740685..654cca4a1 100644 --- a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx +++ b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx @@ -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 = () => { - (getExplorerLayoutStore().showImageSlider = + (explorerLayoutStore.showImageSlider = !explorerLayoutStore.showImageSlider) } className="w-fit px-2 text-[10px]" diff --git a/interface/app/$libraryId/Explorer/View/index.tsx b/interface/app/$libraryId/Explorer/View/index.tsx index 0338edaaf..2f0884da6 100644 --- a/interface/app/$libraryId/Explorer/View/index.tsx +++ b/interface/app/$libraryId/Explorer/View/index.tsx @@ -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'], () => { diff --git a/interface/app/$libraryId/Explorer/index.tsx b/interface/app/$libraryId/Explorer/index.tsx index 16639f608..fb8ce23bb 100644 --- a/interface/app/$libraryId/Explorer/index.tsx +++ b/interface/app/$libraryId/Explorer/index.tsx @@ -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) { useShortcut('showPathBar', (e) => { e.stopPropagation(); - getExplorerLayoutStore().showPathBar = !layoutStore.showPathBar; + explorerLayout.showPathBar = !layoutStore.showPathBar; }); useShortcut('showInspector', (e) => { diff --git a/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx index 4b57f7d1e..77b71ff0e 100644 --- a/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx @@ -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 () => { > (getDebugState().rspcLogger = !debugState.rspcLogger)} + onClick={() => (debugState.rspcLogger = !debugState.rspcLogger)} /> { !debugState.shareFullTelemetry === false && debugState.telemetryLogging ) - getDebugState().telemetryLogging = false; - getDebugState().shareFullTelemetry = !debugState.shareFullTelemetry; + debugState.telemetryLogging = false; + debugState.shareFullTelemetry = !debugState.shareFullTelemetry; }} /> @@ -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; }} /> @@ -137,7 +134,7 @@ export default () => { (getUnitFormatStore().coordinatesFormat = e)} + onChange={(e) => (unitFormatStore.coordinatesFormat = e)} value={formatStore.coordinatesFormat} > DMS @@ -240,7 +233,7 @@ export const Component = () => { (getUnitFormatStore().temperatureFormat = e)} + onChange={(e) => (unitFormatStore.temperatureFormat = e)} value={formatStore.temperatureFormat} > {t('celcius')} diff --git a/interface/app/$libraryId/settings/client/general.tsx b/interface/app/$libraryId/settings/client/general.tsx index 75554de09..54e7c1861 100644 --- a/interface/app/$libraryId/settings/client/general.tsx +++ b/interface/app/$libraryId/settings/client/general.tsx @@ -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 = () => { (getDebugState().enabled = !debugState.enabled)} + onClick={() => (debugState.enabled = !debugState.enabled)} /> { > (telemetryStore.shareFullTelemetry = !fullTelemetry)} + onClick={() => (telemetryState.shareFullTelemetry = !fullTelemetry)} size="md" /> diff --git a/interface/app/demo.react.tsx b/interface/app/demo.react.tsx new file mode 100644 index 000000000..662229dca --- /dev/null +++ b/interface/app/demo.react.tsx @@ -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 ( +
+ + <> + +
Hello from React: {count}
+
{props.demo}
+
CTX: {ctx()}
+ + + +
+ +
+ ); +} + +function Inner() { + const ctx = demoCtx.useContext(); + console.log('FROM REACT 2', ctx()); + return null; +} + +export function Demo2() { + return null; +} diff --git a/interface/app/demo.solid.tsx b/interface/app/demo.solid.tsx index 206e60268..af121795c 100644 --- a/interface/app/demo.solid.tsx +++ b/interface/app/demo.solid.tsx @@ -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 ( -
- -
Hello from Solid: {count()}
+
+ + + +
Hello from Solid: {count()}
+
CTX: {props.demo}
+ + + +
); } -export function renderDemo(element: HTMLDivElement): () => void { - return render(Demo, element); +function Inner() { + const ctx = demoCtx.useContext(); + console.log('FROM SOLID', ctx()); + return
CTX: {ctx()}
; +} + +export function Demo2() { + return null; +} + +export function Demo3(props: { demo: string }) { + const ctx = demoCtx.useContext(); + + return ( +
+
Hello from Solid again: {props.demo}
+
CTX: {ctx()}
+
+ ); } diff --git a/interface/app/index.tsx b/interface/app/index.tsx index a3e5ed08f..7edd661a4 100644 --- a/interface/app/index.tsx +++ b/interface/app/index.tsx @@ -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(null); - - useEffect(() => { - let cleanup = () => {}; - if (ref.current) cleanup = renderDemo(ref.current); - return cleanup; - }, []); - - return
; -} - // 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 ( {useFeatureFlag('debugDragAndDrop') ? : null} - {useFeatureFlag('solidJsDemo') ? : null} + {useFeatureFlag('solidJsDemo') ? ( + + ) : null} + {useFeatureFlag('solidJsDemo') ? : null} diff --git a/interface/app/onboarding/Progress.tsx b/interface/app/onboarding/Progress.tsx index 207cb37b3..58ca88bb6 100644 --- a/interface/app/onboarding/Progress.tsx +++ b/interface/app/onboarding/Progress.tsx @@ -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 = [ diff --git a/interface/app/onboarding/context.tsx b/interface/app/onboarding/context.tsx index 9e3b42c63..3a4fbd103 100644 --- a/interface/app/onboarding/context.tsx +++ b/interface/app/onboarding/context.tsx @@ -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' } }); } diff --git a/interface/app/onboarding/index.tsx b/interface/app/onboarding/index.tsx index d7df4e664..5338953b8 100644 --- a/interface/app/onboarding/index.tsx +++ b/interface/app/onboarding/index.tsx @@ -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 ; + if (onboardingStore.lastActiveScreen && !ctx.library) + return ; return ; }; @@ -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 }); diff --git a/interface/hooks/useTheme.ts b/interface/hooks/useTheme.ts index 4b6a594cc..6830847a6 100644 --- a/interface/hooks/useTheme.ts +++ b/interface/hooks/useTheme.ts @@ -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'); } diff --git a/packages/client/package.json b/packages/client/package.json index 75eb89b24..a82107c10 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -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": { diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts index 6bc293f70..27767ec5d 100644 --- a/packages/client/src/hooks/index.ts +++ b/packages/client/src/hooks/index.ts @@ -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'; diff --git a/packages/client/src/hooks/useExplorerLayoutStore.ts b/packages/client/src/hooks/useExplorerLayoutStore.ts deleted file mode 100644 index 8c76b9f6d..000000000 --- a/packages/client/src/hooks/useExplorerLayoutStore.ts +++ /dev/null @@ -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; -} diff --git a/packages/client/src/hooks/useLibraryContext.tsx b/packages/client/src/hooks/useLibraryContext.tsx index 79f4c7e1e..753bd9a0a 100644 --- a/packages/client/src/hooks/useLibraryContext.tsx +++ b/packages/client/src/hooks/useLibraryContext.tsx @@ -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; } }); diff --git a/packages/client/src/hooks/useLibraryStore.ts b/packages/client/src/hooks/useLibraryStore.ts deleted file mode 100644 index 215f31b30..000000000 --- a/packages/client/src/hooks/useLibraryStore.ts +++ /dev/null @@ -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; -} diff --git a/packages/client/src/hooks/usePlausible.tsx b/packages/client/src/hooks/usePlausible.tsx index e9a0debf1..c434264de 100644 --- a/packages/client/src/hooks/usePlausible.tsx +++ b/packages/client/src/hooks/usePlausible.tsx @@ -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; }; diff --git a/packages/client/src/hooks/useThemeStore.ts b/packages/client/src/hooks/useThemeStore.ts deleted file mode 100644 index aa66ad212..000000000 --- a/packages/client/src/hooks/useThemeStore.ts +++ /dev/null @@ -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'; -} diff --git a/packages/client/src/hooks/useUnitFormatStore.tsx b/packages/client/src/hooks/useUnitFormatStore.tsx deleted file mode 100644 index 11b9e7c99..000000000 --- a/packages/client/src/hooks/useUnitFormatStore.tsx +++ /dev/null @@ -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; -} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 3177c1a58..22aff31bf 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -32,3 +32,4 @@ export * from './lib'; export * from './form'; export * from './cache'; export * from './color'; +export * from './solid'; diff --git a/packages/client/src/lib/index.ts b/packages/client/src/lib/index.ts index a36ba1857..904729832 100644 --- a/packages/client/src/lib/index.ts +++ b/packages/client/src/lib/index.ts @@ -1,3 +1,3 @@ export * from './byte-size'; export * from './passwordStrength'; -export * from './valito'; +export * from './valtio'; diff --git a/packages/client/src/lib/valito.ts b/packages/client/src/lib/valtio.ts similarity index 100% rename from packages/client/src/lib/valito.ts rename to packages/client/src/lib/valtio.ts diff --git a/packages/client/src/solid/context.tsx b/packages/client/src/solid/context.tsx new file mode 100644 index 000000000..5bf51a5ec --- /dev/null +++ b/packages/client/src/solid/context.tsx @@ -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; +}; + +const reactGlobalContext = createReactContext([] as RegisteredContext[]); +const solidGlobalContext = createSolidContext(() => [] as RegisteredContext[]); + +export function createSharedContext(initialValue: T) { + const id = Symbol('shared-context'); + + function Provider(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([])); + + 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); +} diff --git a/packages/client/src/solid/index.ts b/packages/client/src/solid/index.ts new file mode 100644 index 000000000..60f04f19a --- /dev/null +++ b/packages/client/src/solid/index.ts @@ -0,0 +1,6 @@ +export * from './interop'; +export * from './react'; +export * from './solid.solid'; +export * from './useObserver'; +export * from './rq'; +export { createSharedContext } from './context'; diff --git a/packages/client/src/solid/internal.ts b/packages/client/src/solid/internal.ts new file mode 100644 index 000000000..d3e416c71 --- /dev/null +++ b/packages/client/src/solid/internal.ts @@ -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; + } +} diff --git a/packages/client/src/solid/interop.ts b/packages/client/src/solid/interop.ts new file mode 100644 index 000000000..f226e9d3f --- /dev/null +++ b/packages/client/src/solid/interop.ts @@ -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(store: Store) { + 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 = { + 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( + key: string, + mutable: T, + opts?: CreatePersistedMutableOpts +) { + 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; +} diff --git a/packages/client/src/solid/react.tsx b/packages/client/src/solid/react.tsx new file mode 100644 index 000000000..932bb66b4 --- /dev/null +++ b/packages/client/src/solid/react.tsx @@ -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 = + | ({ + root: (props: T) => SolidJSX.Element; + } & T) + | { + root: () => SolidJSX.Element; + }; + +export function WithSolid(props: Props) { + const ref = useRef(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
; +} diff --git a/packages/client/src/solid/rq.ts b/packages/client/src/solid/rq.ts new file mode 100644 index 000000000..2e9435448 --- /dev/null +++ b/packages/client/src/solid/rq.ts @@ -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 diff --git a/packages/client/src/solid/solid.solid.tsx b/packages/client/src/solid/solid.solid.tsx new file mode 100644 index 000000000..dadd833d3 --- /dev/null +++ b/packages/client/src/solid/solid.solid.tsx @@ -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 = + | { + root: FunctionComponent<{}>; + } + | ({ + root: FunctionComponent; + } & T); + +export function WithReact(props: Props) { + 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
; +} diff --git a/packages/client/src/solid/useObserver.ts b/packages/client/src/solid/useObserver.ts new file mode 100644 index 000000000..01bb4d2d9 --- /dev/null +++ b/packages/client/src/solid/useObserver.ts @@ -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(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(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; +} diff --git a/packages/client/src/stores/auth.ts b/packages/client/src/stores/auth.ts index 0679c3451..7fcbb8a1d 100644 --- a/packages/client/src/stores/auth.ts +++ b/packages/client/src/stores/auth.ts @@ -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({ +const store = createMutable({ state: { status: 'loading' } }); export function useStateSnapshot() { - return useSnapshot(store).state; + return useSolidStore(store).state; } nonLibraryClient diff --git a/packages/client/src/hooks/useDebugState.ts b/packages/client/src/stores/debugState.ts similarity index 58% rename from packages/client/src/hooks/useDebugState.ts rename to packages/client/src/stores/debugState.ts index 797c80a17..23fd751f4 100644 --- a/packages/client/src/hooks/useDebugState.ts +++ b/packages/client/src/stores/debugState.ts @@ -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({ + 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); diff --git a/packages/client/src/stores/explorerLayout.ts b/packages/client/src/stores/explorerLayout.ts new file mode 100644 index 000000000..56694d6ef --- /dev/null +++ b/packages/client/src/stores/explorerLayout.ts @@ -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); +} diff --git a/packages/client/src/hooks/useFeatureFlag.tsx b/packages/client/src/stores/featureFlags.tsx similarity index 76% rename from packages/client/src/hooks/useFeatureFlag.tsx rename to packages/client/src/stores/featureFlags.tsx index 9429e04df..c18be8e9d 100644 --- a/packages/client/src/hooks/useFeatureFlag.tsx +++ b/packages/client/src/stores/featureFlags.tsx @@ -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); } }); } diff --git a/packages/client/src/stores/index.ts b/packages/client/src/stores/index.ts index 86948d0df..042b35af4 100644 --- a/packages/client/src/stores/index.ts +++ b/packages/client/src/stores/index.ts @@ -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'; diff --git a/packages/client/src/stores/libraryStore.ts b/packages/client/src/stores/libraryStore.ts new file mode 100644 index 000000000..bc879cd76 --- /dev/null +++ b/packages/client/src/stores/libraryStore.ts @@ -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); +} diff --git a/packages/client/src/hooks/useOnboardingStore.ts b/packages/client/src/stores/onboardingStore.ts similarity index 55% rename from packages/client/src/hooks/useOnboardingStore.ts rename to packages/client/src/stores/onboardingStore.ts index 59e7d7722..d2cbe5a35 100644 --- a/packages/client/src/hooks/useOnboardingStore.ts +++ b/packages/client/src/stores/onboardingStore.ts @@ -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 | 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]; } } diff --git a/packages/client/src/hooks/useTelemetryState.tsx b/packages/client/src/stores/telemetryState.tsx similarity index 56% rename from packages/client/src/hooks/useTelemetryState.tsx rename to packages/client/src/stores/telemetryState.tsx index 8503b66a0..40db306c5 100644 --- a/packages/client/src/hooks/useTelemetryState.tsx +++ b/packages/client/src/stores/telemetryState.tsx @@ -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('sd-telemetryStore', { - shareFullTelemetry: false, // false by default - platform: 'unknown', - buildInfo: undefined -}); +export const telemetryState = createPersistedMutable( + 'sd-explorer-layout', + createMutable({ + shareFullTelemetry: false, // false by default + platform: 'unknown', + buildInfo: undefined + }) +); export function useTelemetryState() { - return useSnapshot(telemetryStore); + return useSolidStore(telemetryState); } diff --git a/packages/client/src/stores/themeStore.ts b/packages/client/src/stores/themeStore.ts new file mode 100644 index 000000000..963261e8e --- /dev/null +++ b/packages/client/src/stores/themeStore.ts @@ -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'; +} diff --git a/packages/client/src/stores/unitFormatStore.tsx b/packages/client/src/stores/unitFormatStore.tsx new file mode 100644 index 000000000..73a022035 --- /dev/null +++ b/packages/client/src/stores/unitFormatStore.tsx @@ -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); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 087441b4c..733dbedfd 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ