mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 13:26:00 -04:00
[ENG-1400] Normalised caching (#1734)
* prototype * `.normalise` helper + only `String` keys * implement it for 'search.paths' * redux devtools * fix * refactor backend * wip: upgrade to rspc fork * mega cursed * Upgrade Specta-related stuff * Upgrade Typescript * Cache debug page * bruh * Fix optimistic library setting * Cache clearing * better timeout * Fix tags page * bit of cleanup --------- Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,7 +14,6 @@ cli/turbo-new.exe
|
||||
cli/turbo.exe
|
||||
storybook-static/
|
||||
.DS_Store
|
||||
cache
|
||||
.env*
|
||||
vendor/
|
||||
data
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -10,9 +10,11 @@
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"fontsource",
|
||||
"ianvs",
|
||||
"ipfs",
|
||||
"Keepsafe",
|
||||
"nodestate",
|
||||
"normalise",
|
||||
"overscan",
|
||||
"pathctx",
|
||||
"prismjs",
|
||||
@@ -26,7 +28,6 @@
|
||||
"tailwindcss",
|
||||
"tanstack",
|
||||
"titlebar",
|
||||
"ianvs",
|
||||
"tsparticles",
|
||||
"unlisten",
|
||||
"upsert",
|
||||
|
||||
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
14
Cargo.toml
14
Cargo.toml
@@ -21,19 +21,19 @@ edition = "2021"
|
||||
repository = "https://github.com/spacedriveapp/spacedrive"
|
||||
|
||||
[workspace.dependencies]
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", branch = "spacedrive", features = [
|
||||
prisma-client-rust = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "9f8ac122e8f2b2e4957b71f48a37e06565adba40", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", branch = "spacedrive", features = [
|
||||
prisma-client-rust-cli = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "9f8ac122e8f2b2e4957b71f48a37e06565adba40", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust", branch = "spacedrive", features = [
|
||||
prisma-client-rust-sdk = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "9f8ac122e8f2b2e4957b71f48a37e06565adba40", features = [
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
|
||||
@@ -44,8 +44,8 @@ tracing-subscriber = { git = "https://github.com/tokio-rs/tracing", rev = "29146
|
||||
tracing-appender = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e" } # Unreleased changes for rolling log deletion
|
||||
|
||||
rspc = { version = "0.1.4" }
|
||||
specta = { version = "1.0.5" }
|
||||
tauri-specta = { version = "1.0.2" }
|
||||
specta = { version = "=2.0.0-rc.7" }
|
||||
tauri-specta = { version = "=2.0.0-rc.4" }
|
||||
|
||||
swift-rs = { version = "1.0.6" }
|
||||
|
||||
@@ -59,6 +59,4 @@ serde_json = { version = "1.0" }
|
||||
if-watch = { git = "https://github.com/oscartbeaumont/if-watch.git", rev = "f732786057e57250e863a9ea0b1874e4cc9907c2" }
|
||||
|
||||
# Beta features
|
||||
specta = { git = "https://github.com/oscartbeaumont/specta", rev = "4bc6e46fc8747cd8d8a07597c1fe13c52aa16a41" }
|
||||
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "adebce542049b982dd251466d4144f4d57e92177" }
|
||||
tauri-specta = { git = "https://github.com/oscartbeaumont/tauri-specta", rev = "c964bef228a90a66effc18cefcba6859c45a8e08" }
|
||||
rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "f3347e2e8bfe3f37bfacc437ca329fe71cdcb048" }
|
||||
|
||||
@@ -13,6 +13,7 @@ use tauri::{
|
||||
WindowEvent,
|
||||
};
|
||||
use tauri_plugins::{sd_error_plugin, sd_server_plugin};
|
||||
use tauri_specta::ts;
|
||||
use tokio::time::sleep;
|
||||
use tracing::error;
|
||||
|
||||
@@ -143,16 +144,6 @@ async fn open_logs_dir(node: tauri::State<'_, Arc<Node>>) -> Result<(), ()> {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(@Oscar): A helper like this should probs exist in tauri-specta
|
||||
macro_rules! tauri_handlers {
|
||||
($($name:path),+) => {{
|
||||
#[cfg(debug_assertions)]
|
||||
tauri_specta::ts::export(specta::collect_types![$($name),+], "../src/commands.ts").unwrap();
|
||||
|
||||
tauri::generate_handler![$($name),+]
|
||||
}};
|
||||
}
|
||||
|
||||
const CLIENT_ID: &str = "2abb241e-40b8-4517-a3e3-5594375c8fbb";
|
||||
|
||||
#[tokio::main]
|
||||
@@ -219,9 +210,41 @@ async fn main() -> tauri::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
let specta_builder = {
|
||||
let specta_builder = ts::builder()
|
||||
.commands(tauri_specta::collect_commands![
|
||||
app_ready,
|
||||
reset_spacedrive,
|
||||
open_logs_dir,
|
||||
refresh_menu_bar,
|
||||
reload_webview,
|
||||
set_menu_bar_item_state,
|
||||
request_fda_macos,
|
||||
file::open_file_paths,
|
||||
file::open_ephemeral_files,
|
||||
file::get_file_path_open_with_apps,
|
||||
file::get_ephemeral_files_open_with_apps,
|
||||
file::open_file_path_with,
|
||||
file::open_ephemeral_file_with,
|
||||
file::reveal_items,
|
||||
theme::lock_app_theme,
|
||||
// TODO: move to plugin w/tauri-specta
|
||||
updater::check_for_update,
|
||||
updater::install_update
|
||||
])
|
||||
// .events(tauri_specta::collect_events![])
|
||||
.config(specta::ts::ExportConfig::default().formatter(specta::ts::formatter::prettier));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let specta_builder = specta_builder.path("../src/commands.ts");
|
||||
|
||||
specta_builder.into_plugin()
|
||||
};
|
||||
|
||||
let app = app
|
||||
.plugin(updater::plugin())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(specta_builder)
|
||||
.setup(move |app| {
|
||||
let app = app.handle();
|
||||
|
||||
@@ -300,26 +323,6 @@ async fn main() -> tauri::Result<()> {
|
||||
})
|
||||
.menu(menu::get_menu())
|
||||
.manage(updater::State::default())
|
||||
.invoke_handler(tauri_handlers![
|
||||
app_ready,
|
||||
reset_spacedrive,
|
||||
open_logs_dir,
|
||||
refresh_menu_bar,
|
||||
reload_webview,
|
||||
set_menu_bar_item_state,
|
||||
request_fda_macos,
|
||||
file::open_file_paths,
|
||||
file::open_ephemeral_files,
|
||||
file::get_file_path_open_with_apps,
|
||||
file::get_ephemeral_files_open_with_apps,
|
||||
file::open_file_path_with,
|
||||
file::open_ephemeral_file_with,
|
||||
file::reveal_items,
|
||||
theme::lock_app_theme,
|
||||
// TODO: move to plugin w/tauri-specta
|
||||
updater::check_for_update,
|
||||
updater::install_update
|
||||
])
|
||||
.build(tauri::generate_context!())?;
|
||||
|
||||
app.run(|_, _| {});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { listen } from '@tauri-apps/api/event';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { startTransition, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { RspcProvider } from '@sd/client';
|
||||
import { CacheProvider, createCache, RspcProvider } from '@sd/client';
|
||||
import {
|
||||
createRoutes,
|
||||
ErrorPage,
|
||||
@@ -19,7 +19,7 @@ import { getSpacedropState } from '@sd/interface/hooks/useSpacedropState';
|
||||
|
||||
import '@sd/ui/style/style.scss';
|
||||
|
||||
import * as commands from './commands';
|
||||
import { commands } from './commands';
|
||||
import { platform } from './platform';
|
||||
import { queryClient } from './query';
|
||||
import { createMemoryRouterWithHistory } from './router';
|
||||
@@ -80,7 +80,9 @@ export default function App() {
|
||||
// we have a minimum delay between creating new tabs as react router can't handle creating tabs super fast
|
||||
const TAB_CREATE_DELAY = 150;
|
||||
|
||||
const routes = createRoutes(platform);
|
||||
const cache = createCache();
|
||||
|
||||
const routes = createRoutes(platform, cache);
|
||||
|
||||
function AppInner() {
|
||||
const [tabs, setTabs] = useState(() => [createTab()]);
|
||||
@@ -142,84 +144,86 @@ function AppInner() {
|
||||
}, [tab.element]);
|
||||
|
||||
return (
|
||||
<RouteTitleContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
setTitle(title) {
|
||||
setTabs((oldTabs) => {
|
||||
const tabs = [...oldTabs];
|
||||
const tab = tabs[tabIndex];
|
||||
if (!tab) return tabs;
|
||||
|
||||
tabs[tabIndex] = { ...tab, title };
|
||||
|
||||
return tabs;
|
||||
});
|
||||
}
|
||||
}),
|
||||
[tabIndex]
|
||||
)}
|
||||
>
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
tabIndex,
|
||||
setTabIndex,
|
||||
tabs: tabs.map(({ router, title }) => ({ router, title })),
|
||||
createTab() {
|
||||
createTabPromise.current = createTabPromise.current.then(
|
||||
() =>
|
||||
new Promise((res) => {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const newTabs = [...tabs, createTab()];
|
||||
|
||||
setTabIndex(newTabs.length - 1);
|
||||
|
||||
return newTabs;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(res, TAB_CREATE_DELAY);
|
||||
})
|
||||
);
|
||||
},
|
||||
removeTab(index: number) {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const tab = tabs[index];
|
||||
<CacheProvider cache={cache}>
|
||||
<RouteTitleContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
setTitle(title) {
|
||||
setTabs((oldTabs) => {
|
||||
const tabs = [...oldTabs];
|
||||
const tab = tabs[tabIndex];
|
||||
if (!tab) return tabs;
|
||||
|
||||
tab.dispose();
|
||||
tabs[tabIndex] = { ...tab, title };
|
||||
|
||||
tabs.splice(index, 1);
|
||||
|
||||
setTabIndex(Math.min(tabIndex, tabs.length - 1));
|
||||
|
||||
return [...tabs];
|
||||
return tabs;
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
}
|
||||
}),
|
||||
[tabIndex]
|
||||
)}
|
||||
>
|
||||
<SpacedriveInterfaceRoot>
|
||||
{tabs.map((tab) =>
|
||||
createPortal(
|
||||
<SpacedriveRouterProvider
|
||||
key={tab.id}
|
||||
routing={{
|
||||
routes,
|
||||
visible: tabIndex === tabs.indexOf(tab),
|
||||
router: tab.router,
|
||||
currentIndex: tab.currentIndex,
|
||||
maxIndex: tab.maxIndex
|
||||
}}
|
||||
/>,
|
||||
tab.element
|
||||
)
|
||||
)}
|
||||
<div ref={ref} />
|
||||
</SpacedriveInterfaceRoot>
|
||||
</TabsContext.Provider>
|
||||
</RouteTitleContext.Provider>
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
tabIndex,
|
||||
setTabIndex,
|
||||
tabs: tabs.map(({ router, title }) => ({ router, title })),
|
||||
createTab() {
|
||||
createTabPromise.current = createTabPromise.current.then(
|
||||
() =>
|
||||
new Promise((res) => {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const newTabs = [...tabs, createTab()];
|
||||
|
||||
setTabIndex(newTabs.length - 1);
|
||||
|
||||
return newTabs;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(res, TAB_CREATE_DELAY);
|
||||
})
|
||||
);
|
||||
},
|
||||
removeTab(index: number) {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const tab = tabs[index];
|
||||
if (!tab) return tabs;
|
||||
|
||||
tab.dispose();
|
||||
|
||||
tabs.splice(index, 1);
|
||||
|
||||
setTabIndex(Math.min(tabIndex, tabs.length - 1));
|
||||
|
||||
return [...tabs];
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SpacedriveInterfaceRoot>
|
||||
{tabs.map((tab) =>
|
||||
createPortal(
|
||||
<SpacedriveRouterProvider
|
||||
key={tab.id}
|
||||
routing={{
|
||||
routes,
|
||||
visible: tabIndex === tabs.indexOf(tab),
|
||||
router: tab.router,
|
||||
currentIndex: tab.currentIndex,
|
||||
maxIndex: tab.maxIndex
|
||||
}}
|
||||
/>,
|
||||
tab.element
|
||||
)
|
||||
)}
|
||||
<div ref={ref} />
|
||||
</SpacedriveInterfaceRoot>
|
||||
</TabsContext.Provider>
|
||||
</RouteTitleContext.Provider>
|
||||
</CacheProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,86 +1,230 @@
|
||||
/* eslint-disable */
|
||||
/** tauri-specta globals **/
|
||||
|
||||
import { invoke as TAURI_INVOKE } from '@tauri-apps/api';
|
||||
import * as TAURI_API_EVENT from '@tauri-apps/api/event';
|
||||
import { type WebviewWindowHandle as __WebviewWindowHandle__ } from '@tauri-apps/api/window';
|
||||
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI_INVOKE__<T>(cmd: string, args?: Record<string, unknown>): Promise<T>;
|
||||
}
|
||||
export const commands = {
|
||||
async appReady(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|app_ready');
|
||||
},
|
||||
async resetSpacedrive(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|reset_spacedrive');
|
||||
},
|
||||
async openLogsDir(): Promise<__Result__<null, null>> {
|
||||
try {
|
||||
return { status: 'ok', data: await TAURI_INVOKE('plugin:tauri-specta|open_logs_dir') };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async refreshMenuBar(): Promise<__Result__<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|refresh_menu_bar')
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async reloadWebview(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|reload_webview');
|
||||
},
|
||||
async setMenuBarItemState(id: string, enabled: boolean): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|set_menu_bar_item_state', { id, enabled });
|
||||
},
|
||||
async requestFdaMacos(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|request_fda_macos');
|
||||
},
|
||||
async openFilePaths(
|
||||
library: string,
|
||||
ids: number[]
|
||||
): Promise<
|
||||
__Result__<
|
||||
(
|
||||
| { t: 'NoLibrary' }
|
||||
| { t: 'NoFile'; c: number }
|
||||
| { t: 'OpenError'; c: [number, string] }
|
||||
| { t: 'AllGood'; c: number }
|
||||
| { t: 'Internal'; c: string }
|
||||
)[],
|
||||
null
|
||||
>
|
||||
> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_file_paths', { library, ids })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openEphemeralFiles(
|
||||
paths: string[]
|
||||
): Promise<__Result__<({ t: 'Ok'; c: string } | { t: 'Err'; c: string })[], null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_ephemeral_files', { paths })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async getFilePathOpenWithApps(
|
||||
library: string,
|
||||
ids: number[]
|
||||
): Promise<__Result__<{ url: string; name: string }[], null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|get_file_path_open_with_apps', {
|
||||
library,
|
||||
ids
|
||||
})
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async getEphemeralFilesOpenWithApps(
|
||||
paths: string[]
|
||||
): Promise<__Result__<{ url: string; name: string }[], null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|get_ephemeral_files_open_with_apps', {
|
||||
paths
|
||||
})
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openFilePathWith(
|
||||
library: string,
|
||||
fileIdsAndUrls: [number, string][]
|
||||
): Promise<__Result__<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_file_path_with', {
|
||||
library,
|
||||
fileIdsAndUrls
|
||||
})
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openEphemeralFileWith(pathsAndUrls: [string, string][]): Promise<__Result__<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_ephemeral_file_with', {
|
||||
pathsAndUrls
|
||||
})
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async revealItems(library: string, items: RevealItem[]): Promise<__Result__<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|reveal_items', { library, items })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async lockAppTheme(themeType: AppThemeType): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|lock_app_theme', { themeType });
|
||||
},
|
||||
async checkForUpdate(): Promise<
|
||||
__Result__<{ version: string; body: string | null } | null, string>
|
||||
> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|check_for_update')
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async installUpdate(): Promise<__Result__<null, string>> {
|
||||
try {
|
||||
return { status: 'ok', data: await TAURI_INVOKE('plugin:tauri-specta|install_update') };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** user-defined types **/
|
||||
|
||||
export type AppThemeType = 'Auto' | 'Light' | 'Dark';
|
||||
export type RevealItem =
|
||||
| { Location: { id: number } }
|
||||
| { FilePath: { id: number } }
|
||||
| { Ephemeral: { path: string } };
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
once: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||
emit: T extends null
|
||||
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||
};
|
||||
|
||||
type __Result__<T, E> = { status: 'ok'; data: T } | { status: 'error'; error: E };
|
||||
|
||||
function __makeEvents__<T extends Record<string, any>>(mappings: Record<keyof T, string>) {
|
||||
return new Proxy(
|
||||
{} as unknown as {
|
||||
[K in keyof T]: __EventObj__<T[K]> & {
|
||||
(handle: __WebviewWindowHandle__): __EventObj__<T[K]>;
|
||||
};
|
||||
},
|
||||
{
|
||||
get: (_, event) => {
|
||||
const name = mappings[event as keyof T];
|
||||
|
||||
return new Proxy((() => {}) as any, {
|
||||
apply: (_, __, [window]: [__WebviewWindowHandle__]) => ({
|
||||
listen: (arg: any) => window.listen(name, arg),
|
||||
once: (arg: any) => window.once(name, arg),
|
||||
emit: (arg: any) => window.emit(name, arg)
|
||||
}),
|
||||
get: (_, command: keyof __EventObj__<any>) => {
|
||||
switch (command) {
|
||||
case 'listen':
|
||||
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
|
||||
case 'once':
|
||||
return (arg: any) => TAURI_API_EVENT.once(name, arg);
|
||||
case 'emit':
|
||||
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Function avoids 'window not defined' in SSR
|
||||
const invoke = () => window.__TAURI_INVOKE__;
|
||||
|
||||
export function appReady() {
|
||||
return invoke()<null>("app_ready")
|
||||
}
|
||||
|
||||
export function resetSpacedrive() {
|
||||
return invoke()<null>("reset_spacedrive")
|
||||
}
|
||||
|
||||
export function openLogsDir() {
|
||||
return invoke()<null>("open_logs_dir")
|
||||
}
|
||||
|
||||
export function refreshMenuBar() {
|
||||
return invoke()<null>("refresh_menu_bar")
|
||||
}
|
||||
|
||||
export function reloadWebview() {
|
||||
return invoke()<null>("reload_webview")
|
||||
}
|
||||
|
||||
export function setMenuBarItemState(id: string, enabled: boolean) {
|
||||
return invoke()<null>("set_menu_bar_item_state", { id,enabled })
|
||||
}
|
||||
|
||||
export function requestFdaMacos() {
|
||||
return invoke()<null>("request_fda_macos")
|
||||
}
|
||||
|
||||
export function openFilePaths(library: string, ids: number[]) {
|
||||
return invoke()<OpenFilePathResult[]>("open_file_paths", { library,ids })
|
||||
}
|
||||
|
||||
export function openEphemeralFiles(paths: string[]) {
|
||||
return invoke()<EphemeralFileOpenResult[]>("open_ephemeral_files", { paths })
|
||||
}
|
||||
|
||||
export function getFilePathOpenWithApps(library: string, ids: number[]) {
|
||||
return invoke()<OpenWithApplication[]>("get_file_path_open_with_apps", { library,ids })
|
||||
}
|
||||
|
||||
export function getEphemeralFilesOpenWithApps(paths: string[]) {
|
||||
return invoke()<OpenWithApplication[]>("get_ephemeral_files_open_with_apps", { paths })
|
||||
}
|
||||
|
||||
export function openFilePathWith(library: string, fileIdsAndUrls: ([number, string])[]) {
|
||||
return invoke()<null>("open_file_path_with", { library,fileIdsAndUrls })
|
||||
}
|
||||
|
||||
export function openEphemeralFileWith(pathsAndUrls: ([string, string])[]) {
|
||||
return invoke()<null>("open_ephemeral_file_with", { pathsAndUrls })
|
||||
}
|
||||
|
||||
export function revealItems(library: string, items: RevealItem[]) {
|
||||
return invoke()<null>("reveal_items", { library,items })
|
||||
}
|
||||
|
||||
export function lockAppTheme(themeType: AppThemeType) {
|
||||
return invoke()<null>("lock_app_theme", { themeType })
|
||||
}
|
||||
|
||||
export function checkForUpdate() {
|
||||
return invoke()<Update | null>("check_for_update")
|
||||
}
|
||||
|
||||
export function installUpdate() {
|
||||
return invoke()<null>("install_update")
|
||||
}
|
||||
|
||||
export type Update = { version: string; body: string | null }
|
||||
export type OpenWithApplication = { url: string; name: string }
|
||||
export type AppThemeType = "Auto" | "Light" | "Dark"
|
||||
export type EphemeralFileOpenResult = { t: "Ok"; c: string } | { t: "Err"; c: string }
|
||||
export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string }
|
||||
export type RevealItem = { Location: { id: number } } | { FilePath: { id: number } } | { Ephemeral: { path: string } }
|
||||
|
||||
@@ -4,7 +4,7 @@ import { homeDir } from '@tauri-apps/api/path';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { OperatingSystem, Platform } from '@sd/interface';
|
||||
|
||||
import * as commands from './commands';
|
||||
import { commands } from './commands';
|
||||
import { env } from './env';
|
||||
import { createUpdater } from './updater';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { proxy, useSnapshot } from 'valtio';
|
||||
import { UpdateStore } from '@sd/interface';
|
||||
import { toast, ToastId } from '@sd/ui';
|
||||
|
||||
import * as commands from './commands';
|
||||
import { commands } from './commands';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -27,9 +27,15 @@ export function createUpdater() {
|
||||
const onInstallCallbacks = new Set<() => void>();
|
||||
|
||||
async function checkForUpdate() {
|
||||
const update = await commands.checkForUpdate();
|
||||
const result = await commands.checkForUpdate();
|
||||
|
||||
if (!update) return null;
|
||||
if (result.status === 'error') {
|
||||
console.error('UPDATER ERROR', result.error);
|
||||
// TODO: Show some UI?
|
||||
return null;
|
||||
}
|
||||
if (!result.data) return null;
|
||||
const update = result.data;
|
||||
|
||||
let id: ToastId | null = null;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
@@ -39,7 +39,9 @@ const DrawerLocations = ({ stackName }: DrawerLocationsProp) => {
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const { data: locations } = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
const result = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const locations = useCache(result.data?.items);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useRef } from 'react';
|
||||
import { ColorValue, Pressable, Text, View } from 'react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
@@ -37,6 +37,8 @@ const DrawerTags = ({ stackName }: DrawerTagsProp) => {
|
||||
const navigation = useNavigation<DrawerNavigationHelpers>();
|
||||
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
useNodes(tags.data?.nodes);
|
||||
const tagData = useCache(tags.data?.items);
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
@@ -47,7 +49,7 @@ const DrawerTags = ({ stackName }: DrawerTagsProp) => {
|
||||
containerStyle={tw`mb-3 ml-1 mt-6`}
|
||||
>
|
||||
<View style={tw`mt-2`}>
|
||||
{tags.data?.map((tag) => (
|
||||
{tagData?.map((tag) => (
|
||||
<DrawerTagItem
|
||||
key={tag.id}
|
||||
tagName={tag.name!}
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
isPath,
|
||||
useLibraryQuery
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes
|
||||
} from '@sd/client';
|
||||
import { InfoPill, PlaceholderPill } from '~/components/primitive/InfoPill';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
@@ -23,6 +25,8 @@ const InfoTagPills = ({ data, style }: Props) => {
|
||||
const tagsQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
|
||||
enabled: objectData != null
|
||||
});
|
||||
useNodes(tagsQuery.data?.nodes);
|
||||
const items = useCache(tagsQuery.data?.items);
|
||||
|
||||
const isDir = data && isPath(data) ? data.item.is_dir : false;
|
||||
|
||||
@@ -35,7 +39,7 @@ const InfoTagPills = ({ data, style }: Props) => {
|
||||
<InfoPill text={filePath.extension} containerStyle={tw`mr-1`} />
|
||||
)}
|
||||
{/* TODO: What happens if I have too many? */}
|
||||
{tagsQuery.data?.map((tag) => (
|
||||
{items?.map((tag) => (
|
||||
<InfoPill
|
||||
key={tag.id}
|
||||
text={tag.name ?? 'Unnamed Tag'}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { useBridgeMutation, usePlausibleEvent } from '@sd/client';
|
||||
import {
|
||||
insertLibrary,
|
||||
useBridgeMutation,
|
||||
useNormalisedCache,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
import { ModalInput } from '~/components/form/Input';
|
||||
import { Modal, ModalRef } from '~/components/layout/Modal';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
@@ -13,6 +18,7 @@ const CreateLibraryModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||
const modalRef = useForwardedRef(ref);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const cache = useNormalisedCache();
|
||||
const [libName, setLibName] = useState('');
|
||||
|
||||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
@@ -20,17 +26,15 @@ const CreateLibraryModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
|
||||
'library.create',
|
||||
{
|
||||
onSuccess: (lib) => {
|
||||
onSuccess: (libRaw) => {
|
||||
cache.withNodes(libRaw.nodes);
|
||||
const lib = cache.withCache(libRaw.item);
|
||||
|
||||
// Reset form
|
||||
setLibName('');
|
||||
|
||||
// We do this instead of invalidating the query because it triggers a full app re-render??
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||
// The invalidation system beat us to it
|
||||
if (libraries.find((l: any) => l.uuid === lib.uuid)) return libraries;
|
||||
|
||||
return [...(libraries || []), lib];
|
||||
});
|
||||
insertLibrary(queryClient, lib);
|
||||
|
||||
// Switch to the new library
|
||||
currentLibraryStore.id = lib.uuid;
|
||||
|
||||
@@ -61,9 +61,9 @@ const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
|
||||
const objectData = data && getItemObject(data);
|
||||
const filePathData = data && getItemFilePath(data);
|
||||
|
||||
const fullObjectData = useLibraryQuery(['files.get', { id: objectData?.id || -1 }], {
|
||||
enabled: objectData?.id !== undefined
|
||||
});
|
||||
// const fullObjectData = useLibraryQuery(['files.get', objectData?.id || -1], {
|
||||
// enabled: objectData?.id !== undefined
|
||||
// });
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import Explorer from '~/components/explorer/Explorer';
|
||||
import { SharedScreenProps } from '~/navigation/SharedScreens';
|
||||
import { getExplorerStore } from '~/stores/explorerStore';
|
||||
@@ -8,6 +8,8 @@ export default function LocationScreen({ navigation, route }: SharedScreenProps<
|
||||
const { id, path } = route.params;
|
||||
|
||||
const location = useLibraryQuery(['locations.get', route.params.id]);
|
||||
useNodes(location.data?.nodes);
|
||||
const locationData = useCache(location.data?.item);
|
||||
|
||||
const { data } = useLibraryQuery([
|
||||
'search.paths',
|
||||
@@ -23,6 +25,8 @@ export default function LocationScreen({ navigation, route }: SharedScreenProps<
|
||||
take: 100
|
||||
}
|
||||
]);
|
||||
const pathsItemsReferences = useMemo(() => data?.items ?? [], [data]);
|
||||
const pathsItems = useCache(pathsItemsReferences);
|
||||
|
||||
useEffect(() => {
|
||||
// Set screen title to location.
|
||||
@@ -36,15 +40,15 @@ export default function LocationScreen({ navigation, route }: SharedScreenProps<
|
||||
});
|
||||
} else {
|
||||
navigation.setOptions({
|
||||
title: location.data?.name ?? 'Location'
|
||||
title: locationData?.name ?? 'Location'
|
||||
});
|
||||
}
|
||||
}, [location.data?.name, navigation, path]);
|
||||
}, [locationData?.name, navigation, path]);
|
||||
|
||||
useEffect(() => {
|
||||
getExplorerStore().locationId = id;
|
||||
getExplorerStore().path = path ?? '';
|
||||
}, [id, path]);
|
||||
|
||||
return <Explorer items={data?.items} />;
|
||||
return <Explorer items={pathsItems} />;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MagnifyingGlass } from 'phosphor-react-native';
|
||||
import { Suspense, useDeferredValue, useMemo, useState } from 'react';
|
||||
import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { getExplorerItemData, SearchFilterArgs, useLibraryQuery } from '@sd/client';
|
||||
import { getExplorerItemData, SearchFilterArgs, useCache, useLibraryQuery } from '@sd/client';
|
||||
import Explorer from '~/components/explorer/Explorer';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { RootStackScreenProps } from '~/navigation';
|
||||
@@ -45,19 +45,20 @@ const SearchScreen = ({ navigation }: RootStackScreenProps<'Search'>) => {
|
||||
}
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const items = query.data?.items;
|
||||
const pathsItemsReferences = useMemo(() => query.data?.items ?? [], [query.data]);
|
||||
const pathsItems = useCache(pathsItemsReferences);
|
||||
|
||||
const items = useMemo(() => {
|
||||
// Mobile does not thave media layout
|
||||
// if (explorerStore.layoutMode !== 'media') return items;
|
||||
// if (explorerStore.layoutMode !== 'media') return pathsItems;
|
||||
|
||||
return (
|
||||
items?.filter((item) => {
|
||||
pathsItems?.filter((item) => {
|
||||
const { kind } = getExplorerItemData(item);
|
||||
return kind === 'Video' || kind === 'Image';
|
||||
}) ?? []
|
||||
);
|
||||
}, [query.data]);
|
||||
}, [pathsItems]);
|
||||
|
||||
return (
|
||||
<View style={twStyle('flex-1', { marginTop: top + 10 })}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import Explorer from '~/components/explorer/Explorer';
|
||||
import { SharedScreenProps } from '~/navigation/SharedScreens';
|
||||
|
||||
@@ -13,15 +13,19 @@ export default function TagScreen({ navigation, route }: SharedScreenProps<'Tag'
|
||||
take: 100
|
||||
}
|
||||
]);
|
||||
useNodes(search.data?.nodes);
|
||||
const searchData = useCache(search.data?.items);
|
||||
|
||||
const tag = useLibraryQuery(['tags.get', id]);
|
||||
useNodes(tag.data?.nodes);
|
||||
const tagData = useCache(tag.data?.item);
|
||||
|
||||
useEffect(() => {
|
||||
// Set screen title to tag name.
|
||||
navigation.setOptions({
|
||||
title: tag.data?.name ?? 'Tag'
|
||||
title: tagData?.name ?? 'Tag'
|
||||
});
|
||||
}, [tag.data?.name, navigation]);
|
||||
}, [tagData?.name, navigation]);
|
||||
|
||||
return <Explorer items={search.data?.items} />;
|
||||
return <Explorer items={searchData} />;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import { z } from 'zod';
|
||||
import {
|
||||
currentLibraryCache,
|
||||
getOnboardingStore,
|
||||
insertLibrary,
|
||||
resetOnboardingStore,
|
||||
telemetryStore,
|
||||
useBridgeMutation,
|
||||
useCachedLibraries,
|
||||
useMultiZodForm,
|
||||
useNormalisedCache,
|
||||
useOnboardingStore,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
@@ -66,13 +68,14 @@ const useFormState = () => {
|
||||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const cache = useNormalisedCache();
|
||||
const createLibrary = useBridgeMutation('library.create', {
|
||||
onSuccess: (lib) => {
|
||||
onSuccess: (libRaw) => {
|
||||
cache.withNodes(libRaw.nodes);
|
||||
const lib = cache.withCache(libRaw.item);
|
||||
|
||||
// We do this instead of invalidating the query because it triggers a full app re-render??
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => [
|
||||
...(libraries || []),
|
||||
lib
|
||||
]);
|
||||
insertLibrary(queryClient, lib);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,13 +89,15 @@ const useFormState = () => {
|
||||
|
||||
try {
|
||||
// show creation screen for a bit for smoothness
|
||||
const [library] = await Promise.all([
|
||||
const [libraryRaw] = await Promise.all([
|
||||
createLibrary.mutateAsync({
|
||||
name: data.NewLibrary.name,
|
||||
default_locations: null
|
||||
}),
|
||||
new Promise((res) => setTimeout(res, 500))
|
||||
]);
|
||||
cache.withNodes(libraryRaw.nodes);
|
||||
const library = cache.withCache(libraryRaw.item);
|
||||
|
||||
if (telemetryStore.shareFullTelemetry) {
|
||||
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CaretRight, Pen, Trash } from 'phosphor-react-native';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Animated, FlatList, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { LibraryConfigWrapped, useBridgeQuery } from '@sd/client';
|
||||
import { LibraryConfigWrapped, useBridgeQuery, useCache, useNodes } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import DeleteLibraryModal from '~/components/modal/confirmModals/DeleteLibraryModal';
|
||||
import { AnimatedButton, FakeButton } from '~/components/primitive/Button';
|
||||
@@ -68,7 +68,9 @@ function LibraryItem({
|
||||
}
|
||||
|
||||
const LibrarySettingsScreen = ({ navigation }: SettingsStackScreenProps<'LibrarySettings'>) => {
|
||||
const { data: libraries } = useBridgeQuery(['library.list']);
|
||||
const libraryList = useBridgeQuery(['library.list']);
|
||||
useNodes(libraryList.data?.nodes);
|
||||
const libraries = useCache(libraryList.data?.items);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { Alert, ScrollView, Text, View } from 'react-native';
|
||||
import { z } from 'zod';
|
||||
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
||||
import { useLibraryMutation, useLibraryQuery, useNormalisedCache, useZodForm } from '@sd/client';
|
||||
import { Input } from '~/components/form/Input';
|
||||
import { Switch } from '~/components/form/Switch';
|
||||
import DeleteLocationModal from '~/components/modal/confirmModals/DeleteLocationModal';
|
||||
@@ -36,6 +36,7 @@ const EditLocationSettingsScreen = ({
|
||||
const { id } = route.params;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
const form = useZodForm({ schema });
|
||||
|
||||
@@ -93,12 +94,15 @@ const EditLocationSettingsScreen = ({
|
||||
}, [form, navigation, onSubmit]);
|
||||
|
||||
useLibraryQuery(['locations.getWithRules', id], {
|
||||
onSuccess: (data) => {
|
||||
onSuccess: (dataRaw) => {
|
||||
cache.withNodes(dataRaw?.nodes);
|
||||
const data = cache.withCache(dataRaw?.item);
|
||||
|
||||
if (data && !form.formState.isDirty)
|
||||
form.reset({
|
||||
displayName: data.name,
|
||||
localPath: data.path,
|
||||
indexer_rules_ids: data.indexer_rules.map((i) => i.indexer_rule.id.toString()),
|
||||
indexer_rules_ids: data.indexer_rules.map((i) => i.id.toString()),
|
||||
generatePreviewMedia: data.generate_preview_media,
|
||||
syncPreviewMedia: data.sync_preview_media,
|
||||
hidden: data.hidden
|
||||
|
||||
@@ -5,8 +5,10 @@ import { Swipeable } from 'react-native-gesture-handler';
|
||||
import {
|
||||
arraysEqual,
|
||||
Location,
|
||||
useCache,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import FolderIcon from '~/components/icons/FolderIcon';
|
||||
@@ -130,7 +132,9 @@ function LocationItem({ location, index, navigation }: LocationItemProps) {
|
||||
}
|
||||
|
||||
const LocationSettingsScreen = ({ navigation }: SettingsStackScreenProps<'LocationSettings'>) => {
|
||||
const { data: locations } = useLibraryQuery(['locations.list']);
|
||||
const result = useLibraryQuery(['locations.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const locations = useCache(result.data?.items);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ArrowLeft, CaretRight, Pen, Trash } from 'phosphor-react-native';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Animated, FlatList, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import DeleteTagModal from '~/components/modal/confirmModals/DeleteTagModal';
|
||||
import CreateTagModal from '~/components/modal/tag/CreateTagModal';
|
||||
@@ -70,7 +70,9 @@ function TagItem({ tag, index }: { tag: Tag; index: number }) {
|
||||
// TODO: Add "New Tag" button
|
||||
|
||||
const TagsSettingsScreen = ({ navigation }: SettingsStackScreenProps<'TagsSettings'>) => {
|
||||
const { data: tags } = useLibraryQuery(['tags.list']);
|
||||
const result = useLibraryQuery(['tags.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const tags = useCache(result.data?.items);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { RspcProvider } from '@sd/client';
|
||||
import { CacheProvider, createCache, RspcProvider } from '@sd/client';
|
||||
import {
|
||||
createRoutes,
|
||||
Platform,
|
||||
@@ -81,7 +81,9 @@ const queryClient = new QueryClient({
|
||||
}
|
||||
});
|
||||
|
||||
const routes = createRoutes(platform);
|
||||
const cache = createCache();
|
||||
|
||||
const routes = createRoutes(platform, cache);
|
||||
|
||||
function App() {
|
||||
const router = useRouter();
|
||||
@@ -102,21 +104,23 @@ function App() {
|
||||
return (
|
||||
<ScreenshotWrapper showControls={!!showControls}>
|
||||
<div ref={domEl} className="App">
|
||||
<RspcProvider queryClient={queryClient}>
|
||||
<PlatformProvider platform={platform}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SpacedriveInterfaceRoot>
|
||||
<SpacedriveRouterProvider
|
||||
routing={{
|
||||
...router,
|
||||
routes,
|
||||
visible: true
|
||||
}}
|
||||
/>
|
||||
</SpacedriveInterfaceRoot>
|
||||
</QueryClientProvider>
|
||||
</PlatformProvider>
|
||||
</RspcProvider>
|
||||
<CacheProvider cache={cache}>
|
||||
<RspcProvider queryClient={queryClient}>
|
||||
<PlatformProvider platform={platform}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SpacedriveInterfaceRoot>
|
||||
<SpacedriveRouterProvider
|
||||
routing={{
|
||||
...router,
|
||||
routes,
|
||||
visible: true
|
||||
}}
|
||||
/>
|
||||
</SpacedriveInterfaceRoot>
|
||||
</QueryClientProvider>
|
||||
</PlatformProvider>
|
||||
</RspcProvider>
|
||||
</CacheProvider>
|
||||
</div>
|
||||
</ScreenshotWrapper>
|
||||
);
|
||||
@@ -126,7 +130,7 @@ export default App;
|
||||
|
||||
function useRouter() {
|
||||
const [router, setRouter] = useState(() => {
|
||||
const router = createBrowserRouter(createRoutes(platform));
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
router.subscribe((event) => {
|
||||
setRouter((router) => {
|
||||
|
||||
@@ -22,16 +22,16 @@ sd-media-metadata = { path = "../crates/media-metadata" }
|
||||
sd-prisma = { path = "../crates/prisma" }
|
||||
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
||||
sd-crypto = { path = "../crates/crypto", features = [
|
||||
"rspc",
|
||||
"specta",
|
||||
"serde",
|
||||
"keymanager",
|
||||
"rspc",
|
||||
"specta",
|
||||
"serde",
|
||||
"keymanager",
|
||||
] }
|
||||
|
||||
sd-cache = { path = "../crates/cache" }
|
||||
sd-images = { path = "../crates/images", features = [
|
||||
"rspc",
|
||||
"serde",
|
||||
"specta",
|
||||
"rspc",
|
||||
"serde",
|
||||
"specta",
|
||||
] }
|
||||
sd-file-ext = { path = "../crates/file-ext" }
|
||||
sd-sync = { path = "../crates/sync" }
|
||||
@@ -40,21 +40,21 @@ sd-utils = { path = "../crates/utils" }
|
||||
sd-core-sync = { path = "./crates/sync" }
|
||||
|
||||
rspc = { workspace = true, features = [
|
||||
"uuid",
|
||||
"chrono",
|
||||
"tracing",
|
||||
"alpha",
|
||||
"unstable",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"tracing",
|
||||
"alpha",
|
||||
"unstable",
|
||||
] }
|
||||
prisma-client-rust = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"macros",
|
||||
"time",
|
||||
"process",
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"macros",
|
||||
"time",
|
||||
"process",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
@@ -82,7 +82,7 @@ http-range = "0.1.5"
|
||||
mini-moka = "0.10.2"
|
||||
serde_with = "3.4.0"
|
||||
notify = { version = "=5.2.0", default-features = false, features = [
|
||||
"macos_fsevent",
|
||||
"macos_fsevent",
|
||||
], optional = true }
|
||||
static_assertions = "1.1.0"
|
||||
serde-hashkey = "0.4.5"
|
||||
|
||||
@@ -115,12 +115,17 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
})
|
||||
.procedure("backup", {
|
||||
R.with2(library())
|
||||
.mutation(|(node, library), _: ()| start_backup(node, library))
|
||||
.mutation(
|
||||
|(node, library), _: ()| async move { Ok(start_backup(node, library).await) },
|
||||
)
|
||||
})
|
||||
.procedure("restore", {
|
||||
R
|
||||
// TODO: Paths as strings is bad but here we want the flexibility of the frontend allowing any path
|
||||
.mutation(|node, path: String| start_restore(node, path.into()))
|
||||
.mutation(|node, path: String| async move {
|
||||
start_restore(node, path.into()).await;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("delete", {
|
||||
R
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
use crate::library::Category;
|
||||
// TODO: Ensure this file has normalised caching setup before reenabling
|
||||
|
||||
use std::{collections::BTreeMap, str::FromStr};
|
||||
// use crate::library::Category;
|
||||
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
use strum::VariantNames;
|
||||
// use std::{collections::BTreeMap, str::FromStr};
|
||||
|
||||
use super::{utils::library, Ctx, R};
|
||||
// use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
// use strum::VariantNames;
|
||||
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router().procedure("list", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
let (categories, queries): (Vec<_>, Vec<_>) = Category::VARIANTS
|
||||
.iter()
|
||||
.map(|category| {
|
||||
let category = Category::from_str(category)
|
||||
.expect("it's alright this category string exists");
|
||||
(
|
||||
category,
|
||||
library.db.object().count(vec![category.to_where_param()]),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
// use super::{utils::library, Ctx, R};
|
||||
|
||||
Ok(categories
|
||||
.into_iter()
|
||||
.zip(
|
||||
library
|
||||
.db
|
||||
._batch(queries)
|
||||
.await?
|
||||
.into_iter()
|
||||
// TODO(@Oscar): rspc bigint support
|
||||
.map(|count| {
|
||||
i32::try_from(count).map_err(|_| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"category item count overflowed 'i32'!".into(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
)
|
||||
.collect::<BTreeMap<_, _>>())
|
||||
})
|
||||
})
|
||||
}
|
||||
// pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
// R.router().procedure("list", {
|
||||
// R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
// let (categories, queries): (Vec<_>, Vec<_>) = Category::VARIANTS
|
||||
// .iter()
|
||||
// .map(|category| {
|
||||
// let category = Category::from_str(category)
|
||||
// .expect("it's alright this category string exists");
|
||||
// (
|
||||
// category,
|
||||
// library.db.object().count(vec![category.to_where_param()]),
|
||||
// )
|
||||
// })
|
||||
// .unzip();
|
||||
|
||||
// Ok(categories
|
||||
// .into_iter()
|
||||
// .zip(
|
||||
// library
|
||||
// .db
|
||||
// ._batch(queries)
|
||||
// .await?
|
||||
// .into_iter()
|
||||
// // TODO(@Oscar): rspc bigint support
|
||||
// .map(|count| {
|
||||
// i32::try_from(count).map_err(|_| {
|
||||
// rspc::Error::new(
|
||||
// ErrorCode::InternalServerError,
|
||||
// "category item count overflowed 'i32'!".into(),
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
// .collect::<Result<Vec<_>, _>>()?,
|
||||
// )
|
||||
// .collect::<BTreeMap<_, _>>())
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
}
|
||||
|
||||
mod library {
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::api::libraries::LibraryConfigWrapped;
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match extract_media_data(full_path).await {
|
||||
match extract_media_data(full_path.clone()).await {
|
||||
Ok(img_media_data) => Ok(Some(MediaMetadata::Image(Box::new(img_media_data)))),
|
||||
Err(MediaDataError::MediaData(sd_media_metadata::Error::NoExifDataOnPath(
|
||||
_,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
api::utils::library,
|
||||
api::{locations::object_with_file_paths, utils::library},
|
||||
invalidate_query,
|
||||
job::Job,
|
||||
library::Library,
|
||||
@@ -21,6 +21,7 @@ use crate::{
|
||||
util::{db::maybe_missing, error::FileIOError},
|
||||
};
|
||||
|
||||
use sd_cache::{CacheNode, Model, NormalisedResult, Reference};
|
||||
use sd_file_ext::kind::ObjectKind;
|
||||
use sd_images::ConvertableExtension;
|
||||
use sd_media_metadata::MediaMetadata;
|
||||
@@ -31,11 +32,11 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use futures::future::join_all;
|
||||
use regex::Regex;
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tokio::{fs, io, task::spawn_blocking};
|
||||
use tracing::{error, warn};
|
||||
@@ -47,19 +48,76 @@ const UNTITLED_FOLDER_STR: &str = "Untitled Folder";
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router()
|
||||
.procedure("get", {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct GetArgs {
|
||||
#[derive(Type, Serialize)]
|
||||
pub struct ObjectWithFilePaths2 {
|
||||
pub id: i32,
|
||||
pub pub_id: Vec<u8>,
|
||||
pub kind: Option<i32>,
|
||||
pub key_id: Option<i32>,
|
||||
pub hidden: Option<bool>,
|
||||
pub favorite: Option<bool>,
|
||||
pub important: Option<bool>,
|
||||
pub note: Option<String>,
|
||||
pub date_created: Option<DateTime<FixedOffset>>,
|
||||
pub date_accessed: Option<DateTime<FixedOffset>>,
|
||||
pub file_paths: Vec<Reference<file_path::Data>>,
|
||||
}
|
||||
|
||||
impl Model for ObjectWithFilePaths2 {
|
||||
fn name() -> &'static str {
|
||||
"Object" // is a duplicate because it's the same entity but with a relation
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectWithFilePaths2 {
|
||||
pub fn from_db(
|
||||
nodes: &mut Vec<CacheNode>,
|
||||
item: object_with_file_paths::Data,
|
||||
) -> Reference<Self> {
|
||||
let this = Self {
|
||||
id: item.id,
|
||||
pub_id: item.pub_id,
|
||||
kind: item.kind,
|
||||
key_id: item.key_id,
|
||||
hidden: item.hidden,
|
||||
favorite: item.favorite,
|
||||
important: item.important,
|
||||
note: item.note,
|
||||
date_created: item.date_created,
|
||||
date_accessed: item.date_accessed,
|
||||
file_paths: item
|
||||
.file_paths
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let id = i.id.to_string();
|
||||
nodes.push(CacheNode::new(id.clone(), i));
|
||||
Reference::new(id)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let id = this.id.to_string();
|
||||
nodes.push(CacheNode::new(id.clone(), this));
|
||||
Reference::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
R.with2(library())
|
||||
.query(|(_, library), args: GetArgs| async move {
|
||||
.query(|(_, library), object_id: i32| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.object()
|
||||
.find_unique(object::id::equals(args.id))
|
||||
.include(object::include!({ file_paths }))
|
||||
.find_unique(object::id::equals(object_id))
|
||||
.include(object_with_file_paths::include())
|
||||
.exec()
|
||||
.await?)
|
||||
.await?
|
||||
.map(|item| {
|
||||
let mut nodes = Vec::new();
|
||||
NormalisedResult {
|
||||
item: ObjectWithFilePaths2::from_db(&mut nodes, item),
|
||||
nodes,
|
||||
}
|
||||
}))
|
||||
})
|
||||
})
|
||||
.procedure("getMediaData", {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: Ensure this file has normalised caching setup before reenabling
|
||||
|
||||
// use rspc::alpha::AlphaRouter;
|
||||
// use rspc::ErrorCode;
|
||||
// use sd_crypto::keys::keymanager::{StoredKey, StoredKeyType};
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
Node,
|
||||
};
|
||||
|
||||
use sd_cache::{Model, Normalise, NormalisedResult, NormalisedResults};
|
||||
use sd_p2p::spacetunnel::RemoteIdentity;
|
||||
use sd_prisma::prisma::{indexer_rule, statistics};
|
||||
|
||||
@@ -35,6 +36,12 @@ pub struct LibraryConfigWrapped {
|
||||
pub config: LibraryConfig,
|
||||
}
|
||||
|
||||
impl Model for LibraryConfigWrapped {
|
||||
fn name() -> &'static str {
|
||||
"LibraryConfigWrapped"
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryConfigWrapped {
|
||||
pub async fn from_library(library: &Library) -> Self {
|
||||
Self {
|
||||
@@ -50,7 +57,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router()
|
||||
.procedure("list", {
|
||||
R.query(|node, _: ()| async move {
|
||||
node.libraries
|
||||
let libraries = node
|
||||
.libraries
|
||||
.get_all()
|
||||
.await
|
||||
.into_iter()
|
||||
@@ -64,7 +72,11 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join()
|
||||
.await
|
||||
.await;
|
||||
|
||||
let (nodes, items) = libraries.normalise(|i| i.uuid.to_string());
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
})
|
||||
})
|
||||
.procedure("statistics", {
|
||||
@@ -279,7 +291,10 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(LibraryConfigWrapped::from_library(&library).await)
|
||||
Ok(NormalisedResult::from(
|
||||
LibraryConfigWrapped::from_library(&library).await,
|
||||
|l| l.uuid.to_string(),
|
||||
))
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -17,9 +17,10 @@ use crate::{
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use directories::UserDirs;
|
||||
use rspc::{self, alpha::AlphaRouter, ErrorCode};
|
||||
use sd_cache::{CacheNode, Model, Normalise, NormalisedResult, NormalisedResults, Reference};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tracing::error;
|
||||
@@ -58,6 +59,34 @@ pub enum ExplorerItem {
|
||||
item: PeerMetadata,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Really this shouldn't be a `Model` but it's easy for now.
|
||||
// In the future we should store the inner data of the variant on behalf of it's existing model so it works cross queries.
|
||||
impl Model for ExplorerItem {
|
||||
fn name() -> &'static str {
|
||||
"ExplorerItem"
|
||||
}
|
||||
}
|
||||
|
||||
impl ExplorerItem {
|
||||
pub fn id(&self) -> String {
|
||||
let ty = match self {
|
||||
ExplorerItem::Path { .. } => "FilePath",
|
||||
ExplorerItem::Object { .. } => "Object",
|
||||
ExplorerItem::Location { .. } => "Location",
|
||||
ExplorerItem::NonIndexedPath { .. } => "NonIndexedPath",
|
||||
ExplorerItem::SpacedropPeer { .. } => "SpacedropPeer",
|
||||
};
|
||||
match self {
|
||||
ExplorerItem::Path { item, .. } => format!("{ty}:{}", item.id),
|
||||
ExplorerItem::Object { item, .. } => format!("{ty}:{}", item.id),
|
||||
ExplorerItem::Location { item, .. } => format!("{ty}:{}", item.id),
|
||||
ExplorerItem::NonIndexedPath { item, .. } => format!("{ty}:{}", item.path),
|
||||
ExplorerItem::SpacedropPeer { item, .. } => format!("{ty}:{}", item.name), // TODO: Use a proper primary key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
pub struct SystemLocations {
|
||||
desktop: Option<PathBuf>,
|
||||
@@ -172,13 +201,17 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router()
|
||||
.procedure("list", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
Ok(library
|
||||
let locations = library
|
||||
.db
|
||||
.location()
|
||||
.find_many(vec![])
|
||||
.order_by(location::date_created::order(SortOrder::Desc))
|
||||
.exec()
|
||||
.await?)
|
||||
.await?;
|
||||
|
||||
let (nodes, items) = locations.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { items, nodes })
|
||||
})
|
||||
})
|
||||
.procedure("get", {
|
||||
@@ -189,10 +222,72 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
.location()
|
||||
.find_unique(location::id::equals(location_id))
|
||||
.exec()
|
||||
.await?)
|
||||
.await?
|
||||
.map(|i| NormalisedResult::from(i, |i| i.id.to_string())))
|
||||
})
|
||||
})
|
||||
.procedure("getWithRules", {
|
||||
#[derive(Type, Serialize)]
|
||||
struct LocationWithIndexerRule {
|
||||
pub id: i32,
|
||||
pub pub_id: Vec<u8>,
|
||||
pub name: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub total_capacity: Option<i32>,
|
||||
pub available_capacity: Option<i32>,
|
||||
pub size_in_bytes: Option<Vec<u8>>,
|
||||
pub is_archived: Option<bool>,
|
||||
pub generate_preview_media: Option<bool>,
|
||||
pub sync_preview_media: Option<bool>,
|
||||
pub hidden: Option<bool>,
|
||||
pub date_created: Option<DateTime<FixedOffset>>,
|
||||
pub instance_id: Option<i32>,
|
||||
pub indexer_rules: Vec<Reference<indexer_rule::Data>>,
|
||||
}
|
||||
|
||||
impl Model for LocationWithIndexerRule {
|
||||
fn name() -> &'static str {
|
||||
"Location" // This is a duplicate identifier as `location::Data` but it's fine because because they are the same entity
|
||||
}
|
||||
}
|
||||
|
||||
impl LocationWithIndexerRule {
|
||||
pub fn from_db(
|
||||
nodes: &mut Vec<CacheNode>,
|
||||
value: location_with_indexer_rules::Data,
|
||||
) -> Reference<Self> {
|
||||
let this = Self {
|
||||
id: value.id,
|
||||
pub_id: value.pub_id,
|
||||
name: value.name,
|
||||
path: value.path,
|
||||
total_capacity: value.total_capacity,
|
||||
available_capacity: value.available_capacity,
|
||||
size_in_bytes: value.size_in_bytes,
|
||||
is_archived: value.is_archived,
|
||||
generate_preview_media: value.generate_preview_media,
|
||||
sync_preview_media: value.sync_preview_media,
|
||||
hidden: value.hidden,
|
||||
date_created: value.date_created,
|
||||
instance_id: value.instance_id,
|
||||
indexer_rules: value
|
||||
.indexer_rules
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let id = i.indexer_rule.id.to_string();
|
||||
|
||||
nodes.push(CacheNode::new(id.clone(), i.indexer_rule));
|
||||
Reference::new(id)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let id = this.id.to_string();
|
||||
nodes.push(CacheNode::new(id.clone(), this));
|
||||
Reference::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
R.with2(library())
|
||||
.query(|(_, library), location_id: location::id::Type| async move {
|
||||
Ok(library
|
||||
@@ -201,7 +296,14 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
.find_unique(location::id::equals(location_id))
|
||||
.include(location_with_indexer_rules::include())
|
||||
.exec()
|
||||
.await?)
|
||||
.await?
|
||||
.map(|location| {
|
||||
let mut nodes = Vec::new();
|
||||
NormalisedResult {
|
||||
item: LocationWithIndexerRule::from_db(&mut nodes, location),
|
||||
nodes,
|
||||
}
|
||||
}))
|
||||
})
|
||||
})
|
||||
.procedure("create", {
|
||||
@@ -477,32 +579,34 @@ fn mount_indexer_rule_routes() -> AlphaRouter<Ctx> {
|
||||
format!("Indexer rule <id={indexer_rule_id}> not found"),
|
||||
)
|
||||
})
|
||||
.map(|i| NormalisedResult::from(i, |i| i.id.to_string()))
|
||||
})
|
||||
})
|
||||
.procedure("list", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.find_many(vec![])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
let rules = library.db.indexer_rule().find_many(vec![]).exec().await?;
|
||||
|
||||
let (nodes, items) = rules.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { items, nodes })
|
||||
})
|
||||
})
|
||||
// list indexer rules for location, returning the indexer rule
|
||||
.procedure("listForLocation", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), location_id: location::id::Type| async move {
|
||||
library
|
||||
let rules = library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.find_many(vec![indexer_rule::locations::some(vec![
|
||||
indexer_rules_in_location::location_id::equals(location_id),
|
||||
])])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.await?;
|
||||
|
||||
let (nodes, items) = rules.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { items, nodes })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
|
||||
use crate::{
|
||||
invalidate_query,
|
||||
job::JobProgressEvent,
|
||||
node::config::{NodeConfig, NodePreferences},
|
||||
Node,
|
||||
};
|
||||
|
||||
use sd_cache::patch_typedef;
|
||||
use sd_p2p::P2PStatus;
|
||||
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
|
||||
use itertools::Itertools;
|
||||
use rspc::{alpha::Rspc, Config, ErrorCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -121,9 +121,11 @@ pub(crate) fn mount() -> Arc<Router> {
|
||||
commit: &'static str,
|
||||
}
|
||||
|
||||
R.query(|_, _: ()| BuildInfo {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
commit: env!("GIT_HASH"),
|
||||
R.query(|_, _: ()| {
|
||||
Ok(BuildInfo {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
commit: env!("GIT_HASH"),
|
||||
})
|
||||
})
|
||||
})
|
||||
.procedure("nodeState", {
|
||||
@@ -198,6 +200,18 @@ pub(crate) fn mount() -> Arc<Router> {
|
||||
.merge("notifications.", notifications::mount())
|
||||
.merge("backups.", backups::mount())
|
||||
.merge("invalidation.", utils::mount_invalidate())
|
||||
.sd_patch_types_dangerously(|type_map| {
|
||||
patch_typedef(type_map);
|
||||
|
||||
let def =
|
||||
<sd_prisma::prisma::object::Data as specta::NamedType>::definition_named_data_type(
|
||||
type_map,
|
||||
);
|
||||
type_map.insert(
|
||||
<sd_prisma::prisma::object::Data as specta::NamedType>::SID,
|
||||
def,
|
||||
);
|
||||
})
|
||||
.build(
|
||||
#[allow(clippy::let_and_return)]
|
||||
{
|
||||
|
||||
@@ -162,6 +162,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
.procedure("test", {
|
||||
R.mutation(|node, _: ()| async move {
|
||||
node.emit_notification(NotificationData::Test, None).await;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("testLibrary", {
|
||||
@@ -170,6 +172,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
library
|
||||
.emit_notification(NotificationData::Test, None)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,9 +51,10 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
})
|
||||
})
|
||||
})
|
||||
.procedure("state", {
|
||||
R.query(|node, _: ()| async move { node.p2p.state() })
|
||||
})
|
||||
// TODO: This has a potentially invalid map key and Specta don't like that. Can bring back in another PR.
|
||||
// .procedure("state", {
|
||||
// R.query(|node, _: ()| async move { Ok(node.p2p.state()) })
|
||||
// })
|
||||
.procedure("spacedrop", {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct SpacedropArgs {
|
||||
@@ -81,20 +82,23 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
match path {
|
||||
Some(path) => node.p2p.accept_spacedrop(id, path).await,
|
||||
None => node.p2p.reject_spacedrop(id).await,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("cancelSpacedrop", {
|
||||
R.mutation(|node, id: Uuid| async move { node.p2p.cancel_spacedrop(id).await })
|
||||
R.mutation(|node, id: Uuid| async move { Ok(node.p2p.cancel_spacedrop(id).await) })
|
||||
})
|
||||
.procedure("pair", {
|
||||
R.mutation(|node, id: RemoteIdentity| async move {
|
||||
node.p2p.pairing.clone().originator(id, node).await
|
||||
Ok(node.p2p.pairing.clone().originator(id, node).await)
|
||||
})
|
||||
})
|
||||
.procedure("pairingResponse", {
|
||||
R.mutation(|node, (pairing_id, decision): (u16, PairingDecision)| {
|
||||
node.p2p.pairing.decision(pairing_id, decision);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::{
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
use sd_cache::{CacheNode, Model, Normalise, Reference};
|
||||
use sd_prisma::prisma::{self, PrismaClient};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
@@ -28,9 +29,16 @@ use super::{Ctx, R};
|
||||
const MAX_TAKE: u8 = 100;
|
||||
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
struct SearchData<T> {
|
||||
struct SearchData<T: Model> {
|
||||
cursor: Option<Vec<u8>>,
|
||||
items: Vec<T>,
|
||||
items: Vec<Reference<T>>,
|
||||
nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
impl<T: Model> Model for SearchData<T> {
|
||||
fn name() -> &'static str {
|
||||
T::name()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug, Clone)]
|
||||
@@ -91,6 +99,13 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
||||
order: Option<EphemeralPathOrder>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
struct EphemeralPathsResult {
|
||||
pub entries: Vec<Reference<ExplorerItem>>,
|
||||
pub errors: Vec<rspc::Error>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
R.with2(library()).query(
|
||||
|(node, library),
|
||||
EphemeralPathSearchArgs {
|
||||
@@ -133,7 +148,13 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
||||
)
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
let (nodes, entries) = paths.entries.normalise(|item| item.id());
|
||||
|
||||
Ok(EphemeralPathsResult {
|
||||
entries,
|
||||
errors: paths.errors,
|
||||
nodes,
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -217,9 +238,12 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
||||
})
|
||||
}
|
||||
|
||||
let (nodes, items) = items.normalise(|item| item.id());
|
||||
|
||||
Ok(SearchData {
|
||||
items,
|
||||
cursor: None,
|
||||
nodes,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -333,7 +357,13 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SearchData { items, cursor })
|
||||
let (nodes, items) = items.normalise(|item| item.id());
|
||||
|
||||
Ok(SearchData {
|
||||
nodes,
|
||||
items,
|
||||
cursor,
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use chrono::{DateTime, Utc};
|
||||
use itertools::{Either, Itertools};
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
use sd_cache::{CacheNode, Normalise, NormalisedResult, NormalisedResults, Reference};
|
||||
use sd_file_ext::kind::ObjectKind;
|
||||
use sd_prisma::{prisma, prisma_sync};
|
||||
use sd_sync::OperationFactory;
|
||||
use sd_utils::uuid_to_bytes;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
use serde_json::json;
|
||||
@@ -26,23 +27,43 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router()
|
||||
.procedure("list", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
Ok(library.db.tag().find_many(vec![]).exec().await?)
|
||||
let tags = library.db.tag().find_many(vec![]).exec().await?;
|
||||
|
||||
let (nodes, items) = tags.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
})
|
||||
})
|
||||
.procedure("getForObject", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), object_id: i32| async move {
|
||||
Ok(library
|
||||
let tags = library
|
||||
.db
|
||||
.tag()
|
||||
.find_many(vec![tag::tag_objects::some(vec![
|
||||
tag_on_object::object_id::equals(object_id),
|
||||
])])
|
||||
.exec()
|
||||
.await?)
|
||||
.await?;
|
||||
|
||||
let (nodes, items) = tags.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
})
|
||||
})
|
||||
.procedure("getWithObjects", {
|
||||
#[derive(Serialize, Type)]
|
||||
pub struct GetWithObjectsResult {
|
||||
pub data: BTreeMap<u32, Vec<Reference<tag::Data>>>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Type)]
|
||||
pub struct ObjectWithDateCreated {
|
||||
object: Reference<object::Data>,
|
||||
date_created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
R.with2(library()).query(
|
||||
|(_, library), object_ids: Vec<object::id::Type>| async move {
|
||||
let Library { db, .. } = library.as_ref();
|
||||
@@ -64,6 +85,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
// This doesn't need normalised caching because it doesn't return whole models.
|
||||
Ok(tags_with_objects
|
||||
.into_iter()
|
||||
.map(|tag| (tag.id, tag.tag_objects))
|
||||
@@ -79,7 +101,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
.tag()
|
||||
.find_unique(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?)
|
||||
.await?
|
||||
.map(|tag| NormalisedResult::from(tag, |i| i.id.to_string())))
|
||||
})
|
||||
})
|
||||
.procedure("create", {
|
||||
|
||||
@@ -282,7 +282,7 @@ pub(crate) fn mount_invalidate() -> AlphaRouter<Ctx> {
|
||||
R.router()
|
||||
.procedure(
|
||||
"test-invalidate",
|
||||
R.query(move |_, _: ()| count.fetch_add(1, Ordering::SeqCst)),
|
||||
R.query(move |_, _: ()| Ok(count.fetch_add(1, Ordering::SeqCst))),
|
||||
)
|
||||
.procedure(
|
||||
"test-invalidate-mutation",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use rspc::alpha::AlphaRouter;
|
||||
use sd_cache::{Normalise, NormalisedResults};
|
||||
|
||||
use crate::volume::get_volumes;
|
||||
|
||||
@@ -6,6 +7,23 @@ use super::{Ctx, R};
|
||||
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router().procedure("list", {
|
||||
R.query(|_, _: ()| async move { Ok(get_volumes().await) })
|
||||
R.query(|_, _: ()| async move {
|
||||
let volumes = get_volumes().await;
|
||||
|
||||
let (nodes, items) = volumes.normalise(|i| {
|
||||
// TODO: This is a really bad key. Once we hook up volumes with the DB fix this!
|
||||
blake3::hash(
|
||||
&i.mount_points
|
||||
.iter()
|
||||
.map(|mp| mp.as_os_str().to_string_lossy().as_bytes().to_vec())
|
||||
.flatten()
|
||||
.collect::<Vec<u8>>(),
|
||||
)
|
||||
.to_hex()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ use crate::node::Platform;
|
||||
|
||||
#[derive(Debug, Clone, Type, Serialize, Deserialize)]
|
||||
pub struct PeerMetadata {
|
||||
pub(super) name: String,
|
||||
pub(super) operating_system: Option<OperatingSystem>,
|
||||
pub(super) version: Option<String>,
|
||||
pub name: String,
|
||||
pub operating_system: Option<OperatingSystem>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
impl Metadata for PeerMetadata {
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::{
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use sd_cache::Model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use specta::Type;
|
||||
@@ -56,6 +57,12 @@ pub struct Volume {
|
||||
pub is_root_filesystem: bool,
|
||||
}
|
||||
|
||||
impl Model for Volume {
|
||||
fn name() -> &'static str {
|
||||
"Volume"
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Volume {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
|
||||
11
crates/cache/Cargo.toml
vendored
Normal file
11
crates/cache/Cargo.toml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "sd-cache"
|
||||
version = "0.0.0"
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
specta.workspace = true
|
||||
198
crates/cache/src/lib.rs
vendored
Normal file
198
crates/cache/src/lib.rs
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use serde::{ser::SerializeMap, Serialize, Serializer};
|
||||
use specta::{Any, DataType, NamedType, Type, TypeMap};
|
||||
|
||||
/// A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
///
|
||||
/// You don't need to use this, it's just a shortcut to avoid having to write out the full type everytime.
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
pub struct NormalisedResults<T: Model + Type> {
|
||||
pub items: Vec<Reference<T>>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
/// A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
///
|
||||
/// You don't need to use this, it's just a shortcut to avoid having to write out the full type everytime.
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
pub struct NormalisedResult<T: Model + Type> {
|
||||
pub item: Reference<T>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
impl<T: Model + Serialize + Type> NormalisedResult<T> {
|
||||
pub fn from(item: T, id_fn: impl Fn(&T) -> String) -> Self {
|
||||
let id = id_fn(&item);
|
||||
Self {
|
||||
item: Reference::new(id.clone()),
|
||||
nodes: vec![CacheNode::new(id, item)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type which can be stored in the cache.
|
||||
pub trait Model {
|
||||
/// Must return a unique identifier for this model within the cache.
|
||||
fn name() -> &'static str;
|
||||
}
|
||||
|
||||
/// A reference to a `CacheNode`.
|
||||
///
|
||||
/// This does not contain the actual data, but instead a reference to it.
|
||||
/// This allows the CacheNode's to be switched out and the query recomputed without any backend communication.
|
||||
///
|
||||
/// If you use a `Reference` in a query, you *must* ensure the corresponding `CacheNode` is also in the query.
|
||||
#[derive(Type, Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Reference<T> {
|
||||
__type: &'static str,
|
||||
__id: String,
|
||||
#[specta(rename = "#type")]
|
||||
ty: PhantomType<T>,
|
||||
}
|
||||
|
||||
impl<T: Model + Type> Reference<T> {
|
||||
pub fn new(key: String) -> Self {
|
||||
Self {
|
||||
__type: "", // This is just to fake the field for Specta
|
||||
__id: key,
|
||||
ty: PhantomType(PhantomData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Model> Serialize for Reference<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
map.serialize_entry("__type", T::name())?;
|
||||
map.serialize_entry("__id", &self.__id)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in the cache.
|
||||
/// This holds the data and is identified by it's type and id.
|
||||
#[derive(Debug, Clone)] // TODO: `Hash, PartialEq, Eq`
|
||||
pub struct CacheNode(
|
||||
&'static str,
|
||||
serde_json::Value,
|
||||
Result<serde_json::Value, Arc<serde_json::Error>>,
|
||||
);
|
||||
|
||||
impl CacheNode {
|
||||
pub fn new<T: Model + Serialize + Type>(key: String, value: T) -> Self {
|
||||
Self(
|
||||
T::name(),
|
||||
key.into(),
|
||||
serde_json::to_value(value).map_err(Arc::new),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Type, Default)]
|
||||
#[specta(rename = "CacheNode", remote = CacheNode)]
|
||||
#[allow(unused)]
|
||||
struct CacheNodeTy {
|
||||
__type: String,
|
||||
__id: String,
|
||||
#[specta(rename = "#node")]
|
||||
node: Any,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeSerdeRepr<'a> {
|
||||
__type: &'static str,
|
||||
__id: &'a serde_json::Value,
|
||||
#[serde(flatten)]
|
||||
v: &'a serde_json::Value,
|
||||
}
|
||||
|
||||
impl Serialize for CacheNode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
NodeSerdeRepr {
|
||||
__type: self.0,
|
||||
__id: &self.1,
|
||||
v: self.2.as_ref().map_err(|err| {
|
||||
serde::ser::Error::custom(format!("Failed to serialise node: {}", err))
|
||||
})?,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for easily normalising data.
|
||||
pub trait Normalise {
|
||||
type Item: Model + Type;
|
||||
|
||||
fn normalise(
|
||||
self,
|
||||
id_fn: impl Fn(&Self::Item) -> String,
|
||||
) -> (Vec<CacheNode>, Vec<Reference<Self::Item>>);
|
||||
}
|
||||
|
||||
impl<T: Model + Serialize + Type> Normalise for Vec<T> {
|
||||
type Item = T;
|
||||
|
||||
fn normalise(
|
||||
self,
|
||||
id_fn: impl Fn(&Self::Item) -> String,
|
||||
) -> (Vec<CacheNode>, Vec<Reference<Self::Item>>) {
|
||||
let mut nodes = Vec::with_capacity(self.len());
|
||||
let mut references = Vec::with_capacity(self.len());
|
||||
|
||||
for item in self.into_iter() {
|
||||
let id = id_fn(&item);
|
||||
nodes.push(CacheNode::new(id.clone(), item));
|
||||
references.push(Reference::new(id));
|
||||
}
|
||||
|
||||
(nodes, references)
|
||||
}
|
||||
}
|
||||
|
||||
/// Basically `PhantomData`.
|
||||
///
|
||||
/// With Specta `PhantomData` is exported as `null`.
|
||||
/// This will export as `T` but serve the same purpose as `PhantomData` (holding a type without it being instantiated).
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct PhantomType<T>(PhantomData<T>);
|
||||
|
||||
/// WARNING: This type is surgically updated within `Reference` in the final typedefs due it being impossible to properly implement.
|
||||
/// Be careful changing it!
|
||||
|
||||
impl<T: Type> Type for PhantomType<T> {
|
||||
fn inline(type_map: &mut TypeMap, generics: &[DataType]) -> DataType {
|
||||
T::inline(type_map, generics)
|
||||
}
|
||||
|
||||
fn reference(type_map: &mut TypeMap, generics: &[DataType]) -> specta::reference::Reference {
|
||||
T::reference(type_map, generics)
|
||||
}
|
||||
|
||||
fn definition(type_map: &mut TypeMap) -> DataType {
|
||||
T::definition(type_map)
|
||||
}
|
||||
}
|
||||
|
||||
// This function is cursed.
|
||||
pub fn patch_typedef(type_map: &mut TypeMap) {
|
||||
#[derive(Type)]
|
||||
#[specta(rename = "Reference")]
|
||||
#[allow(unused)]
|
||||
struct ReferenceTy<T> {
|
||||
__type: &'static str,
|
||||
__id: String,
|
||||
#[specta(rename = "#type")]
|
||||
ty: T,
|
||||
}
|
||||
|
||||
let mut def = <Reference<()> as NamedType>::definition_named_data_type(type_map);
|
||||
def.inner = ReferenceTy::<Any>::definition(type_map);
|
||||
type_map.insert(<Reference<()> as NamedType>::SID, def)
|
||||
}
|
||||
@@ -62,8 +62,9 @@ impl Identity {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct RemoteIdentity(ed25519_dalek::VerifyingKey);
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Type)]
|
||||
#[specta(transparent)]
|
||||
pub struct RemoteIdentity(#[specta(type = String)] ed25519_dalek::VerifyingKey);
|
||||
|
||||
impl Hash for RemoteIdentity {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
@@ -141,15 +142,6 @@ impl FromStr for RemoteIdentity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Type for RemoteIdentity {
|
||||
fn inline(
|
||||
_: specta::DefOpts,
|
||||
_: &[specta::DataType],
|
||||
) -> Result<specta::DataType, specta::ExportError> {
|
||||
Ok(specta::DataType::Primitive(specta::PrimitiveType::String))
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteIdentity {
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityErr> {
|
||||
Ok(Self(ed25519_dalek::VerifyingKey::from_bytes(
|
||||
|
||||
@@ -8,3 +8,4 @@ prisma-client-rust = { workspace = true }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
sd-sync = { path = "../sync" }
|
||||
sd-cache = { path = "../cache" }
|
||||
|
||||
@@ -2,3 +2,33 @@
|
||||
pub mod prisma;
|
||||
#[allow(warnings, unused)]
|
||||
pub mod prisma_sync;
|
||||
|
||||
impl sd_cache::Model for prisma::tag::Data {
|
||||
fn name() -> &'static str {
|
||||
"Tag"
|
||||
}
|
||||
}
|
||||
|
||||
impl sd_cache::Model for prisma::object::Data {
|
||||
fn name() -> &'static str {
|
||||
"Object"
|
||||
}
|
||||
}
|
||||
|
||||
impl sd_cache::Model for prisma::location::Data {
|
||||
fn name() -> &'static str {
|
||||
"Location"
|
||||
}
|
||||
}
|
||||
|
||||
impl sd_cache::Model for prisma::indexer_rule::Data {
|
||||
fn name() -> &'static str {
|
||||
"IndexerRule"
|
||||
}
|
||||
}
|
||||
|
||||
impl sd_cache::Model for prisma::file_path::Data {
|
||||
fn name() -> &'static str {
|
||||
"FilePath"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Plus } from '@phosphor-icons/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, MutableRefObject, RefObject, useMemo, useRef } from 'react';
|
||||
import { RefObject, useMemo, useRef } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ExplorerItem, useLibraryQuery } from '@sd/client';
|
||||
import { ExplorerItem, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Button, dialogManager, ModifierKeys, tw } from '@sd/ui';
|
||||
import CreateDialog, {
|
||||
AssignTagItems,
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
|
||||
function useData({ items }: Props) {
|
||||
const tags = useLibraryQuery(['tags.list'], { suspense: true });
|
||||
useNodes(tags.data?.nodes);
|
||||
|
||||
// Map<tag::id, Vec<object::id>>
|
||||
const tagsWithObjects = useLibraryQuery(
|
||||
@@ -38,7 +39,13 @@ function useData({ items }: Props) {
|
||||
{ suspense: true }
|
||||
);
|
||||
|
||||
return { tags, tagsWithObjects };
|
||||
return {
|
||||
tags: {
|
||||
...tags,
|
||||
data: useCache(tags.data?.items)
|
||||
},
|
||||
tagsWithObjects
|
||||
};
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
|
||||
@@ -37,8 +37,10 @@ import {
|
||||
ObjectKindEnum,
|
||||
ObjectWithFilePaths,
|
||||
useBridgeQuery,
|
||||
useCache,
|
||||
useItemsAsObjects,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
type ExplorerItem
|
||||
} from '@sd/client';
|
||||
import { Button, Divider, DropdownMenu, toast, Tooltip, tw } from '@sd/ui';
|
||||
@@ -172,7 +174,9 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
let filePathData: FilePath | FilePathWithObject | null = null;
|
||||
let ephemeralPathData: NonIndexedPathItem | null = null;
|
||||
|
||||
const locations = useLibraryQuery(['locations.list']);
|
||||
const result = useLibraryQuery(['locations.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const locations = useCache(result.data?.items);
|
||||
|
||||
switch (item.type) {
|
||||
case 'NonIndexedPath': {
|
||||
@@ -209,12 +213,15 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
}, [item]);
|
||||
|
||||
const fileLocations =
|
||||
locations.data?.filter((location) => uniqueLocationIds.includes(location.id)) || [];
|
||||
locations?.filter((location) => uniqueLocationIds.includes(location.id)) || [];
|
||||
|
||||
const readyToFetch = useIsFetchReady(item);
|
||||
const tags = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
|
||||
const tagsQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
|
||||
enabled: objectData != null && readyToFetch
|
||||
});
|
||||
useNodes(tagsQuery.data?.nodes);
|
||||
const tags = useCache(tagsQuery.data?.items);
|
||||
|
||||
const { libraryId } = useZodRouteParams(LibraryIdParamsSchema);
|
||||
|
||||
const queriedFullPath = useLibraryQuery(['files.getPath', filePathData?.id ?? -1], {
|
||||
@@ -332,7 +339,7 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
|
||||
{extension && <InfoPill>{extension}</InfoPill>}
|
||||
|
||||
{tags.data?.map((tag) => (
|
||||
{tags?.map((tag) => (
|
||||
<NavLink key={tag.id} to={`/${libraryId}/tag/${tag.id}`}>
|
||||
<Tooltip label={tag.name || ''} className="flex overflow-hidden">
|
||||
<InfoPill
|
||||
@@ -392,10 +399,12 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
|
||||
|
||||
const { libraryId } = useZodRouteParams(LibraryIdParamsSchema);
|
||||
|
||||
const tags = useLibraryQuery(['tags.list'], {
|
||||
const tagsQuery = useLibraryQuery(['tags.list'], {
|
||||
enabled: readyToFetch && !explorerStore.isDragging,
|
||||
suspense: true
|
||||
});
|
||||
useNodes(tagsQuery.data?.nodes);
|
||||
const tags = useCache(tagsQuery.data?.items);
|
||||
|
||||
const tagsWithObjects = useLibraryQuery(
|
||||
['tags.getWithObjects', selectedObjects.map(({ id }) => id)],
|
||||
@@ -487,7 +496,7 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
|
||||
<InfoPill key={kind}>{`${kind} (${items.length})`}</InfoPill>
|
||||
))}
|
||||
|
||||
{tags.data?.map((tag) => {
|
||||
{tags?.map((tag) => {
|
||||
const objectsWithTag = tagsWithObjects.data?.[tag.id] || [];
|
||||
|
||||
if (objectsWithTag.length === 0) return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UseInfiniteQueryResult, UseQueryResult } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { SearchData } from '@sd/client';
|
||||
import { SearchData, useCache } from '@sd/client';
|
||||
|
||||
export function useExplorerQuery<Q>(
|
||||
query: UseInfiniteQueryResult<SearchData<Q>>,
|
||||
@@ -14,7 +14,7 @@ export function useExplorerQuery<Q>(
|
||||
}
|
||||
}, [query.hasNextPage, query.isFetchingNextPage, query.fetchNextPage]);
|
||||
|
||||
return { query, items, loadMore, count: count.data };
|
||||
return { query, items: useCache(items), loadMore, count: count.data };
|
||||
}
|
||||
|
||||
export type UseExplorerQuery<Q> = ReturnType<typeof useExplorerQuery<Q>>;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
ExplorerItem,
|
||||
ObjectCursor,
|
||||
ObjectOrder,
|
||||
ObjectSearchArgs,
|
||||
useLibraryContext,
|
||||
useNodes,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
|
||||
@@ -23,7 +25,7 @@ export function useObjectsInfiniteQuery({
|
||||
arg.orderAndPagination = { orderOnly: settings.order };
|
||||
}
|
||||
|
||||
return useInfiniteQuery({
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ['search.objects', { library_id: library.uuid, arg }] as const,
|
||||
queryFn: ({ pageParam, queryKey: [_, { arg }] }) => {
|
||||
const cItem: Extract<ExplorerItem, { type: 'Object' }> = pageParam;
|
||||
@@ -66,4 +68,13 @@ export function useObjectsInfiniteQuery({
|
||||
},
|
||||
...args
|
||||
});
|
||||
|
||||
const nodes = useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.nodes) ?? [],
|
||||
[query.data?.pages]
|
||||
);
|
||||
|
||||
useNodes(nodes);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
ExplorerItem,
|
||||
FilePathCursorVariant,
|
||||
@@ -6,6 +7,8 @@ import {
|
||||
FilePathOrder,
|
||||
FilePathSearchArgs,
|
||||
useLibraryContext,
|
||||
useNodes,
|
||||
useNormalisedCache,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
|
||||
@@ -20,15 +23,16 @@ export function usePathsInfiniteQuery({
|
||||
const { library } = useLibraryContext();
|
||||
const ctx = useRspcLibraryContext();
|
||||
const settings = explorerSettings.useSettingsSnapshot();
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
if (settings.order) {
|
||||
arg.orderAndPagination = { orderOnly: settings.order };
|
||||
if (arg.orderAndPagination.orderOnly.field === 'sizeInBytes') delete arg.take;
|
||||
}
|
||||
|
||||
return useInfiniteQuery({
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ['search.paths', { library_id: library.uuid, arg }] as const,
|
||||
queryFn: ({ pageParam, queryKey: [_, { arg }] }) => {
|
||||
queryFn: async ({ pageParam, queryKey: [_, { arg }] }) => {
|
||||
const cItem: Extract<ExplorerItem, { type: 'Path' }> = pageParam;
|
||||
const { order } = settings;
|
||||
|
||||
@@ -120,7 +124,9 @@ export function usePathsInfiniteQuery({
|
||||
|
||||
arg.orderAndPagination = orderAndPagination;
|
||||
|
||||
return ctx.client.query(['search.paths', arg]);
|
||||
const result = await ctx.client.query(['search.paths', arg]);
|
||||
cache.withNodes(result.nodes);
|
||||
return result;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (arg.take === null || arg.take === undefined) return undefined;
|
||||
@@ -130,4 +136,13 @@ export function usePathsInfiniteQuery({
|
||||
onSuccess: () => getExplorerStore().resetNewThumbnails(),
|
||||
...args
|
||||
});
|
||||
|
||||
const nodes = useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.nodes) ?? [],
|
||||
[query.data?.pages]
|
||||
);
|
||||
|
||||
useNodes(nodes);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CheckSquare } from '@phosphor-icons/react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
backendFeatures,
|
||||
features,
|
||||
@@ -31,6 +32,7 @@ export default () => {
|
||||
|
||||
const debugState = useDebugState();
|
||||
const platform = usePlatform();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
@@ -144,6 +146,9 @@ export default () => {
|
||||
<FeatureFlagSelector />
|
||||
<InvalidateDebugPanel />
|
||||
<TestNotifications />
|
||||
<Button size="sm" variant="gray" onClick={() => navigate('./debug/cache')}>
|
||||
Cache Debug
|
||||
</Button>
|
||||
|
||||
{/* {platform.showDevtools && (
|
||||
<SettingContainer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EjectSimple } from '@phosphor-icons/react';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { useBridgeQuery, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Button, toast, tw } from '@sd/ui';
|
||||
import { Icon, IconName } from '~/components';
|
||||
import { useHomeDir } from '~/hooks/useHomeDir';
|
||||
@@ -28,19 +28,23 @@ const SidebarIcon = ({ name }: { name: IconName }) => {
|
||||
};
|
||||
|
||||
export const EphemeralSection = () => {
|
||||
const locations = useLibraryQuery(['locations.list']);
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
|
||||
const homeDir = useHomeDir();
|
||||
const volumes = useBridgeQuery(['volumes.list']);
|
||||
const result = useBridgeQuery(['volumes.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const volumes = useCache(result.data?.items);
|
||||
|
||||
// this will return an array of location ids that are also volumes
|
||||
// { "/Mount/Point": 1, "/Mount/Point2": 2"}
|
||||
const locationIdsForVolumes = useMemo(() => {
|
||||
if (!locations.data || !volumes.data) return {};
|
||||
if (!locations || !volumes) return {};
|
||||
|
||||
const volumePaths = volumes.data.map((volume) => volume.mount_points[0] ?? null);
|
||||
const volumePaths = volumes.map((volume) => volume.mount_points[0] ?? null);
|
||||
|
||||
const matchedLocations = locations.data.filter((location) =>
|
||||
const matchedLocations = locations.filter((location) =>
|
||||
volumePaths.includes(location.path)
|
||||
);
|
||||
|
||||
@@ -57,9 +61,9 @@ export const EphemeralSection = () => {
|
||||
);
|
||||
|
||||
return locationIdsMap;
|
||||
}, [locations.data, volumes.data]);
|
||||
}, [locations, volumes]);
|
||||
|
||||
const mountPoints = (volumes.data || []).flatMap((volume, volumeIndex) =>
|
||||
const mountPoints = (volumes || []).flatMap((volume, volumeIndex) =>
|
||||
volume.mount_points.map((mountPoint, index) =>
|
||||
mountPoint !== homeDir.data
|
||||
? { type: 'volume', volume, mountPoint, volumeIndex, index }
|
||||
|
||||
@@ -5,9 +5,11 @@ import { Link, NavLink } from 'react-router-dom';
|
||||
import {
|
||||
arraysEqual,
|
||||
useBridgeQuery,
|
||||
useCache,
|
||||
useFeatureFlag,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import { Button, Tooltip } from '@sd/ui';
|
||||
@@ -138,6 +140,8 @@ function Devices() {
|
||||
|
||||
function Locations() {
|
||||
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const onlineLocations = useOnlineLocations();
|
||||
|
||||
return (
|
||||
@@ -150,7 +154,7 @@ function Locations() {
|
||||
}
|
||||
>
|
||||
<SeeMore>
|
||||
{locationsQuery.data?.map((location) => (
|
||||
{locations?.map((location) => (
|
||||
<LocationsContextMenu key={location.id} locationId={location.id}>
|
||||
<SidebarLink
|
||||
className="borderradix-state-closed:border-transparent group relative w-full radix-state-open:border-accent"
|
||||
@@ -179,9 +183,11 @@ function Locations() {
|
||||
}
|
||||
|
||||
function Tags() {
|
||||
const tags = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
const result = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const tags = useCache(result.data?.items);
|
||||
|
||||
if (!tags.data?.length) return;
|
||||
if (!tags?.length) return;
|
||||
|
||||
return (
|
||||
<Section
|
||||
@@ -193,7 +199,7 @@ function Tags() {
|
||||
}
|
||||
>
|
||||
<SeeMore>
|
||||
{tags.data?.map((tag) => (
|
||||
{tags?.map((tag) => (
|
||||
<TagsContextMenu tagId={tag.id} key={tag.id}>
|
||||
<SidebarLink
|
||||
className="border radix-state-closed:border-transparent radix-state-open:border-accent"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { CircleDashed, Cube, Folder, Icon, SelectionSlash, Textbox } from '@phosphor-icons/react';
|
||||
import { useState } from 'react';
|
||||
import { InOrNotIn, ObjectKind, SearchFilterArgs, TextMatch, useLibraryQuery } from '@sd/client';
|
||||
import {
|
||||
InOrNotIn,
|
||||
ObjectKind,
|
||||
SearchFilterArgs,
|
||||
TextMatch,
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes
|
||||
} from '@sd/client';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
|
||||
import { SearchOptionItem, SearchOptionSubMenu } from '.';
|
||||
@@ -421,8 +429,10 @@ export const filterRegistry = [
|
||||
},
|
||||
useOptions: () => {
|
||||
const query = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(query.data?.nodes);
|
||||
const locations = useCache(query.data?.items);
|
||||
|
||||
return (query.data ?? []).map((location) => ({
|
||||
return (locations ?? []).map((location) => ({
|
||||
name: location.name!,
|
||||
value: location.id,
|
||||
icon: 'Folder' // Spacedrive folder icon
|
||||
@@ -455,8 +465,10 @@ export const filterRegistry = [
|
||||
},
|
||||
useOptions: () => {
|
||||
const query = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
useNodes(query.data?.nodes);
|
||||
const tags = useCache(query.data?.items);
|
||||
|
||||
return (query.data ?? []).map((tag) => ({
|
||||
return (tags ?? []).map((tag) => ({
|
||||
name: tag.name!,
|
||||
value: tag.id,
|
||||
icon: tag.color || 'CircleDashed'
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { CodeBlock } from '~/components/Codeblock';
|
||||
import { useRouteTitle } from '~/hooks/useRouteTitle';
|
||||
|
||||
// TODO: Bring this back with a button in the sidebar near settings at the bottom
|
||||
export const Component = () => {
|
||||
useRouteTitle('Debug');
|
||||
|
||||
const { data: nodeState } = useBridgeQuery(['nodeState']);
|
||||
const { data: libraryState } = useBridgeQuery(['library.list']);
|
||||
// const { data: jobs } = useLibraryQuery(['jobs.getRunning']);
|
||||
// const { data: jobHistory } = useLibraryQuery(['jobs.getHistory']);
|
||||
// const { mutate: purgeDB } = useBridgeCommand('PurgeDatabase', {
|
||||
// onMutate: () => {
|
||||
// alert('Database purged');
|
||||
// }
|
||||
// });
|
||||
// const { mutate: identifyFiles } = useLibraryMutation('jobs.identifyUniqueFiles');
|
||||
return (
|
||||
<div className="flex flex-col space-y-5 p-5 pb-7 pt-2">
|
||||
<h1 className="text-lg font-bold ">Developer Debugger</h1>
|
||||
{/* <div className="flex flex-row pb-4 space-x-2">
|
||||
<Button
|
||||
className="w-40"
|
||||
variant="gray"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (nodeState && appPropsContext?.onOpen) {
|
||||
appPropsContext.onOpen(nodeState.data_path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Open data folder
|
||||
</Button>
|
||||
</div> */}
|
||||
{/* <h1 className="text-sm font-bold ">Running Jobs</h1>
|
||||
<CodeBlock src={{ ...jobs }} />
|
||||
<h1 className="text-sm font-bold ">Job History</h1>
|
||||
<CodeBlock src={{ ...jobHistory }} /> */}
|
||||
<h1 className="text-sm font-bold ">Node State</h1>
|
||||
<CodeBlock src={{ ...nodeState }} />
|
||||
<h1 className="text-sm font-bold ">Libraries</h1>
|
||||
<CodeBlock src={{ ...libraryState }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
13
interface/app/$libraryId/debug/cache.tsx
Normal file
13
interface/app/$libraryId/debug/cache.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { snapshot } from 'valtio';
|
||||
import { useNormalisedCache } from '@sd/client';
|
||||
|
||||
export function Component() {
|
||||
const cache = useNormalisedCache();
|
||||
const data = snapshot(cache['#cache']);
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1>Cache Debug</h1>
|
||||
<pre className="pt-4">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
interface/app/$libraryId/debug/index.ts
Normal file
5
interface/app/$libraryId/debug/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { RouteObject } from 'react-router';
|
||||
|
||||
export const debugRoutes: RouteObject = {
|
||||
children: [{ path: 'cache', lazy: () => import('./cache') }]
|
||||
};
|
||||
@@ -7,7 +7,9 @@ import { useLocation } from 'react-router';
|
||||
import {
|
||||
ExplorerItem,
|
||||
getExplorerItemData,
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
type EphemeralPathOrder
|
||||
} from '@sd/client';
|
||||
import { Button, Tooltip } from '@sd/ui';
|
||||
@@ -189,13 +191,15 @@ const EphemeralExplorer = memo((props: { args: PathParams }) => {
|
||||
onSuccess: () => getExplorerStore().resetNewThumbnails()
|
||||
}
|
||||
);
|
||||
useNodes(query.data?.nodes);
|
||||
const entries = useCache(query.data?.entries);
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!query.data) return [];
|
||||
if (!entries) return [];
|
||||
|
||||
const ret: ExplorerItem[] = [];
|
||||
|
||||
for (const item of query.data.entries) {
|
||||
for (const item of entries) {
|
||||
if (settingsSnapshot.layoutMode !== 'media') ret.push(item);
|
||||
else {
|
||||
const { kind } = getExplorerItemData(item);
|
||||
@@ -205,7 +209,7 @@ const EphemeralExplorer = memo((props: { args: PathParams }) => {
|
||||
}
|
||||
|
||||
return ret;
|
||||
}, [query.data, settingsSnapshot.layoutMode]);
|
||||
}, [entries, settingsSnapshot.layoutMode]);
|
||||
|
||||
const explorer = useExplorer({
|
||||
items,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { useHomeDir } from '~/hooks/useHomeDir';
|
||||
import { Platform } from '~/util/Platform';
|
||||
|
||||
import { debugRoutes } from './debug';
|
||||
import settingsRoutes from './settings';
|
||||
|
||||
// Routes that should be contained within the standard Page layout
|
||||
@@ -12,9 +13,9 @@ const pageRoutes: RouteObject = {
|
||||
{ path: 'people', lazy: () => import('./people') },
|
||||
{ path: 'media', lazy: () => import('./media') },
|
||||
{ path: 'spaces', lazy: () => import('./spaces') },
|
||||
{ path: 'debug', lazy: () => import('./debug') },
|
||||
{ path: 'sync', lazy: () => import('./sync') },
|
||||
{ path: 'cloud', lazy: () => import('./cloud') }
|
||||
{ path: 'cloud', lazy: () => import('./cloud') },
|
||||
{ path: 'debug', children: [debugRoutes] }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
FilePathOrder,
|
||||
Location,
|
||||
ObjectKindEnum,
|
||||
useCache,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useLibrarySubscription,
|
||||
useNodes,
|
||||
useOnlineLocations,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
@@ -41,12 +43,14 @@ import LocationOptions from './LocationOptions';
|
||||
|
||||
export const Component = () => {
|
||||
const { id: locationId } = useZodRouteParams(LocationIdParamsSchema);
|
||||
const location = useLibraryQuery(['locations.get', locationId], {
|
||||
const result = useLibraryQuery(['locations.get', locationId], {
|
||||
keepPreviousData: true,
|
||||
suspense: true
|
||||
});
|
||||
useNodes(result.data?.nodes);
|
||||
const location = useCache(result.data?.item);
|
||||
|
||||
return <LocationExplorer location={location.data!} />;
|
||||
return <LocationExplorer location={location!} />;
|
||||
};
|
||||
|
||||
const LocationExplorer = ({ location }: { location: Location; path?: string }) => {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||
import { getIcon, iconNames } from '@sd/assets/util';
|
||||
import { useMemo } from 'react';
|
||||
import { FilePathOrder, SearchFilterArgs, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import {
|
||||
FilePathOrder,
|
||||
SearchFilterArgs,
|
||||
useCache,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import { SearchIdParamsSchema } from '~/app/route-schemas';
|
||||
import { useRouteTitle, useZodRouteParams } from '~/hooks';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { iconNames } from '@sd/assets/util';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { byteSize, useDiscoveredPeers, useLibraryQuery } from '@sd/client';
|
||||
import { byteSize, useDiscoveredPeers, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Card } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
import { useCounter } from '~/hooks';
|
||||
@@ -15,6 +15,9 @@ export const Component = () => {
|
||||
const locations = useLibraryQuery(['locations.list'], {
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
useNodes(locations.data?.nodes);
|
||||
// const locations = useCache(result.data?.items);
|
||||
|
||||
const discoveredPeers = useDiscoveredPeers();
|
||||
const info = useMemo(() => {
|
||||
if (locations.data && discoveredPeers) {
|
||||
@@ -33,8 +36,8 @@ export const Component = () => {
|
||||
}[] = [
|
||||
{
|
||||
icon: 'Folder',
|
||||
title: locations.data.length === 1 ? 'Location' : 'Locations',
|
||||
titleCount: locations.data?.length ?? 0,
|
||||
title: locations.data?.items.length === 1 ? 'Location' : 'Locations',
|
||||
titleCount: locations.data?.items.length ?? 0,
|
||||
sub: 'indexed directories'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Suspense } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
||||
import { useCache, useLibraryMutation, useLibraryQuery, useNodes, useZodForm } from '@sd/client';
|
||||
import {
|
||||
Button,
|
||||
dialogManager,
|
||||
@@ -54,21 +54,22 @@ const EditLocationForm = () => {
|
||||
const fullRescan = useLibraryMutation('locations.fullRescan');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const locationData = useLibraryQuery(['locations.getWithRules', locationId], {
|
||||
const locationDataQuery = useLibraryQuery(['locations.getWithRules', locationId], {
|
||||
suspense: true
|
||||
});
|
||||
useNodes(locationDataQuery.data?.nodes);
|
||||
const locationData = useCache(locationDataQuery.data?.item);
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
defaultValues: {
|
||||
indexerRulesIds:
|
||||
locationData.data?.indexer_rules.map((rule) => rule.indexer_rule.id) ?? [],
|
||||
indexerRulesIds: locationData?.indexer_rules.map((rule) => rule.id) ?? [],
|
||||
locationType: 'normal',
|
||||
name: locationData.data?.name ?? '',
|
||||
path: locationData.data?.path ?? '',
|
||||
hidden: locationData.data?.hidden ?? false,
|
||||
syncPreviewMedia: locationData.data?.sync_preview_media ?? false,
|
||||
generatePreviewMedia: locationData.data?.generate_preview_media ?? false
|
||||
name: locationData?.name ?? '',
|
||||
path: locationData?.path ?? '',
|
||||
hidden: locationData?.hidden ?? false,
|
||||
syncPreviewMedia: locationData?.sync_preview_media ?? false,
|
||||
generatePreviewMedia: locationData?.generate_preview_media ?? false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||
import {
|
||||
extractInfoRSPCError,
|
||||
UnionToTuple,
|
||||
useCache,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
usePlausibleEvent,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
@@ -57,13 +59,15 @@ export const AddLocationDialog = ({
|
||||
const listLocations = useLibraryQuery(['locations.list']);
|
||||
const createLocation = useLibraryMutation('locations.create');
|
||||
const relinkLocation = useLibraryMutation('locations.relink');
|
||||
const listIndexerRules = useLibraryQuery(['locations.indexer_rules.list']);
|
||||
const listIndexerRulesQuery = useLibraryQuery(['locations.indexer_rules.list']);
|
||||
useNodes(listIndexerRulesQuery.data?.nodes);
|
||||
const listIndexerRules = useCache(listIndexerRulesQuery.data?.items);
|
||||
const addLocationToLibrary = useLibraryMutation('locations.addLibrary');
|
||||
|
||||
// This is required because indexRules is undefined on first render
|
||||
const indexerRulesIds = useMemo(
|
||||
() => listIndexerRules.data?.filter((rule) => rule.default).map((rule) => rule.id) ?? [],
|
||||
[listIndexerRules.data]
|
||||
() => listIndexerRules?.filter((rule) => rule.default).map((rule) => rule.id) ?? [],
|
||||
[listIndexerRules]
|
||||
);
|
||||
|
||||
const form = useZodForm({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Trash } from '@phosphor-icons/react';
|
||||
import clsx from 'clsx';
|
||||
import { MouseEventHandler, useState } from 'react';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
import { IndexerRule, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { IndexerRule, useCache, useLibraryMutation, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Button, Divider, Label, toast } from '@sd/ui';
|
||||
import { InfoText } from '@sd/ui/src/forms';
|
||||
import { showAlertDialog } from '~/components';
|
||||
@@ -33,7 +33,8 @@ export default function IndexerRuleEditor<T extends IndexerRuleIdFieldType>({
|
||||
...props
|
||||
}: IndexerRuleEditorProps<T>) {
|
||||
const listIndexerRules = useLibraryQuery(['locations.indexer_rules.list']);
|
||||
const indexRules = listIndexerRules.data;
|
||||
useNodes(listIndexerRules.data?.nodes);
|
||||
const indexRules = useCache(listIndexerRules.data?.items);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [selectedRule, setSelectedRule] = useState<IndexerRule | undefined>(undefined);
|
||||
const [toggleNewRule, setToggleNewRule] = useState(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { SearchInput } from '@sd/ui';
|
||||
|
||||
import { Heading } from '../../Layout';
|
||||
@@ -8,17 +8,19 @@ import { AddLocationButton } from './AddLocationButton';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
export const Component = () => {
|
||||
const locations = useLibraryQuery(['locations.list']);
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [debouncedSearch] = useDebounce(search, 200);
|
||||
|
||||
const filteredLocations = useMemo(
|
||||
() =>
|
||||
locations.data?.filter(
|
||||
locations?.filter(
|
||||
(location) => location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
|
||||
) ?? [],
|
||||
[debouncedSearch, locations.data]
|
||||
[debouncedSearch, locations]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
useBridgeMutation,
|
||||
useBridgeQuery,
|
||||
useCache,
|
||||
useConnectedPeers,
|
||||
useDiscoveredPeers,
|
||||
useFeatureFlag
|
||||
useFeatureFlag,
|
||||
useNodes
|
||||
} from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import { startPairing } from '~/app/p2p/pairing';
|
||||
@@ -41,10 +43,17 @@ function IncorrectP2PPairingPane() {
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
const nlmState = useBridgeQuery(['p2p.state'], {
|
||||
refetchInterval: 1000
|
||||
});
|
||||
const libraries = useBridgeQuery(['library.list']);
|
||||
|
||||
const nlmState = {
|
||||
data: JSON.stringify('lol no')
|
||||
};
|
||||
// TODO: Bring this back
|
||||
// useBridgeQuery(['p2p.state'], {
|
||||
// refetchInterval: 1000
|
||||
// });
|
||||
const result = useBridgeQuery(['library.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const libraries = useCache(result.data?.items);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -86,7 +95,7 @@ function IncorrectP2PPairingPane() {
|
||||
</div>
|
||||
<div>
|
||||
<p>Libraries:</p>
|
||||
{libraries.data?.map((v) => (
|
||||
{libraries?.map((v) => (
|
||||
<div key={v.uuid} className="pb-2">
|
||||
<p>
|
||||
{v.config.name} - {v.uuid}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Button, Card, dialogManager } from '@sd/ui';
|
||||
import { Heading } from '~/app/$libraryId/settings/Layout';
|
||||
import { TagsSettingsParamsSchema } from '~/app/route-schemas';
|
||||
@@ -10,11 +10,14 @@ import CreateDialog from './CreateDialog';
|
||||
import EditForm from './EditForm';
|
||||
|
||||
export const Component = () => {
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
const result = useLibraryQuery(['tags.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const tags = useCache(result.data?.items);
|
||||
|
||||
const { id: locationId } = useZodRouteParams(TagsSettingsParamsSchema);
|
||||
const tagSelectedParam = tags.data?.find((tag) => tag.id === locationId);
|
||||
const tagSelectedParam = tags?.find((tag) => tag.id === locationId);
|
||||
const [selectedTag, setSelectedTag] = useState<null | Tag>(
|
||||
tagSelectedParam ?? tags.data?.[0] ?? null
|
||||
tagSelectedParam ?? tags?.[0] ?? null
|
||||
);
|
||||
|
||||
// Update selected tag when the route param changes
|
||||
@@ -24,7 +27,7 @@ export const Component = () => {
|
||||
|
||||
// Set the first tag as selected when the tags list data is first loaded
|
||||
useEffect(() => {
|
||||
if (tags?.data?.length || (0 > 1 && !selectedTag)) setSelectedTag(tags.data?.[0] ?? null);
|
||||
if (tags?.length || (0 > 1 && !selectedTag)) setSelectedTag(tags?.[0] ?? null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -49,7 +52,7 @@ export const Component = () => {
|
||||
/>
|
||||
<Card className="!px-2">
|
||||
<div className="m-1 flex flex-wrap gap-2">
|
||||
{tags.data?.map((tag) => (
|
||||
{tags?.map((tag) => (
|
||||
<div
|
||||
onClick={() => setSelectedTag(tag.id === selectedTag?.id ? null : tag)}
|
||||
key={tag.id}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { LibraryConfigWrapped, useBridgeMutation, usePlausibleEvent, useZodForm } from '@sd/client';
|
||||
import {
|
||||
insertLibrary,
|
||||
useBridgeMutation,
|
||||
useNormalisedCache,
|
||||
usePlausibleEvent,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
import { Dialog, InputField, useDialog, UseDialogProps, z } from '@sd/ui';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
@@ -23,18 +29,18 @@ export default (props: UseDialogProps) => {
|
||||
const createLibrary = useBridgeMutation('library.create');
|
||||
|
||||
const form = useZodForm({ schema });
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
const library = await createLibrary.mutateAsync({
|
||||
const libraryRaw = await createLibrary.mutateAsync({
|
||||
name: data.name,
|
||||
default_locations: null
|
||||
});
|
||||
cache.withNodes(libraryRaw.nodes);
|
||||
const library = cache.withCache(libraryRaw.item);
|
||||
|
||||
queryClient.setQueryData<LibraryConfigWrapped[]>(['library.list'], (libraries) => [
|
||||
...(libraries || []),
|
||||
library
|
||||
]);
|
||||
insertLibrary(queryClient, library);
|
||||
|
||||
submitPlausibleEvent({
|
||||
event: { type: 'libraryCreate' }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useBridgeQuery, useLibraryContext } from '@sd/client';
|
||||
import { useBridgeQuery, useCache, useLibraryContext, useNodes } from '@sd/client';
|
||||
import { Button, dialogManager } from '@sd/ui';
|
||||
|
||||
import { Heading } from '../../Layout';
|
||||
@@ -6,7 +6,9 @@ import CreateDialog from './CreateDialog';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
export const Component = () => {
|
||||
const libraries = useBridgeQuery(['library.list']);
|
||||
const librariesQuery = useBridgeQuery(['library.list']);
|
||||
useNodes(librariesQuery.data?.nodes);
|
||||
const libraries = useCache(librariesQuery.data?.items);
|
||||
|
||||
const { library } = useLibraryContext();
|
||||
|
||||
@@ -31,7 +33,7 @@ export const Component = () => {
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
{libraries.data
|
||||
{libraries
|
||||
?.sort((a, b) => {
|
||||
if (a.uuid === library.uuid) return -1;
|
||||
if (b.uuid === library.uuid) return 1;
|
||||
|
||||
@@ -114,7 +114,7 @@ function calculateGroups(messages: CRDTOperation[]) {
|
||||
const { typ } = curr;
|
||||
|
||||
if ('model' in typ) {
|
||||
const id = stringify(typ.record_id.pub_id);
|
||||
const id = stringify((typ.record_id as any).pub_id);
|
||||
|
||||
const latest = (() => {
|
||||
const latest = acc[acc.length - 1];
|
||||
@@ -146,8 +146,8 @@ function calculateGroups(messages: CRDTOperation[]) {
|
||||
});
|
||||
} else {
|
||||
const id = {
|
||||
item: stringify(typ.relation_item.pub_id),
|
||||
group: stringify(typ.relation_group.pub_id)
|
||||
item: stringify((typ.relation_item as any).pub_id),
|
||||
group: stringify((typ.relation_group as any).pub_id)
|
||||
};
|
||||
|
||||
const latest = (() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ObjectKindEnum, ObjectOrder, useLibraryQuery } from '@sd/client';
|
||||
import { ObjectKindEnum, ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { Icon } from '~/components';
|
||||
import { useRouteTitle, useZodRouteParams } from '~/hooks';
|
||||
@@ -17,9 +17,11 @@ import { TopBarPortal } from '../TopBar/Portal';
|
||||
|
||||
export function Component() {
|
||||
const { id: tagId } = useZodRouteParams(LocationIdParamsSchema);
|
||||
const tag = useLibraryQuery(['tags.get', tagId], { suspense: true });
|
||||
const result = useLibraryQuery(['tags.get', tagId], { suspense: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const tag = useCache(result.data?.item);
|
||||
|
||||
useRouteTitle(tag.data!.name ?? 'Tag');
|
||||
useRouteTitle(tag!.name ?? 'Tag');
|
||||
|
||||
const explorerSettings = useExplorerSettings({
|
||||
settings: useMemo(() => {
|
||||
@@ -32,12 +34,12 @@ export function Component() {
|
||||
|
||||
const fixedFilters = useMemo(
|
||||
() => [
|
||||
{ object: { tags: { in: [tag.data!.id] } } },
|
||||
{ object: { tags: { in: [tag!.id] } } },
|
||||
...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [])
|
||||
],
|
||||
[tag.data, explorerSettingsSnapshot.layoutMode]
|
||||
[tag, explorerSettingsSnapshot.layoutMode]
|
||||
);
|
||||
|
||||
const search = useSearch({
|
||||
@@ -53,7 +55,7 @@ export function Component() {
|
||||
...objects,
|
||||
isFetchingNextPage: objects.query.isFetchingNextPage,
|
||||
settings: explorerSettings,
|
||||
parent: { type: 'Tag', tag: tag.data! }
|
||||
parent: { type: 'Tag', tag: tag! }
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -65,9 +67,9 @@ export function Component() {
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div
|
||||
className="h-[14px] w-[14px] shrink-0 rounded-full"
|
||||
style={{ backgroundColor: tag.data!.color || '#efefef' }}
|
||||
style={{ backgroundColor: tag!.color || '#efefef' }}
|
||||
/>
|
||||
<span className="truncate text-sm font-medium">{tag?.data?.name}</span>
|
||||
<span className="truncate text-sm font-medium">{tag?.name}</span>
|
||||
</div>
|
||||
}
|
||||
right={<DefaultTopBarOptions />}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Navigate, Outlet, redirect, useMatches, type RouteObject } from 'react-router-dom';
|
||||
import { currentLibraryCache, getCachedLibraries, useCachedLibraries } from '@sd/client';
|
||||
import {
|
||||
currentLibraryCache,
|
||||
getCachedLibraries,
|
||||
NormalisedCache,
|
||||
useCachedLibraries
|
||||
} from '@sd/client';
|
||||
import { Dialogs, Toaster } from '@sd/ui';
|
||||
import { RouterErrorBoundary } from '~/ErrorFallback';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { useRoutingContext } from '~/RoutingContext';
|
||||
|
||||
import { Platform } from '..';
|
||||
@@ -17,7 +21,7 @@ import './style.scss';
|
||||
// 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)
|
||||
|
||||
export const createRoutes = (platform: Platform) =>
|
||||
export const createRoutes = (platform: Platform, cache: NormalisedCache) =>
|
||||
[
|
||||
{
|
||||
Component: () => {
|
||||
@@ -54,7 +58,7 @@ export const createRoutes = (platform: Platform) =>
|
||||
return <Navigate to={`${libraryId}`} replace />;
|
||||
},
|
||||
loader: async () => {
|
||||
const libraries = await getCachedLibraries();
|
||||
const libraries = await getCachedLibraries(cache);
|
||||
|
||||
const currentLibrary = libraries.find(
|
||||
(l) => l.uuid === currentLibraryCache.id
|
||||
@@ -76,7 +80,7 @@ export const createRoutes = (platform: Platform) =>
|
||||
path: ':libraryId',
|
||||
lazy: () => import('./$libraryId/Layout'),
|
||||
loader: async ({ params: { libraryId } }) => {
|
||||
const libraries = await getCachedLibraries();
|
||||
const libraries = await getCachedLibraries(cache);
|
||||
const library = libraries.find((l) => l.uuid === libraryId);
|
||||
|
||||
if (!library) {
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
currentLibraryCache,
|
||||
getOnboardingStore,
|
||||
getUnitFormatStore,
|
||||
insertLibrary,
|
||||
resetOnboardingStore,
|
||||
telemetryStore,
|
||||
useBridgeMutation,
|
||||
useCachedLibraries,
|
||||
useMultiZodForm,
|
||||
useNormalisedCache,
|
||||
useOnboardingStore,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
@@ -95,6 +97,7 @@ const useFormState = () => {
|
||||
}
|
||||
|
||||
const createLibrary = useBridgeMutation('library.create');
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
const submit = handleSubmit(
|
||||
async (data) => {
|
||||
@@ -106,20 +109,16 @@ const useFormState = () => {
|
||||
|
||||
try {
|
||||
// show creation screen for a bit for smoothness
|
||||
const [library] = await Promise.all([
|
||||
const [libraryRaw] = await Promise.all([
|
||||
createLibrary.mutateAsync({
|
||||
name: data['new-library'].name,
|
||||
default_locations: data.locations.locations
|
||||
}),
|
||||
new Promise((res) => setTimeout(res, 500))
|
||||
]);
|
||||
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||
// The invalidation system beat us to it
|
||||
if (libraries.find((l: any) => l.uuid === library.uuid)) return libraries;
|
||||
|
||||
return [...(libraries || []), library];
|
||||
});
|
||||
cache.withNodes(libraryRaw.nodes);
|
||||
const library = cache.withCache(libraryRaw.item);
|
||||
insertLibrary(queryClient, library);
|
||||
|
||||
platform.refreshMenuBar && platform.refreshMenuBar();
|
||||
|
||||
|
||||
24
interface/components/Devtools.tsx
Normal file
24
interface/components/Devtools.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defaultContext } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { useDebugState } from '@sd/client';
|
||||
|
||||
export const Devtools = () => {
|
||||
const debugState = useDebugState();
|
||||
|
||||
return (
|
||||
<>
|
||||
{debugState.reactQueryDevtools !== 'disabled' ? (
|
||||
<ReactQueryDevtools
|
||||
position="bottom-right"
|
||||
// The `context={defaultContext}` part is required for this to work on Windows.
|
||||
// Why, idk, don't question it
|
||||
context={defaultContext}
|
||||
toggleButtonProps={{
|
||||
tabIndex: -1,
|
||||
className: debugState.reactQueryDevtools === 'invisible' ? 'opacity-0' : ''
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,7 @@ export const useIsLocationIndexing = (locationId: number): boolean => {
|
||||
group.jobs.some((job) => {
|
||||
if (
|
||||
job.name === 'indexer' &&
|
||||
job.metadata?.location.id === locationId &&
|
||||
(job.metadata as any)?.location.id === locationId &&
|
||||
(job.status === 'Running' || job.status === 'Queued')
|
||||
) {
|
||||
return job.completed_task_count === 0;
|
||||
|
||||
@@ -25,7 +25,7 @@ export const useRedirectToNewLocation = () => {
|
||||
.some(
|
||||
(j) =>
|
||||
j.name === 'indexer' &&
|
||||
j.metadata?.location.id === newLocation &&
|
||||
(j.metadata as any)?.location.id === newLocation &&
|
||||
(j.completed_task_count > 0 || j.completed_at != null)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import '@fontsource/inter/variable.css';
|
||||
|
||||
import { init, Integrations } from '@sentry/browser';
|
||||
import { defaultContext } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import dayjs from 'dayjs';
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
@@ -10,9 +8,9 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { PropsWithChildren, Suspense } from 'react';
|
||||
import { RouterProvider, RouterProviderProps } from 'react-router-dom';
|
||||
import {
|
||||
CacheProvider,
|
||||
NotificationContextProvider,
|
||||
P2PContextProvider,
|
||||
useDebugState,
|
||||
useInvalidateQuery,
|
||||
useLoadBackendFeatureFlags
|
||||
} from '@sd/client';
|
||||
@@ -20,6 +18,7 @@ import { TooltipProvider } from '@sd/ui';
|
||||
|
||||
import { createRoutes } from './app';
|
||||
import { P2P, useP2PErrorToast } from './app/p2p';
|
||||
import { Devtools } from './components/Devtools';
|
||||
import { WithPrismTheme } from './components/TextViewer/prism';
|
||||
import ErrorFallback, { BetterErrorBoundary } from './ErrorFallback';
|
||||
import { useTheme } from './hooks';
|
||||
@@ -42,23 +41,6 @@ init({
|
||||
integrations: [new Integrations.HttpContext(), new Integrations.Dedupe()]
|
||||
});
|
||||
|
||||
const Devtools = () => {
|
||||
const debugState = useDebugState();
|
||||
|
||||
// The `context={defaultContext}` part is required for this to work on Windows.
|
||||
// Why, idk, don't question it
|
||||
return debugState.reactQueryDevtools !== 'disabled' ? (
|
||||
<ReactQueryDevtools
|
||||
position="bottom-right"
|
||||
context={defaultContext}
|
||||
toggleButtonProps={{
|
||||
tabIndex: -1,
|
||||
className: debugState.reactQueryDevtools === 'invisible' ? 'opacity-0' : ''
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export type Router = RouterProviderProps['router'];
|
||||
|
||||
export function SpacedriveRouterProvider(props: {
|
||||
|
||||
251
packages/client/src/cache.tsx
Normal file
251
packages/client/src/cache.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore
|
||||
} from 'react';
|
||||
import { proxy, snapshot, subscribe } from 'valtio';
|
||||
|
||||
import { type CacheNode } from './core';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__REDUX_DEVTOOLS_EXTENSION__: any;
|
||||
}
|
||||
}
|
||||
|
||||
type Store = ReturnType<typeof defaultStore>;
|
||||
type Context = ReturnType<typeof createCache>;
|
||||
export type NormalisedCache = ReturnType<typeof createCache>;
|
||||
|
||||
const defaultStore = () => ({
|
||||
nodes: {} as Record<string, Record<string, unknown>>
|
||||
});
|
||||
|
||||
const Context = createContext<Context>(undefined!);
|
||||
|
||||
export function createCache() {
|
||||
const cache = proxy(defaultStore());
|
||||
return {
|
||||
cache,
|
||||
withNodes(data: CacheNode[] | undefined) {
|
||||
updateNodes(cache, data);
|
||||
},
|
||||
withCache<T>(data: T | undefined): UseCacheResult<T> {
|
||||
return restore(cache, new Map(), data) as any;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function CacheProvider({ cache, children }: PropsWithChildren<{ cache: NormalisedCache }>) {
|
||||
useEffect(() => {
|
||||
if ('__REDUX_DEVTOOLS_EXTENSION__' in window === false) return;
|
||||
|
||||
const devtools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({});
|
||||
|
||||
const unsub = devtools.subscribe((_message: any) => {
|
||||
// console.log(message);
|
||||
});
|
||||
|
||||
devtools.init();
|
||||
subscribe(cache.cache, () => devtools.send('change', snapshot(cache.cache)));
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const requiredKeys = new StableSet<[string, string]>();
|
||||
for (const query of queryClient.getQueryCache().getAll()) {
|
||||
if (query.state.data) scanDataForKeys(cache.cache, requiredKeys, query.state.data);
|
||||
}
|
||||
|
||||
const existingKeys = new StableSet<[string, string]>();
|
||||
Object.entries(cache.cache.nodes).map(([type, value]) => {
|
||||
Object.keys(value).map((id) => existingKeys.add([type, id]));
|
||||
});
|
||||
|
||||
for (const [type, id] of existingKeys.entries()) {
|
||||
// If key is not required. Eg. not in any query within the React Query cache.
|
||||
if (!requiredKeys.has([type, id])) {
|
||||
// Yeet the imposter
|
||||
console.debug('Removing Cache Key: ', type, id);
|
||||
delete cache.cache.nodes?.[type]?.[id];
|
||||
}
|
||||
}
|
||||
|
||||
console.debug('Normalised Cache Cleanup', requiredKeys.size, existingKeys.size);
|
||||
}, 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [cache.cache, queryClient]);
|
||||
|
||||
return <Context.Provider value={cache}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
export function useCacheContext() {
|
||||
const context = useContext(Context);
|
||||
if (!context) throw new Error('Missing `CacheContext` provider!');
|
||||
return context;
|
||||
}
|
||||
|
||||
function scanDataForKeys(cache: Store, keys: StableSet<[string, string]>, item: unknown) {
|
||||
if (item === undefined || item === null) return;
|
||||
if (Array.isArray(item)) {
|
||||
for (const v of item) {
|
||||
scanDataForKeys(cache, keys, v);
|
||||
}
|
||||
} else if (typeof item === 'object') {
|
||||
if ('__type' in item && '__id' in item) {
|
||||
if (typeof item.__type !== 'string') throw new Error('Invalid `__type`');
|
||||
if (typeof item.__id !== 'string') throw new Error('Invalid `__id`');
|
||||
keys.add([item.__type, item.__id]);
|
||||
const result = cache.nodes?.[item.__type]?.[item.__id];
|
||||
if (result) scanDataForKeys(cache, keys, result);
|
||||
}
|
||||
|
||||
for (const [_k, value] of Object.entries(item)) {
|
||||
scanDataForKeys(cache, keys, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function restore(cache: Store, subscribed: Map<string, Set<unknown>>, item: unknown): unknown {
|
||||
if (item === undefined || item === null) {
|
||||
return item;
|
||||
} else if (Array.isArray(item)) {
|
||||
return item.map((v) => restore(cache, subscribed, v));
|
||||
} else if (typeof item === 'object') {
|
||||
if ('__type' in item && '__id' in item) {
|
||||
if (typeof item.__type !== 'string') throw new Error('Invalid `__type`');
|
||||
if (typeof item.__id !== 'string') throw new Error('Invalid `__id`');
|
||||
const result = cache.nodes?.[item.__type]?.[item.__id];
|
||||
if (!result)
|
||||
throw new Error(`Missing node for id '${item.__id}' of type '${item.__type}'`);
|
||||
|
||||
const v = subscribed.get(item.__type);
|
||||
if (v) {
|
||||
v.add(item.__id);
|
||||
} else {
|
||||
subscribed.set(item.__type, new Set([item.__id]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(item).map(([key, value]) => [key, restore(cache, subscribed, value)])
|
||||
);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export function useNodes(data: CacheNode[] | undefined) {
|
||||
const cache = useCacheContext();
|
||||
|
||||
// `useMemo` instead of `useEffect` here is cursed but it needs to run before the `useMemo` in the `useCache` hook.
|
||||
useMemo(() => {
|
||||
updateNodes(cache.cache, data);
|
||||
}, [cache, data]);
|
||||
}
|
||||
|
||||
// Methods to interact with the cache outside of the React lifecycle.
|
||||
export function useNormalisedCache() {
|
||||
const cache = useCacheContext();
|
||||
|
||||
return {
|
||||
'#cache': cache.cache,
|
||||
'withNodes': cache.withNodes,
|
||||
'withCache': cache.withCache
|
||||
};
|
||||
}
|
||||
|
||||
function updateNodes(cache: Store, data: CacheNode[] | undefined) {
|
||||
if (!data) return;
|
||||
|
||||
for (const item of data) {
|
||||
if (!('__type' in item && '__id' in item)) throw new Error('Missing `__type` or `__id`');
|
||||
if (typeof item.__type !== 'string') throw new Error('Invalid `__type`');
|
||||
if (typeof item.__id !== 'string') throw new Error('Invalid `__id`');
|
||||
|
||||
const copy = { ...item } as any;
|
||||
delete copy.__type;
|
||||
delete copy.__id;
|
||||
|
||||
if (!cache.nodes[item.__type]) cache.nodes[item.__type] = {};
|
||||
// TODO: This should be a deepmerge but that would break stuff like `size_in_bytes` or `inode` as the arrays are joined.
|
||||
cache.nodes[item.__type]![item.__id] = copy;
|
||||
}
|
||||
}
|
||||
|
||||
export type UseCacheResult<T> = T extends (infer A)[]
|
||||
? UseCacheResult<A>[]
|
||||
: T extends object
|
||||
? T extends { '__type': any; '__id': string; '#type': infer U }
|
||||
? UseCacheResult<U>
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> }
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> };
|
||||
|
||||
export function useCache<T>(data: T | undefined) {
|
||||
const cache = useCacheContext();
|
||||
const subscribed = useRef(new Map<string, Set<unknown>>()).current;
|
||||
const [i, setI] = useState(0); // TODO: Remove this
|
||||
|
||||
const state = useMemo(
|
||||
() => restore(cache.cache, subscribed, data) as UseCacheResult<T>,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[cache, data, i]
|
||||
);
|
||||
|
||||
return useSyncExternalStore(
|
||||
(onStoreChange) => {
|
||||
return subscribe(cache.cache, (ops) => {
|
||||
for (const [_, key] of ops) {
|
||||
const key_type = key[1] as string;
|
||||
const key_id = key[2] as string;
|
||||
|
||||
const v = subscribed.get(key_type);
|
||||
if (v && v.has(key_id)) {
|
||||
setI((i) => i + 1);
|
||||
onStoreChange();
|
||||
|
||||
break; // We only need to trigger re-render once so we can break
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
() => state
|
||||
);
|
||||
}
|
||||
|
||||
class StableSet<T> {
|
||||
set = new Set<string>();
|
||||
|
||||
get size() {
|
||||
return this.set.size;
|
||||
}
|
||||
|
||||
add(value: T) {
|
||||
this.set.add(JSON.stringify(value));
|
||||
}
|
||||
|
||||
has(value: T) {
|
||||
return this.set.has(JSON.stringify(value));
|
||||
}
|
||||
|
||||
*entries() {
|
||||
for (const v of this.set) {
|
||||
yield JSON.parse(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,43 +8,42 @@ export type Procedures = {
|
||||
{ key: "buildInfo", input: never, result: BuildInfo } |
|
||||
{ key: "cloud.library.get", input: LibraryArgs<null>, result: { uuid: string; name: string; ownerId: string; instances: { id: string; uuid: string; identity: string }[] } | null } |
|
||||
{ key: "cloud.library.list", input: never, result: { uuid: string; name: string; ownerId: string; instances: { id: string; uuid: string }[] }[] } |
|
||||
{ key: "ephemeralFiles.getMediaData", input: string, result: MediaMetadata | null } |
|
||||
{ key: "files.get", input: LibraryArgs<GetArgs>, result: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: FilePath[] } | null } |
|
||||
{ key: "ephemeralFiles.getMediaData", input: string, result: ({ type: "Image" } & ImageMetadata) | ({ type: "Video" } & VideoMetadata) | ({ type: "Audio" } & AudioMetadata) | null } |
|
||||
{ key: "files.get", input: LibraryArgs<number>, result: { item: Reference<ObjectWithFilePaths2>; nodes: CacheNode[] } | null } |
|
||||
{ key: "files.getConvertableImageExtensions", input: never, result: string[] } |
|
||||
{ key: "files.getMediaData", input: LibraryArgs<number>, result: MediaMetadata } |
|
||||
{ key: "files.getPath", input: LibraryArgs<number>, result: string | null } |
|
||||
{ key: "invalidation.test-invalidate", input: never, result: number } |
|
||||
{ key: "jobs.isActive", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "jobs.reports", input: LibraryArgs<null>, result: JobGroup[] } |
|
||||
{ key: "library.list", input: never, result: LibraryConfigWrapped[] } |
|
||||
{ key: "library.list", input: never, result: NormalisedResults<LibraryConfigWrapped> } |
|
||||
{ key: "library.statistics", input: LibraryArgs<null>, result: Statistics } |
|
||||
{ key: "locations.get", input: LibraryArgs<number>, result: Location | null } |
|
||||
{ key: "locations.getWithRules", input: LibraryArgs<number>, result: LocationWithIndexerRules | null } |
|
||||
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
|
||||
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: IndexerRule[] } |
|
||||
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: IndexerRule[] } |
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: Location[] } |
|
||||
{ key: "locations.get", input: LibraryArgs<number>, result: { item: Reference<Location>; nodes: CacheNode[] } | null } |
|
||||
{ key: "locations.getWithRules", input: LibraryArgs<number>, result: { item: Reference<LocationWithIndexerRule>; nodes: CacheNode[] } | null } |
|
||||
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: NormalisedResult<IndexerRule> } |
|
||||
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: NormalisedResults<IndexerRule> } |
|
||||
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: NormalisedResults<IndexerRule> } |
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: NormalisedResults<Location> } |
|
||||
{ key: "locations.systemLocations", input: never, result: SystemLocations } |
|
||||
{ key: "nodeState", input: never, result: NodeState } |
|
||||
{ key: "nodes.listLocations", input: LibraryArgs<string | null>, result: ExplorerItem[] } |
|
||||
{ key: "notifications.dismiss", input: NotificationId, result: null } |
|
||||
{ key: "notifications.dismissAll", input: never, result: null } |
|
||||
{ key: "notifications.get", input: never, result: Notification[] } |
|
||||
{ key: "p2p.state", input: never, result: P2PState } |
|
||||
{ key: "preferences.get", input: LibraryArgs<null>, result: LibraryPreferences } |
|
||||
{ key: "search.ephemeralPaths", input: LibraryArgs<EphemeralPathSearchArgs>, result: NonIndexedFileSystemEntries } |
|
||||
{ key: "search.ephemeralPaths", input: LibraryArgs<EphemeralPathSearchArgs>, result: EphemeralPathsResult } |
|
||||
{ key: "search.objects", input: LibraryArgs<ObjectSearchArgs>, result: SearchData<ExplorerItem> } |
|
||||
{ key: "search.objectsCount", input: LibraryArgs<{ filters?: SearchFilterArgs[] }>, result: number } |
|
||||
{ key: "search.paths", input: LibraryArgs<FilePathSearchArgs>, result: SearchData<ExplorerItem> } |
|
||||
{ key: "search.pathsCount", input: LibraryArgs<{ filters?: SearchFilterArgs[] }>, result: number } |
|
||||
{ key: "search.saved.get", input: LibraryArgs<number>, result: SavedSearch | null } |
|
||||
{ key: "search.saved.get", input: LibraryArgs<number>, result: { id: number; pub_id: number[]; search: string | null; filters: string | null; name: string | null; icon: string | null; description: string | null; date_created: string | null; date_modified: string | null } | null } |
|
||||
{ key: "search.saved.list", input: LibraryArgs<null>, result: SavedSearch[] } |
|
||||
{ key: "sync.messages", input: LibraryArgs<null>, result: CRDTOperation[] } |
|
||||
{ key: "tags.get", input: LibraryArgs<number>, result: Tag | null } |
|
||||
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Tag[] } |
|
||||
{ key: "tags.getWithObjects", input: LibraryArgs<number[]>, result: { [key: number]: { date_created: string | null; object: { id: number } }[] } } |
|
||||
{ key: "tags.list", input: LibraryArgs<null>, result: Tag[] } |
|
||||
{ key: "volumes.list", input: never, result: Volume[] },
|
||||
{ key: "tags.get", input: LibraryArgs<number>, result: { item: Reference<Tag>; nodes: CacheNode[] } | null } |
|
||||
{ key: "tags.getForObject", input: LibraryArgs<number>, result: NormalisedResults<Tag> } |
|
||||
{ key: "tags.getWithObjects", input: LibraryArgs<number[]>, result: { [key in number]: ({ date_created: string | null; object: { id: number } })[] } } |
|
||||
{ key: "tags.list", input: LibraryArgs<null>, result: NormalisedResults<Tag> } |
|
||||
{ key: "volumes.list", input: never, result: NormalisedResults<Volume> },
|
||||
mutations:
|
||||
{ key: "api.sendFeedback", input: Feedback, result: null } |
|
||||
{ key: "auth.logout", input: never, result: null } |
|
||||
@@ -78,7 +77,7 @@ export type Procedures = {
|
||||
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
|
||||
{ key: "jobs.pause", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "jobs.resume", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } |
|
||||
{ key: "library.create", input: CreateLibraryArgs, result: NormalisedResult<LibraryConfigWrapped> } |
|
||||
{ key: "library.delete", input: string, result: null } |
|
||||
{ key: "library.edit", input: EditLibraryArgs, result: null } |
|
||||
{ key: "locations.addLibrary", input: LibraryArgs<LocationCreateArgs>, result: number | null } |
|
||||
@@ -96,7 +95,7 @@ export type Procedures = {
|
||||
{ key: "notifications.testLibrary", input: LibraryArgs<null>, result: null } |
|
||||
{ key: "p2p.acceptSpacedrop", input: [string, string | null], result: null } |
|
||||
{ key: "p2p.cancelSpacedrop", input: string, result: null } |
|
||||
{ key: "p2p.pair", input: string, result: number } |
|
||||
{ key: "p2p.pair", input: RemoteIdentity, result: number } |
|
||||
{ key: "p2p.pairingResponse", input: [number, PairingDecision], result: null } |
|
||||
{ key: "p2p.spacedrop", input: SpacedropArgs, result: string } |
|
||||
{ key: "preferences.update", input: LibraryArgs<LibraryPreferences>, result: null } |
|
||||
@@ -139,13 +138,31 @@ export type CRDTOperation = { instance: string; timestamp: number; id: string; t
|
||||
|
||||
export type CRDTOperationType = SharedOperation | RelationOperation
|
||||
|
||||
export type CacheNode = { __type: string; __id: string; "#node": any }
|
||||
|
||||
export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null }
|
||||
|
||||
export type ChangeNodeNameArgs = { name: string | null; p2p_enabled: boolean | null; p2p_port: MaybeUndefined<number> }
|
||||
|
||||
export type ColorProfile = "Normal" | "Custom" | "HDRNoOriginal" | "HDRWithOriginal" | "OriginalForHDR" | "Panorama" | "PortraitHDR" | "Portrait"
|
||||
|
||||
export type Composite = "Unknown" | "False" | "General" | "Live"
|
||||
export type Composite =
|
||||
/**
|
||||
* The data is present, but we're unable to determine what they mean
|
||||
*/
|
||||
"Unknown" |
|
||||
/**
|
||||
* Not a composite image
|
||||
*/
|
||||
"False" |
|
||||
/**
|
||||
* A general composite image
|
||||
*/
|
||||
"General" |
|
||||
/**
|
||||
* The composite image was captured while shooting
|
||||
*/
|
||||
"Live"
|
||||
|
||||
export type ConvertImageArgs = { location_id: number; file_path_id: number; delete_src: boolean; desired_extension: ConvertableExtension; quality_percentage: number | null }
|
||||
|
||||
@@ -173,6 +190,8 @@ export type EphemeralPathOrder = { field: "name"; value: SortOrder } | { field:
|
||||
|
||||
export type EphemeralPathSearchArgs = { path: string; withHiddenFiles: boolean; order?: EphemeralPathOrder | null }
|
||||
|
||||
export type EphemeralPathsResult = { entries: Reference<ExplorerItem>[]; errors: Error[]; nodes: CacheNode[] }
|
||||
|
||||
export type EphemeralRenameFileArgs = { kind: EphemeralRenameKind }
|
||||
|
||||
export type EphemeralRenameKind = { One: EphemeralRenameOne } | { Many: EphemeralRenameMany }
|
||||
@@ -192,7 +211,7 @@ export type ExplorerItem = { type: "Path"; has_local_thumbnail: boolean; thumbna
|
||||
|
||||
export type ExplorerLayout = "grid" | "list" | "media"
|
||||
|
||||
export type ExplorerSettings<TOrder> = { layoutMode: ExplorerLayout | null; gridItemSize: number | null; gridGap: number | null; mediaColumns: number | null; mediaAspectSquare: boolean | null; mediaViewWithDescendants: boolean | null; openOnDoubleClick: DoubleClickAction | null; showBytesInGridView: boolean | null; colVisibility: { [key: string]: boolean } | null; colSizes: { [key: string]: number } | null; order?: TOrder | null; showHiddenFiles?: boolean }
|
||||
export type ExplorerSettings<TOrder> = { layoutMode: ExplorerLayout | null; gridItemSize: number | null; gridGap: number | null; mediaColumns: number | null; mediaAspectSquare: boolean | null; mediaViewWithDescendants: boolean | null; openOnDoubleClick: DoubleClickAction | null; showBytesInGridView: boolean | null; colVisibility: { [key in string]: boolean } | null; colSizes: { [key in string]: number } | null; order?: TOrder | null; showHiddenFiles?: boolean }
|
||||
|
||||
export type Feedback = { message: string; emoji: number }
|
||||
|
||||
@@ -218,11 +237,52 @@ export type FilePathOrder = { field: "name"; value: SortOrder } | { field: "size
|
||||
|
||||
export type FilePathSearchArgs = { take?: number | null; orderAndPagination?: OrderAndPagination<number, FilePathOrder, FilePathCursor> | null; filters?: SearchFilterArgs[]; groupDirectories?: boolean }
|
||||
|
||||
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; hidden: boolean | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: Object | null }
|
||||
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; hidden: boolean | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null } | null }
|
||||
|
||||
export type Flash = { mode: FlashMode; fired: boolean | null; returned: boolean | null; red_eye_reduction: boolean | null }
|
||||
export type Flash = {
|
||||
/**
|
||||
* Specifies how flash was used (on, auto, off, forced, onvalid)
|
||||
*
|
||||
* [`FlashMode::Unknown`] isn't a valid EXIF state, but it's included as the default,
|
||||
* just in case we're unable to correctly match it to a known (valid) state.
|
||||
*
|
||||
* This type should only ever be evaluated if flash EXIF data is present, so having this as a non-option shouldn't be an issue.
|
||||
*/
|
||||
mode: FlashMode;
|
||||
/**
|
||||
* Did the flash actually fire?
|
||||
*/
|
||||
fired: boolean | null;
|
||||
/**
|
||||
* Did flash return to the camera? (Unsure of the meaning)
|
||||
*/
|
||||
returned: boolean | null;
|
||||
/**
|
||||
* Was red eye reduction used?
|
||||
*/
|
||||
red_eye_reduction: boolean | null }
|
||||
|
||||
export type FlashMode = "Unknown" | "On" | "Off" | "Auto" | "Forced"
|
||||
export type FlashMode =
|
||||
/**
|
||||
* The data is present, but we're unable to determine what they mean
|
||||
*/
|
||||
"Unknown" |
|
||||
/**
|
||||
* FLash was on
|
||||
*/
|
||||
"On" |
|
||||
/**
|
||||
* Flash was off
|
||||
*/
|
||||
"Off" |
|
||||
/**
|
||||
* Flash was set to automatically fire in certain conditions
|
||||
*/
|
||||
"Auto" |
|
||||
/**
|
||||
* Flash was forcefully fired
|
||||
*/
|
||||
"Forced"
|
||||
|
||||
export type FromPattern = { pattern: string; replace_all: boolean }
|
||||
|
||||
@@ -232,10 +292,6 @@ export type GenerateThumbsForLocationArgs = { id: number; path: string; regenera
|
||||
|
||||
export type GetAll = { backups: Backup[]; directory: string }
|
||||
|
||||
export type GetArgs = { id: number }
|
||||
|
||||
export type Header = { id: string; timestamp: string; library_id: string; library_name: string }
|
||||
|
||||
export type IdentifyUniqueFilesArgs = { id: number; path: string }
|
||||
|
||||
export type ImageMetadata = { resolution: Resolution; date_taken: MediaDate | null; location: MediaLocation | null; camera_data: CameraData; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null }
|
||||
@@ -262,10 +318,12 @@ export type JobGroup = { id: string; action: string | null; status: JobStatus; c
|
||||
|
||||
export type JobProgressEvent = { id: string; library_id: string; task_count: number; completed_task_count: number; phase: string; message: string; estimated_completion: string }
|
||||
|
||||
export type JobReport = { id: string; name: string; action: string | null; data: number[] | null; metadata: { [key: string]: any } | null; is_background: boolean; errors_text: string[]; created_at: string | null; started_at: string | null; completed_at: string | null; parent_id: string | null; status: JobStatus; task_count: number; completed_task_count: number; phase: string; message: string; estimated_completion: string }
|
||||
export type JobReport = { id: string; name: string; action: string | null; data: number[] | null; metadata: { [key in string]: JsonValue } | null; is_background: boolean; errors_text: string[]; created_at: string | null; started_at: string | null; completed_at: string | null; parent_id: string | null; status: JobStatus; task_count: number; completed_task_count: number; phase: string; message: string; estimated_completion: string }
|
||||
|
||||
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" | "CompletedWithErrors"
|
||||
|
||||
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }
|
||||
|
||||
/**
|
||||
* Can wrap a query argument to require it to contain a `library_id` and provide helpers for working with libraries.
|
||||
*/
|
||||
@@ -274,15 +332,27 @@ export type LibraryArgs<T> = { library_id: string; arg: T }
|
||||
/**
|
||||
* LibraryConfig holds the configuration for a specific library. This is stored as a '{uuid}.sdlibrary' file.
|
||||
*/
|
||||
export type LibraryConfig = { name: LibraryName; description: string | null; instance_id: number; version: LibraryConfigVersion }
|
||||
export type LibraryConfig = {
|
||||
/**
|
||||
* name is the display name of the library. This is used in the UI and is set by the user.
|
||||
*/
|
||||
name: LibraryName;
|
||||
/**
|
||||
* description is a user set description of the library. This is used in the UI and is set by the user.
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* id of the current instance so we know who this `.db` is. This can be looked up within the `Instance` table.
|
||||
*/
|
||||
instance_id: number; version: LibraryConfigVersion }
|
||||
|
||||
export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9"
|
||||
|
||||
export type LibraryConfigWrapped = { uuid: string; instance_id: string; instance_public_key: string; config: LibraryConfig }
|
||||
export type LibraryConfigWrapped = { uuid: string; instance_id: string; instance_public_key: RemoteIdentity; config: LibraryConfig }
|
||||
|
||||
export type LibraryName = string
|
||||
|
||||
export type LibraryPreferences = { location?: { [key: string]: LocationSettings } }
|
||||
export type LibraryPreferences = { location?: { [key in string]: LocationSettings } }
|
||||
|
||||
export type LightScanArgs = { location_id: number; sub_path: string }
|
||||
|
||||
@@ -309,16 +379,9 @@ export type LocationSettings = { explorer: ExplorerSettings<FilePathOrder> }
|
||||
*/
|
||||
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[]; path: string | null }
|
||||
|
||||
export type LocationWithIndexerRules = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null; indexer_rules: { indexer_rule: IndexerRule }[] }
|
||||
export type LocationWithIndexerRule = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null; indexer_rules: Reference<IndexerRule>[] }
|
||||
|
||||
/**
|
||||
* The configuration for the P2P Manager
|
||||
* DO NOT MAKE BREAKING CHANGES - This is embedded in the `node_config.json`
|
||||
* For future me: `Keypair` is not on here cause hot reloading it hard.
|
||||
*/
|
||||
export type ManagerConfig = { enabled: boolean; port?: number | null }
|
||||
|
||||
export type MaybeUndefined<T> = null | null | T
|
||||
export type MaybeUndefined<T> = null | T
|
||||
|
||||
export type MediaDataOrder = { field: "epochTime"; value: SortOrder }
|
||||
|
||||
@@ -326,7 +389,7 @@ export type MediaDataOrder = { field: "epochTime"; value: SortOrder }
|
||||
* This can be either naive with no TZ (`YYYY-MM-DD HH-MM-SS`) or UTC (`YYYY-MM-DD HH-MM-SS ±HHMM`),
|
||||
* where `±HHMM` is the timezone data. It may be negative if West of the Prime Meridian, or positive if East.
|
||||
*/
|
||||
export type MediaDate = string | string
|
||||
export type MediaDate = string
|
||||
|
||||
export type MediaLocation = { latitude: number; longitude: number; pluscode: PlusCode; altitude: number | null; direction: number | null }
|
||||
|
||||
@@ -334,12 +397,32 @@ export type MediaMetadata = ({ type: "Image" } & ImageMetadata) | ({ type: "Vide
|
||||
|
||||
export type NodePreferences = { thumbnailer: ThumbnailerPreferences }
|
||||
|
||||
export type NodeState = ({ id: string; name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[]; preferences: NodePreferences }) & { data_path: string; p2p: P2PStatus }
|
||||
|
||||
export type NonIndexedFileSystemEntries = { entries: ExplorerItem[]; errors: Error[] }
|
||||
export type NodeState = ({
|
||||
/**
|
||||
* id is a unique identifier for the current node. Each node has a public identifier (this one) and is given a local id for each library (done within the library code).
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record
|
||||
*/
|
||||
name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[]; preferences: NodePreferences }) & { data_path: string; p2p: P2PStatus }
|
||||
|
||||
export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[]; hidden: boolean }
|
||||
|
||||
/**
|
||||
* A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
*
|
||||
* You don't need to use this, it's just a shortcut to avoid having to write out the full type everytime.
|
||||
*/
|
||||
export type NormalisedResult<T> = { item: Reference<T>; nodes: CacheNode[] }
|
||||
|
||||
/**
|
||||
* A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
*
|
||||
* You don't need to use this, it's just a shortcut to avoid having to write out the full type everytime.
|
||||
*/
|
||||
export type NormalisedResults<T> = { items: Reference<T>[]; nodes: CacheNode[] }
|
||||
|
||||
/**
|
||||
* Represents a single notification.
|
||||
*/
|
||||
@@ -369,6 +452,8 @@ export type ObjectValidatorArgs = { id: number; path: string }
|
||||
|
||||
export type ObjectWithFilePaths = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: FilePath[] }
|
||||
|
||||
export type ObjectWithFilePaths2 = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: Reference<FilePath>[] }
|
||||
|
||||
/**
|
||||
* Represents the operating system which the remote peer is running.
|
||||
* This is not used internally and predominantly is designed to be used for display purposes by the embedding application.
|
||||
@@ -382,9 +467,7 @@ export type Orientation = "Normal" | "CW90" | "CW180" | "CW270" | "MirroredVerti
|
||||
/**
|
||||
* TODO: P2P event for the frontend
|
||||
*/
|
||||
export type P2PEvent = { type: "DiscoveredPeer"; identity: string; metadata: PeerMetadata } | { type: "ExpiredPeer"; identity: string } | { type: "ConnectedPeer"; identity: string } | { type: "DisconnectedPeer"; identity: string } | { type: "SpacedropRequest"; id: string; identity: string; peer_name: string; files: string[] } | { type: "SpacedropProgress"; id: string; percent: number } | { type: "SpacedropTimedout"; id: string } | { type: "SpacedropRejected"; id: string } | { type: "PairingRequest"; id: number; name: string; os: OperatingSystem } | { type: "PairingProgress"; id: number; status: PairingStatus }
|
||||
|
||||
export type P2PState = { node: { [key: string]: PeerStatus }; libraries: ([string, { [key: string]: PeerStatus }])[]; self_peer_id: PeerId; self_identity: string; config: ManagerConfig; manager_connected: { [key: PeerId]: string }; manager_connections: PeerId[]; dicovery_services: { [key: string]: { [key: string]: string } | null }; discovery_discovered: { [key: string]: { [key: string]: [PeerId, { [key: string]: string }, string[]] } }; discovery_known: { [key: string]: string[] } }
|
||||
export type P2PEvent = { type: "DiscoveredPeer"; identity: RemoteIdentity; metadata: PeerMetadata } | { type: "ExpiredPeer"; identity: RemoteIdentity } | { type: "ConnectedPeer"; identity: RemoteIdentity } | { type: "DisconnectedPeer"; identity: RemoteIdentity } | { type: "SpacedropRequest"; id: string; identity: RemoteIdentity; peer_name: string; files: string[] } | { type: "SpacedropProgress"; id: string; percent: number } | { type: "SpacedropTimedout"; id: string } | { type: "SpacedropRejected"; id: string } | { type: "PairingRequest"; id: number; name: string; os: OperatingSystem } | { type: "PairingProgress"; id: number; status: PairingStatus }
|
||||
|
||||
export type P2PStatus = { ipv4: ListenerStatus; ipv6: ListenerStatus }
|
||||
|
||||
@@ -392,19 +475,27 @@ export type PairingDecision = { decision: "accept"; libraryId: string } | { deci
|
||||
|
||||
export type PairingStatus = { type: "EstablishingConnection" } | { type: "PairingRequested" } | { type: "LibraryAlreadyExists" } | { type: "PairingDecisionRequest" } | { type: "PairingInProgress"; data: { library_name: string; library_description: string | null } } | { type: "InitialSyncProgress"; data: number } | { type: "PairingComplete"; data: string } | { type: "PairingRejected" }
|
||||
|
||||
export type PeerId = string
|
||||
|
||||
export type PeerMetadata = { name: string; operating_system: OperatingSystem | null; version: string | null }
|
||||
|
||||
export type PeerStatus = "Unavailable" | "Discovered" | "Connected"
|
||||
|
||||
export type PlusCode = string
|
||||
|
||||
export type Range<T> = { from: T } | { to: T }
|
||||
|
||||
export type RelationOperation = { relation_item: any; relation_group: any; relation: string; data: RelationOperationData }
|
||||
/**
|
||||
* A reference to a `CacheNode`.
|
||||
*
|
||||
* This does not contain the actual data, but instead a reference to it.
|
||||
* This allows the CacheNode's to be switched out and the query recomputed without any backend communication.
|
||||
*
|
||||
* If you use a `Reference` in a query, you *must* ensure the corresponding `CacheNode` is also in the query.
|
||||
*/
|
||||
export type Reference<T> = { __type: string; __id: string; "#type": T }
|
||||
|
||||
export type RelationOperationData = "c" | { u: { field: string; value: any } } | "d"
|
||||
export type RelationOperation = { relation_item: JsonValue; relation_group: JsonValue; relation: string; data: RelationOperationData }
|
||||
|
||||
export type RelationOperationData = "c" | { u: { field: string; value: JsonValue } } | "d"
|
||||
|
||||
export type RemoteIdentity = string
|
||||
|
||||
export type RenameFileArgs = { location_id: number; kind: RenameKind }
|
||||
|
||||
@@ -422,11 +513,9 @@ export type Response = { Start: { user_code: string; verification_url: string; v
|
||||
|
||||
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
||||
|
||||
export type SanitisedNodeConfig = { id: string; name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[]; preferences: NodePreferences }
|
||||
|
||||
export type SavedSearch = { id: number; pub_id: number[]; search: string | null; filters: string | null; name: string | null; icon: string | null; description: string | null; date_created: string | null; date_modified: string | null }
|
||||
|
||||
export type SearchData<T> = { cursor: number[] | null; items: T[] }
|
||||
export type SearchData<T> = { cursor: number[] | null; items: Reference<T>[]; nodes: CacheNode[] }
|
||||
|
||||
export type SearchFilterArgs = { filePath: FilePathFilterArgs } | { object: ObjectFilterArgs }
|
||||
|
||||
@@ -434,15 +523,19 @@ export type SetFavoriteArgs = { id: number; favorite: boolean }
|
||||
|
||||
export type SetNoteArgs = { id: number; note: string | null }
|
||||
|
||||
export type SharedOperation = { record_id: any; model: string; data: SharedOperationData }
|
||||
export type SharedOperation = { record_id: JsonValue; model: string; data: SharedOperationData }
|
||||
|
||||
export type SharedOperationData = "c" | { u: { field: string; value: any } } | "d"
|
||||
export type SharedOperationData = "c" | { u: { field: string; value: JsonValue } } | "d"
|
||||
|
||||
export type SingleInvalidateOperationEvent = { key: string; arg: any; result: any | null }
|
||||
export type SingleInvalidateOperationEvent = {
|
||||
/**
|
||||
* This fields are intentionally private.
|
||||
*/
|
||||
key: string; arg: JsonValue; result: JsonValue | null }
|
||||
|
||||
export type SortOrder = "Asc" | "Desc"
|
||||
|
||||
export type SpacedropArgs = { identity: string; file_path: string[] }
|
||||
export type SpacedropArgs = { identity: RemoteIdentity; file_path: string[] }
|
||||
|
||||
export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string }
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { createContext, PropsWithChildren, useContext, useMemo } from 'react';
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react';
|
||||
|
||||
import { NormalisedCache, useCache, useNodes } from '../cache';
|
||||
import { LibraryConfigWrapped } from '../core';
|
||||
import { valtioPersist } from '../lib';
|
||||
import { nonLibraryClient, useBridgeQuery } from '../rspc';
|
||||
|
||||
// The name of the localStorage key for caching library data
|
||||
const libraryCacheLocalStorageKey = 'sd-library-list';
|
||||
const libraryCacheLocalStorageKey = 'sd-library-list2'; // `2` is because the format of this underwent a breaking change when introducing normalised caching
|
||||
|
||||
export const useCachedLibraries = () =>
|
||||
useBridgeQuery(['library.list'], {
|
||||
export const useCachedLibraries = () => {
|
||||
const result = useBridgeQuery(['library.list'], {
|
||||
keepPreviousData: true,
|
||||
initialData: () => {
|
||||
const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
|
||||
@@ -26,22 +27,33 @@ export const useCachedLibraries = () =>
|
||||
},
|
||||
onSuccess: (data) => localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data))
|
||||
});
|
||||
useNodes(result.data?.nodes);
|
||||
|
||||
export async function getCachedLibraries() {
|
||||
return {
|
||||
...result,
|
||||
data: useCache(result.data?.items)
|
||||
};
|
||||
};
|
||||
|
||||
export async function getCachedLibraries(cache: NormalisedCache) {
|
||||
const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
|
||||
|
||||
if (cachedData) {
|
||||
// If we fail to load cached data, it's fine
|
||||
try {
|
||||
return JSON.parse(cachedData) as LibraryConfigWrapped[];
|
||||
const data = JSON.parse(cachedData);
|
||||
cache.withNodes(data.nodes);
|
||||
return cache.withCache(data.items) as LibraryConfigWrapped[];
|
||||
} catch (e) {
|
||||
console.error("Error loading cached 'sd-library-list' data", e);
|
||||
}
|
||||
}
|
||||
|
||||
const libraries = await nonLibraryClient.query(['library.list']);
|
||||
const result = await nonLibraryClient.query(['library.list']);
|
||||
cache.withNodes(result.nodes);
|
||||
const libraries = cache.withCache(result.items);
|
||||
|
||||
localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(libraries));
|
||||
localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(result));
|
||||
|
||||
return libraries;
|
||||
}
|
||||
|
||||
@@ -27,3 +27,4 @@ export * from './core';
|
||||
export * from './utils';
|
||||
export * from './lib';
|
||||
export * from './form';
|
||||
export * from './cache';
|
||||
|
||||
@@ -100,7 +100,7 @@ export function useInvalidateQuery() {
|
||||
for (const op of ops) {
|
||||
match(op)
|
||||
.with({ type: 'single', data: P.select() }, (op) => {
|
||||
let key = [op.key];
|
||||
let key: any[] = [op.key];
|
||||
if (op.arg !== null) {
|
||||
key = key.concat(op.arg);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { ExplorerItem, FilePath, NonIndexedPathItem, Object } from '../core';
|
||||
import type { Object } from '..';
|
||||
import type { ExplorerItem, FilePath, NonIndexedPathItem } from '../core';
|
||||
import { byteSize } from '../lib';
|
||||
import { ObjectKind } from './objectKind';
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ExplorerItem } from '../core';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ExplorerItem, LibraryConfigWrapped } from '../core';
|
||||
|
||||
export * from './objectKind';
|
||||
export * from './explorerItem';
|
||||
@@ -39,3 +41,28 @@ export function formatNumber(n: number) {
|
||||
if (!n) return '0';
|
||||
return Intl.NumberFormat().format(n);
|
||||
}
|
||||
|
||||
export function insertLibrary(queryClient: QueryClient, library: LibraryConfigWrapped) {
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||
// The invalidation system beat us to it
|
||||
if (libraries.items.find((l: any) => l.__id === library.uuid)) return libraries;
|
||||
|
||||
return {
|
||||
items: [
|
||||
...(libraries.items || []),
|
||||
{
|
||||
__type: 'LibraryConfigWrapped',
|
||||
__id: library.uuid
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
...(libraries.nodes || []),
|
||||
{
|
||||
__type: 'LibraryConfigWrapped',
|
||||
__id: library.uuid,
|
||||
...library
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function getTotalTasks(jobs: JobReport[]) {
|
||||
}
|
||||
|
||||
export function getJobNiceActionName(action: string, completed: boolean, job?: JobReport) {
|
||||
const name = job?.metadata?.location?.name || 'Unknown';
|
||||
const name = (job?.metadata?.location as any)?.name || 'Unknown';
|
||||
switch (action) {
|
||||
case 'scan_location':
|
||||
return completed ? `Added location "${name}"` : `Adding location "${name}"`;
|
||||
|
||||
@@ -20,12 +20,12 @@ export function useJobInfo(job: JobReport, realtimeUpdate: JobProgressEvent | nu
|
||||
const isRunning = job.status === 'Running',
|
||||
isQueued = job.status === 'Queued',
|
||||
isPaused = job.status === 'Paused',
|
||||
indexedPath = job.metadata?.data?.location.path,
|
||||
indexedPath = (job.metadata?.data as any)?.location.path,
|
||||
taskCount = realtimeUpdate?.task_count || job.task_count,
|
||||
completedTaskCount = realtimeUpdate?.completed_task_count || job.completed_task_count,
|
||||
phase = realtimeUpdate?.phase,
|
||||
meta = job.metadata,
|
||||
output = meta?.output?.run_metadata;
|
||||
output = (meta?.output as any)?.run_metadata;
|
||||
|
||||
const data = {
|
||||
isRunning,
|
||||
|
||||
Reference in New Issue
Block a user