import { initRspc, wsBatchLink, type AlphaClient } from '@oscartbeaumont-sd/rspc-client/v2'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useEffect, useMemo, useState } from 'react'; import { Link, Navigate, Outlet, redirect, useMatches, useNavigate, type RouteObject } from 'react-router-dom'; import { ClientContextProvider, context, context2, currentLibraryCache, getCachedLibraries, LibraryContextProvider, nonLibraryClient, Procedures, useBridgeQuery, useCachedLibraries, useFeatureFlag, WithSolid, type LibraryProceduresDef, type NonLibraryProceduresDef } from '@sd/client'; import { Button, Dialogs, Toaster, z } from '@sd/ui'; import { RouterErrorBoundary } from '~/ErrorFallback'; import { useRoutingContext } from '~/RoutingContext'; import { Platform, PlatformProvider, usePlatform } from '..'; import libraryRoutes from './$libraryId'; import { DragAndDropDebug } from './$libraryId/debug/dnd'; import { Demo, Demo2 } from './demo.solid'; import onboardingRoutes from './onboarding'; import { RootContext } from './RootContext'; import './style.scss'; import { useZodRouteParams } from '~/hooks'; import { useP2PErrorToast } from './p2p'; // 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) const LibraryIdParamsSchema = z.object({ libraryId: z.string() }); // Broken out so this always runs after the `Toaster` is merged. function P2PErrorToast() { useP2PErrorToast(); return null; } export const createRoutes = (platform: Platform) => [ { Component: () => { const rawPath = useRawRoutePath(); return ( {useFeatureFlag('debugDragAndDrop') ? : null} {useFeatureFlag('solidJsDemo') ? ( ) : null} {useFeatureFlag('solidJsDemo') ? : null} ); }, errorElement: , children: [ { index: true, Component: () => { const libraries = useCachedLibraries(); if (libraries.status !== 'success') return null; if (libraries.data.length === 0) return ; const currentLibrary = libraries.data.find( (l) => l.uuid === currentLibraryCache.id ); const libraryId = currentLibrary ? currentLibrary.uuid : libraries.data[0]?.uuid; return ; }, loader: async () => { const libraries = await getCachedLibraries(nonLibraryClient); const currentLibrary = (libraries || []).find( (l) => l.uuid === currentLibraryCache.id ); const libraryId = currentLibrary ? currentLibrary.uuid : libraries[0]?.uuid; if (libraryId === undefined) return redirect('/onboarding', { replace: true }); return redirect(`/${libraryId}`, { replace: true }); } }, { path: 'onboarding', lazy: () => import('./onboarding/Layout'), children: onboardingRoutes }, { path: 'remote/:node', Component: (props) => , children: [ { path: 'browse', Component: BrowsePage }, { path: ':libraryId', Component: () => { const params = useZodRouteParams(LibraryIdParamsSchema); const result = useBridgeQuery(['library.list']); const libraries = result.data; const library = libraries?.find((l) => l.uuid === params.libraryId); useEffect(() => { if (!result.data) return; if (!library) { alert('Library not found'); // TODO: Redirect } }); if (!library) return <>; // TODO: Using suspense for loading return (
YOUR ON A REMOTE NODE Go Back
); }, children: [ { path: '*', lazy: () => import('./$libraryId/Layout'), children: libraryRoutes(platform) } ] } ] }, { path: ':libraryId', lazy: () => import('./$libraryId/Layout'), loader: async ({ params: { libraryId } }) => { const libraries = await getCachedLibraries(nonLibraryClient); const library = libraries.find((l) => l.uuid === libraryId); if (!library) { const firstLibrary = libraries[0]; if (firstLibrary) return redirect(`/${firstLibrary.uuid}`, { replace: true }); else return redirect('/onboarding', { replace: true }); } return null; }, children: libraryRoutes(platform) } ] } ] satisfies RouteObject[]; const ParamsSchema = z.object({ node: z.string() }); function RemoteLayout() { const platform = usePlatform(); const params = useZodRouteParams(ParamsSchema); // TODO: The caches should instead be prefixed by the remote node ID, instead of completely being recreated but that's too hard to do right now. const [rspcClient, setRspcClient] = useState< [AlphaClient, AlphaClient, QueryClient] >(); useEffect(() => { const endpoint = platform.getRemoteRspcEndpoint(params.node); const links = [ wsBatchLink({ url: endpoint.url }) ]; const client = initRspc({ links }).dangerouslyHookIntoInternals(); const libraryClient = initRspc({ links }).dangerouslyHookIntoInternals({ mapQueryKey: (keyAndInput) => { const libraryId = currentLibraryCache.id; if (libraryId === null) throw new Error('Attempted to do library operation with no library set!'); return [keyAndInput[0], { library_id: libraryId, arg: keyAndInput[1] ?? null }]; } }); setRspcClient([client, libraryClient, new QueryClient()]); return () => { // TODO: We *really* need to cleanup `client` so we aren't leaking all the resources. }; }, [params.node, platform]); // TODO: Detect if the remote node if offline and render something to show that const newPlatform = useMemo( () => ({ ...platform, getThumbnailUrlByThumbKey: (thumbKey) => platform.constructRemoteRspcPath( params.node, `thumbnail/${encodeURIComponent( thumbKey.base_directory_str )}/${encodeURIComponent(thumbKey.shard_hex)}/${encodeURIComponent( thumbKey.cas_id )}.webp` ), getFileUrl: (libraryId, locationLocalId, filePathId) => platform.constructRemoteRspcPath( params.node, `file/${encodeURIComponent(libraryId)}/${encodeURIComponent( locationLocalId )}/${encodeURIComponent(filePathId)}` ), getFileUrlByPath: (path) => platform.constructRemoteRspcPath( params.node, `local-file-by-path/${encodeURIComponent(path)}` ) }) satisfies Platform, [platform, params.node] ); return ( {/* TODO: Maybe library context too? */} {rspcClient && ( )} ); } function BrowsePage() { const navigate = useNavigate(); const result = useBridgeQuery(['library.list']); const libraries = result.data; return (

Browse Libraries On Remote Node:

{libraries?.map((l) => ( ))}
); } /** * Combines the `path` segments of the current route into a single string. * This is useful for things like analytics, where we want the route path * but not the values used in the route params. */ const useRawRoutePath = () => { const { routes } = useRoutingContext(); // `useMatches` returns a list of each matched RouteObject, // we grab the last one as it contains all previous route segments. const lastMatchId = useMatches().slice(-1)[0]?.id; const rawPath = useMemo(() => { const [rawPath] = lastMatchId // Gets a list of the index of each route segment ?.split('-') ?.map((s) => parseInt(s)) // Gets the route object for each segment and appends the `path`, if there is one ?.reduce( ([rawPath, { children }], path) => { // No `children`, nowhere to go if (!children) return [rawPath, { children }] as any; const item = children[path]!; // No `path`, continue without adding to path if (!('path' in item)) return [rawPath, item]; // `path` found, chuck it on the end return [`${rawPath}/${item.path}`, item]; }, ['' as string, { children: routes }] as const ) ?? []; return rawPath ?? '/'; }, [lastMatchId, routes]); return rawPath; };