mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-05 13:52:56 -04:00
Merge remote-tracking branch 'origin/main' into merge-p2p-code
This commit is contained in:
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -155,8 +155,11 @@ jobs:
|
||||
- name: Generate Prisma client
|
||||
uses: ./.github/actions/generate-prisma-client
|
||||
|
||||
- name: Build everything
|
||||
run: cargo build --release
|
||||
- name: Cargo fetch
|
||||
run: cargo fetch
|
||||
|
||||
- name: Check core
|
||||
run: cargo check -p sd-core --release
|
||||
|
||||
- name: Bundle Desktop
|
||||
run: pnpm desktop tauri build
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -65,4 +65,4 @@ examples/*/*.lock
|
||||
|
||||
/sdserver_data
|
||||
.spacedrive
|
||||
dev.db-journal
|
||||
dev.db-journal
|
||||
|
||||
20
.prettierrc.json
Normal file
20
.prettierrc.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"pluginSearchDirs": [
|
||||
"."
|
||||
],
|
||||
"useTabs": true,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSameLine": false,
|
||||
"semi": true,
|
||||
"quoteProps": "consistent",
|
||||
"importOrder": [
|
||||
"^[./]",
|
||||
"^@sd/ui/(.*)$",
|
||||
"^@sd/client/(.*)$",
|
||||
"^@sd/interface/(.*)$"
|
||||
],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"tauri-apps.tauri-vscode",
|
||||
"rust-lang.rust-analyzer",
|
||||
"oscartbeaumont.rspc-vscode"
|
||||
]
|
||||
}
|
||||
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
@@ -12,19 +12,22 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "762ba6c06b9386102aea37538fdfe0b2aa24fd52", features = [
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "078919e3724f35cc00c2402c66ba585b61c88d47", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "762ba6c06b9386102aea37538fdfe0b2aa24fd52", features = [
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "078919e3724f35cc00c2402c66ba585b61c88d47", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
rspc = { version = "0.1.2" }
|
||||
|
||||
[patch.crates-io]
|
||||
# We use this patch so we can compile for the IOS simulator on M1
|
||||
openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl" }
|
||||
openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl", rev = "92c3dec225a9e984884d5b30a517e5d44a24d03b" }
|
||||
|
||||
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "08c19f5e485dfc6841c7e9877c80058402bf71d7" } # TODO: Move back to crates.io when new jsonrpc executor is release
|
||||
@@ -11,32 +11,32 @@
|
||||
"build": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rspc/client": "^0.0.6",
|
||||
"@rspc/tauri": "^0.0.6",
|
||||
"@rspc/client": "^0.1.2",
|
||||
"@rspc/tauri": "^0.1.2",
|
||||
"@sd/client": "workspace:*",
|
||||
"@sd/interface": "workspace:*",
|
||||
"@sd/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^4.0.10",
|
||||
"@tauri-apps/api": "1.0.2",
|
||||
"@tanstack/react-query": "^4.10.1",
|
||||
"@tauri-apps/api": "1.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "1.0.5",
|
||||
"@tauri-apps/cli": "1.1.1",
|
||||
"@tauri-apps/tauricon": "github:tauri-apps/tauricon",
|
||||
"@types/babel-core": "^6.25.7",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/tailwindcss": "^3.1.0",
|
||||
"@vitejs/plugin-react": "^2.0.0",
|
||||
"concurrently": "^7.3.0",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"concurrently": "^7.4.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.54.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.3",
|
||||
"sass": "^1.55.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.4",
|
||||
"vite-plugin-filter-replace": "^0.1.9",
|
||||
"vite-plugin-svgr": "^2.2.1"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
RUST_LOG=spacedrive=debug,sdcore=debug
|
||||
@@ -10,19 +10,19 @@ edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.0.4", features = ["api-all", "macos-private-api"] }
|
||||
rspc = { version = "0.0.5", features = ["tauri"] }
|
||||
tauri = { version = "1.1.1", features = ["api-all", "macos-private-api"] }
|
||||
rspc = { workspace = true, features = ["tauri"] }
|
||||
sd-core = { path = "../../../core", features = ["ffmpeg"] }
|
||||
tokio = { version = "1.17.0", features = ["sync"] }
|
||||
window-shadows = "0.1.2"
|
||||
tracing = "0.1.35"
|
||||
serde = "1.0.144"
|
||||
tokio = { version = "1.21.2", features = ["sync"] }
|
||||
window-shadows = "0.2.0"
|
||||
tracing = "0.1.36"
|
||||
serde = "1.0.145"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease" }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0", features = [] }
|
||||
tauri-build = { version = "1.1.1", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease", features = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createClient } from '@rspc/client';
|
||||
import { TauriTransport } from '@rspc/tauri';
|
||||
import { OperatingSystem, Operations, PlatformProvider, queryClient, rspc } from '@sd/client';
|
||||
import { OperatingSystem, PlatformProvider, Procedures, queryClient, rspc } from '@sd/client';
|
||||
import SpacedriveInterface, { Platform } from '@sd/interface';
|
||||
import { KeybindEvent } from '@sd/interface';
|
||||
import { dialog, invoke, os, shell } from '@tauri-apps/api';
|
||||
@@ -10,7 +10,7 @@ import { createRoot } from 'react-dom/client';
|
||||
|
||||
import '@sd/ui/style';
|
||||
|
||||
const client = createClient<Operations>({
|
||||
const client = createClient<Procedures>({
|
||||
transport: new TauriTransport()
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
|
||||
|
||||
cargo {
|
||||
module = "../../rust"
|
||||
libname = "sdcore"
|
||||
libname = "sd_core_mobile"
|
||||
// profile = 'release',
|
||||
pythonCommand = 'python3'
|
||||
targets = ["arm", "arm64", "x86", "x86_64"]
|
||||
|
||||
@@ -28,7 +28,7 @@ public class SDCore extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
static {
|
||||
System.loadLibrary("sdcore");
|
||||
System.loadLibrary("sd_core_mobile");
|
||||
}
|
||||
|
||||
// is exposed by Rust and is used to register the subscription
|
||||
@@ -47,6 +47,11 @@ public class SDCore extends ReactContextBaseJavaModule {
|
||||
return getCurrentActivity().getFilesDir().toString();
|
||||
}
|
||||
|
||||
public void print(String msg)
|
||||
{
|
||||
System.out.println(msg);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addListener(String eventName)
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- EXFileSystem (14.1.0):
|
||||
- ExpoModulesCore
|
||||
- EXFont (10.2.0):
|
||||
- EXFont (10.2.1):
|
||||
- ExpoModulesCore
|
||||
- Expo (46.0.10):
|
||||
- Expo (46.0.15):
|
||||
- ExpoModulesCore
|
||||
- ExpoKeepAwake (10.2.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoModulesCore (0.11.5):
|
||||
- ExpoModulesCore (0.11.7):
|
||||
- React-Core
|
||||
- ReactCommon/turbomodule/core
|
||||
- EXSplashScreen (0.16.2):
|
||||
@@ -31,7 +31,7 @@ PODS:
|
||||
- glog (0.3.5)
|
||||
- hermes-engine (0.69.4)
|
||||
- libevent (2.1.12)
|
||||
- lottie-ios (3.4.2)
|
||||
- lottie-ios (3.4.3)
|
||||
- lottie-react-native (5.1.4):
|
||||
- lottie-ios (~> 3.4.0)
|
||||
- React-Core
|
||||
@@ -333,7 +333,7 @@ PODS:
|
||||
- React-jsi (= 0.69.4)
|
||||
- React-logger (= 0.69.4)
|
||||
- React-perflogger (= 0.69.4)
|
||||
- RNCAsyncStorage (1.17.7):
|
||||
- RNCAsyncStorage (1.17.10):
|
||||
- React-Core
|
||||
- RNCMaskedView (0.2.7):
|
||||
- React-Core
|
||||
@@ -376,14 +376,14 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- "EXApplication (from `../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.10/node_modules/expo-application/ios`)"
|
||||
- "EXConstants (from `../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.10/node_modules/expo-constants/ios`)"
|
||||
- "EXFileSystem (from `../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.10/node_modules/expo-file-system/ios`)"
|
||||
- "EXFont (from `../node_modules/.pnpm/expo-font@10.2.0_expo@46.0.10/node_modules/expo-font/ios`)"
|
||||
- "Expo (from `../node_modules/.pnpm/expo@46.0.10_@babel+core@7.18.10/node_modules/expo`)"
|
||||
- "ExpoKeepAwake (from `../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.10/node_modules/expo-keep-awake/ios`)"
|
||||
- "ExpoModulesCore (from `../node_modules/.pnpm/expo-modules-core@0.11.5/node_modules/expo-modules-core/ios`)"
|
||||
- "EXSplashScreen (from `../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.10/node_modules/expo-splash-screen/ios`)"
|
||||
- "EXApplication (from `../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.15/node_modules/expo-application/ios`)"
|
||||
- "EXConstants (from `../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.15/node_modules/expo-constants/ios`)"
|
||||
- "EXFileSystem (from `../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.15/node_modules/expo-file-system/ios`)"
|
||||
- "EXFont (from `../node_modules/.pnpm/expo-font@10.2.1_expo@46.0.15/node_modules/expo-font/ios`)"
|
||||
- "Expo (from `../node_modules/.pnpm/expo@46.0.15_@babel+core@7.19.3/node_modules/expo`)"
|
||||
- "ExpoKeepAwake (from `../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.15/node_modules/expo-keep-awake/ios`)"
|
||||
- "ExpoModulesCore (from `../node_modules/.pnpm/expo-modules-core@0.11.7/node_modules/expo-modules-core/ios`)"
|
||||
- "EXSplashScreen (from `../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.15/node_modules/expo-splash-screen/ios`)"
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
@@ -439,21 +439,21 @@ EXTERNAL SOURCES:
|
||||
DoubleConversion:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
EXApplication:
|
||||
:path: "../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.10/node_modules/expo-application/ios"
|
||||
:path: "../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.15/node_modules/expo-application/ios"
|
||||
EXConstants:
|
||||
:path: "../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.10/node_modules/expo-constants/ios"
|
||||
:path: "../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.15/node_modules/expo-constants/ios"
|
||||
EXFileSystem:
|
||||
:path: "../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.10/node_modules/expo-file-system/ios"
|
||||
:path: "../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.15/node_modules/expo-file-system/ios"
|
||||
EXFont:
|
||||
:path: "../node_modules/.pnpm/expo-font@10.2.0_expo@46.0.10/node_modules/expo-font/ios"
|
||||
:path: "../node_modules/.pnpm/expo-font@10.2.1_expo@46.0.15/node_modules/expo-font/ios"
|
||||
Expo:
|
||||
:path: "../node_modules/.pnpm/expo@46.0.10_@babel+core@7.18.10/node_modules/expo"
|
||||
:path: "../node_modules/.pnpm/expo@46.0.15_@babel+core@7.19.3/node_modules/expo"
|
||||
ExpoKeepAwake:
|
||||
:path: "../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.10/node_modules/expo-keep-awake/ios"
|
||||
:path: "../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.15/node_modules/expo-keep-awake/ios"
|
||||
ExpoModulesCore:
|
||||
:path: "../node_modules/.pnpm/expo-modules-core@0.11.5/node_modules/expo-modules-core/ios"
|
||||
:path: "../node_modules/.pnpm/expo-modules-core@0.11.7/node_modules/expo-modules-core/ios"
|
||||
EXSplashScreen:
|
||||
:path: "../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.10/node_modules/expo-splash-screen/ios"
|
||||
:path: "../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.15/node_modules/expo-splash-screen/ios"
|
||||
FBLazyVector:
|
||||
:path: "../node_modules/react-native/Libraries/FBLazyVector"
|
||||
FBReactNativeSpec:
|
||||
@@ -541,10 +541,10 @@ SPEC CHECKSUMS:
|
||||
EXApplication: e418d737a036e788510f2c4ad6c10a7d54d18586
|
||||
EXConstants: 7c44785d41d8e959d527d23d29444277a4d1ee73
|
||||
EXFileSystem: 927e0a8885aa9c49e50fc38eaba2c2389f2f1019
|
||||
EXFont: a5d80bd9b3452b2d5abbce2487da89b0150e6487
|
||||
Expo: fcdb32274e2ca9c7638d3b21b30fb665c6869219
|
||||
EXFont: 06df627203afcb8a3b3152ec06eb2f11f46f0cff
|
||||
Expo: 7e821e708a35d2720ef6baa658e224dd91c4821e
|
||||
ExpoKeepAwake: 0e8f18142e71bbf2c7f6aa66ebed249ba1420320
|
||||
ExpoModulesCore: 5a973701f4400d70254bc836305228731c829010
|
||||
ExpoModulesCore: 2d60ec04c49641afb55fee3faac86fb108c68fe0
|
||||
EXSplashScreen: 799bece80089219b2c989c1082d70f3b00995cda
|
||||
FBLazyVector: c71b8c429a8af2aff1013934a7152e9d9d0c937d
|
||||
FBReactNativeSpec: 3cc5cff7d792e74a875be91e56d6242335016f50
|
||||
@@ -552,7 +552,7 @@ SPEC CHECKSUMS:
|
||||
glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a
|
||||
hermes-engine: 761a544537e62df2a37189389b9d2654dc1f75af
|
||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||
lottie-ios: 6bbc53eef6957e4744a50321507015fba72d8ca6
|
||||
lottie-ios: 9ae750cdc7820fecbd3c2f0cfc493038208fcdc4
|
||||
lottie-react-native: b702fab740cdb952a8e2354713d3beda63ff97b0
|
||||
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
|
||||
RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4
|
||||
@@ -582,7 +582,7 @@ SPEC CHECKSUMS:
|
||||
React-RCTVibration: 9adb4a3cbb598d1bbd46a05256f445e4b8c70603
|
||||
React-runtimeexecutor: 61ee22a8cdf8b6bb2a7fb7b4ba2cc763e5285196
|
||||
ReactCommon: 8f67bd7e0a6afade0f20718f859dc8c2275f2e83
|
||||
RNCAsyncStorage: d81ee5c3db1060afd49ea7045ad460eff82d2b7d
|
||||
RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
|
||||
RNCMaskedView: cb9670ea9239998340eaab21df13fa12a1f9de15
|
||||
RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50
|
||||
RNReanimated: 7faa787e8d4493fbc95fab2ad331fa7625828cfa
|
||||
|
||||
@@ -366,6 +366,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
"\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"",
|
||||
"\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"",
|
||||
@@ -455,6 +456,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
"\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"",
|
||||
"\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"",
|
||||
@@ -579,7 +581,7 @@
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "\"$(inherited)\"";
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
@@ -633,7 +635,7 @@
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "\"$(inherited)\"";
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"@react-navigation/drawer": "^6.4.4",
|
||||
"@react-navigation/native": "^6.0.12",
|
||||
"@react-navigation/stack": "^6.2.3",
|
||||
"@rspc/client": "^0.0.6",
|
||||
"@rspc/react": "^0.0.6",
|
||||
"@rspc/client": "^0.1.2",
|
||||
"@rspc/react": "^0.1.2",
|
||||
"@sd/assets": "file:../../packages/assets",
|
||||
"@tanstack/react-query": "^4.2.3",
|
||||
"byte-size": "^8.1.0",
|
||||
|
||||
BIN
apps/mobile/pnpm-lock.yaml
generated
BIN
apps/mobile/pnpm-lock.yaml
generated
Binary file not shown.
@@ -8,18 +8,18 @@ rust-version = "1.64.0"
|
||||
crate-type = ["staticlib", "cdylib"] # staticlib for IOS and cdylib for Android
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.13.0"
|
||||
once_cell = "1.15.0"
|
||||
sd-core = { path = "../../../core", features = [
|
||||
"mobile",
|
||||
"p2p",
|
||||
], default-features = false }
|
||||
rspc = { version = "0.0.5", features = [] }
|
||||
serde_json = "1.0.83"
|
||||
tokio = "1.20.1"
|
||||
openssl = { version = "0.10.41", features = [
|
||||
rspc = { workspace = true }
|
||||
serde_json = "1.0.85"
|
||||
tokio = "1.21.2"
|
||||
openssl = { version = "0.10.42", features = [
|
||||
"vendored",
|
||||
] } # Override features of transitive dependencies
|
||||
openssl-sys = { version = "0.9.75", features = [
|
||||
openssl-sys = { version = "0.9.76", features = [
|
||||
"vendored",
|
||||
] } # Override features of transitive dependencies to support IOS Simulator on M1
|
||||
|
||||
|
||||
@@ -1,43 +1,72 @@
|
||||
use crate::{CLIENT_CONTEXT, EVENT_SENDER, NODE, RUNTIME};
|
||||
use jni::objects::{JClass, JObject, JString};
|
||||
use jni::JNIEnv;
|
||||
use rspc::Request;
|
||||
use std::panic;
|
||||
|
||||
use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS};
|
||||
use jni::objects::{GlobalRef, JClass, JObject, JString};
|
||||
use jni::{JNIEnv, JavaVM};
|
||||
use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap};
|
||||
use sd_core::Node;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
// fn print(jvm: &JavaVM, class: &GlobalRef, msg: &str) {
|
||||
// let env = jvm.attach_current_thread().unwrap();
|
||||
// env.call_method(
|
||||
// class,
|
||||
// "print",
|
||||
// "(Ljava/lang/String;)V",
|
||||
// &[env
|
||||
// .new_string(msg)
|
||||
// .expect("Couldn't create java string!")
|
||||
// .into()],
|
||||
// )
|
||||
// .unwrap()
|
||||
// .l()
|
||||
// .unwrap();
|
||||
// }
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
) {
|
||||
let jvm = env.get_java_vm().unwrap();
|
||||
let class = env.new_global_ref(class).unwrap();
|
||||
let (tx, mut rx) = unbounded_channel();
|
||||
let _ = EVENT_SENDER.set(tx);
|
||||
let result = panic::catch_unwind(|| {
|
||||
let jvm = env.get_java_vm().unwrap();
|
||||
let class = env.new_global_ref(class).unwrap();
|
||||
let (tx, mut rx) = unbounded_channel();
|
||||
let _ = EVENT_SENDER.set(tx);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let data = match serde_json::to_string(&event) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
println!("Failed to serialize event: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let data = match serde_json::to_string(&event) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
println!("Failed to serialize event: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let env = jvm.attach_current_thread().unwrap();
|
||||
env.call_method(
|
||||
&class,
|
||||
"sendCoreEvent",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[env
|
||||
.new_string(data)
|
||||
.expect("Couldn't create java string!")
|
||||
.into()],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let env = jvm.attach_current_thread().unwrap();
|
||||
env.call_method(
|
||||
&class,
|
||||
"sendCoreEvent",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[env
|
||||
.new_string(data)
|
||||
.expect("Couldn't create java string!")
|
||||
.into()],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
// TODO: Send rspc error or something here so we can show this in the UI.
|
||||
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
|
||||
println!(
|
||||
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -47,60 +76,75 @@ pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg(
|
||||
query: JString,
|
||||
callback: JObject,
|
||||
) {
|
||||
let jvm = env.get_java_vm().unwrap();
|
||||
let query: String = env
|
||||
.get_string(query)
|
||||
.expect("Couldn't get java string!")
|
||||
.into();
|
||||
let class = env.new_global_ref(class).unwrap();
|
||||
let callback = env.new_global_ref(callback).unwrap();
|
||||
let result = panic::catch_unwind(|| {
|
||||
let jvm = env.get_java_vm().unwrap();
|
||||
let query: String = env
|
||||
.get_string(query)
|
||||
.expect("Couldn't get java string!")
|
||||
.into();
|
||||
let class = env.new_global_ref(class).unwrap();
|
||||
let callback = env.new_global_ref(callback).unwrap();
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
let request: Request = serde_json::from_str(&query).unwrap();
|
||||
RUNTIME.spawn(async move {
|
||||
let request: Request = serde_json::from_str(&query).unwrap();
|
||||
|
||||
let node = &mut *NODE.lock().await;
|
||||
let (node, router) = match node {
|
||||
Some(node) => node.clone(),
|
||||
None => {
|
||||
let data_dir: String = {
|
||||
let node = &mut *NODE.lock().await;
|
||||
let (node, router) = match node {
|
||||
Some(node) => node.clone(),
|
||||
None => {
|
||||
let data_dir: String = {
|
||||
let env = jvm.attach_current_thread().unwrap();
|
||||
let data_dir = env
|
||||
.call_method(&class, "getDataDirectory", "()Ljava/lang/String;", &[])
|
||||
.unwrap()
|
||||
.l()
|
||||
.unwrap();
|
||||
|
||||
env.get_string(data_dir.into()).unwrap().into()
|
||||
};
|
||||
|
||||
let new_node = Node::new(data_dir).await.unwrap();
|
||||
node.replace(new_node.clone());
|
||||
new_node
|
||||
}
|
||||
};
|
||||
|
||||
let mut channel = EVENT_SENDER.get().unwrap().clone();
|
||||
let mut resp = Sender::ResponseAndChannel(None, &mut channel);
|
||||
handle_json_rpc(
|
||||
node.get_request_context(),
|
||||
request,
|
||||
&router,
|
||||
&mut resp,
|
||||
&mut SubscriptionMap::Mutex(&SUBSCRIPTIONS),
|
||||
)
|
||||
.await;
|
||||
|
||||
match resp {
|
||||
Sender::Response(Some(resp)) => {
|
||||
let env = jvm.attach_current_thread().unwrap();
|
||||
let data_dir = env
|
||||
.call_method(&class, "getDataDirectory", "()Ljava/lang/String;", &[])
|
||||
.unwrap()
|
||||
.l()
|
||||
.unwrap();
|
||||
|
||||
env.get_string(data_dir.into()).unwrap().into()
|
||||
};
|
||||
|
||||
let new_node = Node::new(data_dir).await.expect("Unable to create node");
|
||||
node.replace(new_node.clone());
|
||||
new_node
|
||||
env.call_method(
|
||||
&callback,
|
||||
"resolve",
|
||||
"(Ljava/lang/Object;)V",
|
||||
&[env
|
||||
.new_string(serde_json::to_string(&resp).unwrap())
|
||||
.expect("Couldn't create java string!")
|
||||
.into()],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
|
||||
let resp = serde_json::to_string(
|
||||
&request
|
||||
.handle(
|
||||
node.get_request_context(),
|
||||
&router,
|
||||
&CLIENT_CONTEXT,
|
||||
EVENT_SENDER.get(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let env = jvm.attach_current_thread().unwrap();
|
||||
env.call_method(
|
||||
&callback,
|
||||
"resolve",
|
||||
"(Ljava/lang/Object;)V",
|
||||
&[env
|
||||
.new_string(resp)
|
||||
.expect("Couldn't create java string!")
|
||||
.into()],
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
// TODO: Send rspc error or something here so we can show this in the UI.
|
||||
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
|
||||
println!(
|
||||
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use crate::{CLIENT_CONTEXT, EVENT_SENDER, NODE, RUNTIME};
|
||||
use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS};
|
||||
use objc::{msg_send, runtime::Object, sel, sel_impl};
|
||||
use objc_foundation::{INSString, NSString};
|
||||
use objc_id::Id;
|
||||
use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap};
|
||||
use sd_core::Node;
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
os::raw::{c_char, c_void},
|
||||
panic,
|
||||
};
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
use objc::{class, msg_send, runtime::Object, sel, sel_impl};
|
||||
use objc_foundation::{INSString, NSString};
|
||||
use objc_id::Id;
|
||||
use rspc::Request;
|
||||
use sd_core::Node;
|
||||
|
||||
extern "C" {
|
||||
fn get_data_directory() -> *const c_char;
|
||||
fn call_resolve(resolve: *const c_void, result: *const c_char);
|
||||
@@ -33,64 +33,81 @@ impl RNPromise {
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) {
|
||||
let id = Id::<Object>::from_ptr(id);
|
||||
let result = panic::catch_unwind(|| {
|
||||
let id = Id::<Object>::from_ptr(id);
|
||||
|
||||
let (tx, mut rx) = unbounded_channel();
|
||||
let _ = EVENT_SENDER.set(tx);
|
||||
let (tx, mut rx) = unbounded_channel();
|
||||
let _ = EVENT_SENDER.set(tx);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let data = match serde_json::to_string(&event) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
println!("Failed to serialize event: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let data = NSString::from_str(&data);
|
||||
let _: () = msg_send![id, sendCoreEvent: data];
|
||||
}
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let data = match serde_json::to_string(&event) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
println!("Failed to serialize event: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let data = NSString::from_str(&data);
|
||||
let _: () = msg_send![id, sendCoreEvent: data];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
// TODO: Send rspc error or something here so we can show this in the UI.
|
||||
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
|
||||
println!("Error in register_core_event_listener: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sd_core_msg(query: *const c_char, resolve: *const c_void) {
|
||||
// This string is cloned to the Rust heap. This is important as Objective-C may remove the query once this function completions but prior to the async block finishing.
|
||||
let query = CStr::from_ptr(query).to_str().unwrap().to_string();
|
||||
let result = panic::catch_unwind(|| {
|
||||
// This string is cloned to the Rust heap. This is important as Objective-C may remove the query once this function completions but prior to the async block finishing.
|
||||
let query = CStr::from_ptr(query).to_str().unwrap().to_string();
|
||||
|
||||
let resolve = RNPromise(resolve);
|
||||
RUNTIME.spawn(async move {
|
||||
let request: Request = serde_json::from_str(&query).unwrap();
|
||||
let resolve = RNPromise(resolve);
|
||||
RUNTIME.spawn(async move {
|
||||
let request: Request = serde_json::from_str(&query).unwrap();
|
||||
|
||||
let node = &mut *NODE.lock().await;
|
||||
let (node, router) = match node {
|
||||
Some(node) => node.clone(),
|
||||
None => {
|
||||
let doc_dir = CStr::from_ptr(get_data_directory())
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let new_node = Node::new(doc_dir).await.unwrap();
|
||||
node.replace(new_node.clone());
|
||||
new_node
|
||||
}
|
||||
};
|
||||
let node = &mut *NODE.lock().await;
|
||||
let (node, router) = match node {
|
||||
Some(node) => node.clone(),
|
||||
None => {
|
||||
let doc_dir = CStr::from_ptr(get_data_directory())
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let new_node = Node::new(doc_dir).await.unwrap();
|
||||
node.replace(new_node.clone());
|
||||
new_node
|
||||
}
|
||||
};
|
||||
|
||||
resolve.resolve(
|
||||
CString::new(
|
||||
serde_json::to_vec(
|
||||
&request
|
||||
.handle(
|
||||
node.get_request_context(),
|
||||
&router,
|
||||
&CLIENT_CONTEXT,
|
||||
EVENT_SENDER.get(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.unwrap(),
|
||||
let mut channel = EVENT_SENDER.get().unwrap().clone();
|
||||
let mut resp = Sender::ResponseAndChannel(None, &mut channel);
|
||||
handle_json_rpc(
|
||||
node.get_request_context(),
|
||||
request,
|
||||
&router,
|
||||
&mut resp,
|
||||
&mut SubscriptionMap::Mutex(&SUBSCRIPTIONS),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match resp {
|
||||
Sender::ResponseAndChannel(Some(resp), _) => {
|
||||
resolve.resolve(CString::new(serde_json::to_vec(&resp).unwrap()).unwrap());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
// TODO: Send rspc error or something here so we can show this in the UI.
|
||||
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
|
||||
println!("Error in sd_core_msg: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use rspc::{ClientContext, Response};
|
||||
use rspc::internal::jsonrpc::{RequestId, Response};
|
||||
use sd_core::{api::Router, Node};
|
||||
use tokio::{
|
||||
runtime::Runtime,
|
||||
sync::{mpsc::UnboundedSender, Mutex},
|
||||
sync::{mpsc::UnboundedSender, oneshot, Mutex},
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
|
||||
|
||||
type LazyNode = Lazy<Mutex<Option<(Arc<Node>, Arc<Router>)>>>;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) static NODE: LazyNode = Lazy::new(|| Mutex::new(None));
|
||||
type NodeType = Lazy<Mutex<Option<(Arc<Node>, Arc<Router>)>>>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) static CLIENT_CONTEXT: Lazy<ClientContext> = Lazy::new(|| ClientContext {
|
||||
subscriptions: Default::default(),
|
||||
});
|
||||
pub(crate) static NODE: NodeType = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) static SUBSCRIPTIONS: Lazy<Mutex<HashMap<RequestId, oneshot::Sender<()>>>> =
|
||||
Lazy::new(Default::default);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) static EVENT_SENDER: OnceCell<UnboundedSender<Response>> = OnceCell::new();
|
||||
|
||||
@@ -22,9 +22,9 @@ import RootNavigator from './navigation';
|
||||
import OnboardingNavigator from './navigation/OnboardingNavigator';
|
||||
import { libraryStore } from './stores/libraryStore';
|
||||
import { onboardingStore } from './stores/onboardingStore';
|
||||
import type { Operations } from './types/bindings';
|
||||
import type { Procedures } from './types/bindings';
|
||||
|
||||
const client = createClient<Operations>({
|
||||
const client = createClient<Procedures>({
|
||||
transport: new ReactNativeTransport()
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||||
import { format } from 'date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useRef } from 'react';
|
||||
import { Button, Pressable, Text, View } from 'react-native';
|
||||
import { ChevronLeftIcon } from 'react-native-heroicons/outline';
|
||||
@@ -98,12 +98,12 @@ export const FileModal = () => {
|
||||
<Divider style={tw`my-4`} />
|
||||
<MetaItem
|
||||
title="Date Created"
|
||||
value={format(new Date(data.date_created), 'MMMM Do yyyy, h:mm:ss aaa')}
|
||||
value={dayjs(data.date_created).format('MMMM Do yyyy, h:mm:ss aaa')}
|
||||
/>
|
||||
<Divider style={tw`my-4`} />
|
||||
<MetaItem
|
||||
title="Date Indexed"
|
||||
value={format(new Date(data.date_indexed), 'MMMM Do yyyy, h:mm:ss aaa')}
|
||||
value={dayjs(data.date_indexed).format('MMMM Do yyyy, h:mm:ss aaa')}
|
||||
/>
|
||||
</>
|
||||
</BottomSheetScrollView>
|
||||
|
||||
@@ -1,144 +1,109 @@
|
||||
import { ClientTransformer, OperationKey, OperationType, RSPCError, Transport } from '@rspc/client';
|
||||
import { OperationType, ProcedureDef, RSPCError, Transport } from '@rspc/client';
|
||||
import { createReactQueryHooks } from '@rspc/react';
|
||||
import {
|
||||
QueryClient,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
useMutation as _useMutation
|
||||
} from '@tanstack/react-query';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { libraryStore } from '../stores/libraryStore';
|
||||
import type { LibraryArgs, Operations } from '../types/bindings';
|
||||
import { getLibraryIdRaw } from '../stores/libraryStore';
|
||||
import { LibraryArgs, Procedures } from '../types/bindings';
|
||||
|
||||
export const queryClient = new QueryClient();
|
||||
export const rspc = createReactQueryHooks<Operations>();
|
||||
export const rspc = createReactQueryHooks<Procedures>();
|
||||
|
||||
const { SDCore } = NativeModules;
|
||||
const eventEmitter = new NativeEventEmitter(NativeModules.SDCore);
|
||||
|
||||
// TODO(@Oscar): Replace this with a better abstraction when it's released in rspc. This relies on internal details of rspc which will change without warning.
|
||||
export class ReactNativeTransport implements Transport {
|
||||
transformer?: ClientTransformer;
|
||||
clientSubscriptionCallback?: (id: string, key: string, value: any) => void;
|
||||
clientSubscriptionCallback?: (id: string, value: any) => void;
|
||||
|
||||
constructor() {
|
||||
const subscriptionEventListener = eventEmitter.addListener('SDCoreEvent', (event) => {
|
||||
const body = JSON.parse(event);
|
||||
if (body.type === 'event') {
|
||||
const { id, key, result } = body;
|
||||
this.clientSubscriptionCallback(id, key, result);
|
||||
} else if (body.type === 'response' || body.type === 'error') {
|
||||
const { id, result } = JSON.parse(event);
|
||||
if (result.type === 'event') {
|
||||
if (this.clientSubscriptionCallback) this.clientSubscriptionCallback(id, result.data);
|
||||
} else if (result.type === 'response' || result.type === 'error') {
|
||||
throw new Error(
|
||||
`Recieved event of type '${body.type}'. This should be impossible with the React Native transport!`
|
||||
`Recieved event of type '${result.type}'. This should be impossible with the React Native transport!`
|
||||
);
|
||||
} else {
|
||||
console.error(`Received event of unknown method '${body.type}'`);
|
||||
console.error(`Received event of unknown method '${result.type}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async doRequest(operation: OperationType, key: OperationKey): Promise<any> {
|
||||
const body = JSON.parse(
|
||||
async doRequest(operation: OperationType, key: string, input: any): Promise<any> {
|
||||
const resp = JSON.parse(
|
||||
await SDCore.sd_core_msg(
|
||||
JSON.stringify({
|
||||
operation,
|
||||
key: this.transformer?.serialize(operation, key) || key
|
||||
id: null,
|
||||
method: operation,
|
||||
params: {
|
||||
path: key,
|
||||
input
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const body = resp.result;
|
||||
if (body.type === 'error') {
|
||||
const { status_code, message } = body;
|
||||
throw new RSPCError(status_code, message);
|
||||
const { code, message } = body;
|
||||
throw new RSPCError(code, message);
|
||||
} else if (body.type === 'response') {
|
||||
return this.transformer?.deserialize(operation, key, body.result) || body.result;
|
||||
return body.data;
|
||||
} else if (body.type !== 'none') {
|
||||
throw new Error(`RSPC ReactNative doRequest received invalid body type '${body?.type}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type NonLibraryQueries = Exclude<Operations['queries'], { key: [any, LibraryArgs<any>] }> &
|
||||
Extract<Operations['queries'], { key: [any] }>;
|
||||
type NonLibraryQuery<K extends string> = Extract<NonLibraryQueries, { key: [K] | [K, any] }>;
|
||||
type NonLibraryQueryKey = NonLibraryQueries['key'][0];
|
||||
type NonLibraryQueryResult<K extends NonLibraryQueryKey> = NonLibraryQuery<K>['result'];
|
||||
type NonLibraryProcedure<T extends keyof Procedures> =
|
||||
| Exclude<Procedures[T], { input: LibraryArgs<any> }>
|
||||
| Extract<Procedures[T], { input: never }>;
|
||||
|
||||
export function useBridgeQuery<K extends NonLibraryQueries['key']>(
|
||||
key: K,
|
||||
options?: UseQueryOptions<NonLibraryQueryResult<K[0]>, RSPCError>
|
||||
): UseQueryResult<NonLibraryQueryResult<K[0]>, RSPCError> {
|
||||
// @ts-ignore
|
||||
return rspc.useQuery(key, options);
|
||||
}
|
||||
|
||||
type LibraryQueries = Extract<Operations['queries'], { key: [string, LibraryArgs<any>] }>;
|
||||
type LibraryQuery<K extends string> = Extract<LibraryQueries, { key: [K, any] }>;
|
||||
type LibraryQueryKey = LibraryQueries['key'][0];
|
||||
type LibraryQueryArgs<K extends string> = LibraryQuery<K>['key'][1] extends LibraryArgs<infer A>
|
||||
? A
|
||||
: never;
|
||||
type LibraryQueryResult<K extends string> = LibraryQuery<K>['result'];
|
||||
|
||||
export function useLibraryQuery<K extends LibraryQueryKey>(
|
||||
key: LibraryQueryArgs<K> extends null | undefined ? [K] : [K, LibraryQueryArgs<K>],
|
||||
options?: UseQueryOptions<LibraryQueryResult<K>, RSPCError>
|
||||
): UseQueryResult<LibraryQueryResult<K>, RSPCError> {
|
||||
const store = useSnapshot(libraryStore);
|
||||
const library_id = store.currentLibraryUuid;
|
||||
if (!library_id) throw new Error(`Attempted to do library query with no library set!`);
|
||||
// @ts-ignore
|
||||
return rspc.useQuery([key[0], { library_id: library_id || '', arg: key[1] || null }], options);
|
||||
}
|
||||
|
||||
type LibraryMutations = Extract<Operations['mutations'], { key: [string, LibraryArgs<any>] }>;
|
||||
type LibraryMutation<K extends LibraryMutationKey> = Extract<LibraryMutations, { key: [K, any] }>;
|
||||
type LibraryMutationKey = LibraryMutations['key'][0];
|
||||
type LibraryMutationArgs<K extends LibraryMutationKey> =
|
||||
LibraryMutation<K>['key'][1] extends LibraryArgs<infer A> ? A : never;
|
||||
type LibraryMutationResult<K extends LibraryMutationKey> = LibraryMutation<K>['result'];
|
||||
export function useLibraryMutation<K extends LibraryMutationKey>(
|
||||
key: K,
|
||||
options?: UseMutationOptions<LibraryMutationResult<K>, RSPCError>
|
||||
) {
|
||||
const ctx = rspc.useContext();
|
||||
const store = useSnapshot(libraryStore);
|
||||
const library_id = store.currentLibraryUuid;
|
||||
if (!library_id) throw new Error(`Attempted to do library query with no library set!`);
|
||||
|
||||
// @ts-ignore
|
||||
return _useMutation<LibraryMutationResult<K>, RSPCError, LibraryMutationArgs<K>>(
|
||||
async (data) => ctx.client.mutation([key, { library_id: library_id || '', arg: data || null }]),
|
||||
{
|
||||
...options,
|
||||
context: rspc.ReactQueryContext
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
type NonLibraryMutations = Exclude<Operations['mutations'], { key: [any, LibraryArgs<any>] }>;
|
||||
type NonLibraryMutation<K extends NonLibraryMutationKey> = Extract<
|
||||
NonLibraryMutations,
|
||||
{ key: [K] | [K, any] }
|
||||
type LibraryProcedures<T extends keyof Procedures> = Exclude<
|
||||
Extract<Procedures[T], { input: LibraryArgs<any> }>,
|
||||
{ input: never }
|
||||
>;
|
||||
type NonLibraryMutationKey = NonLibraryMutations['key'][0];
|
||||
type NonLibraryMutationArgs<K extends NonLibraryMutationKey> = NonLibraryMutation<K>['key'][1];
|
||||
type NonLibraryMutationResult<K extends NonLibraryMutationKey> = NonLibraryMutation<K>['result'];
|
||||
export function useBridgeMutation<K extends NonLibraryMutationKey>(
|
||||
key: K,
|
||||
options?: UseMutationOptions<NonLibraryMutationResult<K>, RSPCError>
|
||||
): UseMutationResult<NonLibraryMutationResult<K>, RSPCError, NonLibraryMutationArgs<K>> {
|
||||
// @ts-ignore
|
||||
return rspc.useMutation(key, options);
|
||||
}
|
||||
|
||||
type MoreConstrainedQueries<T extends ProcedureDef> = T extends any
|
||||
? T['input'] extends LibraryArgs<infer E>
|
||||
? {
|
||||
key: T['key'];
|
||||
input: E;
|
||||
result: T['result'];
|
||||
}
|
||||
: never
|
||||
: never;
|
||||
|
||||
export const useBridgeQuery = rspc.customQuery<NonLibraryProcedure<'queries'>>(
|
||||
(keyAndInput) => keyAndInput as any
|
||||
);
|
||||
|
||||
export const useBridgeMutation = rspc.customMutation<NonLibraryProcedure<'mutations'>>(
|
||||
(keyAndInput) => keyAndInput
|
||||
);
|
||||
|
||||
export const useLibraryQuery = rspc.customQuery<
|
||||
MoreConstrainedQueries<LibraryProcedures<'queries'>>
|
||||
>((keyAndInput) => {
|
||||
const library_id = getLibraryIdRaw();
|
||||
if (library_id === null) throw new Error('Attempted to do library query with no library set!');
|
||||
return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }];
|
||||
});
|
||||
|
||||
export const useLibraryMutation = rspc.customMutation<
|
||||
MoreConstrainedQueries<LibraryProcedures<'mutations'>>
|
||||
>((keyAndInput) => {
|
||||
const library_id = getLibraryIdRaw();
|
||||
if (library_id === null) throw new Error('Attempted to do library query with no library set!');
|
||||
return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }];
|
||||
});
|
||||
|
||||
export function useInvalidateQuery() {
|
||||
const context = rspc.useContext();
|
||||
rspc.useSubscription(['invalidateQuery'], {
|
||||
onNext: (invalidateOperation) => {
|
||||
onData: (invalidateOperation) => {
|
||||
const key = [invalidateOperation.key];
|
||||
if (invalidateOperation.arg !== null) {
|
||||
key.concat(invalidateOperation.arg);
|
||||
|
||||
@@ -27,6 +27,10 @@ export const libraryStore = proxyWithPersist({
|
||||
getStorage: () => StorageEngine
|
||||
});
|
||||
|
||||
export function getLibraryIdRaw(): string | null {
|
||||
return libraryStore.currentLibraryUuid;
|
||||
}
|
||||
|
||||
// this must be used at least once in the app to correct the initial state
|
||||
// is memorized and can be used safely in any component
|
||||
export const useCurrentLibrary = () => {
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
/* eslint-disable */
|
||||
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
|
||||
|
||||
export type Operations = {
|
||||
export type Procedures = {
|
||||
queries:
|
||||
{ key: ["files.readMetadata", LibraryArgs<number>], result: null } |
|
||||
{ key: ["getNode"], result: NodeState } |
|
||||
{ key: ["jobs.getHistory", LibraryArgs<null>], result: Array<JobReport> } |
|
||||
{ key: ["jobs.getRunning", LibraryArgs<null>], result: Array<JobReport> } |
|
||||
{ key: ["library.getStatistics", LibraryArgs<null>], result: Statistics } |
|
||||
{ key: ["library.list"], result: Array<LibraryConfigWrapped> } |
|
||||
{ key: ["locations.getById", LibraryArgs<number>], result: Location | null } |
|
||||
{ key: ["locations.getExplorerData", LibraryArgs<LocationExplorerArgs>], result: ExplorerData } |
|
||||
{ key: ["locations.indexer_rules.get", LibraryArgs<number>], result: IndexerRule } |
|
||||
{ key: ["locations.indexer_rules.list", LibraryArgs<null>], result: Array<IndexerRule> } |
|
||||
{ key: ["locations.list", LibraryArgs<null>], result: Array<{ id: number, pub_id: Array<number>, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } |
|
||||
{ key: ["tags.get", LibraryArgs<number>], result: Tag | null } |
|
||||
{ key: ["tags.getExplorerData", LibraryArgs<number>], result: ExplorerData } |
|
||||
{ key: ["tags.getForFile", LibraryArgs<number>], result: Array<Tag> } |
|
||||
{ key: ["tags.list", LibraryArgs<null>], result: Array<Tag> } |
|
||||
{ key: ["version"], result: string } |
|
||||
{ key: ["volumes.list"], result: Array<Volume> },
|
||||
{ key: "files.readMetadata", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "getNode", input: never, result: NodeState } |
|
||||
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: Array<JobReport> } |
|
||||
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
|
||||
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
|
||||
{ key: "library.list", input: never, result: Array<LibraryConfigWrapped> } |
|
||||
{ key: "locations.getById", input: LibraryArgs<number>, result: Location | null } |
|
||||
{ key: "locations.getExplorerData", input: LibraryArgs<LocationExplorerArgs>, result: ExplorerData } |
|
||||
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
|
||||
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: Array<IndexerRule> } |
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: Array<{ id: number, pub_id: Array<number>, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } |
|
||||
{ key: "tags.get", input: LibraryArgs<number>, result: Tag | null } |
|
||||
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
|
||||
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Array<Tag> } |
|
||||
{ key: "tags.list", input: LibraryArgs<null>, result: Array<Tag> } |
|
||||
{ key: "version", input: never, result: string } |
|
||||
{ key: "volumes.list", input: never, result: Array<Volume> },
|
||||
mutations:
|
||||
{ key: ["files.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["files.setFavorite", LibraryArgs<SetFavoriteArgs>], result: null } |
|
||||
{ key: ["files.setNote", LibraryArgs<SetNoteArgs>], result: null } |
|
||||
{ key: ["jobs.generateThumbsForLocation", LibraryArgs<GenerateThumbsForLocationArgs>], result: null } |
|
||||
{ key: ["jobs.identifyUniqueFiles", LibraryArgs<IdentifyUniqueFilesArgs>], result: null } |
|
||||
{ key: ["library.create", string], result: LibraryConfigWrapped } |
|
||||
{ key: ["library.delete", string], result: null } |
|
||||
{ key: ["library.edit", EditLibraryArgs], result: null } |
|
||||
{ key: ["locations.create", LibraryArgs<LocationCreateArgs>], result: null } |
|
||||
{ key: ["locations.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["locations.fullRescan", LibraryArgs<number>], result: null } |
|
||||
{ key: ["locations.indexer_rules.create", LibraryArgs<IndexerRuleCreateArgs>], result: IndexerRule } |
|
||||
{ key: ["locations.indexer_rules.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["locations.quickRescan", LibraryArgs<null>], result: null } |
|
||||
{ key: ["locations.update", LibraryArgs<LocationUpdateArgs>], result: null } |
|
||||
{ key: ["tags.assign", LibraryArgs<TagAssignArgs>], result: null } |
|
||||
{ key: ["tags.create", LibraryArgs<TagCreateArgs>], result: Tag } |
|
||||
{ key: ["tags.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["tags.update", LibraryArgs<TagUpdateArgs>], result: null },
|
||||
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |
|
||||
{ key: "files.setNote", input: LibraryArgs<SetNoteArgs>, result: null } |
|
||||
{ key: "jobs.generateThumbsForLocation", input: LibraryArgs<GenerateThumbsForLocationArgs>, result: null } |
|
||||
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
|
||||
{ key: "library.create", input: string, result: LibraryConfigWrapped } |
|
||||
{ key: "library.delete", input: string, result: null } |
|
||||
{ key: "library.edit", input: EditLibraryArgs, result: null } |
|
||||
{ key: "locations.create", input: LibraryArgs<LocationCreateArgs>, result: null } |
|
||||
{ key: "locations.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.fullRescan", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.indexer_rules.create", input: LibraryArgs<IndexerRuleCreateArgs>, result: IndexerRule } |
|
||||
{ key: "locations.indexer_rules.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.quickRescan", input: LibraryArgs<null>, result: null } |
|
||||
{ key: "locations.update", input: LibraryArgs<LocationUpdateArgs>, result: null } |
|
||||
{ key: "tags.assign", input: LibraryArgs<TagAssignArgs>, result: null } |
|
||||
{ key: "tags.create", input: LibraryArgs<TagCreateArgs>, result: Tag } |
|
||||
{ key: "tags.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "tags.update", input: LibraryArgs<TagUpdateArgs>, result: null },
|
||||
subscriptions:
|
||||
{ key: ["invalidateQuery"], result: InvalidateOperationEvent } |
|
||||
{ key: ["jobs.newThumbnail", LibraryArgs<null>], result: string }
|
||||
{ key: "invalidateQuery", input: never, result: InvalidateOperationEvent } |
|
||||
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string }
|
||||
};
|
||||
|
||||
export interface ConfigMetadata { version: string | null }
|
||||
|
||||
@@ -5,8 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sd-core = { path = "../../core", features = [] }
|
||||
rspc = { version = "0.0.5", features = ["axum"] }
|
||||
axum = "0.5.13"
|
||||
tokio = { version = "1.17.0", features = ["sync", "rt-multi-thread", "signal"] }
|
||||
tracing = "0.1.35"
|
||||
ctrlc = "3.2.2"
|
||||
rspc = { workspace = true, features = ["axum"] }
|
||||
axum = "0.5.16"
|
||||
tokio = { version = "1.21.2", features = ["sync", "rt-multi-thread", "signal"] }
|
||||
tracing = "0.1.36"
|
||||
ctrlc = "3.2.3"
|
||||
|
||||
@@ -55,8 +55,8 @@ async fn main() {
|
||||
})
|
||||
})
|
||||
.route(
|
||||
"/rspcws",
|
||||
router.axum_ws_handler(move || node.get_request_context()),
|
||||
"/rspc/:id",
|
||||
router.endpoint(move || node.get_request_context()).axum(),
|
||||
)
|
||||
.fallback((|| async { "404 Not Found: We're past the event horizon..." }).into_service());
|
||||
|
||||
|
||||
@@ -8,25 +8,27 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.11",
|
||||
"@rspc/client": "^0.0.6",
|
||||
"@fontsource/inter": "^4.5.13",
|
||||
"@rspc/client": "^0.1.2",
|
||||
"@sd/client": "workspace:*",
|
||||
"@sd/interface": "workspace:*",
|
||||
"@sd/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^4.0.10",
|
||||
"@tanstack/react-query": "^4.10.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.14",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"postcss": "^8.4.17",
|
||||
"rollup-plugin-visualizer": "^5.8.2",
|
||||
"tailwind": "^4.0.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.3",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.4",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-svgr": "^2.2.1",
|
||||
"vite-plugin-tsconfig-paths": "^1.1.0"
|
||||
"vite-plugin-tsconfig-paths": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { WebsocketTransport, createClient } from '@rspc/client';
|
||||
import { Operations, PlatformProvider, queryClient, rspc } from '@sd/client';
|
||||
import { PlatformProvider, Procedures, queryClient, rspc } from '@sd/client';
|
||||
import SpacedriveInterface, { Platform } from '@sd/interface';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const client = createClient<Operations>({
|
||||
const client = createClient<Procedures>({
|
||||
transport: new WebsocketTransport(
|
||||
import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspcws'
|
||||
import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspc/ws'
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { defineConfig } from 'vite';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
import svg from 'vite-plugin-svgr';
|
||||
import tsconfigPaths from 'vite-plugin-tsconfig-paths';
|
||||
|
||||
@@ -10,7 +12,18 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 8002
|
||||
},
|
||||
plugins: [react(), svg({ svgrOptions: { icon: true } }), tsconfigPaths()],
|
||||
plugins: [
|
||||
react(),
|
||||
svg({ svgrOptions: { icon: true } }),
|
||||
tsconfigPaths(),
|
||||
createHtmlPlugin({
|
||||
minify: true
|
||||
}),
|
||||
visualizer({
|
||||
gzipSize: true,
|
||||
brotliSize: true
|
||||
})
|
||||
],
|
||||
root: 'src',
|
||||
publicDir: '../../packages/interface/src/assets',
|
||||
define: {
|
||||
|
||||
@@ -25,43 +25,45 @@ hostname = "0.3.1"
|
||||
# Universal Dependencies
|
||||
base64 = "0.13.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
chrono = { version = "0.4.22", features = ["serde"] }
|
||||
serde_json = "1.0"
|
||||
futures = "0.3"
|
||||
data-encoding = "2.3.2"
|
||||
ring = "0.17.0-alpha.10"
|
||||
ring = "0.17.0-alpha.11"
|
||||
int-enum = "0.4.0"
|
||||
rmp = "^0.8.11"
|
||||
rmp-serde = "^1.1.0"
|
||||
rmp-serde = "^1.1.1"
|
||||
|
||||
# Project dependencies
|
||||
rspc = { workspace = true, features = ["uuid", "chrono", "tracing"] }
|
||||
prisma-client-rust = { workspace = true }
|
||||
rspc = { version = "0.0.5", features = ["uuid", "chrono", "tracing"] }
|
||||
uuid = { version = "1.1.2", features = ["v4", "serde"] }
|
||||
sysinfo = "0.23.9"
|
||||
thiserror = "1.0.30"
|
||||
sysinfo = "0.26.4"
|
||||
thiserror = "1.0.37"
|
||||
|
||||
tokio = { version = "1.17.0", features = [
|
||||
tokio = { version = "1.21.2", features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
] }
|
||||
include_dir = { version = "0.7.2", features = ["glob"] }
|
||||
async-trait = "^0.1.52"
|
||||
image = "0.24.1"
|
||||
async-trait = "^0.1.57"
|
||||
image = "0.24.4"
|
||||
webp = "0.2.2"
|
||||
ffmpeg-next = { version = "5.0.3", optional = true, features = [] }
|
||||
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
|
||||
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
||||
fs_extra = "1.2.0"
|
||||
tracing = "0.1.35"
|
||||
tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
|
||||
tracing = "0.1.36"
|
||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
|
||||
async-stream = "0.3.3"
|
||||
once_cell = "1.13.0"
|
||||
ctor = "0.1.22"
|
||||
once_cell = "1.15.0"
|
||||
ctor = "0.1.23"
|
||||
globset = { version = "^0.4.9", features = ["serde1"] }
|
||||
itertools = "^0.10.3"
|
||||
itertools = "^0.10.5"
|
||||
enumflags2 = "0.7.5"
|
||||
|
||||
openssl-sys = "0.9.76" # We don't use this in the core but it exists so that the workspace level patch doesn't complain that the dependency isn't used.
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "^3.3.0"
|
||||
tracing-test = "^0.2.3"
|
||||
|
||||
@@ -5,42 +5,45 @@ use serde::Deserialize;
|
||||
|
||||
use super::{utils::LibraryRequest, RouterBuilder};
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct SetNoteArgs {
|
||||
pub id: i32,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct SetFavoriteArgs {
|
||||
pub id: i32,
|
||||
pub favorite: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> RouterBuilder {
|
||||
<RouterBuilder>::new()
|
||||
.library_query("readMetadata", |_, _id: i32, _| async move {
|
||||
#[allow(unreachable_code)]
|
||||
Ok(todo!())
|
||||
.library_query("readMetadata", |t| {
|
||||
t(|_, _id: i32, _| async move {
|
||||
#[allow(unreachable_code)]
|
||||
Ok(todo!())
|
||||
})
|
||||
})
|
||||
.library_mutation("setNote", |_, args: SetNoteArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.object()
|
||||
.update(
|
||||
object::id::equals(args.id),
|
||||
vec![object::note::set(args.note)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
.library_mutation("setNote", |t| {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct SetNoteArgs {
|
||||
pub id: i32,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
invalidate_query!(library, "locations.getExplorerData");
|
||||
t(|_, args: SetNoteArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.object()
|
||||
.update(
|
||||
object::id::equals(args.id),
|
||||
vec![object::note::set(args.note)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
invalidate_query!(library, "locations.getExplorerData");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation(
|
||||
"setFavorite",
|
||||
|_, args: SetFavoriteArgs, library| async move {
|
||||
.library_mutation("setFavorite", |t| {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct SetFavoriteArgs {
|
||||
pub id: i32,
|
||||
pub favorite: bool,
|
||||
}
|
||||
|
||||
t(|_, args: SetFavoriteArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.object()
|
||||
@@ -54,17 +57,19 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
invalidate_query!(library, "locations.getExplorerData");
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.library_mutation("delete", |_, id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.object()
|
||||
.delete(object::id::equals(id))
|
||||
.exec()
|
||||
.await?;
|
||||
})
|
||||
})
|
||||
.library_mutation("delete", |t| {
|
||||
t(|_, id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.object()
|
||||
.delete(object::id::equals(id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "locations.getExplorerData");
|
||||
Ok(())
|
||||
invalidate_query!(library, "locations.getExplorerData");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,56 +14,56 @@ use std::path::PathBuf;
|
||||
|
||||
use super::{utils::LibraryRequest, CoreEvent, RouterBuilder};
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct GenerateThumbsForLocationArgs {
|
||||
pub id: i32,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct IdentifyUniqueFilesArgs {
|
||||
pub id: i32,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> RouterBuilder {
|
||||
<RouterBuilder>::new()
|
||||
.library_query("getRunning", |ctx, _: (), _| async move {
|
||||
Ok(ctx.jobs.get_running().await)
|
||||
.library_query("getRunning", |t| {
|
||||
t(|ctx, _: (), _| async move { Ok(ctx.jobs.get_running().await) })
|
||||
})
|
||||
.library_query("getHistory", |_, _: (), library| async move {
|
||||
Ok(JobManager::get_history(&library).await?)
|
||||
.library_query("getHistory", |t| {
|
||||
t(|_, _: (), library| async move { Ok(JobManager::get_history(&library).await?) })
|
||||
})
|
||||
.library_mutation(
|
||||
"generateThumbsForLocation",
|
||||
|_, args: GenerateThumbsForLocationArgs, library| async move {
|
||||
if library
|
||||
.db
|
||||
.location()
|
||||
.count(vec![location::id::equals(args.id)])
|
||||
.exec()
|
||||
.await? == 0
|
||||
{
|
||||
return Err(LocationError::IdNotFound(args.id).into());
|
||||
}
|
||||
.library_mutation("generateThumbsForLocation", |t| {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct GenerateThumbsForLocationArgs {
|
||||
pub id: i32,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
library
|
||||
.spawn_job(Job::new(
|
||||
ThumbnailJobInit {
|
||||
location_id: args.id,
|
||||
path: PathBuf::new(),
|
||||
background: true,
|
||||
},
|
||||
Box::new(ThumbnailJob {}),
|
||||
))
|
||||
.await;
|
||||
t(
|
||||
|_, args: GenerateThumbsForLocationArgs, library| async move {
|
||||
if library
|
||||
.db
|
||||
.location()
|
||||
.count(vec![location::id::equals(args.id)])
|
||||
.exec()
|
||||
.await? == 0
|
||||
{
|
||||
return Err(LocationError::IdNotFound(args.id).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.library_mutation(
|
||||
"identifyUniqueFiles",
|
||||
|_, args: IdentifyUniqueFilesArgs, library| async move {
|
||||
library
|
||||
.spawn_job(Job::new(
|
||||
ThumbnailJobInit {
|
||||
location_id: args.id,
|
||||
path: PathBuf::new(),
|
||||
background: true,
|
||||
},
|
||||
Box::new(ThumbnailJob {}),
|
||||
))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
})
|
||||
.library_mutation("identifyUniqueFiles", |t| {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct IdentifyUniqueFilesArgs {
|
||||
pub id: i32,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
t(|_, args: IdentifyUniqueFilesArgs, library| async move {
|
||||
if fetch_location(&library, args.id).exec().await?.is_none() {
|
||||
return Err(rspc::Error::new(
|
||||
ErrorCode::NotFound,
|
||||
@@ -82,19 +82,21 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.library_subscription("newThumbnail", |ctx, _: (), _| {
|
||||
// TODO: Only return event for the library that was subscribed to
|
||||
})
|
||||
})
|
||||
.library_subscription("newThumbnail", |t| {
|
||||
t(|ctx, _: (), _| {
|
||||
// TODO: Only return event for the library that was subscribed to
|
||||
|
||||
let mut event_bus_rx = ctx.event_bus.subscribe();
|
||||
async_stream::stream! {
|
||||
while let Ok(event) = event_bus_rx.recv().await {
|
||||
match event {
|
||||
CoreEvent::NewThumbnail { cas_id } => yield cas_id,
|
||||
_ => {}
|
||||
let mut event_bus_rx = ctx.event_bus.subscribe();
|
||||
async_stream::stream! {
|
||||
while let Ok(event) = event_bus_rx.recv().await {
|
||||
match event {
|
||||
CoreEvent::NewThumbnail { cas_id } => yield cas_id,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,87 +12,93 @@ use serde::Deserialize;
|
||||
use tokio::fs;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct EditLibraryArgs {
|
||||
pub id: Uuid,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> RouterBuilder {
|
||||
<RouterBuilder>::new()
|
||||
.query("list", |ctx, _: ()| async move {
|
||||
ctx.library_manager.get_all_libraries_config().await
|
||||
.query("list", |t| {
|
||||
t(|ctx, _: ()| async move { ctx.library_manager.get_all_libraries_config().await })
|
||||
})
|
||||
.library_query("getStatistics", |_, _: (), library| async move {
|
||||
let _statistics = library
|
||||
.db
|
||||
.statistics()
|
||||
.find_unique(statistics::id::equals(library.node_local_id))
|
||||
.exec()
|
||||
.await?;
|
||||
.library_query("getStatistics", |t| {
|
||||
t(|_, _: (), library| async move {
|
||||
let _statistics = library
|
||||
.db
|
||||
.statistics()
|
||||
.find_unique(statistics::id::equals(library.node_local_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
// TODO: get from database, not sys
|
||||
let volumes = get_volumes();
|
||||
save_volume(&library).await?;
|
||||
// TODO: get from database, not sys
|
||||
let volumes = get_volumes();
|
||||
save_volume(&library).await?;
|
||||
|
||||
let mut available_capacity: u64 = 0;
|
||||
let mut total_capacity: u64 = 0;
|
||||
if volumes.is_ok() {
|
||||
for volume in volumes? {
|
||||
total_capacity += volume.total_capacity;
|
||||
available_capacity += volume.available_capacity;
|
||||
let mut available_capacity: u64 = 0;
|
||||
let mut total_capacity: u64 = 0;
|
||||
if volumes.is_ok() {
|
||||
for volume in volumes? {
|
||||
total_capacity += volume.total_capacity;
|
||||
available_capacity += volume.available_capacity;
|
||||
}
|
||||
}
|
||||
|
||||
let library_db_size = match fs::metadata(library.config().data_directory()).await {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
let thumbnail_folder_size =
|
||||
get_size(library.config().data_directory().join("thumbnails"));
|
||||
|
||||
use statistics::*;
|
||||
let params = vec![
|
||||
id::set(1), // Each library is a database so only one of these ever exists
|
||||
date_captured::set(Utc::now().into()),
|
||||
total_object_count::set(0),
|
||||
library_db_size::set(library_db_size.to_string()),
|
||||
total_bytes_used::set(0.to_string()),
|
||||
total_bytes_capacity::set(total_capacity.to_string()),
|
||||
total_unique_bytes::set(0.to_string()),
|
||||
total_bytes_free::set(available_capacity.to_string()),
|
||||
preview_media_bytes::set(thumbnail_folder_size.unwrap_or(0).to_string()),
|
||||
];
|
||||
|
||||
Ok(library
|
||||
.db
|
||||
.statistics()
|
||||
.upsert(
|
||||
statistics::id::equals(1), // Each library is a database so only one of these ever exists
|
||||
params.clone(),
|
||||
params,
|
||||
)
|
||||
.exec()
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.mutation("create", |t| {
|
||||
t(|ctx, name: String| async move {
|
||||
Ok(ctx
|
||||
.library_manager
|
||||
.create(LibraryConfig {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.mutation("edit", |t| {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct EditLibraryArgs {
|
||||
pub id: Uuid,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
let library_db_size = match fs::metadata(library.config().data_directory()).await {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
let thumbnail_folder_size =
|
||||
get_size(library.config().data_directory().join("thumbnails"));
|
||||
|
||||
use statistics::*;
|
||||
let params = vec![
|
||||
id::set(1), // Each library is a database so only one of these ever exists
|
||||
date_captured::set(Utc::now().into()),
|
||||
total_object_count::set(0),
|
||||
library_db_size::set(library_db_size.to_string()),
|
||||
total_bytes_used::set(0.to_string()),
|
||||
total_bytes_capacity::set(total_capacity.to_string()),
|
||||
total_unique_bytes::set(0.to_string()),
|
||||
total_bytes_free::set(available_capacity.to_string()),
|
||||
preview_media_bytes::set(thumbnail_folder_size.unwrap_or(0).to_string()),
|
||||
];
|
||||
|
||||
Ok(library
|
||||
.db
|
||||
.statistics()
|
||||
.upsert(
|
||||
statistics::id::equals(1), // Each library is a database so only one of these ever exists
|
||||
params.clone(),
|
||||
params,
|
||||
)
|
||||
.exec()
|
||||
.await?)
|
||||
t(|ctx, args: EditLibraryArgs| async move {
|
||||
Ok(ctx
|
||||
.library_manager
|
||||
.edit(args.id, args.name, args.description)
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.mutation("create", |ctx, name: String| async move {
|
||||
Ok(ctx
|
||||
.library_manager
|
||||
.create(LibraryConfig {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
.await?)
|
||||
})
|
||||
.mutation("edit", |ctx, args: EditLibraryArgs| async move {
|
||||
Ok(ctx
|
||||
.library_manager
|
||||
.edit(args.id, args.name, args.description)
|
||||
.await?)
|
||||
})
|
||||
.mutation("delete", |ctx, id: Uuid| async move {
|
||||
Ok(ctx.library_manager.delete_library(id).await?)
|
||||
.mutation("delete", |t| {
|
||||
t(|ctx, id: Uuid| async move { Ok(ctx.library_manager.delete_library(id).await?) })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,17 +9,11 @@ use crate::{
|
||||
prisma::{file_path, indexer_rule, indexer_rules_in_location, location, object, tag},
|
||||
};
|
||||
|
||||
use rspc::{self, ErrorCode, Type};
|
||||
use rspc::{self, internal::MiddlewareBuilderLike, ErrorCode, Type};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
use super::{utils::LibraryRequest, RouterBuilder};
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
pub struct ExplorerData {
|
||||
pub context: ExplorerContext,
|
||||
pub items: Vec<ExplorerItem>,
|
||||
}
|
||||
use super::{utils::LibraryRequest, Ctx, RouterBuilder};
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -29,9 +23,6 @@ pub enum ExplorerContext {
|
||||
// Space(object_in_space::Data),
|
||||
}
|
||||
|
||||
file_path::include!(file_path_with_object { object });
|
||||
object::include!(object_with_file_paths { file_paths });
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ExplorerItem {
|
||||
@@ -39,36 +30,53 @@ pub enum ExplorerItem {
|
||||
Object(Box<object_with_file_paths::Data>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
|
||||
pub struct LocationExplorerArgs {
|
||||
pub location_id: i32,
|
||||
pub path: String,
|
||||
pub limit: i32,
|
||||
pub cursor: Option<String>,
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
pub struct ExplorerData {
|
||||
pub context: ExplorerContext,
|
||||
pub items: Vec<ExplorerItem>,
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> RouterBuilder {
|
||||
file_path::include!(file_path_with_object { object });
|
||||
object::include!(object_with_file_paths { file_paths });
|
||||
|
||||
// TODO(@Oscar): This return type sucks. Add an upstream rspc solution.
|
||||
pub(crate) fn mount() -> rspc::RouterBuilder<
|
||||
Ctx,
|
||||
(),
|
||||
impl MiddlewareBuilderLike<Ctx, LayerContext = Ctx> + Send + 'static,
|
||||
> {
|
||||
<RouterBuilder>::new()
|
||||
.library_query("list", |_, _: (), library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.location()
|
||||
.find_many(vec![])
|
||||
.include(location::include!({ node }))
|
||||
.exec()
|
||||
.await?)
|
||||
.library_query("list", |t| {
|
||||
t(|_, _: (), library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.location()
|
||||
.find_many(vec![])
|
||||
.include(location::include!({ node }))
|
||||
.exec()
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.library_query("getById", |_, location_id: i32, library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.location()
|
||||
.find_unique(location::id::equals(location_id))
|
||||
.exec()
|
||||
.await?)
|
||||
.library_query("getById", |t| {
|
||||
t(|_, location_id: i32, library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.location()
|
||||
.find_unique(location::id::equals(location_id))
|
||||
.exec()
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.library_query(
|
||||
"getExplorerData",
|
||||
|_, args: LocationExplorerArgs, library| async move {
|
||||
.library_query("getExplorerData", |t| {
|
||||
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
|
||||
pub struct LocationExplorerArgs {
|
||||
pub location_id: i32,
|
||||
pub path: String,
|
||||
pub limit: i32,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
t(|_, args: LocationExplorerArgs, library| async move {
|
||||
let location = library
|
||||
.db
|
||||
.location()
|
||||
@@ -124,119 +132,128 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
},
|
||||
)
|
||||
.library_mutation(
|
||||
"create",
|
||||
|_, args: LocationCreateArgs, library| async move {
|
||||
})
|
||||
})
|
||||
.library_mutation("create", |t| {
|
||||
t(|_, args: LocationCreateArgs, library| async move {
|
||||
let location = args.create(&library).await?;
|
||||
scan_location(&library, location).await?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.library_mutation(
|
||||
"update",
|
||||
|_, args: LocationUpdateArgs, library| async move {
|
||||
})
|
||||
})
|
||||
.library_mutation("update", |t| {
|
||||
t(|_, args: LocationUpdateArgs, library| async move {
|
||||
args.update(&library).await.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.library_mutation("delete", |_, location_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.file_path()
|
||||
.delete_many(vec![file_path::location_id::equals(location_id)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
library
|
||||
.db
|
||||
.indexer_rules_in_location()
|
||||
.delete_many(vec![indexer_rules_in_location::location_id::equals(
|
||||
location_id,
|
||||
)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
library
|
||||
.db
|
||||
.location()
|
||||
.delete(location::id::equals(location_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "locations.list");
|
||||
|
||||
info!("Location {} deleted", location_id);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("fullRescan", |_, location_id: i32, library| async move {
|
||||
scan_location(
|
||||
&library,
|
||||
fetch_location(&library, location_id)
|
||||
.include(indexer_job_location::include())
|
||||
.library_mutation("delete", |t| {
|
||||
t(|_, location_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.file_path()
|
||||
.delete_many(vec![file_path::location_id::equals(location_id)])
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or(LocationError::IdNotFound(location_id))?,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.await?;
|
||||
|
||||
library
|
||||
.db
|
||||
.indexer_rules_in_location()
|
||||
.delete_many(vec![indexer_rules_in_location::location_id::equals(
|
||||
location_id,
|
||||
)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
library
|
||||
.db
|
||||
.location()
|
||||
.delete(location::id::equals(location_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "locations.list");
|
||||
|
||||
info!("Location {} deleted", location_id);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("quickRescan", |_, _: (), _| async move {
|
||||
#[allow(unreachable_code)]
|
||||
Ok(todo!())
|
||||
.library_mutation("fullRescan", |t| {
|
||||
t(|_, location_id: i32, library| async move {
|
||||
scan_location(
|
||||
&library,
|
||||
fetch_location(&library, location_id)
|
||||
.include(indexer_job_location::include())
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or(LocationError::IdNotFound(location_id))?,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
})
|
||||
})
|
||||
.library_mutation("quickRescan", |t| {
|
||||
t(|_, _: (), _| async move {
|
||||
#[allow(unreachable_code)]
|
||||
Ok(todo!())
|
||||
})
|
||||
})
|
||||
.merge("indexer_rules.", mount_indexer_rule_routes())
|
||||
}
|
||||
|
||||
fn mount_indexer_rule_routes() -> RouterBuilder {
|
||||
<RouterBuilder>::new()
|
||||
.library_mutation(
|
||||
"create",
|
||||
|_, args: IndexerRuleCreateArgs, library| async move {
|
||||
.library_mutation("create", |t| {
|
||||
t(|_, args: IndexerRuleCreateArgs, library| async move {
|
||||
args.create(&library).await.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.library_mutation("delete", |_, indexer_rule_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rules_in_location()
|
||||
.delete_many(vec![indexer_rules_in_location::indexer_rule_id::equals(
|
||||
indexer_rule_id,
|
||||
)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.delete(indexer_rule::id::equals(indexer_rule_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_query("get", |_, indexer_rule_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.find_unique(indexer_rule::id::equals(indexer_rule_id))
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::NotFound,
|
||||
format!("Indexer rule <id={indexer_rule_id}> not found"),
|
||||
)
|
||||
})
|
||||
.library_mutation("delete", |t| {
|
||||
t(|_, indexer_rule_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rules_in_location()
|
||||
.delete_many(vec![indexer_rules_in_location::indexer_rule_id::equals(
|
||||
indexer_rule_id,
|
||||
)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.delete(indexer_rule::id::equals(indexer_rule_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_query("list", |_, _: (), library| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.find_many(vec![])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.library_query("get", |t| {
|
||||
t(|_, indexer_rule_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.find_unique(indexer_rule::id::equals(indexer_rule_id))
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::NotFound,
|
||||
format!("Indexer rule <id={indexer_rule_id}> not found"),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.library_query("list", |t| {
|
||||
t(|_, _: (), library| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.find_many(vec![])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -62,12 +63,14 @@ pub(crate) fn mount() -> Arc<Router> {
|
||||
// .export_ts_bindings(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./index.ts")),
|
||||
.set_ts_bindings_header("/* eslint-disable */"),
|
||||
)
|
||||
.query("version", |_, _: ()| env!("CARGO_PKG_VERSION"))
|
||||
.query("getNode", |ctx, _: ()| async move {
|
||||
Ok(NodeState {
|
||||
config: ctx.config.get().await,
|
||||
// We are taking the assumption here that this value is only used on the frontend for display purposes
|
||||
data_path: ctx.config.data_directory().to_string_lossy().into_owned(),
|
||||
.query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION")))
|
||||
.query("getNode", |t| {
|
||||
t(|ctx, _: ()| async move {
|
||||
Ok(NodeState {
|
||||
config: ctx.config.get().await,
|
||||
// We are taking the assumption here that this value is only used on the frontend for display purposes
|
||||
data_path: ctx.config.data_directory().to_string_lossy().into_owned(),
|
||||
})
|
||||
})
|
||||
})
|
||||
.merge("library.", libraries::mount())
|
||||
@@ -77,46 +80,48 @@ pub(crate) fn mount() -> Arc<Router> {
|
||||
.merge("files.", files::mount())
|
||||
.merge("jobs.", jobs::mount())
|
||||
// TODO: Scope the invalidate queries to a specific library (filtered server side)
|
||||
.subscription("invalidateQuery", |ctx, _: ()| {
|
||||
let mut event_bus_rx = ctx.event_bus.subscribe();
|
||||
let mut last = Instant::now();
|
||||
async_stream::stream! {
|
||||
while let Ok(event) = event_bus_rx.recv().await {
|
||||
match event {
|
||||
CoreEvent::InvalidateOperation(op) => yield op,
|
||||
CoreEvent::InvalidateOperationDebounced(op) => {
|
||||
let current = Instant::now();
|
||||
if current.duration_since(last) > Duration::from_millis(1000 / 60) {
|
||||
last = current;
|
||||
yield op;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
.subscription("invalidateQuery", |t| {
|
||||
t(|ctx, _: ()| {
|
||||
let mut event_bus_rx = ctx.event_bus.subscribe();
|
||||
let mut last = Instant::now();
|
||||
async_stream::stream! {
|
||||
while let Ok(event) = event_bus_rx.recv().await {
|
||||
match event {
|
||||
CoreEvent::InvalidateOperation(op) => yield op,
|
||||
CoreEvent::InvalidateOperationDebounced(op) => {
|
||||
let current = Instant::now();
|
||||
if current.duration_since(last) > Duration::from_millis(1000 / 60) {
|
||||
last = current;
|
||||
yield op;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.build()
|
||||
.arced();
|
||||
InvalidRequests::validate(r.clone()); // This validates all invalidation calls.
|
||||
export_ts_bindings(&r);
|
||||
r
|
||||
}
|
||||
|
||||
pub fn export_ts_bindings(r: &Router) {
|
||||
r.export_ts(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../packages/client/src/core.ts"))
|
||||
.expect("Error exporting rspc Typescript bindings!");
|
||||
r.export_ts(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../apps/mobile/src/types/bindings.ts"),
|
||||
)
|
||||
.expect("Error exporting rspc Typescript bindings!");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// This test will ensure the rspc router and all calls to `invalidate_query` are valid and also export an updated version of the Typescript bindings.
|
||||
#[test]
|
||||
fn test_and_export_rspc_bindings() {
|
||||
let r = super::mount();
|
||||
r.export_ts(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../packages/client/src/core.ts"),
|
||||
)
|
||||
.expect("Error exporting rspc Typescript bindings!");
|
||||
r.export_ts(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../apps/mobile/src/types/bindings.ts"),
|
||||
)
|
||||
.expect("Error exporting rspc Typescript bindings!");
|
||||
super::export_ts_bindings(&super::mount());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,168 +12,187 @@ use crate::{
|
||||
|
||||
use super::{utils::LibraryRequest, RouterBuilder};
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct TagCreateArgs {
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Type, Deserialize)]
|
||||
pub struct TagAssignArgs {
|
||||
pub object_id: i32,
|
||||
pub tag_id: i32,
|
||||
pub unassign: bool,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct TagUpdateArgs {
|
||||
pub id: i32,
|
||||
pub name: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> RouterBuilder {
|
||||
RouterBuilder::new()
|
||||
.library_query("list", |_, _: (), library| async move {
|
||||
Ok(library.db.tag().find_many(vec![]).exec().await?)
|
||||
.library_query("list", |t| {
|
||||
t(
|
||||
|_, _: (), library| async move { Ok(library.db.tag().find_many(vec![]).exec().await?) },
|
||||
)
|
||||
})
|
||||
.library_query("getExplorerData", |_, tag_id: i32, library| async move {
|
||||
info!("Getting files for tag {}", tag_id);
|
||||
.library_query("getExplorerData", |t| {
|
||||
t(|_, tag_id: i32, library| async move {
|
||||
info!("Getting files for tag {}", tag_id);
|
||||
|
||||
let tag = library
|
||||
.db
|
||||
.tag()
|
||||
.find_unique(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(ErrorCode::NotFound, format!("Tag <id={tag_id}> not found"))
|
||||
})?;
|
||||
let tag = library
|
||||
.db
|
||||
.tag()
|
||||
.find_unique(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::NotFound,
|
||||
format!("Tag <id={tag_id}> not found"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let files: Vec<ExplorerItem> = library
|
||||
.db
|
||||
.object()
|
||||
.find_many(vec![object::tags::some(vec![
|
||||
tag_on_object::tag_id::equals(tag_id),
|
||||
])])
|
||||
.include(object_with_file_paths::include())
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|mut object| {
|
||||
// sorry brendan
|
||||
// grab the first path and tac on the name
|
||||
let oldest_path = &object.file_paths[0];
|
||||
object.name = Some(oldest_path.name.clone());
|
||||
object.extension = oldest_path.extension.clone();
|
||||
// a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name
|
||||
let objects: Vec<ExplorerItem> = library
|
||||
.db
|
||||
.object()
|
||||
.find_many(vec![object::tags::some(vec![
|
||||
tag_on_object::tag_id::equals(tag_id),
|
||||
])])
|
||||
.include(object_with_file_paths::include())
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|mut object| {
|
||||
// sorry brendan
|
||||
// grab the first path and tac on the name
|
||||
let oldest_path = &object.file_paths[0];
|
||||
object.name = Some(oldest_path.name.clone());
|
||||
object.extension = oldest_path.extension.clone();
|
||||
// a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name
|
||||
|
||||
let thumb_path = library
|
||||
.config()
|
||||
.data_directory()
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(&object.cas_id)
|
||||
.with_extension("webp");
|
||||
let thumb_path = library
|
||||
.config()
|
||||
.data_directory()
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(&object.cas_id)
|
||||
.with_extension("webp");
|
||||
|
||||
object.has_thumbnail = thumb_path.exists();
|
||||
object.has_thumbnail = thumb_path.exists();
|
||||
|
||||
ExplorerItem::Object(Box::new(object))
|
||||
ExplorerItem::Object(Box::new(object))
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Got objects {}", objects.len());
|
||||
|
||||
Ok(ExplorerData {
|
||||
context: ExplorerContext::Tag(tag),
|
||||
items: objects,
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Got files {}", files.len());
|
||||
|
||||
Ok(ExplorerData {
|
||||
context: ExplorerContext::Tag(tag),
|
||||
items: files,
|
||||
})
|
||||
})
|
||||
.library_query("getForFile", |_, object_id: i32, library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.tag()
|
||||
.find_many(vec![tag::tag_objects::some(vec![
|
||||
tag_on_object::object_id::equals(object_id),
|
||||
])])
|
||||
.exec()
|
||||
.await?)
|
||||
})
|
||||
.library_query("get", |_, tag_id: i32, library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.tag()
|
||||
.find_unique(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?)
|
||||
})
|
||||
.library_mutation("create", |_, args: TagCreateArgs, library| async move {
|
||||
let created_tag = library
|
||||
.db
|
||||
.tag()
|
||||
.create(
|
||||
Uuid::new_v4().as_bytes().to_vec(),
|
||||
vec![
|
||||
tag::name::set(Some(args.name)),
|
||||
tag::color::set(Some(args.color)),
|
||||
],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "tags.list");
|
||||
|
||||
Ok(created_tag)
|
||||
})
|
||||
.library_mutation("assign", |_, args: TagAssignArgs, library| async move {
|
||||
if args.unassign {
|
||||
library
|
||||
.library_query("getForObject", |t| {
|
||||
t(|_, object_id: i32, library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.tag_on_object()
|
||||
.delete(tag_on_object::tag_id_object_id(args.tag_id, args.object_id))
|
||||
.tag()
|
||||
.find_many(vec![tag::tag_objects::some(vec![
|
||||
tag_on_object::object_id::equals(object_id),
|
||||
])])
|
||||
.exec()
|
||||
.await?;
|
||||
} else {
|
||||
library
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.library_query("get", |t| {
|
||||
t(|_, tag_id: i32, library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.tag_on_object()
|
||||
.tag()
|
||||
.find_unique(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.library_mutation("create", |t| {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct TagCreateArgs {
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
t(|_, args: TagCreateArgs, library| async move {
|
||||
let created_tag = library
|
||||
.db
|
||||
.tag()
|
||||
.create(
|
||||
tag::id::equals(args.tag_id),
|
||||
object::id::equals(args.object_id),
|
||||
vec![],
|
||||
Uuid::new_v4().as_bytes().to_vec(),
|
||||
vec![
|
||||
tag::name::set(Some(args.name)),
|
||||
tag::color::set(Some(args.color)),
|
||||
],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "tags.list");
|
||||
|
||||
Ok(created_tag)
|
||||
})
|
||||
})
|
||||
.library_mutation("assign", |t| {
|
||||
#[derive(Debug, Type, Deserialize)]
|
||||
pub struct TagAssignArgs {
|
||||
pub object_id: i32,
|
||||
pub tag_id: i32,
|
||||
pub unassign: bool,
|
||||
}
|
||||
|
||||
invalidate_query!(library, "tags.getForFile");
|
||||
t(|_, args: TagAssignArgs, library| async move {
|
||||
if args.unassign {
|
||||
library
|
||||
.db
|
||||
.tag_on_object()
|
||||
.delete(tag_on_object::tag_id_object_id(args.tag_id, args.object_id))
|
||||
.exec()
|
||||
.await?;
|
||||
} else {
|
||||
library
|
||||
.db
|
||||
.tag_on_object()
|
||||
.create(
|
||||
tag::id::equals(args.tag_id),
|
||||
object::id::equals(args.object_id),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
invalidate_query!(library, "tags.getForObject");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("update", |_, args: TagUpdateArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.tag()
|
||||
.update(
|
||||
tag::id::equals(args.id),
|
||||
vec![tag::name::set(args.name), tag::color::set(args.color)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
.library_mutation("update", |t| {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct TagUpdateArgs {
|
||||
pub id: i32,
|
||||
pub name: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
invalidate_query!(library, "tags.list");
|
||||
t(|_, args: TagUpdateArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.tag()
|
||||
.update(
|
||||
tag::id::equals(args.id),
|
||||
vec![tag::name::set(args.name), tag::color::set(args.color)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
invalidate_query!(library, "tags.list");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("delete", |_, tag_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.tag()
|
||||
.delete(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?;
|
||||
.library_mutation("delete", |t| {
|
||||
t(|_, tag_id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.tag()
|
||||
.delete(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "tags.list");
|
||||
invalidate_query!(library, "tags.list");
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::{Future, Stream};
|
||||
use rspc::{internal::specta, ErrorCode, IntoLayerResult, Type};
|
||||
use rspc::{
|
||||
internal::{
|
||||
specta, BuiltProcedureBuilder, MiddlewareBuilderLike, RequestResult,
|
||||
UnbuiltProcedureBuilder,
|
||||
},
|
||||
ErrorCode, Type,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -12,116 +20,172 @@ pub(crate) struct LibraryArgs<T> {
|
||||
pub arg: T,
|
||||
}
|
||||
|
||||
// WARNING: This is system is using internal API's which means it will break between rspc release. I would avoid copying it unless you understand the cost of maintaining it!
|
||||
pub trait LibraryRequest {
|
||||
fn library_query<T, TArg, TResult, TResultMarker>(
|
||||
fn library_query<TUnbuiltResolver, TUnbuiltResult, TUnbuiltResultMarker, TBuiltResolver, TArg>(
|
||||
self,
|
||||
key: &'static str,
|
||||
resolver: fn(Ctx, TArg, LibraryContext) -> TResult,
|
||||
builder: impl FnOnce(
|
||||
UnbuiltProcedureBuilder<Ctx, TUnbuiltResolver>,
|
||||
) -> BuiltProcedureBuilder<TBuiltResolver>,
|
||||
) -> Self
|
||||
where
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static,
|
||||
TResult: Future<Output = Result<T, rspc::Error>> + Send + 'static,
|
||||
T: IntoLayerResult<TResultMarker> + Send + Serialize + specta::Type;
|
||||
TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send,
|
||||
TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static,
|
||||
TUnbuiltResult: RequestResult<TUnbuiltResultMarker> + Send,
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static;
|
||||
|
||||
fn library_mutation<T, TArg, TResult, TResultMarker>(
|
||||
fn library_mutation<
|
||||
TUnbuiltResolver,
|
||||
TUnbuiltResult,
|
||||
TUnbuiltResultMarker,
|
||||
TBuiltResolver,
|
||||
TArg,
|
||||
>(
|
||||
self,
|
||||
key: &'static str,
|
||||
resolver: fn(Ctx, TArg, LibraryContext) -> TResult,
|
||||
builder: impl FnOnce(
|
||||
UnbuiltProcedureBuilder<Ctx, TUnbuiltResolver>,
|
||||
) -> BuiltProcedureBuilder<TBuiltResolver>,
|
||||
) -> Self
|
||||
where
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static,
|
||||
TResult: Future<Output = Result<T, rspc::Error>> + Send + 'static,
|
||||
T: IntoLayerResult<TResultMarker> + Send + Serialize + specta::Type;
|
||||
TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send,
|
||||
TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static,
|
||||
TUnbuiltResult: RequestResult<TUnbuiltResultMarker> + Send,
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static;
|
||||
|
||||
fn library_subscription<T, TArg, TResult>(
|
||||
fn library_subscription<TResolver, TArg, TStream, TResult>(
|
||||
self,
|
||||
key: &'static str,
|
||||
resolver: fn(Ctx, TArg, Uuid) -> T,
|
||||
builder: impl Fn(UnbuiltProcedureBuilder<Ctx, TResolver>) -> BuiltProcedureBuilder<TResolver>,
|
||||
) -> Self
|
||||
where
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static,
|
||||
T: Stream<Item = TResult> + Send + 'static,
|
||||
TResult: Serialize + specta::Type;
|
||||
TArg: DeserializeOwned + specta::Type + 'static,
|
||||
TStream: Stream<Item = TResult> + Send + Sync + 'static,
|
||||
TResult: Serialize + specta::Type,
|
||||
TResolver: Fn(Ctx, TArg, Uuid) -> TStream + Send + Sync + 'static;
|
||||
}
|
||||
|
||||
// Note: This will break with middleware context switching but that's fine for now
|
||||
impl LibraryRequest for rspc::RouterBuilder<Ctx, (), Ctx> {
|
||||
fn library_query<T, TArg, TResult, TResultMarker>(
|
||||
impl<TMiddleware> LibraryRequest for rspc::RouterBuilder<Ctx, (), TMiddleware>
|
||||
where
|
||||
TMiddleware: MiddlewareBuilderLike<Ctx, LayerContext = Ctx> + Send + 'static,
|
||||
{
|
||||
fn library_query<TUnbuiltResolver, TUnbuiltResult, TUnbuiltResultMarker, TBuiltResolver, TArg>(
|
||||
self,
|
||||
key: &'static str,
|
||||
resolver: fn(Ctx, TArg, LibraryContext) -> TResult,
|
||||
builder: impl FnOnce(
|
||||
UnbuiltProcedureBuilder<Ctx, TUnbuiltResolver>,
|
||||
) -> BuiltProcedureBuilder<TBuiltResolver>,
|
||||
) -> Self
|
||||
where
|
||||
TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send,
|
||||
TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static,
|
||||
TUnbuiltResult: RequestResult<TUnbuiltResultMarker> + Send,
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static,
|
||||
TResult: Future<Output = Result<T, rspc::Error>> + Send + 'static,
|
||||
T: IntoLayerResult<TResultMarker> + Send + Serialize + specta::Type,
|
||||
{
|
||||
self.query(key, move |ctx, arg: LibraryArgs<TArg>| async move {
|
||||
let library = ctx
|
||||
.library_manager
|
||||
.get_ctx(arg.library_id)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::BadRequest,
|
||||
"You must specify a valid library to use this operation.".to_string(),
|
||||
)
|
||||
})?;
|
||||
self.query(key, move |t| {
|
||||
let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver);
|
||||
|
||||
resolver(ctx, arg.arg, library).await
|
||||
t(move |ctx, arg: LibraryArgs<TArg>| {
|
||||
let resolver = resolver.clone();
|
||||
async move {
|
||||
let library = ctx
|
||||
.library_manager
|
||||
.get_ctx(arg.library_id)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::BadRequest,
|
||||
"You must specify a valid library to use this operation."
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(resolver(ctx, arg.arg, library)
|
||||
.into_request_future()?
|
||||
.exec()
|
||||
.await?)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn library_mutation<T, TArg, TResult, TResultMarker>(
|
||||
fn library_mutation<
|
||||
TUnbuiltResolver,
|
||||
TUnbuiltResult,
|
||||
TUnbuiltResultMarker,
|
||||
TBuiltResolver,
|
||||
TArg,
|
||||
>(
|
||||
self,
|
||||
key: &'static str,
|
||||
resolver: fn(Ctx, TArg, LibraryContext) -> TResult,
|
||||
builder: impl FnOnce(
|
||||
UnbuiltProcedureBuilder<Ctx, TUnbuiltResolver>,
|
||||
) -> BuiltProcedureBuilder<TBuiltResolver>,
|
||||
) -> Self
|
||||
where
|
||||
TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send,
|
||||
TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static,
|
||||
TUnbuiltResult: RequestResult<TUnbuiltResultMarker> + Send,
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static,
|
||||
TResult: Future<Output = Result<T, rspc::Error>> + Send + 'static,
|
||||
T: IntoLayerResult<TResultMarker> + Send + Serialize + specta::Type,
|
||||
{
|
||||
self.mutation(key, move |ctx, arg: LibraryArgs<TArg>| async move {
|
||||
let library = ctx
|
||||
.library_manager
|
||||
.get_ctx(arg.library_id)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::BadRequest,
|
||||
"You must specify a valid library to use this operation.".to_string(),
|
||||
)
|
||||
})?;
|
||||
self.mutation(key, move |t| {
|
||||
let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver);
|
||||
|
||||
resolver(ctx, arg.arg, library).await
|
||||
t(move |ctx, arg: LibraryArgs<TArg>| {
|
||||
let resolver = resolver.clone();
|
||||
async move {
|
||||
let library = ctx
|
||||
.library_manager
|
||||
.get_ctx(arg.library_id)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::BadRequest,
|
||||
"You must specify a valid library to use this operation."
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(resolver(ctx, arg.arg, library)
|
||||
.into_request_future()?
|
||||
.exec()
|
||||
.await?)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn library_subscription<T, TArg, TResult>(
|
||||
fn library_subscription<TResolver, TArg, TStream, TResult>(
|
||||
self,
|
||||
key: &'static str,
|
||||
resolver: fn(Ctx, TArg, Uuid) -> T,
|
||||
builder: impl Fn(UnbuiltProcedureBuilder<Ctx, TResolver>) -> BuiltProcedureBuilder<TResolver>,
|
||||
) -> Self
|
||||
where
|
||||
TArg: DeserializeOwned + specta::Type + Send + 'static,
|
||||
T: Stream<Item = TResult> + Send + 'static,
|
||||
TArg: DeserializeOwned + specta::Type + 'static,
|
||||
TStream: Stream<Item = TResult> + Send + Sync + 'static,
|
||||
TResult: Serialize + specta::Type,
|
||||
TResolver: Fn(Ctx, TArg, Uuid) -> TStream + Send + Sync + 'static,
|
||||
{
|
||||
self.subscription(key, move |ctx, arg: LibraryArgs<TArg>| {
|
||||
// TODO: Make this fetch the library like the other functions. This needs upstream rspc work to be supported.
|
||||
// let library = ctx
|
||||
// .library_manager
|
||||
// .get_ctx(arg.library_id)
|
||||
// .await
|
||||
// .ok_or_else(|| {
|
||||
// rspc::Error::new(
|
||||
// ErrorCode::BadRequest,
|
||||
// "You must specify a valid library to use this operation.".to_string(),
|
||||
// )
|
||||
// })?;
|
||||
self.subscription(key, |t| {
|
||||
let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver);
|
||||
|
||||
resolver(ctx, arg.arg, arg.library_id)
|
||||
t(move |ctx, arg: LibraryArgs<TArg>| {
|
||||
// TODO(@Oscar): Upstream rspc work to allow this to work
|
||||
// let library = ctx
|
||||
// .library_manager
|
||||
// .get_ctx(arg.library_id)
|
||||
// .await
|
||||
// .ok_or_else(|| {
|
||||
// rspc::Error::new(
|
||||
// ErrorCode::BadRequest,
|
||||
// "You must specify a valid library to use this operation.".to_string(),
|
||||
// )
|
||||
// })?;
|
||||
|
||||
resolver(ctx, arg.arg, arg.library_id)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::volume::get_volumes;
|
||||
|
||||
use super::{Router, RouterBuilder};
|
||||
use super::RouterBuilder;
|
||||
|
||||
pub(crate) fn mount() -> RouterBuilder {
|
||||
<Router>::new().query("list", |_, _: ()| Ok(get_volumes()?))
|
||||
RouterBuilder::new().query("list", |t| t(|_, _: ()| Ok(get_volumes()?)))
|
||||
}
|
||||
|
||||
@@ -77,7 +77,11 @@ impl Node {
|
||||
"desktop=debug"
|
||||
.parse()
|
||||
.expect("Error invalid tracing directive!"),
|
||||
),
|
||||
), // .add_directive(
|
||||
// "rspc=debug"
|
||||
// .parse()
|
||||
// .expect("Error invalid tracing directive!"),
|
||||
// ),
|
||||
)
|
||||
.with(fmt::layer().with_filter(CONSOLE_LOG_FILTER))
|
||||
// .with(
|
||||
|
||||
@@ -29,7 +29,7 @@ pub async fn load_and_migrate(db_url: &str) -> Result<PrismaClient, MigrationErr
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
builder = builder.force_reset();
|
||||
builder = builder.accept_data_loss().force_reset();
|
||||
}
|
||||
|
||||
builder.await?;
|
||||
|
||||
@@ -13,10 +13,10 @@ resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
ffmpeg-sys-next = "5.1.1"
|
||||
thiserror = "1.0.35"
|
||||
thiserror = "1.0.37"
|
||||
webp = "0.2.2"
|
||||
tokio = { version = "1.21.1", features = ["fs", "rt"] }
|
||||
tokio = { version = "1.21.2", features = ["fs", "rt"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.21.1", features = ["fs", "rt", "macros"] }
|
||||
tokio = { version = "1.21.2", features = ["fs", "rt", "macros"] }
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rspc = { version = "0.1.2", features = ["uuid"] }
|
||||
rspc = { workspace = true, features = ["uuid"] }
|
||||
serde = "1.0.145"
|
||||
serde_json = "1.0.85"
|
||||
uhlc = "0.5.1"
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.1.0",
|
||||
"@tauri-apps/cli": "^1.1.1",
|
||||
"@types/babel__core": "^7.1.19",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/node": "^18.8.2",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss": "^8.4.17",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.0",
|
||||
"vite-plugin-solid": "^2.3.0"
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.4",
|
||||
"vite-plugin-solid": "^2.3.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rspc/client": "~0.1.2",
|
||||
"@rspc/solid": "~0.1.2",
|
||||
"@rspc/tauri": "~0.1.2",
|
||||
"@tanstack/solid-query": "4.7.1",
|
||||
"@tanstack/solid-query": "4.10.1",
|
||||
"clsx": "^1.2.1",
|
||||
"solid-js": "^1.4.7"
|
||||
"solid-js": "^1.5.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ tauri-build = { version = "1.1", features = [] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.1", features = ["api-all"] }
|
||||
rspc = { version = "0.1.2", features = ["tauri", "uuid"] }
|
||||
rspc = { workspace = true, features = ["tauri", "uuid"] }
|
||||
sd-sync = { path = "../.." }
|
||||
tokio = { version = "1.21.2", features = ["macros"] }
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
|
||||
12
package.json
12
package.json
@@ -17,19 +17,21 @@
|
||||
"docs": "pnpm --filter @sd/docs -- ",
|
||||
"client": "pnpm --filter @sd/client -- ",
|
||||
"server": "pnpm --filter @sd/server -- ",
|
||||
"prisma": "cd core && cargo prisma",
|
||||
"codegen": "cargo test api::tests::test_and_export_rspc_bindings -- --exact",
|
||||
"typecheck": "pnpm -r exec tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cspell/dict-rust": "^2.0.1",
|
||||
"@cspell/dict-typescript": "^2.0.1",
|
||||
"@evilmartians/lefthook": "^1.0.5",
|
||||
"@cspell/dict-typescript": "^2.0.2",
|
||||
"@evilmartians/lefthook": "^1.1.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
|
||||
"cspell": "^6.4.0",
|
||||
"markdown-link-check": "^3.10.2",
|
||||
"cspell": "^6.12.0",
|
||||
"markdown-link-check": "^3.10.3",
|
||||
"prettier": "^2.7.1",
|
||||
"turbo": "^1.5.5",
|
||||
"turbo-ignore": "^0.3.0",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"overrides": {
|
||||
"vite-plugin-svgr": "https://github.com/spacedriveapp/vite-plugin-svgr#cb4195b69849429cdb18d1f12381676bf9196a84",
|
||||
|
||||
@@ -17,23 +17,24 @@
|
||||
"preset": "scripts/jest/node"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rspc/client": "^0.0.6",
|
||||
"@rspc/react": "^0.0.6",
|
||||
"@rspc/client": "^0.1.2",
|
||||
"@rspc/react": "^0.1.2",
|
||||
"@sd/config": "workspace:*",
|
||||
"@tanstack/react-query": "^4.0.10",
|
||||
"@sd/interface": "workspace:*",
|
||||
"@tanstack/react-query": "^4.10.1",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"immer": "^9.0.15",
|
||||
"lodash": "^4.17.21",
|
||||
"valtio": "^1.7.0",
|
||||
"valtio-persist": "^1.0.2",
|
||||
"zustand": "4.0.0"
|
||||
"zustand": "4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/react": "^18.0.21",
|
||||
"scripts": "*",
|
||||
"tsconfig": "*",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
/* eslint-disable */
|
||||
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
|
||||
|
||||
export type Operations = {
|
||||
export type Procedures = {
|
||||
queries:
|
||||
{ key: ["files.readMetadata", LibraryArgs<number>], result: null } |
|
||||
{ key: ["getNode"], result: NodeState } |
|
||||
{ key: ["jobs.getHistory", LibraryArgs<null>], result: Array<JobReport> } |
|
||||
{ key: ["jobs.getRunning", LibraryArgs<null>], result: Array<JobReport> } |
|
||||
{ key: ["library.getStatistics", LibraryArgs<null>], result: Statistics } |
|
||||
{ key: ["library.list"], result: Array<LibraryConfigWrapped> } |
|
||||
{ key: ["locations.getById", LibraryArgs<number>], result: Location | null } |
|
||||
{ key: ["locations.getExplorerData", LibraryArgs<LocationExplorerArgs>], result: ExplorerData } |
|
||||
{ key: ["locations.indexer_rules.get", LibraryArgs<number>], result: IndexerRule } |
|
||||
{ key: ["locations.indexer_rules.list", LibraryArgs<null>], result: Array<IndexerRule> } |
|
||||
{ key: ["locations.list", LibraryArgs<null>], result: Array<{ id: number, pub_id: Array<number>, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } |
|
||||
{ key: ["tags.get", LibraryArgs<number>], result: Tag | null } |
|
||||
{ key: ["tags.getExplorerData", LibraryArgs<number>], result: ExplorerData } |
|
||||
{ key: ["tags.getForFile", LibraryArgs<number>], result: Array<Tag> } |
|
||||
{ key: ["tags.list", LibraryArgs<null>], result: Array<Tag> } |
|
||||
{ key: ["version"], result: string } |
|
||||
{ key: ["volumes.list"], result: Array<Volume> },
|
||||
{ key: "files.readMetadata", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "getNode", input: never, result: NodeState } |
|
||||
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: Array<JobReport> } |
|
||||
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
|
||||
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
|
||||
{ key: "library.list", input: never, result: Array<LibraryConfigWrapped> } |
|
||||
{ key: "locations.getById", input: LibraryArgs<number>, result: Location | null } |
|
||||
{ key: "locations.getExplorerData", input: LibraryArgs<LocationExplorerArgs>, result: ExplorerData } |
|
||||
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
|
||||
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: Array<IndexerRule> } |
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: Array<{ id: number, pub_id: Array<number>, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } |
|
||||
{ key: "tags.get", input: LibraryArgs<number>, result: Tag | null } |
|
||||
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
|
||||
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Array<Tag> } |
|
||||
{ key: "tags.list", input: LibraryArgs<null>, result: Array<Tag> } |
|
||||
{ key: "version", input: never, result: string } |
|
||||
{ key: "volumes.list", input: never, result: Array<Volume> },
|
||||
mutations:
|
||||
{ key: ["files.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["files.setFavorite", LibraryArgs<SetFavoriteArgs>], result: null } |
|
||||
{ key: ["files.setNote", LibraryArgs<SetNoteArgs>], result: null } |
|
||||
{ key: ["jobs.generateThumbsForLocation", LibraryArgs<GenerateThumbsForLocationArgs>], result: null } |
|
||||
{ key: ["jobs.identifyUniqueFiles", LibraryArgs<IdentifyUniqueFilesArgs>], result: null } |
|
||||
{ key: ["library.create", string], result: LibraryConfigWrapped } |
|
||||
{ key: ["library.delete", string], result: null } |
|
||||
{ key: ["library.edit", EditLibraryArgs], result: null } |
|
||||
{ key: ["locations.create", LibraryArgs<LocationCreateArgs>], result: null } |
|
||||
{ key: ["locations.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["locations.fullRescan", LibraryArgs<number>], result: null } |
|
||||
{ key: ["locations.indexer_rules.create", LibraryArgs<IndexerRuleCreateArgs>], result: IndexerRule } |
|
||||
{ key: ["locations.indexer_rules.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["locations.quickRescan", LibraryArgs<null>], result: null } |
|
||||
{ key: ["locations.update", LibraryArgs<LocationUpdateArgs>], result: null } |
|
||||
{ key: ["tags.assign", LibraryArgs<TagAssignArgs>], result: null } |
|
||||
{ key: ["tags.create", LibraryArgs<TagCreateArgs>], result: Tag } |
|
||||
{ key: ["tags.delete", LibraryArgs<number>], result: null } |
|
||||
{ key: ["tags.update", LibraryArgs<TagUpdateArgs>], result: null },
|
||||
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |
|
||||
{ key: "files.setNote", input: LibraryArgs<SetNoteArgs>, result: null } |
|
||||
{ key: "jobs.generateThumbsForLocation", input: LibraryArgs<GenerateThumbsForLocationArgs>, result: null } |
|
||||
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
|
||||
{ key: "library.create", input: string, result: LibraryConfigWrapped } |
|
||||
{ key: "library.delete", input: string, result: null } |
|
||||
{ key: "library.edit", input: EditLibraryArgs, result: null } |
|
||||
{ key: "locations.create", input: LibraryArgs<LocationCreateArgs>, result: null } |
|
||||
{ key: "locations.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.fullRescan", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.indexer_rules.create", input: LibraryArgs<IndexerRuleCreateArgs>, result: IndexerRule } |
|
||||
{ key: "locations.indexer_rules.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.quickRescan", input: LibraryArgs<null>, result: null } |
|
||||
{ key: "locations.update", input: LibraryArgs<LocationUpdateArgs>, result: null } |
|
||||
{ key: "tags.assign", input: LibraryArgs<TagAssignArgs>, result: null } |
|
||||
{ key: "tags.create", input: LibraryArgs<TagCreateArgs>, result: Tag } |
|
||||
{ key: "tags.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "tags.update", input: LibraryArgs<TagUpdateArgs>, result: null },
|
||||
subscriptions:
|
||||
{ key: ["invalidateQuery"], result: InvalidateOperationEvent } |
|
||||
{ key: ["jobs.newThumbnail", LibraryArgs<null>], result: string }
|
||||
{ key: "invalidateQuery", input: never, result: InvalidateOperationEvent } |
|
||||
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string }
|
||||
};
|
||||
|
||||
export interface ConfigMetadata { version: string | null }
|
||||
|
||||
@@ -22,6 +22,10 @@ export const LibraryContextProvider = ({
|
||||
return <CringeContext.Provider value={{ onNoLibrary }}>{children}</CringeContext.Provider>;
|
||||
};
|
||||
|
||||
export function getLibraryIdRaw(): string | null {
|
||||
return currentLibraryUuidStore.id;
|
||||
}
|
||||
|
||||
// this is a hook to get the current library loaded into the UI. It takes care of a bunch of invariants under the hood.
|
||||
export const useCurrentLibrary = () => {
|
||||
const currentLibraryUuid = useSnapshot(currentLibraryUuidStore).id;
|
||||
|
||||
@@ -1,120 +1,61 @@
|
||||
import { RSPCError } from '@rspc/client';
|
||||
import { ProcedureDef } from '@rspc/client';
|
||||
import { createReactQueryHooks } from '@rspc/react';
|
||||
import {
|
||||
QueryClient,
|
||||
UseInfiniteQueryOptions,
|
||||
UseInfiniteQueryResult,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
useMutation as _useMutation
|
||||
} from '@tanstack/react-query';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { LibraryArgs, Operations } from './core';
|
||||
import { useCurrentLibrary } from './index';
|
||||
import { LibraryArgs, Procedures } from './core';
|
||||
import { getLibraryIdRaw } from './index';
|
||||
|
||||
export const queryClient = new QueryClient();
|
||||
export const rspc = createReactQueryHooks<Operations>();
|
||||
export const rspc = createReactQueryHooks<Procedures>();
|
||||
|
||||
type NonLibraryQueries = Exclude<Operations['queries'], { key: [any, LibraryArgs<any>] }> &
|
||||
Extract<Operations['queries'], { key: [any] }>;
|
||||
type NonLibraryQuery<K extends string> = Extract<NonLibraryQueries, { key: [K] | [K, any] }>;
|
||||
type NonLibraryQueryKey = NonLibraryQueries['key'][0];
|
||||
type NonLibraryQueryResult<K extends NonLibraryQueryKey> = NonLibraryQuery<K>['result'];
|
||||
type NonLibraryProcedure<T extends keyof Procedures> =
|
||||
| Exclude<Procedures[T], { input: LibraryArgs<any> }>
|
||||
| Extract<Procedures[T], { input: never }>;
|
||||
|
||||
export function useBridgeQuery<K extends NonLibraryQueries['key']>(
|
||||
key: K,
|
||||
options?: UseQueryOptions<NonLibraryQueryResult<K[0]>, RSPCError>
|
||||
): UseQueryResult<NonLibraryQueryResult<K[0]>, RSPCError> {
|
||||
// @ts-ignore
|
||||
return rspc.useQuery(key, options);
|
||||
}
|
||||
|
||||
type LibraryQueries = Extract<Operations['queries'], { key: [string, LibraryArgs<any>] }>;
|
||||
type LibraryQuery<K extends string> = Extract<LibraryQueries, { key: [K, any] }>;
|
||||
type LibraryQueryKey = LibraryQueries['key'][0];
|
||||
type LibraryQueryArgs<K extends string> = LibraryQuery<K>['key'][1] extends LibraryArgs<infer A>
|
||||
? A
|
||||
: never;
|
||||
type LibraryQueryResult<K extends string> = LibraryQuery<K>['result'];
|
||||
|
||||
export function useLibraryQuery<K extends LibraryQueryKey>(
|
||||
key: LibraryQueryArgs<K> extends null | undefined ? [K] : [K, LibraryQueryArgs<K>],
|
||||
options?: UseQueryOptions<LibraryQueryResult<K>, RSPCError>
|
||||
): UseQueryResult<LibraryQueryResult<K>, RSPCError> {
|
||||
const { library } = useCurrentLibrary();
|
||||
|
||||
if (!library?.uuid) throw new Error(`Attempted to do library query with no library set!`);
|
||||
// @ts-ignore
|
||||
return rspc.useQuery(
|
||||
// @ts-ignore
|
||||
[key[0], { library_id: library?.uuid || '', arg: key[1] || null }],
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
export function useInfiniteLibraryQuery<K extends LibraryQueryKey>(
|
||||
key: LibraryQueryArgs<K> extends null | undefined ? [K] : [K, LibraryQueryArgs<K>],
|
||||
options?: UseInfiniteQueryOptions<LibraryQueryResult<K>, RSPCError>
|
||||
): UseInfiniteQueryResult<LibraryQueryResult<K>, RSPCError> {
|
||||
const { library } = useCurrentLibrary();
|
||||
|
||||
if (!library?.uuid) throw new Error(`Attempted to do library query with no library set!`);
|
||||
// @ts-ignore
|
||||
return rspc.useInfiniteQuery(
|
||||
// @ts-ignore
|
||||
[key[0], { library_id: library?.uuid || '', arg: key[1] || null }],
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
type LibraryMutations = Extract<Operations['mutations'], { key: [string, LibraryArgs<any>] }>;
|
||||
type LibraryMutation<K extends LibraryMutationKey> = Extract<LibraryMutations, { key: [K, any] }>;
|
||||
type LibraryMutationKey = LibraryMutations['key'][0];
|
||||
type LibraryMutationArgs<K extends LibraryMutationKey> =
|
||||
LibraryMutation<K>['key'][1] extends LibraryArgs<infer A> ? A : never;
|
||||
type LibraryMutationResult<K extends LibraryMutationKey> = LibraryMutation<K>['result'];
|
||||
export function useLibraryMutation<K extends LibraryMutationKey>(
|
||||
key: K,
|
||||
options?: UseMutationOptions<LibraryMutationResult<K>, RSPCError>
|
||||
) {
|
||||
const ctx = rspc.useContext();
|
||||
const { library } = useCurrentLibrary();
|
||||
if (!library?.uuid) throw new Error(`Attempted to do library query with no library set!`);
|
||||
|
||||
// @ts-ignore
|
||||
return _useMutation<LibraryMutationResult<K>, RSPCError, LibraryMutationArgs<K>>(
|
||||
async (data) =>
|
||||
ctx.client.mutation([key, { library_id: library?.uuid || '', arg: data || null }]),
|
||||
{
|
||||
...options,
|
||||
context: rspc.ReactQueryContext
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
type NonLibraryMutations = Exclude<Operations['mutations'], { key: [any, LibraryArgs<any>] }>;
|
||||
type NonLibraryMutation<K extends NonLibraryMutationKey> = Extract<
|
||||
NonLibraryMutations,
|
||||
{ key: [K] | [K, any] }
|
||||
type LibraryProcedures<T extends keyof Procedures> = Exclude<
|
||||
Extract<Procedures[T], { input: LibraryArgs<any> }>,
|
||||
{ input: never }
|
||||
>;
|
||||
type NonLibraryMutationKey = NonLibraryMutations['key'][0];
|
||||
type NonLibraryMutationArgs<K extends NonLibraryMutationKey> = NonLibraryMutation<K>['key'][1];
|
||||
type NonLibraryMutationResult<K extends NonLibraryMutationKey> = NonLibraryMutation<K>['result'];
|
||||
export function useBridgeMutation<K extends NonLibraryMutationKey>(
|
||||
key: K,
|
||||
options?: UseMutationOptions<NonLibraryMutationResult<K>, RSPCError>
|
||||
): UseMutationResult<NonLibraryMutationResult<K>, RSPCError, NonLibraryMutationArgs<K>> {
|
||||
// @ts-ignore
|
||||
return rspc.useMutation(key, options);
|
||||
}
|
||||
|
||||
type MoreConstrainedQueries<T extends ProcedureDef> = T extends any
|
||||
? T['input'] extends LibraryArgs<infer E>
|
||||
? {
|
||||
key: T['key'];
|
||||
input: E;
|
||||
result: T['result'];
|
||||
}
|
||||
: never
|
||||
: never;
|
||||
|
||||
export const useBridgeQuery = rspc.customQuery<NonLibraryProcedure<'queries'>>(
|
||||
(keyAndInput) => keyAndInput as any
|
||||
);
|
||||
|
||||
export const useBridgeMutation = rspc.customMutation<NonLibraryProcedure<'mutations'>>(
|
||||
(keyAndInput) => keyAndInput
|
||||
);
|
||||
|
||||
export const useLibraryQuery = rspc.customQuery<
|
||||
MoreConstrainedQueries<LibraryProcedures<'queries'>>
|
||||
>((keyAndInput) => {
|
||||
const library_id = getLibraryIdRaw();
|
||||
if (library_id === null) throw new Error('Attempted to do library query with no library set!');
|
||||
return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }];
|
||||
});
|
||||
|
||||
export const useLibraryMutation = rspc.customMutation<
|
||||
MoreConstrainedQueries<LibraryProcedures<'mutations'>>
|
||||
>((keyAndInput) => {
|
||||
const library_id = getLibraryIdRaw();
|
||||
if (library_id === null) throw new Error('Attempted to do library query with no library set!');
|
||||
return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }];
|
||||
});
|
||||
|
||||
export function useInvalidateQuery() {
|
||||
const context = rspc.useContext();
|
||||
rspc.useSubscription(['invalidateQuery'], {
|
||||
onNext: (invalidateOperation) => {
|
||||
let key = [invalidateOperation.key];
|
||||
onData: (invalidateOperation) => {
|
||||
const key = [invalidateOperation.key];
|
||||
if (invalidateOperation.arg !== null) {
|
||||
key.concat(invalidateOperation.arg);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
"eslint-react.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"eslint": "^8.21.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||
"@typescript-eslint/parser": "^5.30.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
||||
"@typescript-eslint/parser": "^5.39.0",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,80 +15,81 @@
|
||||
"lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.11",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@heroicons/react": "^2.0.10",
|
||||
"@fontsource/inter": "^4.5.13",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@radix-ui/react-progress": "^0.1.4",
|
||||
"@radix-ui/react-slider": "^0.1.4",
|
||||
"@radix-ui/react-progress": "^1.0.0",
|
||||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-tabs": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"@sd/assets": "workspace:*",
|
||||
"@sd/client": "workspace:*",
|
||||
"@sd/ui": "workspace:*",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tanstack/react-query": "^4.2.3",
|
||||
"@tanstack/react-query-devtools": "^4.0.10",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-query": "^4.10.1",
|
||||
"@tanstack/react-query-devtools": "^4.10.1",
|
||||
"@tanstack/react-virtual": "3.0.0-beta.18",
|
||||
"@vitejs/plugin-react": "^2.0.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"byte-size": "^8.1.0",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.29.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"dayjs": "^1.11.5",
|
||||
"immer": "^9.0.15",
|
||||
"jotai": "^1.7.6",
|
||||
"jotai": "^1.8.4",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-countup": "^6.3.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-countup": "^6.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.33.1",
|
||||
"react-hook-form": "^7.36.1",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-loading-icons": "^1.1.0",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-portal": "^4.2.2",
|
||||
"react-query": "^4.0.0",
|
||||
"react-router": "6.3.0",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-scrollbars-custom": "^4.1.0",
|
||||
"react-query": "^3.39.2",
|
||||
"react-router": "6.4.2",
|
||||
"react-router-dom": "6.4.2",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-spline": "^1.2.1",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"react-virtuoso": "^2.16.5",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^2.19.1",
|
||||
"rooks": "^5.14.0",
|
||||
"tailwindcss": "^3.1.6",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"use-count-up": "^3.0.1",
|
||||
"use-debounce": "^8.0.3",
|
||||
"use-debounce": "^8.0.4",
|
||||
"valtio": "^1.7.0",
|
||||
"valtio-persist": "^1.0.2",
|
||||
"zod": "^3.18.0",
|
||||
"zustand": "4.0.0"
|
||||
"zod": "^3.19.1",
|
||||
"zustand": "4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sd/config": "workspace:*",
|
||||
"@types/babel-core": "^6.25.7",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/node": "^18.6.1",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/node": "^18.8.2",
|
||||
"@types/pretty-bytes": "^5.2.0",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/tailwindcss": "^3.1.0",
|
||||
"@vitejs/plugin-react": "^1.3.1",
|
||||
"concurrently": "^7.3.0",
|
||||
"concurrently": "^7.4.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.3",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.4",
|
||||
"vite-plugin-svgr": "^2.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import '@fontsource/inter/variable.css';
|
||||
import { LibraryContextProvider, queryClient } from '@sd/client';
|
||||
import { QueryClientProvider, 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';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { MemoryRouter, useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -9,6 +13,10 @@ import { AppRouter } from './AppRouter';
|
||||
import { ErrorFallback } from './ErrorFallback';
|
||||
import './style.scss';
|
||||
|
||||
dayjs.extend(advancedFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(duration);
|
||||
|
||||
export default function SpacedriveInterface() {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCurrentLibrary } from '@sd/client';
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { Sidebar } from './components/layout/Sidebar';
|
||||
@@ -29,7 +30,9 @@ export function AppLayout() {
|
||||
>
|
||||
<Sidebar />
|
||||
<div className="relative flex w-full h-screen max-h-screen bg-white dark:bg-gray-650">
|
||||
<Outlet />
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
import { AppLayout } from './AppLayout';
|
||||
import { useKeybindHandler } from './hooks/useKeyboardHandler';
|
||||
import { useCurrentLibrary, useInvalidateQuery } from '@sd/client';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { AppLayout } from './AppLayout';
|
||||
import { NotFound } from './NotFound';
|
||||
import OnboardingScreen from './components/onboarding/Onboarding';
|
||||
import { useKeybindHandler } from './hooks/useKeyboardHandler';
|
||||
import { ContentScreen } from './screens/Content';
|
||||
import { DebugScreen } from './screens/Debug';
|
||||
import { LocationExplorer } from './screens/LocationExplorer';
|
||||
import { OverviewScreen } from './screens/Overview';
|
||||
import { PhotosScreen } from './screens/Photos';
|
||||
import { TagExplorer } from './screens/TagExplorer';
|
||||
import { SettingsScreen } from './screens/settings/Settings';
|
||||
import AppearanceSettings from './screens/settings/client/AppearanceSettings';
|
||||
import ExtensionSettings from './screens/settings/client/ExtensionsSettings';
|
||||
import GeneralSettings from './screens/settings/client/GeneralSettings';
|
||||
import KeybindingSettings from './screens/settings/client/KeybindingSettings';
|
||||
import PrivacySettings from './screens/settings/client/PrivacySettings';
|
||||
import AboutSpacedrive from './screens/settings/info/AboutSpacedrive';
|
||||
import Changelog from './screens/settings/info/Changelog';
|
||||
import Support from './screens/settings/info/Support';
|
||||
import ContactsSettings from './screens/settings/library/ContactsSettings';
|
||||
import KeysSettings from './screens/settings/library/KeysSetting';
|
||||
import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings';
|
||||
import LocationSettings from './screens/settings/library/LocationSettings';
|
||||
import NodesSettings from './screens/settings/library/NodesSettings';
|
||||
import SecuritySettings from './screens/settings/library/SecuritySettings';
|
||||
import SharingSettings from './screens/settings/library/SharingSettings';
|
||||
import SyncSettings from './screens/settings/library/SyncSettings';
|
||||
import TagsSettings from './screens/settings/library/TagsSettings';
|
||||
import ExperimentalSettings from './screens/settings/node/ExperimentalSettings';
|
||||
import LibrarySettings from './screens/settings/node/LibrariesSettings';
|
||||
import P2PSettings from './screens/settings/node/P2PSettings';
|
||||
const DebugScreen = lazy(() => import('./screens/Debug'));
|
||||
const SettingsScreen = lazy(() => import('./screens/settings/Settings'));
|
||||
const TagExplorer = lazy(() => import('./screens/TagExplorer'));
|
||||
const PhotosScreen = lazy(() => import('./screens/Photos'));
|
||||
const OverviewScreen = lazy(() => import('./screens/Overview'));
|
||||
const ContentScreen = lazy(() => import('./screens/Content'));
|
||||
const LocationExplorer = lazy(() => import('./screens/LocationExplorer'));
|
||||
const OnboardingScreen = lazy(() => import('./components/onboarding/Onboarding'));
|
||||
const NotFound = lazy(() => import('./NotFound'));
|
||||
|
||||
const AppearanceSettings = lazy(() => import('./screens/settings/client/AppearanceSettings'));
|
||||
const ExtensionSettings = lazy(() => import('./screens/settings/client/ExtensionsSettings'));
|
||||
const GeneralSettings = lazy(() => import('./screens/settings/client/GeneralSettings'));
|
||||
const KeybindingSettings = lazy(() => import('./screens/settings/client/KeybindingSettings'));
|
||||
const PrivacySettings = lazy(() => import('./screens/settings/client/PrivacySettings'));
|
||||
const AboutSpacedrive = lazy(() => import('./screens/settings/info/AboutSpacedrive'));
|
||||
const Changelog = lazy(() => import('./screens/settings/info/Changelog'));
|
||||
const Support = lazy(() => import('./screens/settings/info/Support'));
|
||||
const ContactsSettings = lazy(() => import('./screens/settings/library/ContactsSettings'));
|
||||
const KeysSettings = lazy(() => import('./screens/settings/library/KeysSetting'));
|
||||
const LibraryGeneralSettings = lazy(
|
||||
() => import('./screens/settings/library/LibraryGeneralSettings')
|
||||
);
|
||||
const LocationSettings = lazy(() => import('./screens/settings/library/LocationSettings'));
|
||||
const NodesSettings = lazy(() => import('./screens/settings/library/NodesSettings'));
|
||||
const SecuritySettings = lazy(() => import('./screens/settings/library/SecuritySettings'));
|
||||
const SharingSettings = lazy(() => import('./screens/settings/library/SharingSettings'));
|
||||
const SyncSettings = lazy(() => import('./screens/settings/library/SyncSettings'));
|
||||
const TagsSettings = lazy(() => import('./screens/settings/library/TagsSettings'));
|
||||
const ExperimentalSettings = lazy(() => import('./screens/settings/node/ExperimentalSettings'));
|
||||
const LibrarySettings = lazy(() => import('./screens/settings/node/LibrariesSettings'));
|
||||
const P2PSettings = lazy(() => import('./screens/settings/node/P2PSettings'));
|
||||
|
||||
export function AppRouter() {
|
||||
const { library } = useCurrentLibrary();
|
||||
@@ -40,56 +44,60 @@ export function AppRouter() {
|
||||
useInvalidateQuery();
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="onboarding" element={<OnboardingScreen />} />
|
||||
<Route element={<AppLayout />}>
|
||||
{/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */}
|
||||
{library === undefined ? (
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<h1 className="p-4 text-white">Please select or create a library in the sidebar.</h1>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Route index element={<Navigate to="/overview" />} />
|
||||
<Route path="overview" element={<OverviewScreen />} />
|
||||
<Route path="content" element={<ContentScreen />} />
|
||||
<Route path="photos" element={<PhotosScreen />} />
|
||||
<Route path="debug" element={<DebugScreen />} />
|
||||
<Route path={'settings'} element={<SettingsScreen />}>
|
||||
<Route index element={<GeneralSettings />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="appearance" element={<AppearanceSettings />} />
|
||||
<Route path="keybindings" element={<KeybindingSettings />} />
|
||||
<Route path="extensions" element={<ExtensionSettings />} />
|
||||
<Route path="p2p" element={<P2PSettings />} />
|
||||
<Route path="contacts" element={<ContactsSettings />} />
|
||||
<Route path="experimental" element={<ExperimentalSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
<Route path="libraries" element={<LibrarySettings />} />
|
||||
<Route path="security" element={<SecuritySettings />} />
|
||||
<Route path="locations" element={<LocationSettings />} />
|
||||
<Route path="sharing" element={<SharingSettings />} />
|
||||
<Route path="sync" element={<SyncSettings />} />
|
||||
<Route path="tags" element={<TagsSettings />} />
|
||||
<Route path="library" element={<LibraryGeneralSettings />} />
|
||||
<Route path="locations" element={<LocationSettings />} />
|
||||
<Route path="tags" element={<TagsSettings />} />
|
||||
<Route path="nodes" element={<NodesSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
<Route path="privacy" element={<PrivacySettings />} />
|
||||
<Route path="about" element={<AboutSpacedrive />} />
|
||||
<Route path="changelog" element={<Changelog />} />
|
||||
<Route path="support" element={<Support />} />
|
||||
</Route>
|
||||
<Route path="location/:id" element={<LocationExplorer />} />
|
||||
<Route path="tag/:id" element={<TagExplorer />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</>
|
||||
)}
|
||||
</Route>
|
||||
</Routes>
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<Routes>
|
||||
<Route path="onboarding" element={<OnboardingScreen />} />
|
||||
<Route element={<AppLayout />}>
|
||||
{/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */}
|
||||
{library === undefined ? (
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<h1 className="text-white p-4">
|
||||
Please select or create a library in the sidebar.
|
||||
</h1>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Route index element={<Navigate to="/overview" />} />
|
||||
<Route path="overview" element={<OverviewScreen />} />
|
||||
<Route path="content" element={<ContentScreen />} />
|
||||
<Route path="photos" element={<PhotosScreen />} />
|
||||
<Route path="debug" element={<DebugScreen />} />
|
||||
<Route path={'settings'} element={<SettingsScreen />}>
|
||||
<Route index element={<GeneralSettings />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="appearance" element={<AppearanceSettings />} />
|
||||
<Route path="keybindings" element={<KeybindingSettings />} />
|
||||
<Route path="extensions" element={<ExtensionSettings />} />
|
||||
<Route path="p2p" element={<P2PSettings />} />
|
||||
<Route path="contacts" element={<ContactsSettings />} />
|
||||
<Route path="experimental" element={<ExperimentalSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
<Route path="libraries" element={<LibrarySettings />} />
|
||||
<Route path="security" element={<SecuritySettings />} />
|
||||
<Route path="locations" element={<LocationSettings />} />
|
||||
<Route path="sharing" element={<SharingSettings />} />
|
||||
<Route path="sync" element={<SyncSettings />} />
|
||||
<Route path="tags" element={<TagsSettings />} />
|
||||
<Route path="library" element={<LibraryGeneralSettings />} />
|
||||
<Route path="locations" element={<LocationSettings />} />
|
||||
<Route path="tags" element={<TagsSettings />} />
|
||||
<Route path="nodes" element={<NodesSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
<Route path="privacy" element={<PrivacySettings />} />
|
||||
<Route path="about" element={<AboutSpacedrive />} />
|
||||
<Route path="changelog" element={<Changelog />} />
|
||||
<Route path="support" element={<Support />} />
|
||||
</Route>
|
||||
<Route path="location/:id" element={<LocationExplorer />} />
|
||||
<Route path="tag/:id" element={<TagExplorer />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</>
|
||||
)}
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@sd/ui';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export function NotFound() {
|
||||
export default function NotFound() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function Explorer(props: Props) {
|
||||
const { library } = useCurrentLibrary();
|
||||
|
||||
rspc.useSubscription(['jobs.newThumbnail', { library_id: library!.uuid, arg: null }], {
|
||||
onNext: (cas_id) => {
|
||||
onData: (cas_id) => {
|
||||
expStore.addNewThumbnail(cas_id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,14 +21,14 @@ import { useSnapshot } from 'valtio';
|
||||
|
||||
const AssignTagMenuItems = (props: { objectId: number }) => {
|
||||
const tags = useLibraryQuery(['tags.list'], { suspense: true });
|
||||
const tagsForFile = useLibraryQuery(['tags.getForFile', props.objectId], { suspense: true });
|
||||
const tagsForObject = useLibraryQuery(['tags.getForObject', props.objectId], { suspense: true });
|
||||
|
||||
const { mutate: assignTag } = useLibraryMutation('tags.assign');
|
||||
|
||||
return (
|
||||
<>
|
||||
{tags.data?.map((tag) => {
|
||||
const active = !!tagsForFile.data?.find((t) => t.id === tag.id);
|
||||
const active = !!tagsForObject.data?.find((t) => t.id === tag.id);
|
||||
|
||||
return (
|
||||
<CM.Item
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Folder } from '../icons/Folder';
|
||||
import { isObject, isPath } from './utils';
|
||||
import videoSvg from '@sd/assets/svgs/video.svg';
|
||||
import zipSvg from '@sd/assets/svgs/zip.svg';
|
||||
import { getExplorerStore, useExplorerStore, usePlatform } from '@sd/client';
|
||||
import { getExplorerStore, usePlatform } from '@sd/client';
|
||||
import { useExplorerStore } from '@sd/client';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { Suspense, lazy, useMemo } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import icons from '../../assets/icons';
|
||||
import { Folder } from '../icons/Folder';
|
||||
import { isObject, isPath } from './utils';
|
||||
|
||||
interface Props {
|
||||
data: ExplorerItem;
|
||||
size: number;
|
||||
@@ -19,63 +19,68 @@ interface Props {
|
||||
kind?: 'video' | 'image' | 'audio' | 'zip' | 'other';
|
||||
}
|
||||
|
||||
const icons = import.meta.glob('../../../../assets/icons/*.svg');
|
||||
|
||||
export default function FileThumb({ data, ...props }: Props) {
|
||||
const platform = usePlatform();
|
||||
// const store = useExplorerStore();
|
||||
const store = useExplorerStore();
|
||||
|
||||
if (isPath(data) && data.is_dir)
|
||||
return <Folder className={props.iconClassNames} size={props.size * 0.7} />;
|
||||
const Icon = useMemo(() => {
|
||||
const icon = icons[`../../../../assets/icons/${data.extension as any}.svg`];
|
||||
|
||||
const Icon = icon
|
||||
? lazy(() => icon().then((v) => ({ default: (v as any).ReactComponent })))
|
||||
: undefined;
|
||||
return Icon;
|
||||
}, [data.extension]);
|
||||
|
||||
if (isPath(data) && data.is_dir) return <Folder size={props.size * 0.7} />;
|
||||
|
||||
const cas_id = isObject(data) ? data.cas_id : data.object?.cas_id;
|
||||
|
||||
if (cas_id) {
|
||||
// this won't work
|
||||
const new_thumbnail = !!getExplorerStore().newThumbnails[cas_id];
|
||||
if (!cas_id) return <div></div>;
|
||||
|
||||
const has_thumbnail = isObject(data)
|
||||
? data.has_thumbnail
|
||||
: isPath(data)
|
||||
? data.object?.has_thumbnail
|
||||
: new_thumbnail;
|
||||
const has_thumbnail = isObject(data)
|
||||
? data.has_thumbnail
|
||||
: isPath(data)
|
||||
? data.object?.has_thumbnail
|
||||
: !!store.newThumbnails[cas_id];
|
||||
|
||||
const url = platform.getThumbnailUrlById(cas_id);
|
||||
const url = platform.getThumbnailUrlById(cas_id);
|
||||
|
||||
if (has_thumbnail && url)
|
||||
return (
|
||||
if (has_thumbnail && url)
|
||||
return (
|
||||
<img
|
||||
style={props.style}
|
||||
decoding="async"
|
||||
// width={props.size}
|
||||
className={clsx('pointer-events-none z-90', props.className)}
|
||||
src={url}
|
||||
/>
|
||||
);
|
||||
|
||||
if (props.kind === 'video') {
|
||||
return (
|
||||
<div className="">
|
||||
<img
|
||||
style={props.style}
|
||||
decoding="async"
|
||||
// width={props.size}
|
||||
className={clsx('pointer-events-none', props.className)}
|
||||
src={url}
|
||||
src={videoSvg}
|
||||
className={clsx('w-full overflow-hidden h-full', props.iconClassNames)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (props.kind === 'video') {
|
||||
return (
|
||||
<div className="">
|
||||
<img
|
||||
src={videoSvg}
|
||||
className={clsx('w-full overflow-hidden h-full', props.iconClassNames)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (props.kind === 'zip') {
|
||||
return (
|
||||
<div className="">
|
||||
<img src={zipSvg} className={clsx('w-full overflow-hidden h-full')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (props.kind === 'zip') {
|
||||
return (
|
||||
<div className="">
|
||||
<img src={zipSvg} className={clsx('w-full overflow-hidden h-full')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = icons[data.extension as keyof typeof icons];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ width: props.size * 0.8, height: props.size * 0.8 }}
|
||||
className={clsx('relative m-auto transition duration-200 ', props.iconClassNames)}
|
||||
className="relative m-auto transition duration-200 "
|
||||
>
|
||||
<svg
|
||||
// BACKGROUND
|
||||
@@ -89,10 +94,12 @@ export default function FileThumb({ data, ...props }: Props) {
|
||||
</svg>
|
||||
{Icon && (
|
||||
<div className="absolute flex flex-col items-center justify-center w-full h-full mt-0.5 ">
|
||||
<Icon
|
||||
className={clsx('w-full h-full ')}
|
||||
style={{ width: props.size * 0.45, height: props.size * 0.45 }}
|
||||
/>
|
||||
<Suspense fallback={<></>}>
|
||||
<Icon
|
||||
className={clsx('w-full h-full ')}
|
||||
style={{ width: props.size * 0.45, height: props.size * 0.45 }}
|
||||
/>
|
||||
</Suspense>
|
||||
<span className="text-xs font-bold text-center uppercase cursor-default text-gray-450">
|
||||
{data.extension}
|
||||
</span>
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import { ShareIcon } from '@heroicons/react/24/solid';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { ExplorerContext, ExplorerItem, FilePath, Location, Object } from '@sd/client';
|
||||
import { Button, TextArea } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import moment from 'moment';
|
||||
import { Link } from 'phosphor-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import types from '../../constants/file-types.json';
|
||||
// import types from '../../constants/file-types.json';
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
import FileThumb from './FileThumb';
|
||||
import { Divider } from './inspector/Divider';
|
||||
@@ -15,6 +6,15 @@ import FavoriteButton from './inspector/FavoriteButton';
|
||||
import { MetaItem } from './inspector/MetaItem';
|
||||
import Note from './inspector/Note';
|
||||
import { isObject } from './utils';
|
||||
import { ShareIcon } from '@heroicons/react/24/solid';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { ExplorerContext, ExplorerItem } from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { Link } from 'phosphor-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
context?: ExplorerContext;
|
||||
@@ -22,6 +22,11 @@ interface Props {
|
||||
}
|
||||
|
||||
export const Inspector = (props: Props) => {
|
||||
const { data: types } = useQuery(
|
||||
['_file-types'],
|
||||
() => import('../../constants/file-types.json')
|
||||
);
|
||||
|
||||
const is_dir = props.data?.type === 'Path' ? props.data.is_dir : false;
|
||||
|
||||
const objectData = props.data ? (isObject(props.data) ? props.data : props.data.object) : null;
|
||||
@@ -36,120 +41,118 @@ export const Inspector = (props: Props) => {
|
||||
}, [props.data?.id]);
|
||||
|
||||
// this is causing LAG
|
||||
const { data: tags } = useLibraryQuery(['tags.getForFile', objectData?.id || -1], {
|
||||
const tags = useLibraryQuery(['tags.getForObject', objectData?.id || -1], {
|
||||
enabled: readyToFetch
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-2 pt-0.5 max-h-screen custom-scroll inspector-scroll pr-1 m-1 overflow-x-hidden">
|
||||
<div className="">
|
||||
{!!props.data && (
|
||||
<>
|
||||
<div className="flex items-center justify-center w-full py-4 overflow-hidden rounded-md">
|
||||
<FileThumb
|
||||
iconClassNames="!my-10"
|
||||
size={230}
|
||||
className="!m-0 flex flex-shrink flex-grow-0"
|
||||
data={props.data}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full pt-0.5 pb-4 overflow-hidden select-text">
|
||||
<h3 className="flex mt-3 ml-3 text-base font-bold">
|
||||
{props.data?.name}
|
||||
{props.data?.extension && `.${props.data.extension}`}
|
||||
</h3>
|
||||
{objectData && (
|
||||
<div className="flex flex-row m-3 space-x-2">
|
||||
<Tooltip label="Favorite">
|
||||
<FavoriteButton data={objectData} />
|
||||
</Tooltip>
|
||||
<Tooltip label="Share">
|
||||
<Button size="sm" noPadding>
|
||||
<ShareIcon className="w-[18px] h-[18px]" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Link">
|
||||
<Button size="sm" noPadding>
|
||||
<Link className="w-[18px] h-[18px]" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{!!tags?.length && (
|
||||
<>
|
||||
<Divider />
|
||||
<MetaItem
|
||||
// title="Tags"
|
||||
value={
|
||||
<div className="flex flex-wrap mt-1.5 gap-1.5">
|
||||
{tags?.map((tag) => (
|
||||
<div
|
||||
// onClick={() => setSelectedTag(tag.id === selectedTag ? null : tag.id)}
|
||||
key={tag.id}
|
||||
className={clsx(
|
||||
'flex items-center rounded px-1.5 py-0.5'
|
||||
// selectedTag === tag.id && 'ring'
|
||||
)}
|
||||
style={{ backgroundColor: tag.color + 'CC' }}
|
||||
>
|
||||
<span className="text-xs text-white drop-shadow-md">{tag.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{props.context?.type == 'Location' && props.data?.type === 'Path' && (
|
||||
<>
|
||||
<Divider />
|
||||
<MetaItem
|
||||
title="URI"
|
||||
value={`${props.context.local_path}/${props.data.materialized_path}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
<MetaItem
|
||||
title="Date Created"
|
||||
value={moment(props.data?.date_created).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
/>
|
||||
<Divider />
|
||||
<MetaItem
|
||||
title="Date Indexed"
|
||||
value={moment(props.data?.date_indexed).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
/>
|
||||
{!is_dir && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className="flex flex-row items-center px-3 py-2 meta-item">
|
||||
{props.data?.extension && (
|
||||
<span className="inline px-1 mr-1 text-xs font-bold uppercase bg-gray-500 rounded-md text-gray-150">
|
||||
{props.data?.extension}
|
||||
</span>
|
||||
)}
|
||||
<p className="text-xs text-gray-600 break-all truncate dark:text-gray-300">
|
||||
{props.data?.extension
|
||||
? //@ts-ignore
|
||||
types[props.data.extension.toUpperCase()]?.descriptions.join(' / ')
|
||||
: 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
{objectData && (
|
||||
<>
|
||||
<Note data={objectData} />
|
||||
<Divider />
|
||||
{objectData.cas_id && (
|
||||
<MetaItem title="Unique Content ID" value={objectData.cas_id} />
|
||||
)}
|
||||
</>
|
||||
<div className="p-2 pr-1 overflow-x-hidden custom-scroll inspector-scroll pb-[55px]">
|
||||
{!!props.data && (
|
||||
<>
|
||||
<div className="flex bg-black items-center justify-center w-full h-64 mb-[10px] overflow-hidden rounded-lg ">
|
||||
<FileThumb
|
||||
iconClassNames="!my-10"
|
||||
size={230}
|
||||
className="!m-0 flex flex-shrink flex-grow-0"
|
||||
data={props.data}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full pt-0.5 pb-4 overflow-hidden bg-white rounded-lg shadow select-text dark:shadow-gray-700 dark:bg-gray-550 dark:bg-opacity-40">
|
||||
<h3 className="pt-3 pl-3 text-base font-bold">
|
||||
{props.data?.name}
|
||||
{props.data?.extension && `.${props.data.extension}`}
|
||||
</h3>
|
||||
{objectData && (
|
||||
<div className="flex flex-row m-3 space-x-2">
|
||||
<Tooltip label="Favorite">
|
||||
<FavoriteButton data={objectData} />
|
||||
</Tooltip>
|
||||
<Tooltip label="Share">
|
||||
<Button size="sm" noPadding>
|
||||
<ShareIcon className="w-[18px] h-[18px]" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Link">
|
||||
<Button size="sm" noPadding>
|
||||
<Link className="w-[18px] h-[18px]" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{tags?.data && tags.data.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<MetaItem
|
||||
// title="Tags"
|
||||
value={
|
||||
<div className="flex flex-wrap mt-1.5 gap-1.5">
|
||||
{tags?.data?.map((tag) => (
|
||||
<div
|
||||
// onClick={() => setSelectedTag(tag.id === selectedTag ? null : tag.id)}
|
||||
key={tag.id}
|
||||
className={clsx(
|
||||
'flex items-center rounded px-1.5 py-0.5'
|
||||
// selectedTag === tag.id && 'ring'
|
||||
)}
|
||||
style={{ backgroundColor: tag.color + 'CC' }}
|
||||
>
|
||||
<span className="text-xs text-white drop-shadow-md">{tag.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{props.context?.type == 'Location' && props.data?.type === 'Path' && (
|
||||
<>
|
||||
<Divider />
|
||||
<MetaItem
|
||||
title="URI"
|
||||
value={`${props.context.local_path}/${props.data.materialized_path}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
<MetaItem
|
||||
title="Date Created"
|
||||
value={dayjs(props.data?.date_created).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
/>
|
||||
<Divider />
|
||||
<MetaItem
|
||||
title="Date Indexed"
|
||||
value={dayjs(props.data?.date_indexed).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
/>
|
||||
{!is_dir && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className="flex flex-row items-center px-3 py-2 meta-item">
|
||||
{props.data?.extension && (
|
||||
<span className="inline px-1 mr-1 text-xs font-bold uppercase bg-gray-500 rounded-md text-gray-150">
|
||||
{props.data?.extension}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 break-all truncate dark:text-gray-300">
|
||||
{props.data?.extension
|
||||
? //@ts-ignore
|
||||
types[props.data.extension.toUpperCase()]?.descriptions.join(' / ')
|
||||
: 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
{objectData && (
|
||||
<>
|
||||
<Note data={objectData} />
|
||||
<Divider />
|
||||
{objectData.cas_id && (
|
||||
<MetaItem title="Unique Content ID" value={objectData.cas_id} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client';
|
||||
import { ExplorerContext, ExplorerItem, FilePath } from '@sd/client';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useKey, useOnWindowResize, useWindowSize } from 'rooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import FileItem from './FileItem';
|
||||
import FileRow from './FileRow';
|
||||
import { isPath } from './utils';
|
||||
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client';
|
||||
import { ExplorerContext, ExplorerItem } from '@sd/client';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useKey, useOnWindowResize } from 'rooks';
|
||||
|
||||
const TOP_BAR_HEIGHT = 50;
|
||||
const GRID_TEXT_AREA_HEIGHT = 25;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Object as SDObject } from '@sd/client';
|
||||
import { TextArea } from '@sd/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Divider } from './Divider';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useLibraryQuery } from '@sd/client';
|
||||
import { JobReport } from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { formatDistanceToNow, formatDuration } from 'date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import { ArrowsClockwise } from 'phosphor-react';
|
||||
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
@@ -70,12 +70,12 @@ export function JobsManager() {
|
||||
<span className="text-xs opacity-60">
|
||||
{job.status === 'Failed' ? 'Failed after' : 'Took'}{' '}
|
||||
{job.seconds_elapsed
|
||||
? formatDuration({ seconds: job.seconds_elapsed })
|
||||
? dayjs.duration({ seconds: job.seconds_elapsed }).humanize()
|
||||
: 'less than a second'}
|
||||
</span>
|
||||
<span className="mx-1 opacity-30">•</span>
|
||||
<span className="text-xs opacity-60">
|
||||
{formatDistanceToNow(new Date(job.date_created))} ago
|
||||
{dayjs(job.date_created).toNow(true)} ago
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs opacity-60">{job.data}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ContentScreen: React.FC<unknown> = (props) => {
|
||||
export default function ContentScreen() {
|
||||
// const [address, setAddress] = React.useState('');
|
||||
return <div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll"></div>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useBridgeQuery, useLibraryMutation, useLibraryQuery, usePlatform } from
|
||||
import CodeBlock from '../components/primitive/Codeblock';
|
||||
|
||||
// TODO: Bring this back with a button in the sidebar near settings at the bottom
|
||||
export const DebugScreen: React.FC<unknown> = (props) => {
|
||||
export default function DebugScreen() {
|
||||
const platform = usePlatform();
|
||||
const { data: nodeState } = useBridgeQuery(['getNode']);
|
||||
const { data: libraryState } = useBridgeQuery(['library.list']);
|
||||
@@ -45,4 +45,4 @@ export const DebugScreen: React.FC<unknown> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function useExplorerParams() {
|
||||
return { location_id, path, limit };
|
||||
}
|
||||
|
||||
export const LocationExplorer: React.FC<unknown> = () => {
|
||||
export default function LocationExplorer() {
|
||||
const { location_id, path } = useExplorerParams();
|
||||
const { library } = useCurrentLibrary();
|
||||
|
||||
@@ -38,4 +38,4 @@ export const LocationExplorer: React.FC<unknown> = () => {
|
||||
<Explorer data={explorerData.data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ const StatItem: React.FC<StatItemProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const OverviewScreen = () => {
|
||||
export default function OverviewScreen() {
|
||||
const platform = usePlatform();
|
||||
const { data: libraryStatistics, isLoading: isStatisticsLoading } = useLibraryQuery([
|
||||
'library.getStatistics'
|
||||
@@ -191,4 +191,4 @@ export const OverviewScreen = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const PhotosScreen: React.FC<unknown> = (props) => {
|
||||
export default function PhotosScreen() {
|
||||
return (
|
||||
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll">
|
||||
<div className="flex flex-col space-y-5 pb-7">
|
||||
@@ -9,4 +9,4 @@ export const PhotosScreen: React.FC<unknown> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
|
||||
|
||||
import Explorer from '../components/explorer/Explorer';
|
||||
|
||||
export const TagExplorer: React.FC<unknown> = () => {
|
||||
export default function TagExplorer() {
|
||||
const { id } = useParams();
|
||||
const { library } = useCurrentLibrary();
|
||||
|
||||
@@ -16,4 +16,4 @@ export const TagExplorer: React.FC<unknown> = () => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
SettingsScreenContainer
|
||||
} from '../../components/settings/SettingsScreenContainer';
|
||||
|
||||
export const SettingsScreen: React.FC = () => {
|
||||
export default function SettingsScreen() {
|
||||
return (
|
||||
<SettingsScreenContainer>
|
||||
<SettingsHeading className="!mt-0">Client</SettingsHeading>
|
||||
@@ -100,4 +100,4 @@ export const SettingsScreen: React.FC = () => {
|
||||
</SidebarLink>
|
||||
</SettingsScreenContainer>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,22 +17,22 @@
|
||||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@heroicons/react": "^2.0.10",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@radix-ui/react-context-menu": "^1.0.0",
|
||||
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"class-variance-authority": "^0.2.3",
|
||||
"clsx": "^1.2.1",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss": "^8.4.17",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook": "^6.5.10",
|
||||
"tailwindcss": "^3.1.6"
|
||||
"storybook": "^6.5.12",
|
||||
"tailwindcss": "^3.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.9",
|
||||
"@babel/core": "^7.19.3",
|
||||
"@sd/config": "workspace:*",
|
||||
"@storybook/addon-actions": "^6.5.12",
|
||||
"@storybook/addon-essentials": "^6.5.12",
|
||||
@@ -44,18 +44,18 @@
|
||||
"@storybook/preset-scss": "^1.0.3",
|
||||
"@storybook/react": "^6.5.12",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@tailwindcss/line-clamp": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@types/react": "^18.0.15",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"babel-loader": "^8.2.5",
|
||||
"css-loader": "^6.7.1",
|
||||
"postcss-loader": "^7.0.1",
|
||||
"sass": "^1.54.0",
|
||||
"sass": "^1.55.0",
|
||||
"sass-loader": "^13.0.2",
|
||||
"storybook-tailwind-dark-mode": "^1.0.12",
|
||||
"storybook-tailwind-dark-mode": "^1.0.15",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user