mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-08 07:12:49 +00:00
Merge branch 'main' into closable-job-manager
This commit is contained in:
commit
8c4cdb1ede
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
|
@ -37,6 +37,30 @@ jobs:
|
||||||
- name: Perform typechecks
|
- name: Perform typechecks
|
||||||
run: pnpm typecheck
|
run: pnpm typecheck
|
||||||
|
|
||||||
|
eslint:
|
||||||
|
name: ESLint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.2.2
|
||||||
|
with:
|
||||||
|
version: 7.x.x
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm dependencies
|
||||||
|
run: pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Perform linting
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
rustfmt:
|
rustfmt:
|
||||||
name: rustfmt
|
name: rustfmt
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -5477,15 +5477,30 @@ dependencies = [
|
||||||
"webp",
|
"webp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sd-core-android"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"jni",
|
||||||
|
"sd-core-mobile",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sd-core-ios"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"objc",
|
||||||
|
"objc-foundation",
|
||||||
|
"objc_id",
|
||||||
|
"sd-core-mobile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sd-core-mobile"
|
name = "sd-core-mobile"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"jni",
|
|
||||||
"objc",
|
|
||||||
"objc-foundation",
|
|
||||||
"objc_id",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
"openssl",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
|
|
|
@ -7,7 +7,7 @@ members = [
|
||||||
# "crates/p2p/tunnel/utils",
|
# "crates/p2p/tunnel/utils",
|
||||||
"apps/cli",
|
"apps/cli",
|
||||||
"apps/desktop/src-tauri",
|
"apps/desktop/src-tauri",
|
||||||
"apps/mobile/rust",
|
"apps/mobile/rust/*",
|
||||||
"apps/server",
|
"apps/server",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"repositoryURL": "https://github.com/brendonovich/swift-rs",
|
"repositoryURL": "https://github.com/brendonovich/swift-rs",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "833e29ba333f1dfe303eaa21de78c4f8c5a3f2ff",
|
"revision": "c3003bc0c28a6742d3da341b61887d8e072fda0a",
|
||||||
"version": null
|
"version": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// Dependencies declare other packages that this package depends on.
|
// Dependencies declare other packages that this package depends on.
|
||||||
.package(url: "https://github.com/brendonovich/swift-rs", revision: "833e29ba333f1dfe303eaa21de78c4f8c5a3f2ff"),
|
.package(url: "https://github.com/brendonovich/swift-rs", revision: "c3003bc0c28a6742d3da341b61887d8e072fda0a"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
|
|
@ -17,8 +17,10 @@ use tauri::{
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod macos;
|
mod macos;
|
||||||
|
|
||||||
mod menu;
|
mod menu;
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require('@sd/config/eslint-react.js'),
|
extends: [require.resolve('@sd/config/eslint/web.js')],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: './tsconfig.json'
|
project: './tsconfig.json'
|
||||||
},
|
}
|
||||||
ignorePatterns: ['**/*.js', '**/*.json', 'node_modules', 'public', 'dist', 'vite.config.ts']
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"server": "ts-node ./server",
|
"server": "ts-node ./server",
|
||||||
"server:prod": "cross-env NODE_ENV=production ts-node ./server",
|
"server:prod": "cross-env NODE_ENV=production ts-node ./server",
|
||||||
"lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc -b"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
BIN
apps/landing/public/app-ui-explorer.webp
Normal file
BIN
apps/landing/public/app-ui-explorer.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
BIN
apps/landing/public/database-table-plus.webp
Normal file
BIN
apps/landing/public/database-table-plus.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 185 KiB |
BIN
apps/landing/public/schema.webp
Normal file
BIN
apps/landing/public/schema.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
|
@ -61,7 +61,7 @@ export function Footer() {
|
||||||
<FooterLink link="/team">Team</FooterLink>
|
<FooterLink link="/team">Team</FooterLink>
|
||||||
<FooterLink link="/docs/product/resources/faq">FAQ</FooterLink>
|
<FooterLink link="/docs/product/resources/faq">FAQ</FooterLink>
|
||||||
<FooterLink link="/careers">Careers</FooterLink>
|
<FooterLink link="/careers">Careers</FooterLink>
|
||||||
<FooterLink link="/changelog">Changelog</FooterLink>
|
<FooterLink link="/docs/changelog/beta/0.1.0">Changelog</FooterLink>
|
||||||
<FooterLink link="/blog">Blog</FooterLink>
|
<FooterLink link="/blog">Blog</FooterLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col col-span-1 space-y-2 pointer-events-none">
|
<div className="flex flex-col col-span-1 space-y-2 pointer-events-none">
|
||||||
|
@ -98,11 +98,11 @@ export function Footer() {
|
||||||
<FooterLink blank link="https://github.com/spacedriveapp/spacedrive/blob/main/LICENSE">
|
<FooterLink blank link="https://github.com/spacedriveapp/spacedrive/blob/main/LICENSE">
|
||||||
License
|
License
|
||||||
</FooterLink>
|
</FooterLink>
|
||||||
<div className="opacity-50 pointer-events-none">
|
<div>
|
||||||
<FooterLink link="#">Privacy</FooterLink>
|
<FooterLink link="/docs/company/legal/privacy">Privacy</FooterLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="opacity-50 pointer-events-none">
|
<div>
|
||||||
<FooterLink link="#">Terms</FooterLink>
|
<FooterLink link="/docs/company/legal/terms">Terms</FooterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require('@sd/config/eslint-react-native.js'),
|
extends: [require.resolve('@sd/config/eslint/reactNative.js')],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: './tsconfig.json'
|
project: './tsconfig.json'
|
||||||
|
|
|
@ -6,8 +6,8 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
|
apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
|
||||||
|
|
||||||
cargo {
|
cargo {
|
||||||
module = "../../rust"
|
module = "../../rust/android"
|
||||||
libname = "sd_core_mobile"
|
libname = "sd_core_android"
|
||||||
pythonCommand = 'python3'
|
pythonCommand = 'python3'
|
||||||
profile = 'release'
|
profile = 'release'
|
||||||
targets = ["arm", "arm64", "x86", "x86_64"]
|
targets = ["arm", "arm64", "x86", "x86_64"]
|
||||||
|
|
|
@ -28,7 +28,7 @@ public class SDCore extends ReactContextBaseJavaModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
System.loadLibrary("sd_core_mobile");
|
System.loadLibrary("sd_core_android");
|
||||||
}
|
}
|
||||||
|
|
||||||
// is exposed by Rust and is used to register the subscription
|
// is exposed by Rust and is used to register the subscription
|
||||||
|
@ -78,4 +78,4 @@ public class SDCore extends ReactContextBaseJavaModule {
|
||||||
.emit("SDCoreEvent", body);
|
.emit("SDCoreEvent", body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,11 @@ PODS:
|
||||||
- EXMediaLibrary (15.0.0):
|
- EXMediaLibrary (15.0.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- React-Core
|
- React-Core
|
||||||
- Expo (47.0.10):
|
- Expo (47.0.13):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoKeepAwake (11.0.1):
|
- ExpoKeepAwake (11.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoModulesCore (1.1.0):
|
- ExpoModulesCore (1.1.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- EXSplashScreen (0.17.5):
|
- EXSplashScreen (0.17.5):
|
||||||
|
@ -564,9 +564,9 @@ SPEC CHECKSUMS:
|
||||||
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
|
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
|
||||||
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
|
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
|
||||||
EXMediaLibrary: b1c4f78878e45f6a359aff3a059e1660c41b73ab
|
EXMediaLibrary: b1c4f78878e45f6a359aff3a059e1660c41b73ab
|
||||||
Expo: a694d89d2461fdfc6b977bf489bf7d341ed03bca
|
Expo: b9fa98bf260992312ee3c424400819fb9beadafe
|
||||||
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
|
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
|
||||||
ExpoModulesCore: fc7e27657bc33878e1451c30cef481020518f2e1
|
ExpoModulesCore: 65ae09e2b2d3dd8ece30a5acc83c569968125ab0
|
||||||
EXSplashScreen: 3e989924f61a8dd07ee4ea584c6ba14be9b51949
|
EXSplashScreen: 3e989924f61a8dd07ee4ea584c6ba14be9b51949
|
||||||
FBLazyVector: affa4ba1bfdaac110a789192f4d452b053a86624
|
FBLazyVector: affa4ba1bfdaac110a789192f4d452b053a86624
|
||||||
FBReactNativeSpec: fe8b5f1429cfe83a8d72dc8ed61dc7704cac8745
|
FBReactNativeSpec: fe8b5f1429cfe83a8d72dc8ed61dc7704cac8745
|
||||||
|
|
|
@ -148,7 +148,7 @@
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
EE23FABC21723647AF9773BD /* [CP] Check Pods Manifest.lock */,
|
EE23FABC21723647AF9773BD /* [CP] Check Pods Manifest.lock */,
|
||||||
FD10A7F022414F080027D42C /* Start Packager */,
|
FD10A7F022414F080027D42C /* Start Packager */,
|
||||||
55B1130D28AB3061006C377F /* Build Spacedrive Core */,
|
350DC403297BF2B8009CD6A1 /* Build sd-core-ios */,
|
||||||
13B07F871A680F5B00A75B9A /* Sources */,
|
13B07F871A680F5B00A75B9A /* Sources */,
|
||||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||||
|
@ -224,6 +224,25 @@
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "export NODE_BINARY=node\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\n`node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n";
|
shellScript = "export NODE_BINARY=node\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\n`node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n";
|
||||||
};
|
};
|
||||||
|
350DC403297BF2B8009CD6A1 /* Build sd-core-ios */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Build sd-core-ios";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/zsh;
|
||||||
|
shellScript = "env -i CONFIGURATION=$CONFIGURATION PLATFORM_NAME=$PLATFORM_NAME ./build-rust.sh\n";
|
||||||
|
};
|
||||||
37F7445AEFB5A9FBEDBB3916 /* [CP] Copy Pods Resources */ = {
|
37F7445AEFB5A9FBEDBB3916 /* [CP] Copy Pods Resources */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -244,29 +263,6 @@
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Spacedrive/Pods-Spacedrive-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Spacedrive/Pods-Spacedrive-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
55B1130D28AB3061006C377F /* Build Spacedrive Core */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
alwaysOutOfDate = 1;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"../../../core/src/*.rs",
|
|
||||||
"../rust/src/*.rs",
|
|
||||||
"../../../core/src/*/*.rs",
|
|
||||||
);
|
|
||||||
name = "Build Spacedrive Core";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
"../../../target/sdcore-universal-ios.a",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/zsh;
|
|
||||||
shellScript = "set -e\n\nif [[ -n \"${DEVELOPER_SDK_DIR:-}\" ]]; then\n # Assume we're in Xcode, which means we're probably cross-compiling.\n # In this case, we need to add an extra library search path for build scripts and proc-macros,\n # which run on the host instead of the target.\n # (macOS Big Sur does not have linkable libraries in /usr/lib/.)\n export LIBRARY_PATH=\"${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}\"\nfi\n\nCARGO_FLAGS=\nif [[ \"$BUILDVARIANT\" != \"debug\" ]]; then\n CARGO_FLAGS=--release\nfi\n\nTARGET_DIRECTORY=../../../target\nif [[ $PLATFORM_NAME = \"iphonesimulator\" ]]\nthen\n cargo build -p sd-core-mobile $CARGO_FLAGS --lib --target aarch64-apple-ios-sim\n lipo -create -output $TARGET_DIRECTORY/libsd_core_mobile-iossim.a $TARGET_DIRECTORY/aarch64-apple-ios-sim/release/libsd_core_mobile.a\nelse\n cargo build -p sd-core-mobile $CARGO_FLAGS --lib --target aarch64-apple-ios\n lipo -create -output $TARGET_DIRECTORY/libsd_core_mobile-ios.a $TARGET_DIRECTORY/aarch64-apple-ios/release/libsd_core_mobile.a\nfi\n";
|
|
||||||
};
|
|
||||||
EE23FABC21723647AF9773BD /* [CP] Check Pods Manifest.lock */ = {
|
EE23FABC21723647AF9773BD /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -424,13 +420,13 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
"-lc++",
|
"-lc++",
|
||||||
"-lsd_core_mobile-ios",
|
"-lsd_core_ios-ios",
|
||||||
);
|
);
|
||||||
"OTHER_LDFLAGS[sdk=iphonesimulator*]" = (
|
"OTHER_LDFLAGS[sdk=iphonesimulator*]" = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
"-lc++",
|
"-lc++",
|
||||||
"-lsd_core_mobile-iossim",
|
"-lsd_core_ios-iossim",
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.spacedrive.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.spacedrive.app;
|
||||||
|
@ -519,13 +515,13 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
"-lc++",
|
"-lc++",
|
||||||
"-lsd_core_mobile-ios",
|
"-lsd_core_ios-ios",
|
||||||
);
|
);
|
||||||
"OTHER_LDFLAGS[sdk=iphonesimulator*]" = (
|
"OTHER_LDFLAGS[sdk=iphonesimulator*]" = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
"-lc++",
|
"-lc++",
|
||||||
"-lsd_core_mobile-iossim",
|
"-lsd_core_ios-iossim",
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.spacedrive.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.spacedrive.app;
|
||||||
|
|
19
apps/mobile/ios/build-rust.sh
Executable file
19
apps/mobile/ios/build-rust.sh
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#! /bin/zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TARGET_DIRECTORY=../../../target
|
||||||
|
|
||||||
|
CARGO_FLAGS=
|
||||||
|
if [[ $CONFIGURATION != "Debug" ]]; then
|
||||||
|
CARGO_FLAGS=--release
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $PLATFORM_NAME = "iphonesimulator" ]]
|
||||||
|
then
|
||||||
|
cargo build -p sd-core-ios --target aarch64-apple-ios-sim
|
||||||
|
lipo -create -output $TARGET_DIRECTORY/libsd_core_ios-iossim.a $TARGET_DIRECTORY/aarch64-apple-ios-sim/debug/libsd_core_ios.a
|
||||||
|
else
|
||||||
|
cargo build -p sd-core-ios --target aarch64-apple-ios
|
||||||
|
lipo -create -output $TARGET_DIRECTORY/libsd_core_ios-ios.a $TARGET_DIRECTORY/aarch64-apple-ios/debug/libsd_core_ios.a
|
||||||
|
fi
|
|
@ -10,7 +10,7 @@
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"xcode": "open ios/spacedrive.xcworkspace",
|
"xcode": "open ios/spacedrive.xcworkspace",
|
||||||
"android-studio": "open -a '/Applications/Android Studio.app' ./android",
|
"android-studio": "open -a '/Applications/Android Studio.app' ./android",
|
||||||
"lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit",
|
"lint": "eslint src",
|
||||||
"postinstall": "node scripts/postinstall.js",
|
"postinstall": "node scripts/postinstall.js",
|
||||||
"typecheck": "tsc -b"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "sd-core-mobile"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.64.0"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["staticlib", "cdylib"] # staticlib for IOS and cdylib for Android
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
once_cell = "1.15.0"
|
|
||||||
sd-core = { path = "../../../core", features = [
|
|
||||||
"mobile",
|
|
||||||
"p2p",
|
|
||||||
], default-features = false }
|
|
||||||
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.76", features = [
|
|
||||||
"vendored",
|
|
||||||
] } # Override features of transitive dependencies to support IOS Simulator on M1
|
|
||||||
futures = "0.3.24"
|
|
||||||
tracing = "0.1.37"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
|
||||||
objc = "0.2.7"
|
|
||||||
objc_id = "0.1.1"
|
|
||||||
objc-foundation = "0.1.1"
|
|
||||||
|
|
||||||
# This is `not(ios)` instead of `android` because of https://github.com/mozilla/rust-android-gradle/issues/93
|
|
||||||
[target.'cfg(not(target_os = "ios"))'.dependencies]
|
|
||||||
jni = "0.19.0"
|
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "ios"))'.features]
|
|
||||||
default = ["sd-core/android"]
|
|
||||||
|
|
19
apps/mobile/rust/android/Cargo.toml
Normal file
19
apps/mobile/rust/android/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "sd-core-android"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.64.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# Android can use dynamic linking since all FFI is done via JNI
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# FFI
|
||||||
|
jni = "0.19.0"
|
||||||
|
|
||||||
|
# Core mobile handling stuff
|
||||||
|
sd-core-mobile = { path = "../mobile", features = ["android"] }
|
||||||
|
|
||||||
|
# Other
|
||||||
|
tracing = "0.1.37"
|
105
apps/mobile/rust/android/src/lib.rs
Normal file
105
apps/mobile/rust/android/src/lib.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
use jni::{
|
||||||
|
objects::{JClass, JObject, JString},
|
||||||
|
JNIEnv,
|
||||||
|
};
|
||||||
|
|
||||||
|
use sd_core_mobile::*;
|
||||||
|
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener(
|
||||||
|
env: JNIEnv,
|
||||||
|
class: JClass,
|
||||||
|
) {
|
||||||
|
let result = panic::catch_unwind(|| {
|
||||||
|
let jvm = env.get_java_vm().unwrap();
|
||||||
|
let class = env.new_global_ref(class).unwrap();
|
||||||
|
|
||||||
|
spawn_core_event_listener(move |data| {
|
||||||
|
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]
|
||||||
|
pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg(
|
||||||
|
env: JNIEnv,
|
||||||
|
class: JClass,
|
||||||
|
query: JString,
|
||||||
|
callback: JObject,
|
||||||
|
) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
let data_directory = {
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
|
||||||
|
handle_core_msg(query, data_directory, move |result| match result {
|
||||||
|
Ok(data) => {
|
||||||
|
let env = jvm.attach_current_thread().unwrap();
|
||||||
|
env.call_method(
|
||||||
|
&callback,
|
||||||
|
"resolve",
|
||||||
|
"(Ljava/lang/Object;)V",
|
||||||
|
&[env
|
||||||
|
.new_string(data)
|
||||||
|
.expect("Couldn't create java string!")
|
||||||
|
.into()],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// TODO: handle error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
// TODO: This log statement doesn't work. I recon the JNI env is being dropped before it's called.
|
||||||
|
error!(
|
||||||
|
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
23
apps/mobile/rust/ios/Cargo.toml
Normal file
23
apps/mobile/rust/ios/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "sd-core-ios"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
rust-version = "1.64.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# iOS requires static linking
|
||||||
|
# Makes sense considering this lib needs to link against call_resolve and get_data_directory,
|
||||||
|
# which are only available when linking against the app's ObjC
|
||||||
|
crate-type = ["staticlib"]
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
|
# FFI
|
||||||
|
objc = "0.2.7"
|
||||||
|
objc_id = "0.1.1"
|
||||||
|
objc-foundation = "0.1.1"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Core mobile handling stuff
|
||||||
|
sd-core-mobile = { path = "../mobile" }
|
81
apps/mobile/rust/ios/src/lib.rs
Normal file
81
apps/mobile/rust/ios/src/lib.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
#![cfg(target_os = "ios")]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
ffi::{CStr, CString},
|
||||||
|
os::raw::{c_char, c_void},
|
||||||
|
panic,
|
||||||
|
};
|
||||||
|
|
||||||
|
use objc::{msg_send, runtime::Object, sel, sel_impl};
|
||||||
|
use objc_foundation::{INSString, NSString};
|
||||||
|
use objc_id::Id;
|
||||||
|
|
||||||
|
use sd_core_mobile::*;
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn get_data_directory() -> *const c_char;
|
||||||
|
fn call_resolve(resolve: *const c_void, result: *const c_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This struct wraps the function pointer which represent a Javascript Promise. We wrap the
|
||||||
|
// function pointers in a struct so we can unsafely assert to Rust that they are `Send`.
|
||||||
|
// We know they are send as we have ensured Objective-C won't deallocate the function pointer
|
||||||
|
// until `call_resolve` is called.
|
||||||
|
struct RNPromise(*const c_void);
|
||||||
|
|
||||||
|
unsafe impl Send for RNPromise {}
|
||||||
|
|
||||||
|
impl RNPromise {
|
||||||
|
// resolve the promise
|
||||||
|
unsafe fn resolve(self, result: CString) {
|
||||||
|
call_resolve(self.0, result.as_ptr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) {
|
||||||
|
let result = panic::catch_unwind(|| {
|
||||||
|
let id = Id::<Object>::from_ptr(id);
|
||||||
|
|
||||||
|
spawn_core_event_listener(move |data| {
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
let data_directory = CStr::from_ptr(get_data_directory())
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
handle_core_msg(query, data_directory, |result| {
|
||||||
|
match result {
|
||||||
|
Ok(data) => resolve.resolve(CString::new(data).unwrap()),
|
||||||
|
Err(_) => {
|
||||||
|
// TODO: handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
26
apps/mobile/rust/mobile/Cargo.toml
Normal file
26
apps/mobile/rust/mobile/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "sd-core-mobile"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.64.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
android = ["sd-core/android"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
once_cell = "1.15.0"
|
||||||
|
sd-core = { path = "../../../../core", features = [
|
||||||
|
"mobile",
|
||||||
|
"p2p",
|
||||||
|
], default-features = false }
|
||||||
|
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.76", features = [
|
||||||
|
"vendored",
|
||||||
|
] } # Override features of transitive dependencies to support IOS Simulator on M1
|
||||||
|
futures = "0.3.24"
|
||||||
|
tracing = "0.1.37"
|
106
apps/mobile/rust/mobile/src/lib.rs
Normal file
106
apps/mobile/rust/mobile/src/lib.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
use futures::future::join_all;
|
||||||
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
|
use rspc::internal::jsonrpc::*;
|
||||||
|
use sd_core::{api::Router, Node};
|
||||||
|
use serde_json::{from_str, from_value, to_string, Value};
|
||||||
|
use std::{collections::HashMap, marker::Send, sync::Arc};
|
||||||
|
use tokio::{
|
||||||
|
runtime::Runtime,
|
||||||
|
sync::{
|
||||||
|
mpsc::{unbounded_channel, UnboundedSender},
|
||||||
|
oneshot, Mutex,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
pub static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
|
||||||
|
|
||||||
|
pub type NodeType = Lazy<Mutex<Option<(Arc<Node>, Arc<Router>)>>>;
|
||||||
|
|
||||||
|
pub static NODE: NodeType = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
pub static SUBSCRIPTIONS: Lazy<Mutex<HashMap<RequestId, oneshot::Sender<()>>>> =
|
||||||
|
Lazy::new(Default::default);
|
||||||
|
|
||||||
|
pub static EVENT_SENDER: OnceCell<UnboundedSender<Response>> = OnceCell::new();
|
||||||
|
|
||||||
|
pub fn handle_core_msg(
|
||||||
|
query: String,
|
||||||
|
data_dir: String,
|
||||||
|
callback: impl FnOnce(Result<String, String>) + Send + 'static,
|
||||||
|
) {
|
||||||
|
RUNTIME.spawn(async move {
|
||||||
|
let (node, router) = {
|
||||||
|
let node = &mut *NODE.lock().await;
|
||||||
|
match node {
|
||||||
|
Some(node) => node.clone(),
|
||||||
|
None => {
|
||||||
|
// TODO: probably don't unwrap
|
||||||
|
let new_node = Node::new(data_dir).await.unwrap();
|
||||||
|
node.replace(new_node.clone());
|
||||||
|
new_node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let reqs = match from_str::<Value>(&query).and_then(|v| match v.is_array() {
|
||||||
|
true => from_value::<Vec<Request>>(v),
|
||||||
|
false => from_value::<Request>(v).map(|v| vec![v]),
|
||||||
|
}) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config!
|
||||||
|
callback(Err(query));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let responses = join_all(reqs.into_iter().map(|request| {
|
||||||
|
let node = node.clone();
|
||||||
|
let router = router.clone();
|
||||||
|
async move {
|
||||||
|
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::ResponseAndChannel(resp, _) => resp,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
callback(Ok(serde_json::to_string(
|
||||||
|
&responses.into_iter().flatten().collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.unwrap()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_core_event_listener(callback: impl Fn(String) + Send + 'static) {
|
||||||
|
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 to_string(&event) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(err) => {
|
||||||
|
println!("Failed to serialize event: {}", err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
callback(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,177 +0,0 @@
|
||||||
use std::panic;
|
|
||||||
|
|
||||||
use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS};
|
|
||||||
use futures::future::join_all;
|
|
||||||
use jni::objects::{JClass, JObject, JString};
|
|
||||||
use jni::JNIEnv;
|
|
||||||
use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap};
|
|
||||||
use sd_core::Node;
|
|
||||||
use serde_json::Value;
|
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener(
|
|
||||||
env: JNIEnv,
|
|
||||||
class: JClass,
|
|
||||||
) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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]
|
|
||||||
pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg(
|
|
||||||
env: JNIEnv,
|
|
||||||
class: JClass,
|
|
||||||
query: JString,
|
|
||||||
callback: JObject,
|
|
||||||
) {
|
|
||||||
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 (node, router) = {
|
|
||||||
let node = &mut *NODE.lock().await;
|
|
||||||
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;
|
|
||||||
let new_node = match new_node {
|
|
||||||
Ok(new_node) => new_node,
|
|
||||||
Err(err) => {
|
|
||||||
info!("677 {:?}", err);
|
|
||||||
|
|
||||||
// TODO: Android return?
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
node.replace(new_node.clone());
|
|
||||||
new_node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let reqs =
|
|
||||||
match serde_json::from_str::<Value>(&query).and_then(|v| match v.is_array() {
|
|
||||||
true => serde_json::from_value::<Vec<Request>>(v),
|
|
||||||
false => serde_json::from_value::<Request>(v).map(|v| vec![v]),
|
|
||||||
}) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(err) => {
|
|
||||||
error!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let resps = join_all(reqs.into_iter().map(|request| {
|
|
||||||
let node = node.clone();
|
|
||||||
let router = router.clone();
|
|
||||||
async move {
|
|
||||||
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::ResponseAndChannel(resp, _) => resp,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let env = jvm.attach_current_thread().unwrap();
|
|
||||||
env.call_method(
|
|
||||||
&callback,
|
|
||||||
"resolve",
|
|
||||||
"(Ljava/lang/Object;)V",
|
|
||||||
&[env
|
|
||||||
.new_string(
|
|
||||||
serde_json::to_string(&resps.into_iter().flatten().collect::<Vec<_>>())
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.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?
|
|
||||||
|
|
||||||
// TODO: This log statement doesn't work. I recon the JNI env is being dropped before it's called.
|
|
||||||
error!(
|
|
||||||
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS};
|
|
||||||
use futures::future::join_all;
|
|
||||||
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 serde_json::Value;
|
|
||||||
use std::{
|
|
||||||
ffi::{CStr, CString},
|
|
||||||
os::raw::{c_char, c_void},
|
|
||||||
panic,
|
|
||||||
};
|
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
fn get_data_directory() -> *const c_char;
|
|
||||||
fn call_resolve(resolve: *const c_void, result: *const c_char);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This struct wraps the function pointer which represent a Javascript Promise. We wrap the
|
|
||||||
// function pointers in a struct so we can unsafely assert to Rust that they are `Send`.
|
|
||||||
// We know they are send as we have ensured Objective-C won't deallocate the function pointer
|
|
||||||
// until `call_resolve` is called.
|
|
||||||
struct RNPromise(*const c_void);
|
|
||||||
|
|
||||||
unsafe impl Send for RNPromise {}
|
|
||||||
|
|
||||||
impl RNPromise {
|
|
||||||
// resolve the promise
|
|
||||||
unsafe fn resolve(self, result: CString) {
|
|
||||||
call_resolve(self.0, result.as_ptr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) {
|
|
||||||
let result = panic::catch_unwind(|| {
|
|
||||||
let id = Id::<Object>::from_ptr(id);
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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 reqs =
|
|
||||||
match serde_json::from_str::<Value>(&query).and_then(|v| match v.is_array() {
|
|
||||||
true => serde_json::from_value::<Vec<Request>>(v),
|
|
||||||
false => serde_json::from_value::<Request>(v).map(|v| vec![v]),
|
|
||||||
}) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(err) => {
|
|
||||||
println!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config!
|
|
||||||
|
|
||||||
resolve.resolve(
|
|
||||||
CString::new(serde_json::to_vec(&(vec![] as Vec<Request>)).unwrap())
|
|
||||||
.unwrap(),
|
|
||||||
); // TODO: Proper error handling
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let resps = join_all(reqs.into_iter().map(|request| async move {
|
|
||||||
let node = &mut *NODE.lock().await;
|
|
||||||
let (node, router) = match node {
|
|
||||||
Some(node) => node.clone(),
|
|
||||||
None => {
|
|
||||||
let data_dir = CStr::from_ptr(get_data_directory())
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
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::ResponseAndChannel(resp, _) => resp,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
resolve.resolve(
|
|
||||||
CString::new(
|
|
||||||
serde_json::to_vec(&resps.into_iter().filter_map(|v| v).collect::<Vec<_>>())
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.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 sd_core_msg: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
|
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
|
||||||
use rspc::internal::jsonrpc::{RequestId, Response};
|
|
||||||
use sd_core::{api::Router, Node};
|
|
||||||
use tokio::{
|
|
||||||
runtime::Runtime,
|
|
||||||
sync::{mpsc::UnboundedSender, oneshot, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
|
|
||||||
|
|
||||||
type NodeType = Lazy<Mutex<Option<(Arc<Node>, Arc<Router>)>>>;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
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();
|
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
|
||||||
mod ios;
|
|
||||||
|
|
||||||
/// This is `not(ios)` instead of `android` because of https://github.com/mozilla/rust-android-gradle/issues/93
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
|
||||||
mod android;
|
|
|
@ -2,6 +2,8 @@ use crate::{
|
||||||
invalidate_query,
|
invalidate_query,
|
||||||
job::Job,
|
job::Job,
|
||||||
object::fs::{
|
object::fs::{
|
||||||
|
copy::{FileCopierJob, FileCopierJobInit},
|
||||||
|
cut::{FileCutterJob, FileCutterJobInit},
|
||||||
decrypt::{FileDecryptorJob, FileDecryptorJobInit},
|
decrypt::{FileDecryptorJob, FileDecryptorJobInit},
|
||||||
delete::{FileDeleterJob, FileDeleterJobInit},
|
delete::{FileDeleterJob, FileDeleterJobInit},
|
||||||
encrypt::{FileEncryptorJob, FileEncryptorJobInit},
|
encrypt::{FileEncryptorJob, FileEncryptorJobInit},
|
||||||
|
@ -122,6 +124,30 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
library.spawn_job(Job::new(args, FileEraserJob {})).await;
|
library.spawn_job(Job::new(args, FileEraserJob {})).await;
|
||||||
invalidate_query!(library, "locations.getExplorerData");
|
invalidate_query!(library, "locations.getExplorerData");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.library_mutation("duplicateFiles", |t| {
|
||||||
|
t(|_, args: FileCopierJobInit, library| async move {
|
||||||
|
library.spawn_job(Job::new(args, FileCopierJob {})).await;
|
||||||
|
invalidate_query!(library, "locations.getExplorerData");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.library_mutation("copyFiles", |t| {
|
||||||
|
t(|_, args: FileCopierJobInit, library| async move {
|
||||||
|
library.spawn_job(Job::new(args, FileCopierJob {})).await;
|
||||||
|
invalidate_query!(library, "locations.getExplorerData");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.library_mutation("cutFiles", |t| {
|
||||||
|
t(|_, args: FileCutterJobInit, library| async move {
|
||||||
|
library.spawn_job(Job::new(args, FileCutterJob {})).await;
|
||||||
|
invalidate_query!(library, "locations.getExplorerData");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,12 @@ use crate::{
|
||||||
library::LibraryContext,
|
library::LibraryContext,
|
||||||
location::indexer::indexer_job::{IndexerJob, INDEXER_JOB_NAME},
|
location::indexer::indexer_job::{IndexerJob, INDEXER_JOB_NAME},
|
||||||
object::{
|
object::{
|
||||||
|
fs::{
|
||||||
|
copy::{FileCopierJob, COPY_JOB_NAME},
|
||||||
|
cut::{FileCutterJob, CUT_JOB_NAME},
|
||||||
|
delete::{FileDeleterJob, DELETE_JOB_NAME},
|
||||||
|
erase::{FileEraserJob, ERASE_JOB_NAME},
|
||||||
|
},
|
||||||
identifier_job::full_identifier_job::{FullFileIdentifierJob, FULL_IDENTIFIER_JOB_NAME},
|
identifier_job::full_identifier_job::{FullFileIdentifierJob, FULL_IDENTIFIER_JOB_NAME},
|
||||||
preview::{ThumbnailJob, THUMBNAIL_JOB_NAME},
|
preview::{ThumbnailJob, THUMBNAIL_JOB_NAME},
|
||||||
validation::validator_job::{ObjectValidatorJob, VALIDATOR_JOB_NAME},
|
validation::validator_job::{ObjectValidatorJob, VALIDATOR_JOB_NAME},
|
||||||
|
@ -217,6 +223,26 @@ impl JobManager {
|
||||||
.dispatch_job(ctx, Job::resume(paused_job, ObjectValidatorJob {})?)
|
.dispatch_job(ctx, Job::resume(paused_job, ObjectValidatorJob {})?)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
CUT_JOB_NAME => {
|
||||||
|
Arc::clone(&self)
|
||||||
|
.dispatch_job(ctx, Job::resume(paused_job, FileCutterJob {})?)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
COPY_JOB_NAME => {
|
||||||
|
Arc::clone(&self)
|
||||||
|
.dispatch_job(ctx, Job::resume(paused_job, FileCopierJob {})?)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
DELETE_JOB_NAME => {
|
||||||
|
Arc::clone(&self)
|
||||||
|
.dispatch_job(ctx, Job::resume(paused_job, FileDeleterJob {})?)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
ERASE_JOB_NAME => {
|
||||||
|
Arc::clone(&self)
|
||||||
|
.dispatch_job(ctx, Job::resume(paused_job, FileEraserJob {})?)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!(
|
error!(
|
||||||
"Unknown job type: {}, id: {}",
|
"Unknown job type: {}, id: {}",
|
||||||
|
|
|
@ -47,6 +47,10 @@ pub enum JobError {
|
||||||
MissingData { value: String },
|
MissingData { value: String },
|
||||||
#[error("Location manager error: {0}")]
|
#[error("Location manager error: {0}")]
|
||||||
LocationManager(#[from] LocationManagerError),
|
LocationManager(#[from] LocationManagerError),
|
||||||
|
#[error("error converting/handling OS strings")]
|
||||||
|
OsStr,
|
||||||
|
#[error("error converting/handling paths")]
|
||||||
|
Path,
|
||||||
|
|
||||||
// Specific job errors
|
// Specific job errors
|
||||||
#[error("Indexer error: {0}")]
|
#[error("Indexer error: {0}")]
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext};
|
||||||
|
|
||||||
|
use std::{hash::Hash, path::PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
use super::{context_menu_fs_info, get_path_from_location_id, osstr_to_string, FsInfo, ObjectType};
|
||||||
|
|
||||||
|
pub struct FileCopierJob {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct FileCopierJobState {
|
||||||
|
pub target_path: PathBuf, // target dir prefix too
|
||||||
|
pub source_path: PathBuf,
|
||||||
|
pub root_type: ObjectType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Hash, Type)]
|
||||||
|
pub struct FileCopierJobInit {
|
||||||
|
pub source_location_id: i32,
|
||||||
|
pub source_path_id: i32,
|
||||||
|
pub target_location_id: i32,
|
||||||
|
pub target_path: PathBuf,
|
||||||
|
pub target_file_name_suffix: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct FileCopierJobStep {
|
||||||
|
pub source_fs_info: FsInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const COPY_JOB_NAME: &str = "file_copier";
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl StatefulJob for FileCopierJob {
|
||||||
|
type Init = FileCopierJobInit;
|
||||||
|
type Data = FileCopierJobState;
|
||||||
|
type Step = FileCopierJobStep;
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
COPY_JOB_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||||
|
let source_fs_info = context_menu_fs_info(
|
||||||
|
&ctx.library_ctx.db,
|
||||||
|
state.init.source_location_id,
|
||||||
|
state.init.source_path_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut full_target_path =
|
||||||
|
get_path_from_location_id(&ctx.library_ctx.db, state.init.target_location_id).await?;
|
||||||
|
|
||||||
|
// add the currently viewed subdirectory to the location root
|
||||||
|
full_target_path.push(&state.init.target_path);
|
||||||
|
|
||||||
|
// extension wizardry for cloning and such
|
||||||
|
// if no suffix has been selected, just use the file name
|
||||||
|
// if a suffix is provided and it's a directory, use the directory name + suffix
|
||||||
|
// if a suffix is provided and it's a file, use the (file name + suffix).extension
|
||||||
|
let file_name = osstr_to_string(source_fs_info.obj_path.file_name())?;
|
||||||
|
|
||||||
|
let target_file_name = state.init.target_file_name_suffix.as_ref().map_or_else(
|
||||||
|
|| Ok::<_, JobError>(file_name.clone()),
|
||||||
|
|s| match source_fs_info.obj_type {
|
||||||
|
ObjectType::Directory => Ok(format!("{file_name}{s}")),
|
||||||
|
ObjectType::File => Ok(osstr_to_string(source_fs_info.obj_path.file_stem())?
|
||||||
|
+ s + &source_fs_info.obj_path.extension().map_or_else(
|
||||||
|
|| Ok::<_, JobError>(String::new()),
|
||||||
|
|x| Ok(format!(".{}", x.to_str().ok_or(JobError::OsStr)?)),
|
||||||
|
)?),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
full_target_path.push(target_file_name);
|
||||||
|
|
||||||
|
state.data = Some(FileCopierJobState {
|
||||||
|
target_path: full_target_path,
|
||||||
|
source_path: source_fs_info.obj_path.clone(),
|
||||||
|
root_type: source_fs_info.obj_type.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
state.steps = [FileCopierJobStep { source_fs_info }].into_iter().collect();
|
||||||
|
|
||||||
|
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_step(
|
||||||
|
&self,
|
||||||
|
ctx: WorkerContext,
|
||||||
|
state: &mut JobState<Self>,
|
||||||
|
) -> Result<(), JobError> {
|
||||||
|
let step = &state.steps[0];
|
||||||
|
let info = &step.source_fs_info;
|
||||||
|
|
||||||
|
let job_state = state.data.as_ref().ok_or(JobError::MissingData {
|
||||||
|
value: String::from("job state"),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match info.obj_type {
|
||||||
|
ObjectType::File => {
|
||||||
|
let mut path = job_state.target_path.clone();
|
||||||
|
|
||||||
|
if job_state.root_type == ObjectType::Directory {
|
||||||
|
// if root type is a dir, we need to preserve structure by making paths relative
|
||||||
|
path.push(
|
||||||
|
info.obj_path
|
||||||
|
.strip_prefix(&job_state.source_path)
|
||||||
|
.map_err(|_| JobError::Path)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("Copying from {:?} to {:?}", info.obj_path, path);
|
||||||
|
|
||||||
|
tokio::fs::copy(&info.obj_path, &path).await?;
|
||||||
|
}
|
||||||
|
ObjectType::Directory => {
|
||||||
|
// if this is the very first path, create the target dir
|
||||||
|
// fixes copying dirs with no child directories
|
||||||
|
if job_state.root_type == ObjectType::Directory
|
||||||
|
&& job_state.source_path == info.obj_path
|
||||||
|
{
|
||||||
|
tokio::fs::create_dir_all(&job_state.target_path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dir = tokio::fs::read_dir(&info.obj_path).await?;
|
||||||
|
while let Some(entry) = dir.next_entry().await? {
|
||||||
|
if entry.metadata().await?.is_dir() {
|
||||||
|
state.steps.push_back(FileCopierJobStep {
|
||||||
|
source_fs_info: FsInfo {
|
||||||
|
obj_id: None,
|
||||||
|
obj_name: String::new(),
|
||||||
|
obj_path: entry.path(),
|
||||||
|
obj_type: ObjectType::Directory,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(
|
||||||
|
job_state.target_path.join(
|
||||||
|
entry
|
||||||
|
.path()
|
||||||
|
.strip_prefix(&job_state.source_path)
|
||||||
|
.map_err(|_| JobError::Path)?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
state.steps.push_back(FileCopierJobStep {
|
||||||
|
source_fs_info: FsInfo {
|
||||||
|
obj_id: None,
|
||||||
|
obj_name: osstr_to_string(Some(&entry.file_name()))?,
|
||||||
|
obj_path: entry.path(),
|
||||||
|
obj_type: ObjectType::File,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||||
|
state.step_number + 1,
|
||||||
|
)]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finalize(&self, _ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||||
|
Ok(Some(serde_json::to_value(&state.init)?))
|
||||||
|
}
|
||||||
|
}
|
91
core/src/object/fs/cut.rs
Normal file
91
core/src/object/fs/cut.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext};
|
||||||
|
|
||||||
|
use std::{hash::Hash, path::PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
use super::{context_menu_fs_info, get_path_from_location_id, FsInfo};
|
||||||
|
|
||||||
|
pub struct FileCutterJob {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct FileCutterJobState {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Hash, Type)]
|
||||||
|
pub struct FileCutterJobInit {
|
||||||
|
pub source_location_id: i32,
|
||||||
|
pub source_path_id: i32,
|
||||||
|
pub target_location_id: i32,
|
||||||
|
pub target_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct FileCutterJobStep {
|
||||||
|
pub source_fs_info: FsInfo,
|
||||||
|
pub target_directory: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CUT_JOB_NAME: &str = "file_cutter";
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl StatefulJob for FileCutterJob {
|
||||||
|
type Init = FileCutterJobInit;
|
||||||
|
type Data = FileCutterJobState;
|
||||||
|
type Step = FileCutterJobStep;
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
CUT_JOB_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||||
|
let source_fs_info = context_menu_fs_info(
|
||||||
|
&ctx.library_ctx.db,
|
||||||
|
state.init.source_location_id,
|
||||||
|
state.init.source_path_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut full_target_path =
|
||||||
|
get_path_from_location_id(&ctx.library_ctx.db, state.init.target_location_id).await?;
|
||||||
|
full_target_path.push(&state.init.target_path);
|
||||||
|
|
||||||
|
state.steps = [FileCutterJobStep {
|
||||||
|
source_fs_info,
|
||||||
|
target_directory: full_target_path,
|
||||||
|
}]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_step(
|
||||||
|
&self,
|
||||||
|
ctx: WorkerContext,
|
||||||
|
state: &mut JobState<Self>,
|
||||||
|
) -> Result<(), JobError> {
|
||||||
|
let step = &state.steps[0];
|
||||||
|
let source_info = &step.source_fs_info;
|
||||||
|
|
||||||
|
let full_output = step
|
||||||
|
.target_directory
|
||||||
|
.join(source_info.obj_path.file_name().ok_or(JobError::OsStr)?);
|
||||||
|
|
||||||
|
trace!("Cutting {:?} to {:?}", source_info.obj_path, full_output);
|
||||||
|
|
||||||
|
tokio::fs::rename(&source_info.obj_path, &full_output).await?;
|
||||||
|
|
||||||
|
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||||
|
state.step_number + 1,
|
||||||
|
)]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finalize(&self, _ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||||
|
Ok(Some(serde_json::to_value(&state.init)?))
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ pub struct FileDeleterJobStep {
|
||||||
pub fs_info: FsInfo,
|
pub fs_info: FsInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
const JOB_NAME: &str = "file_deleter";
|
pub const DELETE_JOB_NAME: &str = "file_deleter";
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl StatefulJob for FileDeleterJob {
|
impl StatefulJob for FileDeleterJob {
|
||||||
|
@ -29,7 +29,7 @@ impl StatefulJob for FileDeleterJob {
|
||||||
type Step = FileDeleterJobStep;
|
type Step = FileDeleterJobStep;
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
JOB_NAME
|
DELETE_JOB_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use super::{context_menu_fs_info, FsInfo, ObjectType};
|
|
||||||
use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext};
|
use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext};
|
||||||
|
|
||||||
|
use std::{hash::Hash, path::PathBuf};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use std::{collections::VecDeque, hash::Hash, path::PathBuf};
|
|
||||||
use tokio::{fs::OpenOptions, io::AsyncWriteExt};
|
use tokio::{fs::OpenOptions, io::AsyncWriteExt};
|
||||||
use tracing::warn;
|
use tracing::{trace, warn};
|
||||||
|
|
||||||
|
use super::{context_menu_fs_info, osstr_to_string, FsInfo, ObjectType};
|
||||||
|
|
||||||
pub struct FileEraserJob {}
|
pub struct FileEraserJob {}
|
||||||
|
|
||||||
|
@ -26,16 +29,16 @@ pub struct FileEraserJobStep {
|
||||||
pub fs_info: FsInfo,
|
pub fs_info: FsInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
const JOB_NAME: &str = "file_eraser";
|
pub const ERASE_JOB_NAME: &str = "file_eraser";
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl StatefulJob for FileEraserJob {
|
impl StatefulJob for FileEraserJob {
|
||||||
type Data = FileEraserJobState;
|
|
||||||
type Init = FileEraserJobInit;
|
type Init = FileEraserJobInit;
|
||||||
|
type Data = FileEraserJobState;
|
||||||
type Step = FileEraserJobStep;
|
type Step = FileEraserJobStep;
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
JOB_NAME
|
ERASE_JOB_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||||
|
@ -51,8 +54,7 @@ impl StatefulJob for FileEraserJob {
|
||||||
root_type: fs_info.obj_type.clone(),
|
root_type: fs_info.obj_type.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
state.steps = VecDeque::new();
|
state.steps = [FileEraserJobStep { fs_info }].into_iter().collect();
|
||||||
state.steps.push_back(FileEraserJobStep { fs_info });
|
|
||||||
|
|
||||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||||
|
|
||||||
|
@ -64,7 +66,7 @@ impl StatefulJob for FileEraserJob {
|
||||||
ctx: WorkerContext,
|
ctx: WorkerContext,
|
||||||
state: &mut JobState<Self>,
|
state: &mut JobState<Self>,
|
||||||
) -> Result<(), JobError> {
|
) -> Result<(), JobError> {
|
||||||
let step = state.steps[0].clone();
|
let step = &state.steps[0];
|
||||||
let info = &step.fs_info;
|
let info = &step.fs_info;
|
||||||
|
|
||||||
// need to handle stuff such as querying prisma for all paths of a file, and deleting all of those if requested (with a checkbox in the ui)
|
// need to handle stuff such as querying prisma for all paths of a file, and deleting all of those if requested (with a checkbox in the ui)
|
||||||
|
@ -75,7 +77,7 @@ impl StatefulJob for FileEraserJob {
|
||||||
let mut file = OpenOptions::new()
|
let mut file = OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(info.obj_path.clone())
|
.open(&info.obj_path)
|
||||||
.await?;
|
.await?;
|
||||||
let file_len = file.metadata().await?.len();
|
let file_len = file.metadata().await?.len();
|
||||||
|
|
||||||
|
@ -85,29 +87,29 @@ impl StatefulJob for FileEraserJob {
|
||||||
file.flush().await?;
|
file.flush().await?;
|
||||||
drop(file);
|
drop(file);
|
||||||
|
|
||||||
tokio::fs::remove_file(info.obj_path.clone()).await?;
|
trace!("Erasing file: {:?}", info.obj_path);
|
||||||
|
|
||||||
|
tokio::fs::remove_file(&info.obj_path).await?;
|
||||||
}
|
}
|
||||||
ObjectType::Directory => {
|
ObjectType::Directory => {
|
||||||
let mut dir = tokio::fs::read_dir(&info.obj_path).await?;
|
let mut dir = tokio::fs::read_dir(&info.obj_path).await?;
|
||||||
while let Some(entry) = dir.next_entry().await? {
|
while let Some(entry) = dir.next_entry().await? {
|
||||||
if entry.metadata().await?.is_dir() {
|
if entry.metadata().await?.is_dir() {
|
||||||
let obj_type = ObjectType::Directory;
|
|
||||||
state.steps.push_back(FileEraserJobStep {
|
state.steps.push_back(FileEraserJobStep {
|
||||||
fs_info: FsInfo {
|
fs_info: FsInfo {
|
||||||
obj_id: None,
|
obj_id: None,
|
||||||
obj_name: String::new(),
|
obj_name: String::new(),
|
||||||
obj_path: entry.path(),
|
obj_path: entry.path(),
|
||||||
obj_type,
|
obj_type: ObjectType::Directory,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let obj_type = ObjectType::File;
|
|
||||||
state.steps.push_back(FileEraserJobStep {
|
state.steps.push_back(FileEraserJobStep {
|
||||||
fs_info: FsInfo {
|
fs_info: FsInfo {
|
||||||
obj_id: None,
|
obj_id: None,
|
||||||
obj_name: entry.file_name().to_str().unwrap().to_string(),
|
obj_name: osstr_to_string(Some(&entry.file_name()))?,
|
||||||
obj_path: entry.path(),
|
obj_path: entry.path(),
|
||||||
obj_type,
|
obj_type: ObjectType::File,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -124,9 +126,9 @@ impl StatefulJob for FileEraserJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finalize(&self, _ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
async fn finalize(&self, _ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||||
if let Some(info) = state.data.clone() {
|
if let Some(ref info) = state.data {
|
||||||
if info.root_type == ObjectType::Directory {
|
if info.root_type == ObjectType::Directory {
|
||||||
tokio::fs::remove_dir_all(info.root_path).await?;
|
tokio::fs::remove_dir_all(&info.root_path).await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("missing job state, unable to fully finalise erase job");
|
warn!("missing job state, unable to fully finalise erase job");
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
job::JobError,
|
job::JobError,
|
||||||
prisma::{file_path, location, PrismaClient},
|
prisma::{file_path, location, PrismaClient},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::{ffi::OsStr, path::PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod copy;
|
||||||
|
pub mod cut;
|
||||||
pub mod decrypt;
|
pub mod decrypt;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod encrypt;
|
pub mod encrypt;
|
||||||
|
@ -26,11 +28,20 @@ pub struct FsInfo {
|
||||||
pub obj_type: ObjectType,
|
pub obj_type: ObjectType,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn context_menu_fs_info(
|
pub fn osstr_to_string(os_str: Option<&OsStr>) -> Result<String, JobError> {
|
||||||
|
let string = os_str
|
||||||
|
.ok_or(JobError::OsStr)?
|
||||||
|
.to_str()
|
||||||
|
.ok_or(JobError::OsStr)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_path_from_location_id(
|
||||||
db: &PrismaClient,
|
db: &PrismaClient,
|
||||||
location_id: i32,
|
location_id: i32,
|
||||||
path_id: i32,
|
) -> Result<PathBuf, JobError> {
|
||||||
) -> Result<FsInfo, JobError> {
|
|
||||||
let location = db
|
let location = db
|
||||||
.location()
|
.location()
|
||||||
.find_unique(location::id::equals(location_id))
|
.find_unique(location::id::equals(location_id))
|
||||||
|
@ -40,6 +51,22 @@ pub async fn context_menu_fs_info(
|
||||||
value: String::from("location which matches location_id"),
|
value: String::from("location which matches location_id"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
location
|
||||||
|
.local_path
|
||||||
|
.as_ref()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.ok_or(JobError::MissingData {
|
||||||
|
value: String::from("path when cast as `PathBuf`"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn context_menu_fs_info(
|
||||||
|
db: &PrismaClient,
|
||||||
|
location_id: i32,
|
||||||
|
path_id: i32,
|
||||||
|
) -> Result<FsInfo, JobError> {
|
||||||
|
let location_path = get_path_from_location_id(db, location_id).await?;
|
||||||
|
|
||||||
let item = db
|
let item = db
|
||||||
.file_path()
|
.file_path()
|
||||||
.find_unique(file_path::location_id_id(location_id, path_id))
|
.find_unique(file_path::location_id_id(location_id, path_id))
|
||||||
|
@ -49,18 +76,7 @@ pub async fn context_menu_fs_info(
|
||||||
value: String::from("file_path that matches both location id and path id"),
|
value: String::from("file_path that matches both location id and path id"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let obj_path = [
|
let obj_path = location_path.join(&item.materialized_path);
|
||||||
location
|
|
||||||
.local_path
|
|
||||||
.as_ref()
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or(JobError::MissingData {
|
|
||||||
value: String::from("path when cast as `PathBuf`"),
|
|
||||||
})?,
|
|
||||||
item.materialized_path.clone().into(),
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// i don't know if this covers symlinks
|
// i don't know if this covers symlinks
|
||||||
let obj_type = if item.is_dir {
|
let obj_type = if item.is_dir {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
use crate::{primitives::BLOCK_SIZE, Result};
|
||||||
|
|
||||||
use rand::{RngCore, SeedableRng};
|
use rand::{RngCore, SeedableRng};
|
||||||
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
|
||||||
|
|
||||||
use crate::{primitives::BLOCK_SIZE, Result};
|
|
||||||
|
|
||||||
/// This is used for erasing a file.
|
/// This is used for erasing a file.
|
||||||
///
|
///
|
||||||
/// It requires the file size, a stream and the amount of passes (to overwrite the entire stream with random data)
|
/// It requires the file size, a stream and the amount of passes (to overwrite the entire stream with random data)
|
||||||
|
@ -16,7 +16,7 @@ use crate::{primitives::BLOCK_SIZE, Result};
|
||||||
/// This also does not factor in temporary files, caching, thumbnails, etc.
|
/// This also does not factor in temporary files, caching, thumbnails, etc.
|
||||||
pub async fn erase<RW>(stream: &mut RW, size: usize, passes: usize) -> Result<()>
|
pub async fn erase<RW>(stream: &mut RW, size: usize, passes: usize) -> Result<()>
|
||||||
where
|
where
|
||||||
RW: AsyncReadExt + AsyncWriteExt + AsyncSeekExt + Unpin,
|
RW: AsyncReadExt + AsyncWriteExt + AsyncSeekExt + Unpin + Send,
|
||||||
{
|
{
|
||||||
let block_count = size / BLOCK_SIZE;
|
let block_count = size / BLOCK_SIZE;
|
||||||
let additional = size % BLOCK_SIZE;
|
let additional = size % BLOCK_SIZE;
|
||||||
|
|
|
@ -1,7 +1,24 @@
|
||||||
---
|
---
|
||||||
index: 4
|
index: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
|
||||||
prisma client rust, sqlite, migrations, backup
|
### Schema
|
||||||
|
|
||||||
|
Our data schema is defined using Prisma, you can [view it on GitHub](https://github.com/spacedriveapp/spacedrive/blob/main/core/prisma/schema.prisma).
|
||||||
|
![A cool screenshot of the Spacedrive schema](/schema.webp)
|
||||||
|
|
||||||
|
### Prisma Client Rust
|
||||||
|
|
||||||
|
We use Prisma Client Rust as a database ORM, it allows us to use Prisma to define our schema and generate migrations based on modifications to that schema.
|
||||||
|
|
||||||
|
### Migrations
|
||||||
|
|
||||||
|
Migrations are run by the Prisma migration engine on app launch.
|
||||||
|
|
||||||
|
### Database file
|
||||||
|
|
||||||
|
The databases file is SQLite and can be opened in any SQL viewer.
|
||||||
|
|
||||||
|
![A Spacedrive library database file open in Table Plus](/database-table-plus.webp)
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
---
|
---
|
||||||
index: 10
|
index: 4
|
||||||
---
|
---
|
||||||
|
|
||||||
# Explorer
|
# Explorer
|
||||||
|
|
||||||
using the interface, features
|
### Grid view
|
||||||
|
|
||||||
|
### List View
|
||||||
|
|
||||||
|
### Columns View
|
||||||
|
|
||||||
|
### Media View
|
||||||
|
|
||||||
|
### Timeline View
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
|
@ -4,4 +4,4 @@ index: 10
|
||||||
|
|
||||||
# Extensions
|
# Extensions
|
||||||
|
|
||||||
extended functionality of Spacedrive
|
Extensions are planned but nothing is yet set in stone.
|
||||||
|
|
|
@ -4,4 +4,8 @@ index: 1
|
||||||
|
|
||||||
# Libraries
|
# Libraries
|
||||||
|
|
||||||
A library is the database that Spacedrive stores all file structures and metadata. It can be synchronized with other [Nodes]()
|
A library is the database that Spacedrive stores all file structures and metadata. It can be synchronized with other [Nodes]().
|
||||||
|
|
||||||
|
To learn how data is synchronized check out the documentation on [Sync](/docs/developers/architecture/sync).
|
||||||
|
|
||||||
|
Libraries can be encrypted with a passphrase set by the user.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
index: 3
|
index: 5
|
||||||
---
|
---
|
||||||
|
|
||||||
# Spaces
|
# Spaces
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
---
|
---
|
||||||
index: 10
|
index: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
|
||||||
|
A CLI is planned, it would allow a user or application to connect and control a running Spacedrive node on a local or remote system.
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
index: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
|
||||||
|
This doc should just show how to import the core and use it in a Rust context.
|
|
@ -7,3 +7,5 @@ index: 0
|
||||||
```rust
|
```rust
|
||||||
pub struct DeveloperDocumentation;
|
pub struct DeveloperDocumentation;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This documentation is a work in progress, you will find unfinished or empty sections.
|
||||||
|
|
|
@ -3,7 +3,9 @@ name: Introduction
|
||||||
index: 0
|
index: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
# Welcome to Spacedrive
|
# Meet Spacedrive
|
||||||
|
|
||||||
|
![image](/app-ui-explorer.webp)
|
||||||
|
|
||||||
Spacedrive is a cross-platform file manager. It connects your devices together to help you organize files from anywhere.
|
Spacedrive is a cross-platform file manager. It connects your devices together to help you organize files from anywhere.
|
||||||
|
|
||||||
|
|
|
@ -4,3 +4,25 @@ index: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
# Terminology
|
# Terminology
|
||||||
|
|
||||||
|
Some useful Spacedrive related terminology.
|
||||||
|
|
||||||
|
### `Library`
|
||||||
|
|
||||||
|
A Library is the database that powers Spacedrive, all metadata and directory structures are saved in the Library. Libraries can be synced between devices and a user can have multiple libraries loaded in the Spacedrive app at once. [Learn more →](/docs/developers/architecture/libraries)
|
||||||
|
|
||||||
|
### `Object`
|
||||||
|
|
||||||
|
Objects are a fancy name for files, we call them Objects because we identify them uniquely based on a cryptographic hash of the contents. Objects in Spacedrive can come in a wide variety of kinds to provide a broad range of context. [Learn more →](/docs/developers/architecture/objects)
|
||||||
|
|
||||||
|
### `Location`
|
||||||
|
|
||||||
|
Locations are places Spacedrive will look for files, usually a directory on a mounted volume, but could also be a cloud service. [Learn more →](/docs/developers/architecture/locations)
|
||||||
|
|
||||||
|
### `Node`
|
||||||
|
|
||||||
|
A Node is a device or server running the Spacedrive core. Nodes can host libraries and communicate with other Nodes to sync data. [Learn more →](/docs/developers/architecture/nodes)
|
||||||
|
|
||||||
|
### `Preview Media`
|
||||||
|
|
||||||
|
Preview media refers to highly compressed image or video content for an Object, it is generated by Spacedrive and synced between your devices. [Learn more →](/docs/developers/architecture/preview-media)
|
||||||
|
|
|
@ -21,7 +21,9 @@
|
||||||
"client": "pnpm --filter @sd/client -- ",
|
"client": "pnpm --filter @sd/client -- ",
|
||||||
"prisma": "cd core && cargo prisma",
|
"prisma": "cd core && cargo prisma",
|
||||||
"codegen": "cargo test -p sd-core api::tests::test_and_export_rspc_bindings -- --exact",
|
"codegen": "cargo test -p sd-core api::tests::test_and_export_rspc_bindings -- --exact",
|
||||||
"typecheck": "turbo run typecheck"
|
"typecheck": "turbo run typecheck",
|
||||||
|
"lint": "turbo run lint",
|
||||||
|
"clean": "rimraf node_modules/ **/node_modules/ target/ **/.build/ **/.next/ **/dist/**"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
@ -38,6 +40,7 @@
|
||||||
"lint-staged": "^13.1.0",
|
"lint-staged": "^13.1.0",
|
||||||
"markdown-link-check": "^3.10.3",
|
"markdown-link-check": "^3.10.3",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.3",
|
||||||
|
"rimraf": "^4.1.1",
|
||||||
"turbo": "^1.5.5",
|
"turbo": "^1.5.5",
|
||||||
"turbo-ignore": "^0.3.0",
|
"turbo-ignore": "^0.3.0",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require('@sd/config/eslint-react.js'),
|
extends: [require.resolve('@sd/config/eslint/web.js')],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: './tsconfig.json'
|
project: './tsconfig.json'
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit",
|
"lint": "eslint src",
|
||||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
||||||
"typecheck": "tsc -b",
|
"typecheck": "tsc -b",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
|
|
|
@ -33,9 +33,12 @@ export type Procedures = {
|
||||||
{ key: "tags.list", input: LibraryArgs<null>, result: Array<Tag> } |
|
{ key: "tags.list", input: LibraryArgs<null>, result: Array<Tag> } |
|
||||||
{ key: "volumes.list", input: never, result: Array<Volume> },
|
{ key: "volumes.list", input: never, result: Array<Volume> },
|
||||||
mutations:
|
mutations:
|
||||||
|
{ key: "files.copyFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
|
||||||
|
{ key: "files.cutFiles", input: LibraryArgs<FileCutterJobInit>, result: null } |
|
||||||
{ key: "files.decryptFiles", input: LibraryArgs<FileDecryptorJobInit>, result: null } |
|
{ key: "files.decryptFiles", input: LibraryArgs<FileDecryptorJobInit>, result: null } |
|
||||||
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
|
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
|
||||||
{ key: "files.deleteFiles", input: LibraryArgs<FileDeleterJobInit>, result: null } |
|
{ key: "files.deleteFiles", input: LibraryArgs<FileDeleterJobInit>, result: null } |
|
||||||
|
{ key: "files.duplicateFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
|
||||||
{ key: "files.encryptFiles", input: LibraryArgs<FileEncryptorJobInit>, result: null } |
|
{ key: "files.encryptFiles", input: LibraryArgs<FileEncryptorJobInit>, result: null } |
|
||||||
{ key: "files.eraseFiles", input: LibraryArgs<FileEraserJobInit>, result: null } |
|
{ key: "files.eraseFiles", input: LibraryArgs<FileEraserJobInit>, result: null } |
|
||||||
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |
|
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |
|
||||||
|
@ -96,6 +99,10 @@ export interface ExplorerData { context: ExplorerContext, items: Array<ExplorerI
|
||||||
|
|
||||||
export type ExplorerItem = { type: "Path" } & FilePathWithObject | { type: "Object" } & ObjectWithFilePaths
|
export type ExplorerItem = { type: "Path" } & FilePathWithObject | { type: "Object" } & ObjectWithFilePaths
|
||||||
|
|
||||||
|
export interface FileCopierJobInit { source_location_id: number, source_path_id: number, target_location_id: number, target_path: string, target_file_name_suffix: string | null }
|
||||||
|
|
||||||
|
export interface FileCutterJobInit { source_location_id: number, source_path_id: number, target_location_id: number, target_path: string }
|
||||||
|
|
||||||
export interface FileDecryptorJobInit { location_id: number, path_id: number, output_path: string | null, password: string | null, save_to_library: boolean | null }
|
export interface FileDecryptorJobInit { location_id: number, path_id: number, output_path: string | null, password: string | null, save_to_library: boolean | null }
|
||||||
|
|
||||||
export interface FileDeleterJobInit { location_id: number, path_id: number }
|
export interface FileDeleterJobInit { location_id: number, path_id: number }
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
declare global {
|
declare global {
|
||||||
|
// eslint-disable-next-line
|
||||||
var isDev: boolean;
|
var isDev: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
'react-native/react-native': true
|
|
||||||
},
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true
|
|
||||||
},
|
|
||||||
ecmaVersion: 12,
|
|
||||||
sourceType: 'module'
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended'
|
|
||||||
],
|
|
||||||
plugins: ['react', 'react-native'],
|
|
||||||
rules: {
|
|
||||||
'react/display-name': 'off',
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
'react/no-unescaped-entities': 'off',
|
|
||||||
'react/react-in-jsx-scope': 'off',
|
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'no-control-regex': 'off',
|
|
||||||
'no-mixed-spaces-and-tabs': ['warn', 'smart-tabs'],
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
name: 'react-native',
|
|
||||||
importNames: ['SafeAreaView'],
|
|
||||||
message: 'Import SafeAreaView from react-native-safe-area-context instead'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'react-native',
|
|
||||||
importNames: ['Button'],
|
|
||||||
message: 'Import Button from ~/components instead.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
ignorePatterns: ['**/*.js', '**/*.json', 'node_modules', 'android', 'ios', '.expo'],
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,8 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
|
@ -16,7 +12,8 @@ module.exports = {
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'prettier'
|
'prettier',
|
||||||
|
'turbo'
|
||||||
],
|
],
|
||||||
plugins: ['react'],
|
plugins: ['react'],
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -24,7 +21,7 @@ module.exports = {
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'react/no-unescaped-entities': 'off',
|
'react/no-unescaped-entities': 'off',
|
||||||
'react/react-in-jsx-scope': 'off',
|
'react/react-in-jsx-scope': 'off',
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
'react-hooks/rules-of-hooks': 'warn',
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
@ -32,10 +29,12 @@ module.exports = {
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-empty-interface': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
'no-control-regex': 'off',
|
'no-control-regex': 'off',
|
||||||
'no-mixed-spaces-and-tabs': ['warn', 'smart-tabs']
|
'no-mixed-spaces-and-tabs': ['warn', 'smart-tabs']
|
||||||
},
|
},
|
||||||
ignorePatterns: ['**/*.js', '**/*.json', 'node_modules'],
|
ignorePatterns: ['dist', '**/*.js', '**/*.json', 'node_modules'],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: 'detect'
|
version: 'detect'
|
27
packages/config/eslint/reactNative.js
Normal file
27
packages/config/eslint/reactNative.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: [require.resolve('./base.js')],
|
||||||
|
env: {
|
||||||
|
'react-native/react-native': true
|
||||||
|
},
|
||||||
|
plugins: ['react-native'],
|
||||||
|
ignorePatterns: ['android', 'ios', '.expo'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
name: 'react-native',
|
||||||
|
importNames: ['SafeAreaView'],
|
||||||
|
message: 'Import SafeAreaView from react-native-safe-area-context instead'
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// name: 'react-native',
|
||||||
|
// importNames: ['Button'],
|
||||||
|
// message: 'Import Button from ~/components instead.'
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
8
packages/config/eslint/web.js
Normal file
8
packages/config/eslint/web.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: [require.resolve('./base.js')],
|
||||||
|
ignorePatterns: ['public', 'vite.config.ts'],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true
|
||||||
|
}
|
||||||
|
};
|
3
packages/config/index.js
Normal file
3
packages/config/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
vite: require('./vite')
|
||||||
|
};
|
|
@ -3,16 +3,18 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
"./*": "./*",
|
||||||
"./vite": "./vite/index.js"
|
"./vite": "./vite/index.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"eslint-react.js"
|
"eslint-react.js"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||||
"@typescript-eslint/parser": "^5.39.0",
|
"@typescript-eslint/parser": "^5.48.2",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^8.24.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-config-turbo": "^0.0.7",
|
||||||
"eslint-plugin-react": "^7.31.8",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0"
|
"eslint-plugin-react-hooks": "^4.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require('@sd/config/eslint-react.js'),
|
extends: [require.resolve('@sd/config/eslint/web.js')],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: './tsconfig.json'
|
project: './tsconfig.json'
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"icons": "./scripts/generateSvgImports.mjs",
|
"icons": "./scripts/generateSvgImports.mjs",
|
||||||
"lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc -b",
|
"typecheck": "tsc -b",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
"@splinetool/runtime": "^0.9.128",
|
"@splinetool/runtime": "^0.9.128",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tanstack/react-query": "^4.12.0",
|
"@tanstack/react-query": "^4.12.0",
|
||||||
"@tanstack/react-query-devtools": "^4.12.0",
|
"@tanstack/react-query-devtools": "^4.22.0",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.18",
|
"@tanstack/react-virtual": "3.0.0-beta.18",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
"@zxcvbn-ts/core": "^2.1.0",
|
"@zxcvbn-ts/core": "^2.1.0",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Input, useZodForm, z } from '@sd/ui/src/forms';
|
||||||
|
|
||||||
const schema = z.object({ path: z.string() });
|
const schema = z.object({ path: z.string() });
|
||||||
|
|
||||||
interface Props extends UseDialogProps {}
|
type Props = UseDialogProps;
|
||||||
|
|
||||||
export default function AddLocationDialog(props: Props) {
|
export default function AddLocationDialog(props: Props) {
|
||||||
const dialog = useDialog(props);
|
const dialog = useDialog(props);
|
||||||
|
|
|
@ -14,7 +14,7 @@ const schema = z.object({
|
||||||
filePath: z.string()
|
filePath: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface BackupRestorationDialogProps extends UseDialogProps {}
|
export type BackupRestorationDialogProps = UseDialogProps;
|
||||||
|
|
||||||
export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
|
|
|
@ -20,7 +20,7 @@ const schema = z.object({
|
||||||
hashing_algorithm: z.string()
|
hashing_algorithm: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props extends UseDialogProps {}
|
type Props = UseDialogProps;
|
||||||
|
|
||||||
export default function CreateLibraryDialog(props: Props) {
|
export default function CreateLibraryDialog(props: Props) {
|
||||||
const dialog = useDialog(props);
|
const dialog = useDialog(props);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useZodForm, z } from '@sd/ui/src/forms';
|
||||||
import { getHashingAlgorithmString } from '~/screens/settings/library/KeysSetting';
|
import { getHashingAlgorithmString } from '~/screens/settings/library/KeysSetting';
|
||||||
import { SelectOptionKeyList } from '../key/KeyList';
|
import { SelectOptionKeyList } from '../key/KeyList';
|
||||||
|
|
||||||
interface KeyViewerDialogProps extends UseDialogProps {}
|
type KeyViewerDialogProps = UseDialogProps;
|
||||||
|
|
||||||
export const KeyUpdater = (props: {
|
export const KeyUpdater = (props: {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { showAlertDialog } from '~/util/dialog';
|
||||||
import { generatePassword } from '../key/KeyMounter';
|
import { generatePassword } from '../key/KeyMounter';
|
||||||
import { PasswordMeter } from '../key/PasswordMeter';
|
import { PasswordMeter } from '../key/PasswordMeter';
|
||||||
|
|
||||||
export interface MasterPasswordChangeDialogProps extends UseDialogProps {}
|
export type MasterPasswordChangeDialogProps = UseDialogProps;
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
masterPassword: z.string(),
|
masterPassword: z.string(),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { ExplorerData, rspc, useCurrentLibrary } from '@sd/client';
|
import { ExplorerData, rspc, useCurrentLibrary } from '@sd/client';
|
||||||
import { useExplorerStore } from '~/hooks/useExplorerStore';
|
import { useExplorerStore } from '~/hooks/useExplorerStore';
|
||||||
import { Inspector } from '../explorer/Inspector';
|
import { Inspector } from '../explorer/Inspector';
|
||||||
|
@ -32,50 +32,50 @@ export default function Explorer(props: Props) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
const onScroll = useCallback((y: number) => {
|
||||||
<>
|
setScrollSegments((old) => {
|
||||||
<div className="relative">
|
return {
|
||||||
<ExplorerContextMenu>
|
...old,
|
||||||
<div className="relative flex flex-col w-full">
|
mainList: y
|
||||||
<TopBar showSeparator={separateTopBar} />
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<ExplorerContextMenu>
|
||||||
|
<div className="relative flex flex-col w-full">
|
||||||
|
<TopBar showSeparator={separateTopBar} />
|
||||||
|
|
||||||
|
<div className="relative flex flex-row w-full max-h-full app-background">
|
||||||
|
{props.data && (
|
||||||
|
<VirtualizedList
|
||||||
|
data={props.data.items}
|
||||||
|
context={props.data.context}
|
||||||
|
onScroll={onScroll}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{expStore.showInspector && (
|
||||||
|
<div className="flex min-w-[260px] max-w-[260px]">
|
||||||
|
<Inspector
|
||||||
|
onScroll={(e) => {
|
||||||
|
const y = (e.target as HTMLElement).scrollTop;
|
||||||
|
|
||||||
<div className="relative flex flex-row w-full max-h-full app-background">
|
|
||||||
{props.data && (
|
|
||||||
<VirtualizedList
|
|
||||||
data={props.data.items || []}
|
|
||||||
context={props.data.context}
|
|
||||||
onScroll={(y) => {
|
|
||||||
setScrollSegments((old) => {
|
setScrollSegments((old) => {
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
mainList: y
|
inspector: y
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
key={props.data?.items[expStore.selectedRowIndex]?.id}
|
||||||
|
data={props.data?.items[expStore.selectedRowIndex]}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
{expStore.showInspector && (
|
)}
|
||||||
<div className="flex min-w-[260px] max-w-[260px]">
|
|
||||||
<Inspector
|
|
||||||
onScroll={(e) => {
|
|
||||||
const y = (e.target as HTMLElement).scrollTop;
|
|
||||||
|
|
||||||
setScrollSegments((old) => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
inspector: y
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
key={props.data?.items[expStore.selectedRowIndex]?.id}
|
|
||||||
data={props.data?.items[expStore.selectedRowIndex]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ExplorerContextMenu>
|
</div>
|
||||||
</div>
|
</ExplorerContextMenu>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import {
|
import {
|
||||||
ArrowBendUpRight,
|
ArrowBendUpRight,
|
||||||
|
Clipboard,
|
||||||
|
Copy,
|
||||||
|
FileX,
|
||||||
Image,
|
Image,
|
||||||
LockSimple,
|
LockSimple,
|
||||||
LockSimpleOpen,
|
LockSimpleOpen,
|
||||||
Package,
|
Package,
|
||||||
Plus,
|
Plus,
|
||||||
Repeat,
|
Repeat,
|
||||||
|
Scissors,
|
||||||
Share,
|
Share,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
TagSimple,
|
TagSimple,
|
||||||
|
@ -16,8 +20,9 @@ import { PropsWithChildren, useMemo } from 'react';
|
||||||
import { ExplorerItem, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
import { ExplorerItem, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
import { ContextMenu as CM } from '@sd/ui';
|
import { ContextMenu as CM } from '@sd/ui';
|
||||||
import { dialogManager } from '@sd/ui';
|
import { dialogManager } from '@sd/ui';
|
||||||
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
import { CutCopyType, getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
||||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||||
|
import { useExplorerParams } from '~/screens/LocationExplorer';
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
import { showAlertDialog } from '~/util/dialog';
|
import { showAlertDialog } from '~/util/dialog';
|
||||||
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
|
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
|
||||||
|
@ -96,11 +101,14 @@ function OpenInNativeExplorer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExplorerContextMenu(props: PropsWithChildren) {
|
export function ExplorerContextMenu(props: PropsWithChildren) {
|
||||||
const store = getExplorerStore();
|
const store = useExplorerStore();
|
||||||
|
const params = useExplorerParams();
|
||||||
|
|
||||||
const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation');
|
const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation');
|
||||||
const objectValidator = useLibraryMutation('jobs.objectValidator');
|
const objectValidator = useLibraryMutation('jobs.objectValidator');
|
||||||
const rescanLocation = useLibraryMutation('locations.fullRescan');
|
const rescanLocation = useLibraryMutation('locations.fullRescan');
|
||||||
|
const copyFiles = useLibraryMutation('files.copyFiles');
|
||||||
|
const cutFiles = useLibraryMutation('files.cutFiles');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
@ -131,6 +139,45 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
|
||||||
icon={Repeat}
|
icon={Repeat}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Paste"
|
||||||
|
keybind="⌘V"
|
||||||
|
hidden={!store.cutCopyState.active}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (store.cutCopyState.actionType == CutCopyType.Copy) {
|
||||||
|
store.locationId &&
|
||||||
|
copyFiles.mutate({
|
||||||
|
source_location_id: store.cutCopyState.sourceLocationId,
|
||||||
|
source_path_id: store.cutCopyState.sourcePathId,
|
||||||
|
target_location_id: store.locationId,
|
||||||
|
target_path: params.path,
|
||||||
|
target_file_name_suffix: null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
store.locationId &&
|
||||||
|
cutFiles.mutate({
|
||||||
|
source_location_id: store.cutCopyState.sourceLocationId,
|
||||||
|
source_path_id: store.cutCopyState.sourcePathId,
|
||||||
|
target_location_id: store.locationId,
|
||||||
|
target_path: params.path
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon={Clipboard}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Deselect"
|
||||||
|
hidden={!store.cutCopyState.active}
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
...store.cutCopyState,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={FileX}
|
||||||
|
/>
|
||||||
|
|
||||||
<CM.SubMenu label="More actions..." icon={Plus}>
|
<CM.SubMenu label="More actions..." icon={Plus}>
|
||||||
<CM.Item
|
<CM.Item
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -160,6 +207,8 @@ export interface FileItemContextMenuProps extends PropsWithChildren {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) {
|
export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) {
|
||||||
|
const store = useExplorerStore();
|
||||||
|
const params = useExplorerParams();
|
||||||
const objectData = props.item ? (isObject(props.item) ? props.item : props.item.object) : null;
|
const objectData = props.item ? (isObject(props.item) ? props.item : props.item.object) : null;
|
||||||
|
|
||||||
const hasMasterPasswordQuery = useLibraryQuery(['keys.hasMasterPassword']);
|
const hasMasterPasswordQuery = useLibraryQuery(['keys.hasMasterPassword']);
|
||||||
|
@ -172,6 +221,8 @@ export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) {
|
||||||
const hasMountedKeys =
|
const hasMountedKeys =
|
||||||
mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false;
|
mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false;
|
||||||
|
|
||||||
|
const copyFiles = useLibraryMutation('files.copyFiles');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CM.ContextMenu trigger={props.children}>
|
<CM.ContextMenu trigger={props.children}>
|
||||||
|
@ -186,7 +237,59 @@ export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) {
|
||||||
<CM.Separator />
|
<CM.Separator />
|
||||||
|
|
||||||
<CM.Item label="Rename" />
|
<CM.Item label="Rename" />
|
||||||
<CM.Item label="Duplicate" keybind="⌘D" />
|
<CM.Item
|
||||||
|
label="Duplicate"
|
||||||
|
keybind="⌘D"
|
||||||
|
onClick={(e) => {
|
||||||
|
copyFiles.mutate({
|
||||||
|
source_location_id: store.locationId!,
|
||||||
|
source_path_id: props.item.id,
|
||||||
|
target_location_id: store.locationId!,
|
||||||
|
target_path: params.path,
|
||||||
|
target_file_name_suffix: ' - Clone'
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Cut"
|
||||||
|
keybind="⌘X"
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
sourceLocationId: store.locationId!,
|
||||||
|
sourcePathId: props.item.id,
|
||||||
|
actionType: CutCopyType.Cut,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={Scissors}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Copy"
|
||||||
|
keybind="⌘C"
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
sourceLocationId: store.locationId!,
|
||||||
|
sourcePathId: props.item.id,
|
||||||
|
actionType: CutCopyType.Copy,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={Copy}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Deselect"
|
||||||
|
hidden={!store.cutCopyState.active}
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
...store.cutCopyState,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={FileX}
|
||||||
|
/>
|
||||||
|
|
||||||
<CM.Separator />
|
<CM.Separator />
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useKey, useOnWindowResize } from 'rooks';
|
import { useKey, useOnWindowResize } from 'rooks';
|
||||||
import { ExplorerContext, ExplorerItem } from '@sd/client';
|
import { ExplorerContext, ExplorerItem } from '@sd/client';
|
||||||
|
@ -17,7 +17,7 @@ interface Props {
|
||||||
onScroll?: (posY: number) => void;
|
onScroll?: (posY: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VirtualizedList: React.FC<Props> = ({ data, context, onScroll }) => {
|
export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const innerRef = useRef<HTMLDivElement>(null);
|
const innerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export const VirtualizedList: React.FC<Props> = ({ data, context, onScroll }) =>
|
||||||
el.addEventListener('scroll', onElementScroll);
|
el.addEventListener('scroll', onElementScroll);
|
||||||
|
|
||||||
return () => el.removeEventListener('scroll', onElementScroll);
|
return () => el.removeEventListener('scroll', onElementScroll);
|
||||||
}, [scrollRef, onScroll]);
|
}, [onScroll]);
|
||||||
|
|
||||||
const rowVirtualizer = useVirtualizer({
|
const rowVirtualizer = useVirtualizer({
|
||||||
count: amountOfRows,
|
count: amountOfRows,
|
||||||
|
@ -169,7 +169,7 @@ export const VirtualizedList: React.FC<Props> = ({ data, context, onScroll }) =>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
interface WrappedItemProps {
|
interface WrappedItemProps {
|
||||||
item: ExplorerItem;
|
item: ExplorerItem;
|
||||||
|
@ -179,7 +179,7 @@ interface WrappedItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap either list item or grid item with click logic as it is the same for both
|
// Wrap either list item or grid item with click logic as it is the same for both
|
||||||
const WrappedItem: React.FC<WrappedItemProps> = ({ item, index, isSelected, kind }) => {
|
const WrappedItem = memo(({ item, index, isSelected, kind }: WrappedItemProps) => {
|
||||||
const [_, setSearchParams] = useSearchParams();
|
const [_, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const onDoubleClick = useCallback(() => {
|
const onDoubleClick = useCallback(() => {
|
||||||
|
@ -191,6 +191,7 @@ const WrappedItem: React.FC<WrappedItemProps> = ({ item, index, isSelected, kind
|
||||||
}, [isSelected, index]);
|
}, [isSelected, index]);
|
||||||
|
|
||||||
const ItemComponent = kind === 'list' ? FileRow : FileItem;
|
const ItemComponent = kind === 'list' ? FileRow : FileItem;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemComponent
|
<ItemComponent
|
||||||
data={item}
|
data={item}
|
||||||
|
@ -200,18 +201,4 @@ const WrappedItem: React.FC<WrappedItemProps> = ({ item, index, isSelected, kind
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
// // Memorize the item so that it doesn't get re-rendered when the selection changes
|
|
||||||
// return useMemo(() => {
|
|
||||||
// const ItemComponent = kind === 'list' ? FileRow : FileItem;
|
|
||||||
// return (
|
|
||||||
// <ItemComponent
|
|
||||||
// data={item}
|
|
||||||
// index={index}
|
|
||||||
// onClick={onClick}
|
|
||||||
// onDoubleClick={onDoubleClick}
|
|
||||||
// selected={isSelected}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// }, [item, index, isSelected]);
|
|
||||||
};
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
ArrowsClockwise,
|
ArrowsClockwise,
|
||||||
Camera,
|
Camera,
|
||||||
|
Copy,
|
||||||
|
DotsThree,
|
||||||
Eye,
|
Eye,
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
Folder,
|
Folder,
|
||||||
|
@ -10,6 +12,7 @@ import {
|
||||||
LockSimpleOpen,
|
LockSimpleOpen,
|
||||||
Pause,
|
Pause,
|
||||||
Question,
|
Question,
|
||||||
|
Scissors,
|
||||||
Trash,
|
Trash,
|
||||||
TrashSimple,
|
TrashSimple,
|
||||||
X
|
X
|
||||||
|
@ -66,6 +69,18 @@ const getNiceData = (job: JobReport): Record<string, JobNiceData> => ({
|
||||||
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
||||||
}`,
|
}`,
|
||||||
icon: Trash
|
icon: Trash
|
||||||
|
},
|
||||||
|
file_copier: {
|
||||||
|
name: `Copied ${numberWithCommas(job.task_count)} ${
|
||||||
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
||||||
|
}`,
|
||||||
|
icon: Copy
|
||||||
|
},
|
||||||
|
file_cutter: {
|
||||||
|
name: `Moved ${numberWithCommas(job.task_count)} ${
|
||||||
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
||||||
|
}`,
|
||||||
|
icon: Scissors
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,11 @@ export enum ExplorerKind {
|
||||||
Space
|
Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CutCopyType {
|
||||||
|
Cut,
|
||||||
|
Copy
|
||||||
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
locationId: null as number | null,
|
locationId: null as number | null,
|
||||||
layoutMode: 'grid' as ExplorerLayoutMode,
|
layoutMode: 'grid' as ExplorerLayoutMode,
|
||||||
|
@ -21,7 +26,13 @@ const state = {
|
||||||
multiSelectIndexes: [] as number[],
|
multiSelectIndexes: [] as number[],
|
||||||
contextMenuObjectId: null as number | null,
|
contextMenuObjectId: null as number | null,
|
||||||
contextMenuActiveObject: null as object | null,
|
contextMenuActiveObject: null as object | null,
|
||||||
newThumbnails: {} as Record<string, boolean>
|
newThumbnails: {} as Record<string, boolean>,
|
||||||
|
cutCopyState: {
|
||||||
|
sourceLocationId: 0,
|
||||||
|
sourcePathId: 0,
|
||||||
|
actionType: CutCopyType.Cut,
|
||||||
|
active: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onLibraryChange(() => getExplorerStore().reset());
|
onLibraryChange(() => getExplorerStore().reset());
|
||||||
|
|
|
@ -22,7 +22,7 @@ function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolea
|
||||||
<p className="mt-0.5 text-xs text-ink-dull">{props.library.uuid}</p>
|
<p className="mt-0.5 text-xs text-ink-dull">{props.library.uuid}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<Button className="!p-1.5" onClick={() => {}} variant="gray">
|
<Button className="!p-1.5" variant="gray">
|
||||||
<Tooltip label="TODO">
|
<Tooltip label="TODO">
|
||||||
<Database className="w-4 h-4" />
|
<Database className="w-4 h-4" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"types": ["vite-plugin-svgr/client", "vite/client"]
|
"types": ["vite-plugin-svgr/client", "vite/client"]
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
"exclude": ["dist"],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../ui"
|
"path": "../ui"
|
||||||
|
|
|
@ -18,7 +18,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// Dependencies declare other packages that this package depends on.
|
// Dependencies declare other packages that this package depends on.
|
||||||
// .package(url: /* package url */, from: "1.0.0"),
|
// .package(url: /* package url */, from: "1.0.0"),
|
||||||
.package(url: "https://github.com/brendonovich/swift-rs", revision: "833e29ba333f1dfe303eaa21de78c4f8c5a3f2ff"),
|
.package(url: "https://github.com/brendonovich/swift-rs", revision: "c3003bc0c28a6742d3da341b61887d8e072fda0a"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
|
7
packages/ui/.eslintrc.js
Normal file
7
packages/ui/.eslintrc.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: [require.resolve('@sd/config/eslint/web.js')],
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: './tsconfig.json'
|
||||||
|
}
|
||||||
|
};
|
|
@ -16,6 +16,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"storybook": "start-storybook -p 6006",
|
"storybook": "start-storybook -p 6006",
|
||||||
"storybook:build": "build-storybook",
|
"storybook:build": "build-storybook",
|
||||||
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc -b",
|
"typecheck": "tsc -b",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
|
|
|
@ -75,7 +75,7 @@ function Remover({ id }: { id: number }) {
|
||||||
() => () => {
|
() => () => {
|
||||||
dialogManager.remove(id);
|
dialogManager.remove(id);
|
||||||
},
|
},
|
||||||
[]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -39,21 +39,24 @@ type DropdownItemProps =
|
||||||
VariantProps<typeof itemStyles>;
|
VariantProps<typeof itemStyles>;
|
||||||
|
|
||||||
export const Item = ({ to, className, icon: Icon, children, ...props }: DropdownItemProps) => {
|
export const Item = ({ to, className, icon: Icon, children, ...props }: DropdownItemProps) => {
|
||||||
let content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{Icon && <Icon weight="bold" className={itemIconStyles(props)} />}
|
{Icon && <Icon weight="bold" className={itemIconStyles(props)} />}
|
||||||
<span className="text-left">{children}</span>
|
<span className="text-left">{children}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
return (
|
||||||
return to ? (
|
<Menu.Item>
|
||||||
<Link {...props} to={to} className={clsx(itemStyles(props), className)}>
|
{to ? (
|
||||||
{content}
|
<Link {...props} to={to} className={clsx(itemStyles(props), className)}>
|
||||||
</Link>
|
{content}
|
||||||
) : (
|
</Link>
|
||||||
<button {...props} className={clsx(itemStyles(props), className)}>
|
) : (
|
||||||
{content}
|
<button {...props} className={clsx(itemStyles(props), className)}>
|
||||||
</button>
|
{content}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@ import { forwardRef } from 'react';
|
||||||
|
|
||||||
export interface SwitchProps
|
export interface SwitchProps
|
||||||
extends VariantProps<typeof switchStyles>,
|
extends VariantProps<typeof switchStyles>,
|
||||||
SwitchPrimitive.SwitchProps {}
|
SwitchPrimitive.SwitchProps {
|
||||||
|
thumbClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const switchStyles = cva(
|
const switchStyles = cva(
|
||||||
[
|
[
|
||||||
|
@ -46,9 +48,9 @@ const thumbStyles = cva(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(
|
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(
|
||||||
({ size, className, ...props }, ref) => (
|
({ size, className, thumbClassName, ...props }, ref) => (
|
||||||
<SwitchPrimitive.Root {...props} ref={ref} className={switchStyles({ size, className })}>
|
<SwitchPrimitive.Root {...props} ref={ref} className={switchStyles({ size, className })}>
|
||||||
<SwitchPrimitive.Thumb className={thumbStyles({ size, className })} />
|
<SwitchPrimitive.Thumb className={thumbStyles({ size, className: thumbClassName })} />
|
||||||
</SwitchPrimitive.Root>
|
</SwitchPrimitive.Root>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,6 +17,7 @@ type TailwindFactory = {
|
||||||
<T>(c: T): ClassnameFactory<T>;
|
<T>(c: T): ClassnameFactory<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-ignore-next-line
|
||||||
export const tw = new Proxy((() => {}) as unknown as TailwindFactory, {
|
export const tw = new Proxy((() => {}) as unknown as TailwindFactory, {
|
||||||
get: (_, property: string) => twFactory(property),
|
get: (_, property: string) => twFactory(property),
|
||||||
apply: (_, __, [el]: [React.ReactElement]) => twFactory(el)
|
apply: (_, __, [el]: [React.ReactElement]) => twFactory(el)
|
||||||
|
|
|
@ -16,6 +16,7 @@ importers:
|
||||||
lint-staged: ^13.1.0
|
lint-staged: ^13.1.0
|
||||||
markdown-link-check: ^3.10.3
|
markdown-link-check: ^3.10.3
|
||||||
prettier: ^2.8.3
|
prettier: ^2.8.3
|
||||||
|
rimraf: ^4.1.1
|
||||||
turbo: ^1.5.5
|
turbo: ^1.5.5
|
||||||
turbo-ignore: ^0.3.0
|
turbo-ignore: ^0.3.0
|
||||||
typescript: ^4.9.4
|
typescript: ^4.9.4
|
||||||
|
@ -29,6 +30,7 @@ importers:
|
||||||
lint-staged: 13.1.0
|
lint-staged: 13.1.0
|
||||||
markdown-link-check: 3.10.3
|
markdown-link-check: 3.10.3
|
||||||
prettier: 2.8.3
|
prettier: 2.8.3
|
||||||
|
rimraf: 4.1.1
|
||||||
turbo: 1.7.0
|
turbo: 1.7.0
|
||||||
turbo-ignore: 0.3.0
|
turbo-ignore: 0.3.0
|
||||||
typescript: 4.9.4
|
typescript: 4.9.4
|
||||||
|
@ -388,10 +390,11 @@ importers:
|
||||||
|
|
||||||
packages/config:
|
packages/config:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@typescript-eslint/eslint-plugin': ^5.39.0
|
'@typescript-eslint/eslint-plugin': ^5.48.2
|
||||||
'@typescript-eslint/parser': ^5.39.0
|
'@typescript-eslint/parser': ^5.48.2
|
||||||
eslint: ^8.24.0
|
eslint: ^8.24.0
|
||||||
eslint-config-prettier: ^8.5.0
|
eslint-config-prettier: ^8.5.0
|
||||||
|
eslint-config-turbo: ^0.0.7
|
||||||
eslint-plugin-react: ^7.31.8
|
eslint-plugin-react: ^7.31.8
|
||||||
eslint-plugin-react-hooks: ^4.6.0
|
eslint-plugin-react-hooks: ^4.6.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -399,6 +402,7 @@ importers:
|
||||||
'@typescript-eslint/parser': 5.48.2_eslint@8.32.0
|
'@typescript-eslint/parser': 5.48.2_eslint@8.32.0
|
||||||
eslint: 8.32.0
|
eslint: 8.32.0
|
||||||
eslint-config-prettier: 8.6.0_eslint@8.32.0
|
eslint-config-prettier: 8.6.0_eslint@8.32.0
|
||||||
|
eslint-config-turbo: 0.0.7_eslint@8.32.0
|
||||||
eslint-plugin-react: 7.32.1_eslint@8.32.0
|
eslint-plugin-react: 7.32.1_eslint@8.32.0
|
||||||
eslint-plugin-react-hooks: 4.6.0_eslint@8.32.0
|
eslint-plugin-react-hooks: 4.6.0_eslint@8.32.0
|
||||||
|
|
||||||
|
@ -421,7 +425,7 @@ importers:
|
||||||
'@splinetool/runtime': ^0.9.128
|
'@splinetool/runtime': ^0.9.128
|
||||||
'@tailwindcss/forms': ^0.5.3
|
'@tailwindcss/forms': ^0.5.3
|
||||||
'@tanstack/react-query': ^4.12.0
|
'@tanstack/react-query': ^4.12.0
|
||||||
'@tanstack/react-query-devtools': ^4.12.0
|
'@tanstack/react-query-devtools': ^4.22.0
|
||||||
'@tanstack/react-virtual': 3.0.0-beta.18
|
'@tanstack/react-virtual': 3.0.0-beta.18
|
||||||
'@types/babel-core': ^6.25.7
|
'@types/babel-core': ^6.25.7
|
||||||
'@types/byte-size': ^8.1.0
|
'@types/byte-size': ^8.1.0
|
||||||
|
@ -8292,7 +8296,7 @@ packages:
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.12
|
'@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.12
|
||||||
magic-string: 0.26.7
|
magic-string: 0.26.7
|
||||||
react-refresh: 0.14.0
|
react-refresh: 0.14.0
|
||||||
vite: 4.0.4_sass@1.57.1
|
vite: 4.0.4_@types+node@18.11.18
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -11637,6 +11641,15 @@ packages:
|
||||||
eslint: 8.32.0
|
eslint: 8.32.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-config-turbo/0.0.7_eslint@8.32.0:
|
||||||
|
resolution: {integrity: sha512-WbrGlyfs94rOXrhombi1wjIAYGdV2iosgJRndOZtmDQeq5GLTzYmBUCJQZWtLBEBUPCj96RxZ2OL7Cn+xv/Azg==}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>6.6.0'
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.32.0
|
||||||
|
eslint-plugin-turbo: 0.0.7_eslint@8.32.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-react-hooks/4.6.0_eslint@8.32.0:
|
/eslint-plugin-react-hooks/4.6.0_eslint@8.32.0:
|
||||||
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -11685,6 +11698,14 @@ packages:
|
||||||
string.prototype.matchall: 4.0.8
|
string.prototype.matchall: 4.0.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-plugin-turbo/0.0.7_eslint@8.32.0:
|
||||||
|
resolution: {integrity: sha512-iajOH8eD4jha3duztGVBD1BEmvNrQBaA/y3HFHf91vMDRYRwH7BpHSDFtxydDpk5ghlhRxG299SFxz7D6z4MBQ==}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>6.6.0'
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.32.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-scope/4.0.3:
|
/eslint-scope/4.0.3:
|
||||||
resolution: {integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==}
|
resolution: {integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
@ -18179,6 +18200,12 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
|
/rimraf/4.1.1:
|
||||||
|
resolution: {integrity: sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ripemd160/2.0.2:
|
/ripemd160/2.0.2:
|
||||||
resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
|
resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -20804,7 +20831,7 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.0.2
|
'@rollup/pluginutils': 5.0.2
|
||||||
'@svgr/core': 6.5.1
|
'@svgr/core': 6.5.1
|
||||||
vite: 4.0.4_sass@1.57.1
|
vite: 4.0.4_@types+node@18.11.18
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -20900,7 +20927,6 @@ packages:
|
||||||
rollup: 3.10.0
|
rollup: 3.10.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite/4.0.4_ovmyjmuuyckt3r3gpaexj2onji:
|
/vite/4.0.4_ovmyjmuuyckt3r3gpaexj2onji:
|
||||||
resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==}
|
resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==}
|
||||||
|
@ -20968,6 +20994,7 @@ packages:
|
||||||
sass: 1.57.1
|
sass: 1.57.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/vlq/1.0.1:
|
/vlq/1.0.1:
|
||||||
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
|
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
|
||||||
|
|
Loading…
Reference in a new issue