Merge branch 'main' into eng-1748-spacedrop-refactor-spacedrop-cloud

This commit is contained in:
Utku Bakir 2024-05-10 11:45:54 -04:00
commit 287f7acc95
491 changed files with 12527 additions and 5209 deletions

View file

@ -11,9 +11,11 @@ codegen
Condvar
dashmap
davidmytton
dayjs
deel
elon
encryptor
Exif
Flac
graps
haden
@ -59,7 +61,9 @@ storedkey
stringly
thumbstrips
tobiaslutke
tokio
typecheck
uuid
vdfs
vijay
zacharysmith

View file

@ -17,10 +17,9 @@ runs:
steps:
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@stable
uses: IronCoreLabs/rust-toolchain@v1
with:
target: ${{ inputs.target }}
toolchain: '1.75'
components: clippy, rustfmt
- name: Cache Rust Dependencies

View file

@ -62,12 +62,14 @@ runs:
shell: bash
if: ${{ runner.os == 'Linux' }}
run: |
dpkg -l | grep i386
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386"
sudo dpkg --remove-architecture i386
set -eux
if dpkg -l | grep i386; then
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386" || true
sudo dpkg --remove-architecture i386 || true
fi
# https://github.com/actions/runner-images/issues/9546#issuecomment-2014940361
sudo apt-get remove libunwind-*
sudo apt-get remove libunwind-* || true
- name: Setup Rust and Dependencies
uses: ./.github/actions/setup-rust

View file

@ -40,15 +40,15 @@ jobs:
target: x86_64-pc-windows-msvc
# - host: windows-latest
# target: aarch64-pc-windows-msvc
- host: ubuntu-20.04
- host: ubuntu-22.04
target: x86_64-unknown-linux-gnu
# - host: ubuntu-20.04
# - host: ubuntu-22.04
# target: x86_64-unknown-linux-musl
# - host: ubuntu-20.04
# - host: uubuntu-22.04
# target: aarch64-unknown-linux-gnu
# - host: ubuntu-20.04
# - host: ubuntu-22.04
# target: aarch64-unknown-linux-musl
# - host: ubuntu-20.04
# - host: ubuntu-22.04
# target: armv7-unknown-linux-gnueabihf
name: 'Make Cache'
runs-on: ${{ matrix.settings.host }}

View file

@ -178,7 +178,7 @@ jobs:
runs-on: ${{ matrix.platform }}
strategy:
matrix:
platform: [ubuntu-20.04, macos-14, windows-latest]
platform: [ubuntu-22.04, macos-14, windows-latest]
permissions:
contents: read
timeout-minutes: 45
@ -241,7 +241,7 @@ jobs:
# runs-on: ${{ matrix.platform }}
# strategy:
# matrix:
# platform: [ubuntu-20.04, macos-latest, windows-latest]
# platform: [ubuntu-22.04, macos-latest, windows-latest]
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4

View file

@ -32,17 +32,17 @@ jobs:
arch: x86_64
# - host: windows-latest
# target: aarch64-pc-windows-msvc
- host: ubuntu-20.04
- host: ubuntu-22.04
target: x86_64-unknown-linux-gnu
bundles: deb
os: linux
arch: x86_64
# - host: ubuntu-20.04
# - host: ubuntu-22.04
# target: x86_64-unknown-linux-musl
# - host: ubuntu-20.04
# - host: ubuntu-22.04
# target: aarch64-unknown-linux-gnu
# bundles: deb
# - host: ubuntu-20.04
# - host: ubuntu-22.04
# target: aarch64-unknown-linux-musl
name: Desktop - Main ${{ matrix.settings.target }}
runs-on: ${{ matrix.settings.host }}
@ -101,8 +101,8 @@ jobs:
run: |
pnpm tauri build --ci -v --target ${{ matrix.settings.target }} --bundles ${{ matrix.settings.bundles }},updater
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}

3
.vscode/launch.json vendored
View file

@ -12,8 +12,7 @@
"args": [
"build",
"--manifest-path=./apps/desktop/src-tauri/Cargo.toml",
"--no-default-features",
"--features=ai-models"
"--no-default-features"
],
"problemMatcher": "$rustc"
},

3
.vscode/tasks.json vendored
View file

@ -56,8 +56,7 @@
"command": "run",
"args": [
"--manifest-path=./apps/desktop/src-tauri/Cargo.toml",
"--no-default-features",
"--features=ai-models"
"--no-default-features"
],
"env": {
"RUST_BACKTRACE": "short"

View file

@ -89,7 +89,7 @@ To run the landing page:
If you encounter any issues, ensure that you are using the following versions of Rust, Node and Pnpm:
- Rust version: **1.75**
- Rust version: **1.78**
- Node version: **18.18**
- Pnpm version: **9.0.6**
@ -166,8 +166,8 @@ Once that has completed, run `xcode-select --install` in the terminal to install
Also ensure that Rosetta is installed, as a few of our dependencies require it. You can install Rosetta with `softwareupdate --install-rosetta --agree-to-license`.
### Translations
Check out the [i18n README](interface/locales/README.md) for more information on how to contribute to translations.
Check out the [i18n README](interface/locales/README.md) for more information on how to contribute to translations.
### Credits

2459
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,33 +21,27 @@ repository = "https://github.com/spacedriveapp/spacedrive"
[workspace.dependencies]
# First party dependencies
prisma-client-rust = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "f99d6f5566570f3ab1edecb7a172ad25b03d95af", features = [
"specta",
"sqlite-create-many",
prisma-client-rust = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "528ab1cd02c25a1b183c0a8bc44e28954fdd0bfd", features = [
"migrations",
"sqlite",
], default-features = false }
prisma-client-rust-cli = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "f99d6f5566570f3ab1edecb7a172ad25b03d95af", features = [
"specta",
"sqlite",
"sqlite-create-many",
], default-features = false }
prisma-client-rust-cli = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "528ab1cd02c25a1b183c0a8bc44e28954fdd0bfd", features = [
"migrations",
"specta",
"sqlite",
"sqlite-create-many",
], default-features = false }
prisma-client-rust-sdk = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "528ab1cd02c25a1b183c0a8bc44e28954fdd0bfd", features = [
"sqlite",
], default-features = false }
prisma-client-rust-sdk = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "f99d6f5566570f3ab1edecb7a172ad25b03d95af", features = [
"sqlite",
], default-features = false }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-appender = "0.2.3"
rspc = { version = "0.1.4" }
specta = { version = "=2.0.0-rc.7" }
tauri-specta = { version = "=2.0.0-rc.4" }
specta = { version = "=2.0.0-rc.11" }
tauri-specta = { version = "=2.0.0-rc.8" }
swift-rs = { version = "1.0.6" }
# Third party dependencies used by one or more of our crates
anyhow = "1.0.75"
async-channel = "2.0.0"
@ -55,18 +49,19 @@ async-trait = "0.1.77"
axum = "=0.6.20"
base64 = "0.21.5"
blake3 = "1.5.0"
chrono = "0.4.31"
chrono = "0.4.38"
clap = "4.4.7"
futures = "0.3.30"
futures-concurrency = "7.4.3"
futures-concurrency = "7.6.0"
globset = "^0.4.13"
hex = "0.4.3"
http = "0.2.9"
image = "0.24.7"
image = "0.25.1"
itertools = "0.12.0"
lending-stream = "1.0.0"
libc = "0.2"
normpath = "1.1.1"
once_cell = "1.18.0"
once_cell = "1.19.0"
pin-project-lite = "0.2.13"
rand = "0.8.5"
rand_chacha = "0.3.1"
@ -84,23 +79,20 @@ thiserror = "1.0.50"
tokio = "1.36.0"
tokio-stream = "0.1.14"
tokio-util = "0.7.10"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-appender = "0.2.3"
tracing-test = "^0.2.4"
uhlc = "=0.5.2"
uuid = "1.5.0"
webp = "0.2.6"
[workspace.dev-dependencies]
tracing-test = { version = "^0.2.4" }
webp = "0.3.0"
[patch.crates-io]
# Proper IOS Support
if-watch = { git = "https://github.com/oscartbeaumont/if-watch.git", rev = "a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3" }
# We hack it to the high heavens
rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "f3347e2e8bfe3f37bfacc437ca329fe71cdcb048" }
# `cursor_position` method
tauri = { git = "https://github.com/spacedriveapp/tauri.git", rev = "8409af71a83d631ff9d1cd876c441a57511a1cbd" }
tao = { git = "https://github.com/spacedriveapp/tao", rev = "7880adbc090402c44fbcf006669458fa82623403" }
rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "ab12964b140991e0730c3423693533fba71efb03" }
# Add `Control::open_stream_with_addrs`
libp2p = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" }
@ -110,6 +102,9 @@ libp2p-stream = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev
blake3 = { git = "https://github.com/spacedriveapp/blake3.git", rev = "d3aab416c12a75c2bfabce33bcd594e428a79069" }
# Due to image crate version bump
pdfium-render = { git = "https://github.com/fogodev/pdfium-render.git", rev = "e7aa1111f441c49e857cebda15b4e51b24356aaa" }
[profile.dev]
# Make compilation faster on macOS
split-debuginfo = "unpacked"
@ -122,3 +117,11 @@ opt-level = 3
[profile.dev.package."*"]
opt-level = 3
incremental = false
# Optimize release builds
[profile.release]
panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
strip = true # Remove debug symbols

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -7,9 +7,9 @@ edition = { workspace = true }
[dependencies]
tokio = { workspace = true, features = ["fs"] }
libc = "0.2"
libc = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
# WARNING: gtk should follow the same version used by tauri
# https://github.com/tauri-apps/tauri/blob/441eb4f4a5f9af206752c2e287975eb8d5ccfd01/core/tauri/Cargo.toml#L95
gtk = { version = "0.15", features = [ "v3_20" ] }
# https://github.com/tauri-apps/tauri/blob/tauri-v2.0.0-beta.17/core/tauri/Cargo.toml#L85C1-L85C51
gtk = { version = "0.18", features = [ "v3_24" ] }

View file

@ -1,32 +1,15 @@
use std::path::{Path, PathBuf};
use std::path::Path;
use gtk::{
gio::{
content_type_guess,
prelude::AppInfoExt,
prelude::{AppLaunchContextExt, FileExt},
AppInfo, AppLaunchContext, DesktopAppInfo, File as GioFile, ResourceError,
content_type_guess, prelude::AppInfoExt, prelude::FileExt, AppInfo, AppLaunchContext,
DesktopAppInfo, File as GioFile, ResourceError,
},
glib::error::Error as GlibError,
prelude::IsA,
};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::env::remove_prefix_from_pathlist;
fn remove_prefix_from_env_in_ctx(
ctx: &impl IsA<AppLaunchContext>,
env_name: &str,
prefix: &impl AsRef<Path>,
) {
if let Some(value) = remove_prefix_from_pathlist(env_name, prefix) {
ctx.setenv(env_name, value);
} else {
ctx.unsetenv(env_name);
}
}
thread_local! {
static LAUNCH_CTX: AppLaunchContext = {
// TODO: Display supports requires GDK, which can only run on the main thread
@ -36,8 +19,8 @@ thread_local! {
// "This is an Glib type conversion, it should never fail because GDKAppLaunchContext is a subclass of AppLaunchContext"
// )).unwrap_or_default();
let ctx = AppLaunchContext::default();
ctx
AppLaunchContext::default()
}
}

View file

@ -1,29 +1,13 @@
use std::{
collections::HashSet,
env,
ffi::{CStr, OsStr, OsString},
ffi::{CStr, OsStr},
mem,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
path::PathBuf,
ptr,
};
fn version(version_str: &str) -> i32 {
let mut version_parts: Vec<i32> = version_str
.split('.')
.take(4) // Take up to 4 components
.map(|part| part.parse().unwrap_or(0))
.collect();
// Pad with zeros if needed
version_parts.resize_with(4, Default::default);
(version_parts[0] * 1_000_000_000)
+ (version_parts[1] * 1_000_000)
+ (version_parts[2] * 1_000)
+ version_parts[3]
}
pub fn get_current_user_home() -> Option<PathBuf> {
use libc::{getpwuid_r, getuid, passwd, ERANGE};
@ -193,23 +177,6 @@ pub fn normalize_environment() {
.expect("PATH must be successfully normalized");
}
pub(crate) fn remove_prefix_from_pathlist(
env_name: &str,
prefix: &impl AsRef<Path>,
) -> Option<OsString> {
env::var_os(env_name).and_then(|value| {
let mut dirs = env::split_paths(&value)
.filter(|dir| !(dir.as_os_str().is_empty() || dir.starts_with(prefix)))
.peekable();
if dirs.peek().is_none() {
None
} else {
Some(env::join_paths(dirs).expect("Should not fail because we are only filtering a pathlist retrieved from the environmnet"))
}
})
}
// Check if snap by looking if SNAP is set and not empty and that the SNAP directory exists
pub fn is_snap() -> bool {
if let Some(snap) = std::env::var_os("SNAP") {

View file

@ -8,7 +8,7 @@ edition = { workspace = true }
[dependencies]
normpath = { workspace = true }
thiserror = { workspace = true }
libc = "0.2"
libc = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.51"

View file

@ -20,7 +20,10 @@
"@sd/ui": "workspace:*",
"@t3-oss/env-core": "^0.7.1",
"@tanstack/react-query": "^4.36.1",
"@tauri-apps/api": "1.5.1",
"@tauri-apps/api": "next",
"@tauri-apps/plugin-dialog": "2.0.0-beta.2",
"@tauri-apps/plugin-os": "2.0.0-beta.2",
"@tauri-apps/plugin-shell": "2.0.0-beta.2",
"consistent-hash": "^1.2.2",
"immer": "^10.0.3",
"react": "^18.2.0",
@ -31,7 +34,7 @@
"devDependencies": {
"@sd/config": "workspace:*",
"@sentry/vite-plugin": "^2.16.0",
"@tauri-apps/cli": "^1.5.11",
"@tauri-apps/cli": "next",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"sass": "^1.72.0",

View file

@ -1,6 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
gen/
WixTools
*.dll
*.dll.*

View file

@ -28,26 +28,25 @@ tracing = { workspace = true }
tauri-specta = { workspace = true, features = ["typescript"] }
uuid = { workspace = true, features = ["serde"] }
thiserror.workspace = true
directories = "5.0.1"
opener = { version = "0.6.1", features = ["reveal"] }
tauri = { version = "=1.5.3", features = [
tauri = { version = "=2.0.0-beta.17", features = [
"macos-private-api",
"path-all",
"protocol-all",
"os-all",
"shell-all",
"dialog-all",
"linux-protocol-headers",
"updater",
"window-all",
"native-tls-vendored",
"tracing",
"unstable",
"linux-libxdo",
] }
directories = "5.0.1"
tauri-plugin-updater = "2.0.0-beta"
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", rev = "1fa4d30eabb3768e1e97fa56f275408db2955b32" } # "2.0.0-beta"
tauri-plugin-os = "2.0.0-beta"
tauri-plugin-shell = "2.0.0-beta"
serde_json.workspace = true
strum = { workspace = true, features = ["derive"] }
[target.'cfg(target_os = "linux")'.dependencies]
sd-desktop-linux = { path = "../crates/linux" }
webkit2gtk = { version = "0.18.2", features = ["v2_2"] }
# https://github.com/tauri-apps/tauri/blob/tauri-v2.0.0-beta.17/core/tauri/Cargo.toml#L86
webkit2gtk = { version = "=2.0.1", features = ["v2_38"] }
[target.'cfg(target_os = "macos")'.dependencies]
sd-desktop-macos = { path = "../crates/macos" }
@ -57,9 +56,18 @@ sd-desktop-windows = { path = "../crates/windows" }
webview2-com = "0.19.1"
[build-dependencies]
tauri-build = "1.5.0"
tauri-build = "=2.0.0-beta.13"
[features]
default = ["custom-protocol"]
devtools = ["tauri/devtools"]
ai-models = ["sd-core/ai"]
custom-protocol = ["tauri/custom-protocol"]
# Optimize release builds
[profile.release]
panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
strip = true # Remove debug symbols

View file

@ -0,0 +1,24 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"image:default",
"resources:default",
"menu:default",
"tray:default",
"webview:default",
"webview:allow-internal-toggle-devtools",
"os:allow-os-type",
"window:allow-start-dragging",
"dialog:allow-open",
"window:allow-close",
"window:allow-minimize",
"window:allow-toggle-maximize"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -3,25 +3,17 @@
windows_subsystem = "windows"
)]
use std::{
collections::HashMap,
fs,
path::PathBuf,
process::Command,
sync::{Arc, Mutex, PoisonError},
time::Duration,
};
use std::{fs, path::PathBuf, process::Command, sync::Arc, time::Duration};
use menu::{set_enabled, MenuEvent};
use sd_core::{Node, NodeError};
use sd_fda::DiskAccess;
use serde::{Deserialize, Serialize};
use tauri::{
api::path, ipc::RemoteDomainAccessScope, window::PlatformWebview, AppHandle, FileDropEvent,
Manager, Window, WindowEvent,
};
use tauri::{async_runtime::block_on, webview::PlatformWebview, AppHandle, Manager, WindowEvent};
use tauri_plugins::{sd_error_plugin, sd_server_plugin};
use tauri_specta::{collect_events, ts, Event};
use tauri_specta::{collect_events, ts};
use tokio::task::block_in_place;
use tokio::time::sleep;
use tracing::error;
@ -34,7 +26,7 @@ mod updater;
#[tauri::command(async)]
#[specta::specta]
async fn app_ready(app_handle: AppHandle) {
let window = app_handle.get_window("main").unwrap();
let window = app_handle.get_webview_window("main").unwrap();
window.show().unwrap();
}
@ -47,22 +39,19 @@ async fn request_fda_macos() {
#[tauri::command(async)]
#[specta::specta]
async fn set_menu_bar_item_state(_window: tauri::Window, _id: String, _enabled: bool) {
#[cfg(target_os = "macos")]
{
_window
.menu_handle()
.get_item(&_id)
.set_enabled(_enabled)
.expect("Unable to modify menu item");
}
async fn set_menu_bar_item_state(window: tauri::Window, event: MenuEvent, enabled: bool) {
let menu = window
.menu()
.expect("unable to get menu for current window");
set_enabled(&menu, event, enabled);
}
#[tauri::command(async)]
#[specta::specta]
async fn reload_webview(app_handle: AppHandle) {
app_handle
.get_window("main")
.get_webview_window("main")
.expect("Error getting window handle")
.with_webview(reload_webview_inner)
.expect("Error while reloading webview");
@ -77,7 +66,7 @@ fn reload_webview_inner(webview: PlatformWebview) {
}
#[cfg(target_os = "linux")]
{
use webkit2gtk::traits::WebViewExt;
use webkit2gtk::WebViewExt;
webview.inner().reload();
}
@ -95,8 +84,10 @@ fn reload_webview_inner(webview: PlatformWebview) {
#[tauri::command(async)]
#[specta::specta]
async fn reset_spacedrive(app_handle: AppHandle) {
let data_dir = path::data_dir()
.unwrap_or_else(|| PathBuf::from("./"))
let data_dir = app_handle
.path()
.data_dir()
.unwrap_or_else(|_| PathBuf::from("./"))
.join("spacedrive");
#[cfg(debug_assertions)]
@ -112,25 +103,9 @@ async fn reset_spacedrive(app_handle: AppHandle) {
#[tauri::command(async)]
#[specta::specta]
async fn refresh_menu_bar(
_node: tauri::State<'_, Arc<Node>>,
_app_handle: AppHandle,
) -> Result<(), ()> {
#[cfg(target_os = "macos")]
{
let menu_handles: Vec<tauri::window::MenuHandle> = _app_handle
.windows()
.iter()
.map(|x| x.1.menu_handle())
.collect();
let has_library = !_node.libraries.get_all().await.is_empty();
for menu in menu_handles {
menu::set_library_locked_menu_items_enabled(menu, has_library);
}
}
async fn refresh_menu_bar(node: tauri::State<'_, Arc<Node>>, app: AppHandle) -> Result<(), ()> {
let has_library = !node.libraries.get_all().await.is_empty();
menu::refresh_menu_bar(&app, has_library);
Ok(())
}
@ -199,11 +174,6 @@ pub enum DragAndDropEvent {
Cancelled,
}
#[derive(Default)]
pub struct DragAndDropState {
windows: HashMap<tauri::Window, tokio::task::JoinHandle<()>>,
}
const CLIENT_ID: &str = "2abb241e-40b8-4517-a3e3-5594375c8fbb";
#[tokio::main]
@ -211,46 +181,8 @@ async fn main() -> tauri::Result<()> {
#[cfg(target_os = "linux")]
sd_desktop_linux::normalize_environment();
let data_dir = path::data_dir()
.unwrap_or_else(|| PathBuf::from("./"))
.join("spacedrive");
#[cfg(debug_assertions)]
let data_dir = data_dir.join("dev");
// The `_guard` must be assigned to variable for flushing remaining logs on main exit through Drop
let (_guard, result) = match Node::init_logger(&data_dir) {
Ok(guard) => (
Some(guard),
Node::new(data_dir, sd_core::Env::new(CLIENT_ID)).await,
),
Err(err) => (None, Err(NodeError::Logger(err))),
};
let app = tauri::Builder::default();
let (node_router, app) = match result {
Ok((node, router)) => (Some((node, router)), app),
Err(err) => {
error!("Error starting up the node: {err:#?}");
(None, app.plugin(sd_error_plugin(err)))
}
};
let (node, router) = node_router.expect("Unable to get the node or router");
let should_clear_localstorage = node.libraries.get_all().await.is_empty();
let app = app
.plugin(rspc::integrations::tauri::plugin(router, {
let node = node.clone();
move || node.clone()
}))
.plugin(sd_server_plugin(node.clone()).await.unwrap()) // TODO: Handle `unwrap`
.manage(node.clone());
let specta_builder = {
let specta_builder = ts::builder()
let (invoke_handler, register_events) = {
let builder = ts::builder()
.events(collect_events![DragAndDropEvent])
.commands(tauri_specta::collect_commands![
app_ready,
@ -269,224 +201,145 @@ async fn main() -> tauri::Result<()> {
file::open_ephemeral_file_with,
file::reveal_items,
theme::lock_app_theme,
// TODO: move to plugin w/tauri-specta
updater::check_for_update,
updater::install_update
])
.config(specta::ts::ExportConfig::default().formatter(specta::ts::formatter::prettier));
#[cfg(debug_assertions)]
let specta_builder = specta_builder.path("../src/commands.ts");
let builder = builder.path("../src/commands.ts");
specta_builder.into_plugin()
builder.build().unwrap()
};
let file_drop_status = Arc::new(Mutex::new(DragAndDropState::default()));
let app = app
.plugin(updater::plugin())
// .plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(specta_builder)
tauri::Builder::default()
.invoke_handler(invoke_handler)
.setup(move |app| {
let app = app.handle();
// We need a the app handle to determine the data directory now.
// This means all the setup code has to be within `setup`, however it doesn't support async so we `block_on`.
block_in_place(|| {
block_on(async move {
register_events(app);
println!("setup");
let data_dir = app
.path()
.data_dir()
.unwrap_or_else(|_| PathBuf::from("./"))
.join("spacedrive");
app.windows().iter().for_each(|(_, window)| {
if should_clear_localstorage {
println!("bruh?");
window.eval("localStorage.clear();").ok();
}
#[cfg(debug_assertions)]
let data_dir = data_dir.join("dev");
tokio::spawn({
let window = window.clone();
async move {
sleep(Duration::from_secs(3)).await;
if !window.is_visible().unwrap_or(true) {
// This happens if the JS bundle crashes and hence doesn't send ready event.
println!(
"Window did not emit `app_ready` event fast enough. Showing window..."
);
window.show().expect("Main window should show");
// The `_guard` must be assigned to variable for flushing remaining logs on main exit through Drop
let (_guard, result) = match Node::init_logger(&data_dir) {
Ok(guard) => (
Some(guard),
Node::new(data_dir, sd_core::Env::new(CLIENT_ID)).await,
),
Err(err) => (None, Err(NodeError::Logger(err))),
};
let handle = app.handle();
let (node, router) = match result {
Ok(r) => r,
Err(err) => {
error!("Error starting up the node: {err:#?}");
handle.plugin(sd_error_plugin(err))?;
return Ok(());
}
}
});
};
#[cfg(target_os = "windows")]
window.set_decorations(true).unwrap();
let should_clear_localstorage = node.libraries.get_all().await.is_empty();
#[cfg(target_os = "macos")]
{
use sd_desktop_macos::{blur_window_background, set_titlebar_style};
handle.plugin(rspc::integrations::tauri::plugin(router, {
let node = node.clone();
move || node.clone()
}))?;
handle.plugin(sd_server_plugin(node.clone(), handle).await.unwrap())?; // TODO: Handle `unwrap`
handle.manage(node.clone());
let nswindow = window.ns_window().unwrap();
unsafe { set_titlebar_style(&nswindow, false) };
unsafe { blur_window_background(&nswindow) };
tokio::spawn({
let libraries = node.libraries.clone();
let menu_handle = window.menu_handle();
async move {
if libraries.get_all().await.is_empty() {
menu::set_library_locked_menu_items_enabled(menu_handle, false);
handle.windows().iter().for_each(|(_, window)| {
if should_clear_localstorage {
println!("bruh?");
for webview in window.webviews() {
webview.eval("localStorage.clear();").ok();
}
}
tokio::spawn({
let window = window.clone();
async move {
sleep(Duration::from_secs(3)).await;
if !window.is_visible().unwrap_or(true) {
// This happens if the JS bundle crashes and hence doesn't send ready event.
println!(
"Window did not emit `app_ready` event fast enough. Showing window..."
);
window.show().expect("Main window should show");
}
}
});
#[cfg(target_os = "windows")]
window.set_decorations(false).unwrap();
#[cfg(target_os = "macos")]
{
use sd_desktop_macos::{blur_window_background, set_titlebar_style};
let nswindow = window.ns_window().unwrap();
unsafe { set_titlebar_style(&nswindow, false) };
unsafe { blur_window_background(&nswindow) };
}
});
}
});
// Configure IPC for custom protocol
app.ipc_scope().configure_remote_access(
RemoteDomainAccessScope::new("localhost")
.allow_on_scheme("spacedrive")
.add_window("main"),
);
Ok(())
Ok(())
})
})
})
.on_menu_event(menu::handle_menu_event)
.on_window_event(move |event| match event.event() {
.on_window_event(move |window, event| match event {
// macOS expected behavior is for the app to not exit when the main window is closed.
// Instead, the window is hidden and the dock icon remains so that on user click it should show the window again.
#[cfg(target_os = "macos")]
WindowEvent::CloseRequested { api, .. } => {
// TODO: make this multi-window compatible in the future
event
.window()
window
.app_handle()
.hide()
.expect("Window should hide on macOS");
api.prevent_close();
}
WindowEvent::FileDrop(drop) => {
let window = event.window();
let mut file_drop_status = file_drop_status
.lock()
.unwrap_or_else(PoisonError::into_inner);
match drop {
FileDropEvent::Hovered(paths) => {
// Look this shouldn't happen but let's be sure we don't leak threads.
if file_drop_status.windows.contains_key(window) {
return;
}
// We setup a thread to keep emitting the updated position of the cursor
// It will be killed when the `FileDropEvent` is finished or cancelled.
let paths = paths.clone();
file_drop_status.windows.insert(window.clone(), {
let window = window.clone();
tokio::spawn(async move {
let (mut last_x, mut last_y) = (0.0, 0.0);
loop {
let (x, y) = mouse_position(&window);
let x_diff = difference(x, last_x);
let y_diff = difference(y, last_y);
// If the mouse hasn't moved much we will "debounce" the event
if x_diff > 28.0 || y_diff > 28.0 {
last_x = x;
last_y = y;
DragAndDropEvent::Hovered {
paths: paths
.iter()
.filter_map(|x| x.to_str().map(|x| x.to_string()))
.collect(),
x,
y,
}
.emit(&window)
.ok();
}
sleep(Duration::from_millis(125)).await;
}
})
});
}
FileDropEvent::Dropped(paths) => {
if let Some(handle) = file_drop_status.windows.remove(window) {
handle.abort();
}
let (x, y) = mouse_position(window);
DragAndDropEvent::Dropped {
paths: paths
.iter()
.filter_map(|x| x.to_str().map(|x| x.to_string()))
.collect(),
x,
y,
}
.emit(window)
.ok();
}
FileDropEvent::Cancelled => {
if let Some(handle) = file_drop_status.windows.remove(window) {
handle.abort();
}
DragAndDropEvent::Cancelled.emit(window).ok();
}
_ => unreachable!(),
}
}
WindowEvent::Resized(_) => {
let (_state, command) = if event
.window()
.is_fullscreen()
.expect("Can't get fullscreen state")
{
(true, "window_fullscreened")
} else {
(false, "window_not_fullscreened")
};
let (_state, command) =
if window.is_fullscreen().expect("Can't get fullscreen state") {
(true, "window_fullscreened")
} else {
(false, "window_not_fullscreened")
};
event
.window()
window
.emit("keybind", command)
.expect("Unable to emit window event");
#[cfg(target_os = "macos")]
{
let nswindow = event.window().ns_window().unwrap();
let nswindow = window.ns_window().unwrap();
unsafe { sd_desktop_macos::set_titlebar_style(&nswindow, _state) };
}
}
_ => {}
})
.menu(menu::get_menu())
.menu(menu::setup_menu)
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_shell::init())
// TODO: Bring back Tauri Plugin Window State - it was buggy so we removed it.
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(updater::plugin())
.manage(updater::State::default())
.build(tauri::generate_context!())?;
.build(tauri::generate_context!())?
.run(|_, _| {});
app.run(|_, _| {});
Ok(())
}
// Get the mouse position relative to the window
fn mouse_position(window: &Window) -> (f64, f64) {
// We apply the OS scaling factor.
// Tauri/Webkit *should* be responsible for this but it would seem it is bugged on the current webkit/tauri/wry/tao version.
// Using newer Webkit did fix this automatically but I can't for the life of me work out how to get the right glibc versions in CI so we can't ship it.
let scale_factor = window.scale_factor().unwrap();
let window_pos = window.outer_position().unwrap();
let cursor_pos = window.cursor_position().unwrap();
(
(cursor_pos.x - window_pos.x as f64) / scale_factor,
(cursor_pos.y - window_pos.y as f64) / scale_factor,
)
}
// The distance between two numbers as a positive integer.
fn difference(a: f64, b: f64) -> f64 {
let x = a - b;
if x < 0.0 {
x * -1.0
} else {
x
}
}

View file

@ -1,191 +1,271 @@
use tauri::{Manager, Menu, WindowMenuEvent, Wry};
use std::str::FromStr;
#[cfg(target_os = "macos")]
use tauri::{AboutMetadata, CustomMenuItem, MenuItem, Submenu};
use serde::Deserialize;
use specta::Type;
use tauri::{
menu::{Menu, MenuItemKind},
AppHandle, Manager, Wry,
};
use tracing::error;
pub fn get_menu() -> Menu {
#[cfg(target_os = "macos")]
{
custom_menu_bar()
}
#[cfg(not(target_os = "macos"))]
{
Menu::new()
}
#[derive(
Debug, Clone, Copy, Type, Deserialize, strum::EnumString, strum::AsRefStr, strum::Display,
)]
pub enum MenuEvent {
NewLibrary,
NewFile,
NewDirectory,
AddLocation,
OpenOverview,
OpenSearch,
OpenSettings,
ReloadExplorer,
SetLayoutGrid,
SetLayoutList,
SetLayoutMedia,
ToggleDeveloperTools,
NewWindow,
ReloadWebview,
}
// update this whenever you add something which requires a valid library to use
#[cfg(target_os = "macos")]
const LIBRARY_LOCKED_MENU_IDS: [&str; 12] = [
"new_window",
"open_overview",
"open_search",
"open_settings",
"reload_explorer",
"layout_grid",
"layout_list",
"layout_media",
"new_file",
"new_directory",
"new_library", // disabled because the first one should at least be done via onboarding
"add_location",
/// Menu items which require a library to be open to use.
/// They will be disabled/enabled automatically.
const LIBRARY_LOCKED_MENU_IDS: &[MenuEvent] = &[
MenuEvent::NewWindow,
MenuEvent::OpenOverview,
MenuEvent::OpenSearch,
MenuEvent::OpenSettings,
MenuEvent::ReloadExplorer,
MenuEvent::SetLayoutGrid,
MenuEvent::SetLayoutList,
MenuEvent::SetLayoutMedia,
MenuEvent::NewFile,
MenuEvent::NewDirectory,
MenuEvent::NewLibrary,
MenuEvent::AddLocation,
];
#[cfg(target_os = "macos")]
fn custom_menu_bar() -> Menu {
let app_menu = Menu::new()
.add_native_item(MenuItem::About(
"Spacedrive".to_string(),
AboutMetadata::new()
.authors(vec!["Spacedrive Technology Inc.".to_string()])
.license("AGPL-3.0-only")
.version(env!("CARGO_PKG_VERSION"))
.website("https://spacedrive.com/")
.website_label("Spacedrive.com"),
))
.add_native_item(MenuItem::Separator)
.add_item(CustomMenuItem::new("new_library", "New Library").disabled()) // TODO(brxken128): add keybind handling here
.add_submenu(Submenu::new(
"Library",
Menu::new()
.add_item(CustomMenuItem::new("library_<uuid>", "Library 1").disabled())
.add_item(CustomMenuItem::new("library_<uuid2>", "Library 2").disabled()), // TODO: enumerate libraries and make this a library selector
))
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit);
pub fn setup_menu(app: &AppHandle) -> tauri::Result<Menu<Wry>> {
app.on_menu_event(move |app, event| {
if let Ok(event) = MenuEvent::from_str(&event.id().0) {
handle_menu_event(event, app);
} else {
println!("Unknown menu event: {}", event.id().0);
}
});
let file_menu = Menu::new()
.add_item(
CustomMenuItem::new("new_file", "New File")
.accelerator("CmdOrCtrl+N")
.disabled(), // TODO(brxken128): add keybind handling here
)
.add_item(
CustomMenuItem::new("new_directory", "New Directory")
.accelerator("CmdOrCtrl+D")
.disabled(), // TODO(brxken128): add keybind handling here
)
.add_item(CustomMenuItem::new("add_location", "Add Location").disabled()); // TODO(brxken128): add keybind handling here;
#[cfg(not(target_os = "macos"))]
{
Menu::new(app)
}
#[cfg(target_os = "macos")]
{
use tauri::menu::{AboutMetadataBuilder, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
let edit_menu = Menu::new()
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::SelectAll);
let app_menu = SubmenuBuilder::new(app, "Spacedrive")
.about(Some(
AboutMetadataBuilder::new()
.authors(Some(vec!["Spacedrive Technology Inc.".to_string()]))
.license(Some(env!("CARGO_PKG_VERSION")))
.version(Some(env!("CARGO_PKG_VERSION")))
.website(Some("https://spacedrive.com/"))
.website_label(Some("Spacedrive.com"))
.build(),
))
.separator()
.item(
&MenuItemBuilder::with_id(MenuEvent::NewLibrary, "New Library")
.accelerator("Cmd+Shift+T")
.build(app)?,
)
// .item(
// &SubmenuBuilder::new(app, "Libraries")
// // TODO: Implement this
// .items(&[])
// .build()?,
// )
.separator()
.hide()
.hide_others()
.show_all()
.separator()
.quit()
.build()?;
let view_menu = Menu::new()
.add_item(CustomMenuItem::new("open_overview", "Overview").accelerator("CmdOrCtrl+."))
.add_item(CustomMenuItem::new("open_search", "Search").accelerator("CmdOrCtrl+F"))
.add_item(CustomMenuItem::new("open_settings", "Settings").accelerator("CmdOrCtrl+Comma"))
.add_item(
CustomMenuItem::new("reload_explorer", "Reload explorer").accelerator("CmdOrCtrl+R"),
)
.add_submenu(Submenu::new(
"Layout",
Menu::new()
.add_item(CustomMenuItem::new("layout_grid", "Grid (Default)").disabled())
.add_item(CustomMenuItem::new("layout_list", "List").disabled())
.add_item(CustomMenuItem::new("layout_media", "Media").disabled()),
));
// .add_item(
// CustomMenuItem::new("command_pallete", "Command Pallete")
// .accelerator("CmdOrCtrl+P"),
// )
let file_menu = SubmenuBuilder::new(app, "File")
.item(
&MenuItemBuilder::with_id(MenuEvent::NewFile, "New File")
.accelerator("CmdOrCtrl+N")
.build(app)?,
)
.item(
&MenuItemBuilder::with_id(MenuEvent::NewDirectory, "New Directory")
.accelerator("CmdOrCtrl+D")
.build(app)?,
)
.item(
&MenuItemBuilder::with_id(MenuEvent::AddLocation, "Add Location")
// .accelerator("") // TODO
.build(app)?,
)
.build()?;
#[cfg(debug_assertions)]
let view_menu = view_menu.add_native_item(MenuItem::Separator).add_item(
CustomMenuItem::new("toggle_devtools", "Toggle Developer Tools")
.accelerator("CmdOrCtrl+Shift+Alt+I"),
);
let edit_menu = SubmenuBuilder::new(app, "Edit")
.copy()
.cut()
.paste()
.redo()
.undo()
.select_all()
.build()?;
let window_menu = Menu::new()
.add_native_item(MenuItem::Minimize)
.add_native_item(MenuItem::Zoom)
.add_item(
CustomMenuItem::new("new_window", "New Window")
.accelerator("CmdOrCtrl+Shift+N")
.disabled(),
)
.add_item(CustomMenuItem::new("close_window", "Close Window").accelerator("CmdOrCtrl+W"))
.add_native_item(MenuItem::EnterFullScreen)
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("reload_app", "Reload Webview").accelerator("CmdOrCtrl+Shift+R"),
let view_menu = SubmenuBuilder::new(app, "View")
.item(
&MenuItemBuilder::with_id(MenuEvent::OpenOverview, "Open Overview")
.accelerator("CmdOrCtrl+.")
.build(app)?,
)
.item(
&MenuItemBuilder::with_id(MenuEvent::OpenSearch, "Search")
.accelerator("CmdOrCtrl+F")
.build(app)?,
)
.item(
&MenuItemBuilder::with_id(MenuEvent::OpenSettings, "Settings")
.accelerator("CmdOrCtrl+Comma")
.build(app)?,
)
.item(
&MenuItemBuilder::with_id(MenuEvent::ReloadExplorer, "Open Explorer")
.accelerator("CmdOrCtrl+R")
.build(app)?,
)
.item(
&SubmenuBuilder::new(app, "Layout")
.item(
&MenuItemBuilder::with_id(MenuEvent::SetLayoutGrid, "Grid (Default)")
// .accelerator("") // TODO
.build(app)?,
)
.item(
&MenuItemBuilder::with_id(MenuEvent::SetLayoutList, "List")
// .accelerator("") // TODO
.build(app)?,
)
.item(
&MenuItemBuilder::with_id(MenuEvent::SetLayoutMedia, "Media")
// .accelerator("") // TODO
.build(app)?,
)
.build()?,
);
#[cfg(debug_assertions)]
let view_menu = view_menu.separator().item(
&MenuItemBuilder::with_id(MenuEvent::ToggleDeveloperTools, "Toggle Developer Tools")
.accelerator("CmdOrCtrl+Shift+Alt+I")
.build(app)?,
);
Menu::new()
.add_submenu(Submenu::new("Spacedrive", app_menu))
.add_submenu(Submenu::new("File", file_menu))
.add_submenu(Submenu::new("Edit", edit_menu))
.add_submenu(Submenu::new("View", view_menu))
.add_submenu(Submenu::new("Window", window_menu))
let view_menu = view_menu.build()?;
let window_menu = SubmenuBuilder::new(app, "Window")
.minimize()
.item(
&MenuItemBuilder::with_id(MenuEvent::NewWindow, "New Window")
.accelerator("CmdOrCtrl+Shift+N")
.build(app)?,
)
.close_window()
.fullscreen()
.item(
&MenuItemBuilder::with_id(MenuEvent::ReloadWebview, "Reload Webview")
.accelerator("CmdOrCtrl+Shift+R")
.build(app)?,
)
.build()?;
let menu = MenuBuilder::new(app)
.item(&app_menu)
.item(&file_menu)
.item(&edit_menu)
.item(&view_menu)
.item(&window_menu)
.build()?;
for event in LIBRARY_LOCKED_MENU_IDS {
set_enabled(&menu, *event, false);
}
Ok(menu)
}
}
pub fn handle_menu_event(event: WindowMenuEvent<Wry>) {
match event.menu_item_id() {
"quit" => {
let app = event.window().app_handle();
app.exit(0);
}
"reload_explorer" => event.window().emit("keybind", "reload_explorer").unwrap(),
"open_settings" => event.window().emit("keybind", "open_settings").unwrap(),
"open_overview" => event.window().emit("keybind", "open_overview").unwrap(),
"close_window" => {
#[cfg(target_os = "macos")]
tauri::AppHandle::hide(&event.window().app_handle()).unwrap();
pub fn handle_menu_event(event: MenuEvent, app: &AppHandle) {
let webview = app
.get_webview_window("main")
.expect("unable to find window");
#[cfg(not(target_os = "macos"))]
{
let window = event.window();
#[cfg(debug_assertions)]
if window.is_devtools_open() {
window.close_devtools();
} else {
window.close().unwrap();
}
#[cfg(not(debug_assertions))]
window.close().unwrap();
match event {
// TODO: Use Tauri Specta with frontend instead of this
MenuEvent::NewLibrary => webview.emit("keybind", "new_library").unwrap(),
MenuEvent::NewFile => webview.emit("keybind", "new_file").unwrap(),
MenuEvent::NewDirectory => webview.emit("keybind", "new_directory").unwrap(),
MenuEvent::AddLocation => webview.emit("keybind", "add_location").unwrap(),
MenuEvent::OpenOverview => webview.emit("keybind", "open_overview").unwrap(),
MenuEvent::OpenSearch => webview.emit("keybind", "open_search".to_string()).unwrap(),
MenuEvent::OpenSettings => webview.emit("keybind", "open_settings").unwrap(),
MenuEvent::ReloadExplorer => webview.emit("keybind", "reload_explorer").unwrap(),
MenuEvent::SetLayoutGrid => webview.emit("keybind", "set_layout_grid").unwrap(),
MenuEvent::SetLayoutList => webview.emit("keybind", "set_layout_list").unwrap(),
MenuEvent::SetLayoutMedia => webview.emit("keybind", "set_layout_media").unwrap(),
MenuEvent::ToggleDeveloperTools =>
{
#[cfg(feature = "devtools")]
if webview.is_devtools_open() {
webview.close_devtools();
} else {
webview.open_devtools();
}
}
"open_search" => event
.window()
.emit("keybind", "open_search".to_string())
.unwrap(),
"reload_app" => {
event
.window()
MenuEvent::NewWindow => {
// TODO: Implement this
}
MenuEvent::ReloadWebview => {
webview
.with_webview(crate::reload_webview_inner)
.expect("Error while reloading webview");
}
#[cfg(debug_assertions)]
"toggle_devtools" => {
let window = event.window();
if window.is_devtools_open() {
window.close_devtools();
} else {
window.open_devtools();
}
}
_ => {}
}
}
/// If any are explicitly marked with `.disabled()` in the `custom_menu_bar()` function, this won't have an effect.
/// We include them in the locked menu IDs anyway for future-proofing, in-case someone forgets.
#[cfg(target_os = "macos")]
pub fn set_library_locked_menu_items_enabled(handle: tauri::window::MenuHandle, enabled: bool) {
LIBRARY_LOCKED_MENU_IDS
.iter()
.try_for_each(|id| handle.get_item(id).set_enabled(enabled))
.expect("Unable to disable menu items (there are no libraries present, so certain options should be hidden)");
// Enable/disable all items in `LIBRARY_LOCKED_MENU_IDS`
pub fn refresh_menu_bar(app: &AppHandle, enabled: bool) {
let menu = app
.get_window("main")
.expect("unable to find window")
.menu()
.expect("unable to get menu for current window");
for event in LIBRARY_LOCKED_MENU_IDS {
set_enabled(&menu, *event, enabled);
}
}
pub fn set_enabled(menu: &Menu<Wry>, event: MenuEvent, enabled: bool) {
let result = match menu.get(event.as_ref()) {
Some(MenuItemKind::MenuItem(i)) => i.set_enabled(enabled),
Some(MenuItemKind::Submenu(i)) => i.set_enabled(enabled),
Some(MenuItemKind::Predefined(_)) => return,
Some(MenuItemKind::Check(i)) => i.set_enabled(enabled),
Some(MenuItemKind::Icon(i)) => i.set_enabled(enabled),
None => {
error!("Unable to get menu item: {event:?}");
return;
}
};
if let Err(e) = result {
error!("Error setting menu item state: {e:#?}");
}
}

View file

@ -18,7 +18,7 @@ use hyper::server::{accept::Accept, conn::AddrIncoming};
use rand::{distributions::Alphanumeric, Rng};
use sd_core::{custom_uri, Node, NodeError};
use serde::Deserialize;
use tauri::{async_runtime::block_on, plugin::TauriPlugin, RunEvent, Runtime};
use tauri::{async_runtime::block_on, plugin::TauriPlugin, AppHandle, Manager, RunEvent, Runtime};
use thiserror::Error;
use tokio::{net::TcpListener, task::block_in_place};
use tracing::info;
@ -51,6 +51,7 @@ pub enum SdServerPluginError {
/// We also spin up multiple servers so we can load balance image requests between them to avoid any issue with browser connection limits.
pub async fn sd_server_plugin<R: Runtime>(
node: Arc<Node>,
app_handle: &AppHandle,
) -> Result<TauriPlugin<R>, SdServerPluginError> {
let auth_token: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
@ -95,15 +96,21 @@ pub async fn sd_server_plugin<R: Runtime>(
.expect("Error with HTTP server!"); // TODO: Panic handling
});
let script = format!(
r#"window.__SD_CUSTOM_SERVER_AUTH_TOKEN__ = "{auth_token}"; window.__SD_CUSTOM_URI_SERVER__ = [{}];"#,
[listen_addra, listen_addrb, listen_addrc, listen_addrd]
.iter()
.map(|addr| format!("'http://{addr}'"))
.collect::<Vec<_>>()
.join(","),
);
for (_, window) in app_handle.webview_windows() {
window.eval(&script).ok();
}
Ok(tauri::plugin::Builder::new("sd-server")
.js_init_script(format!(
r#"window.__SD_CUSTOM_SERVER_AUTH_TOKEN__ = "{auth_token}"; window.__SD_CUSTOM_URI_SERVER__ = [{}];"#,
[listen_addra, listen_addrb, listen_addrc, listen_addrd]
.iter()
.map(|addr| format!("'http://{addr}'"))
.collect::<Vec<_>>()
.join(","),
))
.js_init_script(script)
.on_event(move |_app, e| {
if let RunEvent::Exit { .. } = e {
block_in_place(|| {

View file

@ -1,17 +1,16 @@
use tauri::{plugin::TauriPlugin, Manager, Runtime};
use tauri_plugin_updater::{Update as TauriPluginUpdate, UpdaterExt};
use tokio::sync::Mutex;
#[derive(Debug, Clone, specta::Type, serde::Serialize)]
pub struct Update {
pub version: String,
pub body: Option<String>,
}
impl Update {
fn new(update: &tauri::updater::UpdateResponse<impl tauri::Runtime>) -> Self {
fn new(update: &TauriPluginUpdate) -> Self {
Self {
version: update.latest_version().to_string(),
body: update.body().map(ToString::to_string),
version: update.version.clone(),
}
}
}
@ -21,12 +20,12 @@ pub struct State {
install_lock: Mutex<()>,
}
async fn get_update(
app: tauri::AppHandle,
) -> Result<tauri::updater::UpdateResponse<impl tauri::Runtime>, String> {
tauri::updater::builder(app)
async fn get_update(app: tauri::AppHandle) -> Result<Option<TauriPluginUpdate>, String> {
app.updater_builder()
.header("X-Spacedrive-Version", "stable")
.map_err(|e| e.to_string())?
.build()
.map_err(|e| e.to_string())?
.check()
.await
.map_err(|e| e.to_string())
@ -45,19 +44,19 @@ pub enum UpdateEvent {
#[tauri::command]
#[specta::specta]
pub async fn check_for_update(app: tauri::AppHandle) -> Result<Option<Update>, String> {
app.emit_all("updater", UpdateEvent::Loading).ok();
app.emit("updater", UpdateEvent::Loading).ok();
let update = match get_update(app.clone()).await {
Ok(update) => update,
Err(e) => {
app.emit_all("updater", UpdateEvent::Error(e.clone())).ok();
app.emit("updater", UpdateEvent::Error(e.clone())).ok();
return Err(e);
}
};
let update = update.is_update_available().then(|| Update::new(&update));
let update = update.map(|update| Update::new(&update));
app.emit_all(
app.emit(
"updater",
update
.clone()
@ -81,11 +80,12 @@ pub async fn install_update(
Err(_) => return Err("Update already installing".into()),
};
app.emit_all("updater", UpdateEvent::Installing).ok();
app.emit("updater", UpdateEvent::Installing).ok();
get_update(app.clone())
.await?
.download_and_install()
.ok_or_else(|| "No update required".to_string())?
.download_and_install(|_, _| {}, || {})
.await
.map_err(|e| e.to_string())?;

View file

@ -1,88 +1,16 @@
{
"package": {
"productName": "Spacedrive"
},
"productName": "Spacedrive",
"version": "0.2.13",
"identifier": "com.spacedrive.desktop",
"build": {
"distDir": "../dist",
"devPath": "http://localhost:8001",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..."
"devUrl": "http://localhost:8001",
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop...",
"frontendDist": "../dist"
},
"tauri": {
"app": {
"withGlobalTauri": true,
"macOSPrivateApi": true,
"bundle": {
"active": true,
"publisher": "Spacedrive Technology Inc.",
"category": "Productivity",
"targets": ["deb", "msi", "dmg", "updater"],
"identifier": "com.spacedrive.desktop",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": {},
"externalBin": [],
"copyright": "Spacedrive Technology Inc.",
"shortDescription": "Spacedrive",
"longDescription": "Cross-platform universal file explorer, powered by an open-source virtual distributed filesystem.",
"deb": {
"files": {
"/usr/share/spacedrive/models/yolov8s.onnx": "../../.deps/models/yolov8s.onnx"
},
"depends": ["libc6"]
},
"macOS": {
"minimumSystemVersion": "10.15",
"exceptionDomain": null,
"entitlements": null,
"frameworks": ["../../.deps/Spacedrive.framework"]
},
"windows": {
"certificateThumbprint": null,
"webviewInstallMode": { "type": "embedBootstrapper", "silent": true },
"digestAlgorithm": "sha256",
"timestampUrl": "",
"wix": {
"dialogImagePath": "icons/WindowsDialogImage.bmp",
"bannerPath": "icons/WindowsBanner.bmp"
}
}
},
"updater": {
"active": true,
"dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEZBMURCMkU5NEU3NDAyOEMKUldTTUFuUk82YklkK296dlkxUGkrTXhCT3ZMNFFVOWROcXNaS0RqWU1kMUdRV2tDdFdIS0Y3YUsK",
"endpoints": [
"https://spacedrive.com/api/releases/tauri/{{version}}/{{target}}/{{arch}}"
]
},
"allowlist": {
"all": false,
"window": {
"all": true
},
"path": {
"all": true
},
"shell": {
"all": true
},
"protocol": {
"all": true,
"assetScope": ["*"]
},
"os": {
"all": true
},
"dialog": {
"all": true,
"open": true,
"save": true
}
},
"windows": [
{
"title": "Spacedrive",
@ -96,14 +24,65 @@
"alwaysOnTop": false,
"focus": false,
"visible": false,
"fileDropEnabled": true,
"dragDropEnabled": true,
"decorations": true,
"transparent": true,
"center": true
}
],
"security": {
"csp": "default-src spacedrive: webkit-pdfjs-viewer: asset: https://asset.localhost blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
"csp": "default-src webkit-pdfjs-viewer: asset: https://asset.localhost blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
}
},
"bundle": {
"active": true,
"targets": ["deb", "msi", "dmg", "updater"],
"publisher": "Spacedrive Technology Inc.",
"copyright": "Spacedrive Technology Inc.",
"category": "Productivity",
"shortDescription": "Spacedrive",
"longDescription": "Cross-platform universal file explorer, powered by an open-source virtual distributed filesystem.",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"deb": {
"files": {
"/usr/share/spacedrive/models/yolov8s.onnx": "../../.deps/models/yolov8s.onnx"
},
"depends": ["libc6", "libxdo3"]
}
},
"macOS": {
"minimumSystemVersion": "10.15",
"exceptionDomain": null,
"entitlements": null,
"frameworks": ["../../.deps/Spacedrive.framework"]
},
"windows": {
"certificateThumbprint": null,
"webviewInstallMode": { "type": "embedBootstrapper", "silent": true },
"digestAlgorithm": "sha256",
"timestampUrl": "",
"wix": {
"dialogImagePath": "icons/WindowsDialogImage.bmp",
"bannerPath": "icons/WindowsBanner.bmp"
}
}
},
"plugins": {
"updater": {
"active": true,
"dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEZBMURCMkU5NEU3NDAyOEMKUldTTUFuUk82YklkK296dlkxUGkrTXhCT3ZMNFFVOWROcXNaS0RqWU1kMUdRV2tDdFdIS0Y3YUsK",
"endpoints": [
"https://spacedrive.com/api/releases/tauri/{{version}}/{{target}}/{{arch}}"
]
}
}
}

View file

@ -1,52 +1,46 @@
/** tauri-specta globals **/
import { invoke as TAURI_INVOKE } from '@tauri-apps/api';
import { invoke as TAURI_INVOKE } from '@tauri-apps/api/core';
import * as TAURI_API_EVENT from '@tauri-apps/api/event';
import { type WebviewWindowHandle as __WebviewWindowHandle__ } from '@tauri-apps/api/window';
import { type WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow';
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
export const commands = {
async appReady(): Promise<null> {
return await TAURI_INVOKE('plugin:tauri-specta|app_ready');
async appReady(): Promise<void> {
await TAURI_INVOKE('app_ready');
},
async resetSpacedrive(): Promise<null> {
return await TAURI_INVOKE('plugin:tauri-specta|reset_spacedrive');
async resetSpacedrive(): Promise<void> {
await TAURI_INVOKE('reset_spacedrive');
},
async openLogsDir(): Promise<__Result__<null, null>> {
async openLogsDir(): Promise<Result<null, null>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('plugin:tauri-specta|open_logs_dir') };
return { status: 'ok', data: await TAURI_INVOKE('open_logs_dir') };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async refreshMenuBar(): Promise<__Result__<null, null>> {
async refreshMenuBar(): Promise<Result<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|refresh_menu_bar')
};
return { status: 'ok', data: await TAURI_INVOKE('refresh_menu_bar') };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async reloadWebview(): Promise<null> {
return await TAURI_INVOKE('plugin:tauri-specta|reload_webview');
async reloadWebview(): Promise<void> {
await TAURI_INVOKE('reload_webview');
},
async setMenuBarItemState(id: string, enabled: boolean): Promise<null> {
return await TAURI_INVOKE('plugin:tauri-specta|set_menu_bar_item_state', { id, enabled });
async setMenuBarItemState(event: MenuEvent, enabled: boolean): Promise<void> {
await TAURI_INVOKE('set_menu_bar_item_state', { event, enabled });
},
async requestFdaMacos(): Promise<null> {
return await TAURI_INVOKE('plugin:tauri-specta|request_fda_macos');
async requestFdaMacos(): Promise<void> {
await TAURI_INVOKE('request_fda_macos');
},
async openTrashInOsExplorer(): Promise<__Result__<null, null>> {
async openTrashInOsExplorer(): Promise<Result<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|open_trash_in_os_explorer')
};
return { status: 'ok', data: await TAURI_INVOKE('open_trash_in_os_explorer') };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
@ -55,36 +49,17 @@ export const commands = {
async openFilePaths(
library: string,
ids: number[]
): Promise<
__Result__<
(
| { t: 'NoLibrary' }
| { t: 'NoFile'; c: number }
| { t: 'OpenError'; c: [number, string] }
| { t: 'AllGood'; c: number }
| { t: 'Internal'; c: string }
)[],
null
>
> {
): Promise<Result<OpenFilePathResult[], null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|open_file_paths', { library, ids })
};
return { status: 'ok', data: await TAURI_INVOKE('open_file_paths', { library, ids }) };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async openEphemeralFiles(
paths: string[]
): Promise<__Result__<({ t: 'Ok'; c: string } | { t: 'Err'; c: string })[], null>> {
async openEphemeralFiles(paths: string[]): Promise<Result<EphemeralFileOpenResult[], null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|open_ephemeral_files', { paths })
};
return { status: 'ok', data: await TAURI_INVOKE('open_ephemeral_files', { paths }) };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
@ -93,14 +68,11 @@ export const commands = {
async getFilePathOpenWithApps(
library: string,
ids: number[]
): Promise<__Result__<{ url: string; name: string }[], null>> {
): Promise<Result<OpenWithApplication[], null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|get_file_path_open_with_apps', {
library,
ids
})
data: await TAURI_INVOKE('get_file_path_open_with_apps', { library, ids })
};
} catch (e) {
if (e instanceof Error) throw e;
@ -109,13 +81,11 @@ export const commands = {
},
async getEphemeralFilesOpenWithApps(
paths: string[]
): Promise<__Result__<{ url: string; name: string }[], null>> {
): Promise<Result<OpenWithApplication[], null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|get_ephemeral_files_open_with_apps', {
paths
})
data: await TAURI_INVOKE('get_ephemeral_files_open_with_apps', { paths })
};
} catch (e) {
if (e instanceof Error) throw e;
@ -125,63 +95,50 @@ export const commands = {
async openFilePathWith(
library: string,
fileIdsAndUrls: [number, string][]
): Promise<__Result__<null, null>> {
): Promise<Result<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|open_file_path_with', {
library,
fileIdsAndUrls
})
data: await TAURI_INVOKE('open_file_path_with', { library, fileIdsAndUrls })
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async openEphemeralFileWith(pathsAndUrls: [string, string][]): Promise<__Result__<null, null>> {
async openEphemeralFileWith(pathsAndUrls: [string, string][]): Promise<Result<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|open_ephemeral_file_with', {
pathsAndUrls
})
data: await TAURI_INVOKE('open_ephemeral_file_with', { pathsAndUrls })
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async revealItems(library: string, items: RevealItem[]): Promise<__Result__<null, null>> {
async revealItems(library: string, items: RevealItem[]): Promise<Result<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|reveal_items', { library, items })
};
return { status: 'ok', data: await TAURI_INVOKE('reveal_items', { library, items }) };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async lockAppTheme(themeType: AppThemeType): Promise<null> {
return await TAURI_INVOKE('plugin:tauri-specta|lock_app_theme', { themeType });
async lockAppTheme(themeType: AppThemeType): Promise<void> {
await TAURI_INVOKE('lock_app_theme', { themeType });
},
async checkForUpdate(): Promise<
__Result__<{ version: string; body: string | null } | null, string>
> {
async checkForUpdate(): Promise<Result<Update | null, string>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|check_for_update')
};
return { status: 'ok', data: await TAURI_INVOKE('check_for_update') };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async installUpdate(): Promise<__Result__<null, string>> {
async installUpdate(): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('plugin:tauri-specta|install_update') };
return { status: 'ok', data: await TAURI_INVOKE('install_update') };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
@ -192,7 +149,7 @@ export const commands = {
export const events = __makeEvents__<{
dragAndDropEvent: DragAndDropEvent;
}>({
dragAndDropEvent: 'plugin:tauri-specta:drag-and-drop-event'
dragAndDropEvent: 'drag-and-drop-event'
});
/** user-defined types **/
@ -202,10 +159,34 @@ export type DragAndDropEvent =
| { type: 'Hovered'; paths: string[]; x: number; y: number }
| { type: 'Dropped'; paths: string[]; x: number; y: number }
| { type: 'Cancelled' };
export type EphemeralFileOpenResult = { t: 'Ok'; c: string } | { t: 'Err'; c: string };
export type MenuEvent =
| 'NewLibrary'
| 'NewFile'
| 'NewDirectory'
| 'AddLocation'
| 'OpenOverview'
| 'OpenSearch'
| 'OpenSettings'
| 'ReloadExplorer'
| 'SetLayoutGrid'
| 'SetLayoutList'
| 'SetLayoutMedia'
| 'ToggleDeveloperTools'
| 'NewWindow'
| 'ReloadWebview';
export type OpenFilePathResult =
| { t: 'NoLibrary' }
| { t: 'NoFile'; c: number }
| { t: 'OpenError'; c: [number, string] }
| { t: 'AllGood'; c: number }
| { t: 'Internal'; c: string };
export type OpenWithApplication = { url: string; name: string };
export type RevealItem =
| { Location: { id: number } }
| { FilePath: { id: number } }
| { Ephemeral: { path: string } };
export type Update = { version: string };
type __EventObj__<T> = {
listen: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
@ -215,13 +196,13 @@ type __EventObj__<T> = {
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
type __Result__<T, E> = { status: 'ok'; data: T } | { status: 'error'; error: E };
export type Result<T, E> = { status: 'ok'; data: T } | { status: 'error'; error: E };
function __makeEvents__<T extends Record<string, any>>(mappings: Record<keyof T, string>) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindowHandle__): __EventObj__<T[K]>;
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
@ -229,7 +210,7 @@ function __makeEvents__<T extends Record<string, any>>(mappings: Record<keyof T,
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindowHandle__]) => ({
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg)

View file

@ -1,7 +1,8 @@
import { dialog, invoke, os, shell } from '@tauri-apps/api';
import { confirm } from '@tauri-apps/api/dialog';
import { invoke } from '@tauri-apps/api/core';
import { homeDir } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/api/shell';
import { confirm, open as dialogOpen, save as dialogSave } from '@tauri-apps/plugin-dialog';
import { type } from '@tauri-apps/plugin-os';
import { open as shellOpen } from '@tauri-apps/plugin-shell';
// @ts-expect-error: Doesn't have a types package.
import ConsistentHash from 'consistent-hash';
import { OperatingSystem, Platform } from '@sd/interface';
@ -16,12 +17,12 @@ const customUriServerUrl = (window as any).__SD_CUSTOM_URI_SERVER__ as string[]
const queryParams = customUriAuthToken ? `?token=${encodeURIComponent(customUriAuthToken)}` : '';
async function getOs(): Promise<OperatingSystem> {
switch (await os.type()) {
case 'Linux':
switch (await type()) {
case 'linux':
return 'linux';
case 'Windows_NT':
case 'windows':
return 'windows';
case 'Darwin':
case 'macos':
return 'macOS';
default:
return 'unknown';
@ -64,15 +65,18 @@ export const platform = {
constructServerUrl(
`/remote/${encodeURIComponent(remote_identity)}/uri/${path}?token=${customUriAuthToken}`
),
openLink: shell.open,
openLink: shellOpen,
getOs,
openDirectoryPickerDialog: (opts) => {
const result = dialog.open({ directory: true, ...opts });
const result = dialogOpen({ directory: true, ...opts });
if (opts?.multiple) return result as any; // Tauri don't properly type narrow on `multiple` argument
return result;
},
openFilePickerDialog: () => dialog.open(),
saveFilePickerDialog: (opts) => dialog.save(opts),
openFilePickerDialog: () =>
dialogOpen({
multiple: true
}).then((result) => result?.map((r) => r.path) ?? null),
saveFilePickerDialog: (opts) => dialogSave(opts),
showDevtools: () => invoke('show_devtools'),
confirm: (msg, cb) => confirm(msg).then(cb),
subscribeToDragAndDropEvents: (cb) =>

View file

@ -25,6 +25,15 @@ export default defineConfig(({ mode }) => {
server: {
port: 8001
},
build: {
rollupOptions: {
treeshake: 'recommended',
external: [
// Don't bundle Fda video for non-macOS platforms
process.platform !== 'darwin' && /^@sd\/assets\/videos\/Fda.mp4$/
].filter(Boolean)
}
},
plugins: [
devtoolsPlugin,
process.env.SENTRY_AUTH_TOKEN &&

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 319 KiB

View file

@ -12,6 +12,7 @@ export type Platform = {
version?: string;
links?: Array<{ name: string; arch: string }>;
disabled?: boolean;
note?: string;
};
export const platforms = {
@ -37,7 +38,8 @@ export const platforms = {
os: 'linux',
icon: LinuxLogo,
version: 'deb',
links: [{ name: 'x86_64', arch: 'x86_64' }]
links: [{ name: 'x86_64', arch: 'x86_64' }],
note: 'Supports Ubuntu 22.04+, Debian Bookworm+, Linux Mint 21+, PopOS 22.04+'
},
docker: { name: 'Docker', icon: Docker },
android: { name: 'Android', icon: AndroidLogo, version: '10+', disabled: true },

View file

@ -22,13 +22,17 @@ export function Downloads({ latestVersion }: Props) {
const plausible = usePlausible();
const formattedVersion = (() => {
const [formattedVersion, note] = (() => {
const platform = selectedPlatform ?? currentPlatform;
if (!platform?.version) return;
if (platform.name === 'Linux') return platform.version;
return `${platform.name} ${platform.version}`;
return platform
? [
platform.version &&
(platform.name === 'Linux'
? platform.version
: `${platform.name} ${platform.version}`),
platform.note
]
: [];
})();
return (
@ -95,6 +99,12 @@ export function Downloads({ latestVersion }: Props) {
{formattedVersion}
</>
)}
{note && (
<>
<span className="mx-2 opacity-50">|</span>
{note}
</>
)}
</p>
{/* Platform icons */}
<div className="relative z-10 mt-5 flex gap-3">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -1,7 +1,7 @@
[package]
name = "sd-mobile-android"
version = "0.1.0"
rust-version = "1.64.0"
rust-version = "1.64"
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }

View file

@ -1,7 +1,7 @@
[package]
name = "sd-mobile-core"
version = "0.1.0"
rust-version = "1.64.0"
rust-version = "1.64"
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }

View file

@ -1,7 +1,7 @@
[package]
name = "sd-mobile-ios"
version = "0.1.0"
rust-version = "1.64.0"
rust-version = "1.64"
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }

View file

@ -41,6 +41,7 @@
"expo-av": "^13.10.5",
"expo-blur": "^12.9.2",
"expo-build-properties": "~0.11.1",
"expo-haptics": "~12.8.1",
"expo-image": "^1.10.6",
"expo-linking": "~6.2.2",
"expo-media-library": "~15.9.1",

View file

@ -3,11 +3,10 @@ import {
ArchiveBox,
Briefcase,
Clock,
DotsThreeOutline,
DotsThree,
Heart,
Images,
MapPin,
Tag,
UserFocus
} from 'phosphor-react-native';
import { Text, View } from 'react-native';
@ -42,7 +41,7 @@ const BrowseCategories = () => {
style={tw`h-9 w-9 rounded-full`}
variant="gray"
>
<DotsThreeOutline weight="fill" size={16} color={'white'} />
<DotsThree weight="bold" size={20} color={'white'} />
</Button>
</View>
<View style={tw`flex-row flex-wrap gap-2`}>

View file

@ -1,8 +1,8 @@
import { useNavigation } from '@react-navigation/native';
import { DotsThreeOutline, Plus } from 'phosphor-react-native';
import { useLibraryQuery } from '@sd/client';
import { DotsThree, Plus } from 'phosphor-react-native';
import { useRef } from 'react';
import { Text, View } from 'react-native';
import { useLibraryQuery } from '@sd/client';
import { ModalRef } from '~/components/layout/Modal';
import { tw } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
@ -43,7 +43,7 @@ const BrowseLocations = () => {
style={tw`h-9 w-9 rounded-full`}
variant="gray"
>
<DotsThreeOutline weight="fill" size={16} color={'white'} />
<DotsThree weight="bold" size={20} color={'white'} />
</Button>
</View>
</View>

View file

@ -1,8 +1,8 @@
import { useNavigation } from '@react-navigation/native';
import { DotsThreeOutline, Plus } from 'phosphor-react-native';
import { useLibraryQuery } from '@sd/client';
import { DotsThree, Plus } from 'phosphor-react-native';
import React, { useRef } from 'react';
import { Text, View } from 'react-native';
import { useLibraryQuery } from '@sd/client';
import { ModalRef } from '~/components/layout/Modal';
import { tw } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
@ -40,7 +40,7 @@ const BrowseTags = () => {
style={tw`w-9 rounded-full`}
variant="gray"
>
<DotsThreeOutline weight="fill" size={16} color={'white'} />
<DotsThree weight="bold" size={20} color={'white'} />
</Button>
</View>
</View>

View file

@ -2,7 +2,13 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript
import { useNavigation } from '@react-navigation/native';
import { useRef } from 'react';
import { Pressable, Text, View } from 'react-native';
import { arraysEqual, byteSize, Location, useLibraryQuery, useOnlineLocations } from '@sd/client';
import {
arraysEqual,
humanizeSize,
Location,
useLibraryQuery,
useOnlineLocations
} from '@sd/client';
import { ModalRef } from '~/components/layout/Modal';
import { tw, twStyle } from '~/lib/tailwind';
@ -45,7 +51,7 @@ const DrawerLocationItem: React.FC<DrawerLocationItemProps> = ({
</View>
<View style={tw`rounded-md border border-app-lightborder bg-app-box px-1 py-0.5`}>
<Text style={tw`text-[11px] font-medium text-ink-dull`} numberOfLines={1}>
{`${byteSize(location.size_in_bytes)}`}
{`${humanizeSize(location.size_in_bytes)}`}
</Text>
</View>
</View>

View file

@ -100,7 +100,8 @@ const TagColumn = ({ tags, dataAmount }: TagColumnProps) => {
onPress={() =>
navigation.navigate('BrowseStack', {
screen: 'Tag',
params: { id: tag.id, color: tag.color }
params: { id: tag.id, color: tag.color },
initial: false
})
}
tagColor={tag.color as ColorValue}

View file

@ -8,6 +8,7 @@ import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
import { useExplorerStore } from '~/stores/explorerStore';
import { useActionsModalStore } from '~/stores/modalStore';
import * as Haptics from 'expo-haptics';
import { tw } from '~/lib/tailwind';
import ScreenContainer from '../layout/ScreenContainer';
import FileItem from './FileItem';
@ -39,6 +40,7 @@ const Explorer = (props: Props) => {
const { modalRef, setData } = useActionsModalStore();
function handlePress(data: ExplorerItem) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
if (isPath(data) && data.item.is_dir && data.item.location_id !== null) {
navigation.push('Location', {
id: data.item.location_id,
@ -50,6 +52,12 @@ const Explorer = (props: Props) => {
}
}
function handleLongPress(data: ExplorerItem) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setData(data);
modalRef.current?.present();
}
return (
<ScreenContainer tabHeight={props.tabHeight} scrollview={false} style={'gap-0 py-0'}>
<Menu />
@ -71,7 +79,10 @@ const Explorer = (props: Props) => {
: item.item.id.toString()
}
renderItem={({ item }) => (
<Pressable onPress={() => handlePress(item)}>
<Pressable
onPress={() => handlePress(item)}
onLongPress={() => handleLongPress(item)}
>
{store.layoutMode === 'grid' ? (
<FileItem data={item} />
) : (

View file

@ -1,6 +1,6 @@
import { ExplorerItem, getItemFilePath } from '@sd/client';
import React from 'react';
import { Text, View } from 'react-native';
import { ExplorerItem, getItemFilePath } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { getExplorerStore } from '~/stores/explorerStore';
@ -20,7 +20,7 @@ const FileRow = ({ data }: FileRowProps) => {
})}
>
<FileThumb data={data} size={0.6} />
<View style={tw`ml-3`}>
<View style={tw`ml-3 max-w-[80%]`}>
<Text numberOfLines={1} style={tw`text-center text-xs font-medium text-ink-dull`}>
{filePath?.name}
{filePath?.extension && `.${filePath.extension}`}

View file

@ -1,8 +1,5 @@
import { DocumentDirectoryPath } from '@dr.pogodin/react-native-fs';
import { getIcon } from '@sd/assets/util';
import { Image } from 'expo-image';
import { useEffect, useLayoutEffect, useMemo, useState, type PropsWithChildren } from 'react';
import { View } from 'react-native';
import {
getExplorerItemData,
getItemFilePath,
@ -10,6 +7,9 @@ import {
isDarkTheme,
type ExplorerItem
} from '@sd/client';
import { Image } from 'expo-image';
import { useEffect, useLayoutEffect, useMemo, useState, type PropsWithChildren } from 'react';
import { View } from 'react-native';
import { flattenThumbnailKey, useExplorerStore } from '~/stores/explorerStore';
import { tw } from '../../lib/tailwind';

View file

@ -1,7 +1,6 @@
import { AnimatePresence, MotiView } from 'moti';
import { MonitorPlay, Rows, SlidersHorizontal, SquaresFour } from 'phosphor-react-native';
import { Rows, SquaresFour } from 'phosphor-react-native';
import { Pressable, View } from 'react-native';
import { toast } from '~/components/primitive/Toast';
import { tw } from '~/lib/tailwind';
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
@ -12,43 +11,41 @@ const Menu = () => {
return (
<AnimatePresence>
{store.toggleMenu && (
<MotiView
{store.toggleMenu && (
<MotiView
from={{ translateY: -70 }}
animate={{ translateY: 0 }}
exit={{ translateY: -70 }}
transition={{
type: 'timing',
duration: 300,
repeat: 0,
repeatReverse: false
}}
exit={{ translateY: -70 }}
style={tw`w-screen flex-row items-center justify-between border-b border-app-cardborder bg-app-header px-5 py-3`}
>
<View
style={tw`w-screen flex-row items-center justify-between border-b border-app-cardborder bg-app-header px-7 py-4`}
>
<SortByMenu />
<View style={tw`flex-row gap-3`}>
<Pressable onPress={() => (getExplorerStore().layoutMode = 'grid')}>
{store.layoutMode === 'grid' ? (
<Pressable hitSlop={12} onPress={() => (getExplorerStore().layoutMode = 'list')}>
<Rows
weight='fill'
color={tw.color('text-ink-faint'
)}
size={23}
/>
</Pressable>
) : (
<Pressable hitSlop={12} onPress={() => (getExplorerStore().layoutMode = 'grid')}>
<SquaresFour
weight='fill'
color={tw.color(
store.layoutMode === 'grid'
? 'text-accent'
: 'text-ink-dull'
'text-ink-faint'
)}
size={23}
/>
</Pressable>
<Pressable onPress={() => (getExplorerStore().layoutMode = 'list')}>
<Rows
color={tw.color(
store.layoutMode === 'list'
? 'text-accent'
: 'text-ink-dull'
)}
size={23}
/>
</Pressable>
<Pressable
)}
{/* <Pressable
onPress={() => toast.error('Media view is not available yet...')}
// onPress={() => (getExplorerStore().layoutMode = 'media')}
>
@ -60,12 +57,10 @@ const Menu = () => {
)}
size={23}
/>
</Pressable>
</Pressable> */}
</View>
<SortByMenu />
</View>
</MotiView>
)}
)}
</AnimatePresence>
);
};

View file

@ -1,61 +1,78 @@
import { ArrowDown, ArrowUp } from 'phosphor-react-native';
import { useState } from 'react';
import { ArrowDown, ArrowUp, CaretDown, Check } from 'phosphor-react-native';
import { Text, View } from 'react-native';
import { Menu, MenuItem } from '~/components/primitive/Menu';
import { tw } from '~/lib/tailwind';
import { SortOptionsType, getSearchStore, useSearchStore } from '~/stores/searchStore';
const sortOptions = {
none: 'None',
name: 'Name',
kind: 'Kind',
favorite: 'Favorite',
date_created: 'Date Created',
date_modified: 'Date Modified',
date_last_opened: 'Date Last Opened'
};
sizeInBytes: 'Size',
dateIndexed: 'Date Indexed',
dateCreated: 'Date Created',
dateModified: 'Date Modified',
dateAccessed: 'Date Accessed',
dateTaken: 'Date Taken',
} satisfies Record<SortOptionsType['by'], string>;
type SortByType = keyof typeof sortOptions;
const sortOrder = ['Asc', 'Desc'] as SortOptionsType['direction'][];
const ArrowUpIcon = () => <ArrowUp weight="bold" size={16} color={tw.color('ink-dull')} />;
const ArrowDownIcon = () => <ArrowDown weight="bold" size={16} color={tw.color('ink-dull')} />;
const ArrowUpIcon = <ArrowUp style={tw`ml-0.5`} weight="bold" size={14} color={tw.color('ink-dull')} />;
const ArrowDownIcon = <ArrowDown style={tw`ml-0.5`} weight="bold" size={14} color={tw.color('ink-dull')} />;
const SortByMenu = () => {
const [sortBy, setSortBy] = useState<SortByType>('name');
const [sortDirection, setSortDirection] = useState('asc' as 'asc' | 'desc');
const searchStore = useSearchStore();
return (
<View style={tw`flex-row items-center gap-1.5`}>
<Menu
trigger={
<View style={tw`flex flex-row items-center`}>
<Text style={tw`mr-0.5 font-medium text-ink-dull`}>{sortOptions[sortBy]}</Text>
{sortDirection === 'asc' ? <ArrowUpIcon /> : <ArrowDownIcon />}
</View>
}
trigger={<Trigger activeOption={sortOptions[searchStore.sort.by]} />}
>
{Object.entries(sortOptions).map(([value, text]) => (
{(Object.entries(sortOptions) as [[SortOptionsType['by'], string]]).map(([value, text], idx) => (
<View key={value}>
<MenuItem
key={value}
icon={
value === sortBy
? sortDirection === 'asc'
? ArrowUpIcon
: ArrowDownIcon
: undefined
}
icon={value === searchStore.sort.by ? Check : undefined}
text={text}
value={value}
onSelect={() => {
if (value === sortBy) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
return;
}
// Reset sort direction to descending
sortDirection === 'asc' && setSortDirection('desc');
setSortBy(value as SortByType);
}}
onSelect={() => getSearchStore().sort.by = value}
/>
{idx !== Object.keys(sortOptions).length - 1 && <View style={tw`border-b border-app-cardborder`} />}
</View>
))}
</Menu>
<Menu
trigger={<Trigger
triggerIcon={searchStore.sort.direction === 'Asc' ? ArrowUpIcon : ArrowDownIcon}
activeOption={searchStore.sort.direction}
/>
}
>
{sortOrder.map((value, idx) => (
<View key={value}>
<MenuItem
icon={value === searchStore.sort.direction ? Check : undefined}
text={value === 'Asc' ? 'Ascending' : 'Descending'}
onSelect={() => getSearchStore().sort.direction = value}
/>
{idx !== 1 && <View style={tw`border-b border-app-cardborder`} />}
</View>
))}
</Menu>
</View>
);
};
interface Props {
activeOption: string;
triggerIcon?: React.ReactNode;
}
const Trigger = ({activeOption, triggerIcon}: Props) => {
return (
<View style={tw`flex flex-row items-center rounded-md border border-app-inputborder p-1.5`}>
<Text style={tw`mr-0.5 text-ink-dull`}>{activeOption}</Text>
{triggerIcon ? triggerIcon : <CaretDown style={tw`ml-0.5`} weight="bold" size={16} color={tw.color('ink-dull')} />}
</View>
)
}
export default SortByMenu;

View file

@ -1,8 +1,8 @@
import { useQueryClient } from '@tanstack/react-query';
import { Object as SDObject, useLibraryMutation } from '@sd/client';
import * as Haptics from 'expo-haptics';
import { Heart } from 'phosphor-react-native';
import { useState } from 'react';
import { Pressable, PressableProps } from 'react-native';
import { Object as SDObject, useLibraryMutation } from '@sd/client';
type Props = {
data: SDObject;
@ -10,13 +10,13 @@ type Props = {
};
const FavoriteButton = (props: Props) => {
const queryClient = useQueryClient();
const [favorite, setFavorite] = useState(props.data.favorite);
const { mutate: toggleFavorite, isLoading } = useLibraryMutation('files.setFavorite', {
onSuccess: () => {
// TODO: Invalidate search queries
setFavorite(!favorite);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
});

View file

@ -1,5 +1,3 @@
import React from 'react';
import { Alert, Pressable, View, ViewStyle } from 'react-native';
import {
ExplorerItem,
getExplorerItemData,
@ -8,6 +6,8 @@ import {
isPath,
useLibraryQuery
} from '@sd/client';
import React from 'react';
import { Alert, Pressable, View, ViewStyle } from 'react-native';
import { InfoPill, PlaceholderPill } from '~/components/primitive/InfoPill';
import { tw, twStyle } from '~/lib/tailwind';
@ -30,7 +30,7 @@ const InfoTagPills = ({ data, style }: Props) => {
return (
<View style={twStyle('mt-1 flex flex-row flex-wrap', style)}>
{/* Kind */}
<InfoPill containerStyle={tw`mr-1`} text={getExplorerItemData(data).kind} />
<InfoPill containerStyle={tw`mr-1`} text={isDir ? 'Folder' : getExplorerItemData(data).kind} />
{/* Extension */}
{filePath?.extension && (
<InfoPill text={filePath.extension} containerStyle={tw`mr-1`} />

View file

@ -1,7 +1,7 @@
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types';
import { RouteProp, useNavigation } from '@react-navigation/native';
import { NativeStackHeaderProps } from '@react-navigation/native-stack';
import { ArrowLeft, DotsThreeOutline, MagnifyingGlass } from 'phosphor-react-native';
import { ArrowLeft, DotsThree, MagnifyingGlass } from 'phosphor-react-native';
import { Platform, Pressable, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { tw, twStyle } from '~/lib/tailwind';
@ -43,29 +43,14 @@ export default function DynamicHeader({
<HeaderIconKind routeParams={optionsRoute?.params} kind={kind} />
<Text
numberOfLines={1}
style={tw`max-w-[200px] text-xl font-bold text-white`}
style={tw`max-w-[200px] text-lg font-bold text-white`}
>
{headerRoute?.options.title}
</Text>
</View>
</View>
<View style={tw`flex-row gap-3`}>
{explorerMenu && (
<Pressable
hitSlop={12}
onPress={() => {
getExplorerStore().toggleMenu = !explorerStore.toggleMenu;
}}
>
<DotsThreeOutline
size={24}
color={tw.color(
explorerStore.toggleMenu ? 'text-accent' : 'text-zinc-300'
)}
/>
</Pressable>
)}
<Pressable
<View style={tw`flex-row gap-6`}>
<Pressable
hitSlop={12}
onPress={() => {
navigation.navigate('SearchStack', {
@ -74,11 +59,27 @@ export default function DynamicHeader({
}}
>
<MagnifyingGlass
size={24}
size={20}
weight="bold"
color={tw.color('text-zinc-300')}
/>
</Pressable>
{explorerMenu && (
<Pressable
hitSlop={12}
onPress={() => {
getExplorerStore().toggleMenu = !explorerStore.toggleMenu;
}}
>
<DotsThree
size={24}
weight='bold'
color={tw.color(
explorerStore.toggleMenu ? 'text-accent' : 'text-zinc-300'
)}
/>
</Pressable>
)}
</View>
</View>
</View>
@ -94,7 +95,7 @@ interface HeaderIconKindProps {
const HeaderIconKind = ({ routeParams, kind }: HeaderIconKindProps) => {
switch (kind) {
case 'location':
return <Icon size={30} name="Folder" />;
return <Icon size={24} name="Folder" />;
case 'tag':
return (
<View

View file

@ -2,6 +2,7 @@ import { Text, View } from 'react-native';
import { ClassInput } from 'twrnc';
import { twStyle } from '~/lib/tailwind';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Icon, IconName } from '../icons/Icon';
interface Props {
@ -10,14 +11,17 @@ interface Props {
style?: ClassInput; //Tailwind classes
iconSize?: number; //Size of the icon
textSize?: ClassInput; //Size of the text
includeHeaderHeight?: boolean; //Height of the header
}
const Empty = ({ description, icon, style, textSize = 'text-sm', iconSize = 38 }: Props) => {
const Empty = ({ description, icon, style, includeHeaderHeight = false, textSize = 'text-sm', iconSize = 38 }: Props) => {
const headerHeight = useSafeAreaInsets().top;
return (
<View
style={twStyle(
`relative mx-auto h-auto w-full flex-col items-center justify-center overflow-hidden
rounded-md border border-dashed border-sidebar-line p-4`,
{marginBottom: includeHeaderHeight ? headerHeight : 0},
style
)}
>

View file

@ -1,6 +1,6 @@
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
import { Pressable, Text, View } from 'react-native';
import { arraysEqual, byteSize, Location, useOnlineLocations } from '@sd/client';
import { arraysEqual, humanizeSize, Location, useOnlineLocations } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import FolderIcon from '../icons/FolderIcon';
@ -47,7 +47,7 @@ const GridLocation: React.FC<GridLocationProps> = ({ location, modalRef }: GridL
</Text>
</View>
<Text style={tw`text-left text-[13px] font-bold text-ink-dull`} numberOfLines={1}>
{`${byteSize(location.size_in_bytes)}`}
{`${humanizeSize(location.size_in_bytes)}`}
</Text>
</Card>
);

View file

@ -1,9 +1,9 @@
import { useNavigation } from '@react-navigation/native';
import { Location, arraysEqual, byteSize, useOnlineLocations } from '@sd/client';
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
import { DotsThreeVertical } from 'phosphor-react-native';
import { useRef } from 'react';
import { Pressable, Text, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { arraysEqual, humanizeSize, Location, useOnlineLocations } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
@ -62,22 +62,16 @@ const ListLocation = ({ location }: ListLocationProps) => {
</View>
</View>
<View style={tw`flex-row items-center gap-3`}>
<View
style={tw`rounded-md border border-app-box bg-app p-1.5`}
>
<View style={tw`rounded-md border border-app-box bg-app p-1.5`}>
<Text
style={tw`text-left text-xs font-medium text-ink-dull`}
numberOfLines={1}
>
{`${byteSize(location.size_in_bytes)}`}
{`${humanizeSize(location.size_in_bytes)}`}
</Text>
</View>
<Pressable hitSlop={24} onPress={() => swipeRef.current?.openRight()}>
<DotsThreeOutlineVertical
weight="fill"
size={20}
color={tw.color('ink-dull')}
/>
<DotsThreeVertical weight="bold" size={20} color={tw.color('ink-dull')} />
</Pressable>
</View>
</Card>

View file

@ -14,9 +14,9 @@ import { PropsWithChildren, useRef } from 'react';
import { Pressable, Text, View, ViewStyle } from 'react-native';
import FileViewer from 'react-native-file-viewer';
import {
byteSize,
getIndexedItemFilePath,
getItemObject,
humanizeSize,
useLibraryMutation,
useLibraryQuery
} from '@sd/client';
@ -34,7 +34,7 @@ type ActionsContainerProps = PropsWithChildren<{
}>;
const ActionsContainer = ({ children, style }: ActionsContainerProps) => (
<View style={twStyle('rounded-lg bg-app-box py-3.5', style)}>{children}</View>
<View style={twStyle('rounded-lg border border-app-box bg-app py-3.5', style)}>{children}</View>
);
type ActionsItemProps = {
@ -61,7 +61,7 @@ const ActionsItem = ({ icon, onPress, title, isDanger = false }: ActionsItemProp
);
};
const ActionDivider = () => <View style={tw`my-3.5 h-[0.5px] bg-app-line/80`} />;
const ActionDivider = () => <View style={tw`my-3.5 h-[0.5px] bg-app-box`} />;
export const ActionsModal = () => {
const fileInfoRef = useRef<ModalRef>(null);
@ -119,7 +119,7 @@ export const ActionsModal = () => {
</Text>
<View style={tw`flex flex-row`}>
<Text style={tw`text-xs text-ink-faint`}>
{`${byteSize(filePath?.size_in_bytes_bytes)}`},
{`${humanizeSize(filePath?.size_in_bytes_bytes)}`},
</Text>
<Text style={tw`text-xs text-ink-faint`}>
{' '}

View file

@ -2,7 +2,7 @@ import dayjs from 'dayjs';
import { Barcode, CaretLeft, Clock, Cube, Icon, SealCheck, Snowflake } from 'phosphor-react-native';
import { forwardRef } from 'react';
import { Pressable, Text, View } from 'react-native';
import { byteSize, getItemFilePath, getItemObject, type ExplorerItem } from '@sd/client';
import { getItemFilePath, humanizeSize, type ExplorerItem } from '@sd/client';
import FileThumb from '~/components/explorer/FileThumb';
import InfoTagPills from '~/components/explorer/sections/InfoTagPills';
import { Modal, ModalScrollView, type ModalRef } from '~/components/layout/Modal';
@ -39,18 +39,9 @@ type FileInfoModalProps = {
const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
const { data } = props;
const modalRef = useForwardedRef(ref);
const item = data?.item;
const objectData = data && getItemObject(data);
const filePathData = data && getItemFilePath(data);
// const fullObjectData = useLibraryQuery(['files.get', objectData?.id || -1], {
// enabled: objectData?.id !== undefined
// });
return (
<Modal
ref={modalRef}
@ -82,16 +73,8 @@ const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
<MetaItem
title="Size"
icon={Cube}
value={`${byteSize(filePathData?.size_in_bytes_bytes)}`}
value={`${humanizeSize(filePathData?.size_in_bytes_bytes)}`}
/>
{/* Duration */}
{/* {fullObjectData.data?.media_data?.duration && (
<MetaItem
title="Duration"
value={fullObjectData.data.media_data.duration}
icon={Clock}
/>
)} */}
{/* Created */}
{data.type !== 'SpacedropPeer' && (
<MetaItem

View file

@ -1,8 +1,8 @@
import { useNavigation } from '@react-navigation/native';
import { DotsThreeOutline } from 'phosphor-react-native';
import { useLibraryQuery } from '@sd/client';
import { DotsThree } from 'phosphor-react-native';
import React from 'react';
import { Text, View } from 'react-native';
import { useLibraryQuery } from '@sd/client';
import { tw } from '~/lib/tailwind';
import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack';
@ -24,7 +24,7 @@ export default function CategoriesScreen() {
style={tw`h-9 w-9 rounded-full`}
variant="gray"
>
<DotsThreeOutline weight="fill" size={16} color={'white'} />
<DotsThree weight='bold' size={20} color={'white'} />
</Button>
</View>
<View style={tw`flex-row flex-wrap gap-2`}>

View file

@ -4,7 +4,7 @@ import { UseQueryResult } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { Platform, Text, View } from 'react-native';
import { ClassInput } from 'twrnc/dist/esm/types';
import { byteSize, Statistics, StatisticsResponse, useLibraryContext } from '@sd/client';
import { humanizeSize, Statistics, StatisticsResponse, useLibraryContext } from '@sd/client';
import useCounter from '~/hooks/useCounter';
import { tw, twStyle } from '~/lib/tailwind';
@ -26,7 +26,7 @@ interface StatItemProps {
}
const StatItem = ({ title, bytes, isLoading, style }: StatItemProps) => {
const { value, unit } = byteSize(bytes);
const { value, unit } = humanizeSize(bytes);
const count = useCounter({ name: title, end: value });

View file

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Text, View } from 'react-native';
import { AnimatedCircularProgress } from 'react-native-circular-progress';
import { byteSize } from '@sd/client';
import { humanizeSize } from '@sd/client';
import { tw } from '~/lib/tailwind';
import { Icon, IconName } from '../icons/Icon';
@ -20,12 +20,12 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
const [mounted, setMounted] = useState(false);
const { totalSpace, freeSpace, usedSpaceSpace } = useMemo(() => {
const totalSpace = byteSize(stats.totalSpace);
const freeSpace = stats.freeSpace == null ? totalSpace : byteSize(stats.freeSpace);
const totalSpace = humanizeSize(stats.totalSpace);
const freeSpace = stats.freeSpace == null ? totalSpace : humanizeSize(stats.freeSpace);
return {
totalSpace,
freeSpace,
usedSpaceSpace: byteSize(totalSpace.original - freeSpace.original)
usedSpaceSpace: humanizeSize(totalSpace.original - freeSpace.original)
};
}, [stats]);
@ -34,7 +34,7 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
}, []);
const progress = useMemo(() => {
if (!mounted || totalSpace.original === 0n) return 0;
if (!mounted || totalSpace.original === 0) return 0;
return Math.floor((usedSpaceSpace.value / totalSpace.value) * 100);
}, [mounted, totalSpace, usedSpaceSpace]);

View file

@ -5,26 +5,25 @@ import {
MenuOptionProps,
MenuOptions,
MenuTrigger,
Menu as PMenu,
renderers
Menu as PMenu
} from 'react-native-popup-menu';
import { tw } from '~/lib/tailwind';
import { ClassInput } from 'twrnc';
import { tw, twStyle } from '~/lib/tailwind';
type MenuProps = {
trigger: React.ReactNode;
children: React.ReactNode[] | React.ReactNode;
triggerStyle?: ClassInput;
};
// TODO: Still looks a bit off...
export const Menu = (props: MenuProps) => (
<View>
<PMenu renderer={renderers.NotAnimatedContextMenu}>
<PMenu style={twStyle(props.triggerStyle)}>
<MenuTrigger>{props.trigger}</MenuTrigger>
<MenuOptions optionsContainerStyle={tw`rounded bg-app-menu p-1`}>
<MenuOptions optionsContainerStyle={tw`rounded-md border border-app-cardborder bg-app-menu p-1`}>
{props.children}
</MenuOptions>
</PMenu>
</View>
);
type MenuItemProps = {
@ -35,16 +34,16 @@ export const MenuItem = ({ icon, ...props }: MenuItemProps) => {
const Icon = icon;
return (
<View style={tw`flex flex-row items-center`}>
<View style={tw`flex flex-1 flex-row items-center`}>
{Icon && (
<View style={tw`ml-1`}>
<Icon />
<Icon size={16} style={tw`text-ink`} />
</View>
)}
<MenuOption
{...props}
customStyles={{
optionText: tw`py-0.5 text-sm font-medium text-ink`
optionText: tw`w-full py-1 text-sm font-medium text-ink`
}}
style={tw`flex flex-row items-center`}
/>

View file

@ -5,7 +5,7 @@ import { tw } from '~/lib/tailwind';
import { getSearchStore } from '~/stores/searchStore';
interface Props {
placeholder: string;
placeholder?: string;
}
export default function Search({ placeholder }: Props) {

View file

@ -1,9 +1,9 @@
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
import { Tag } from '@sd/client';
import { DotsThreeVertical } from 'phosphor-react-native';
import { useRef } from 'react';
import { Pressable, Text, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { ClassInput } from 'twrnc';
import { Tag } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import RightActions from './RightActions';
@ -40,11 +40,11 @@ const ListTag = ({ tag, tagStyle }: ListTagProps) => {
</Text>
</View>
<Pressable onPress={() => swipeRef.current?.openRight()}>
<DotsThreeOutlineVertical
weight="fill"
size={20}
color={tw.color('ink-dull')}
/>
<DotsThreeVertical
weight="bold"
size={20}
color={tw.color('ink-dull')}
/>
</Pressable>
</View>
</Swipeable>

View file

@ -0,0 +1,30 @@
import { FilePathOrder } from "@sd/client";
import { SortOptionsType, useSearchStore } from "~/stores/searchStore";
/**
* This hook provides a sorting order object based on user preferences
* for constructing the order query.
*/
export const useSortBy = (): FilePathOrder | null => {
const searchStore = useSearchStore();
const { by, direction } = searchStore.sort;
// if no sort by field is selected, return null
if (by === 'none') return null;
// some sort by fields have common keys
const common = { field: by, value: direction };
const fields: Record<Exclude<SortOptionsType['by'], 'none'>,any> = {
name: common,
sizeInBytes: common,
dateIndexed: common,
dateCreated: common,
dateModified: common,
dateAccessed: { field: "object", value: { field: "dateAccessed", value: direction} },
dateTaken: { field: "object", value: {field: 'mediaData', value: { field: "epochTime", value: direction}} }
};
return fields[by];
};

View file

@ -9,6 +9,7 @@ import Rive, { RiveRef } from 'rive-react-native';
import { Style } from 'twrnc/dist/esm/types';
import { tw } from '~/lib/tailwind';
import * as Haptics from 'expo-haptics';
import { RootStackParamList } from '.';
import BrowseStack, { BrowseStackParamList } from './tabs/BrowseStack';
import NetworkStack, { NetworkStackParamList } from './tabs/NetworkStack';
@ -145,6 +146,7 @@ export default function TabNavigator() {
})}
listeners={() => ({
focus: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setActiveIndex(index);
},
})}

View file

@ -1,6 +1,8 @@
import { useEffect } from 'react';
import { useLibraryQuery, useObjectsExplorerQuery } from '@sd/client';
import { useEffect } from 'react';
import Explorer from '~/components/explorer/Explorer';
import Empty from '~/components/layout/Empty';
import { tw } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
export default function TagScreen({ navigation, route }: BrowseStackScreenProps<'Tag'>) {
@ -21,5 +23,14 @@ export default function TagScreen({ navigation, route }: BrowseStackScreenProps<
});
}, [tagData?.name, navigation]);
return <Explorer {...objects} />;
return <Explorer
isEmpty={objects.count === 0}
emptyComponent={<Empty
includeHeaderHeight
icon={'Tags'}
style={tw`flex-1 items-center justify-center border-0`}
textSize="text-md"
iconSize={100}
description={'No items assigned to this tag'}
/>} {...objects} />;
}

View file

@ -1,6 +1,6 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import { AppLogo, BloomOne } from '@sd/assets/images';
import { SdMobIntro } from '@sd/assets/videos';
import SdMobIntro from '@sd/assets/videos/SdMobIntro.mp4';
import { ResizeMode, Video } from 'expo-av';
import { Image } from 'expo-image';
import { MotiView } from 'moti';

View file

@ -1,6 +1,6 @@
import { useIsFocused } from '@react-navigation/native';
import { usePathsExplorerQuery } from '@sd/client';
import { ArrowLeft, DotsThreeOutline, FunnelSimple } from 'phosphor-react-native';
import { ArrowLeft, DotsThree, FunnelSimple } from 'phosphor-react-native';
import { Suspense, useDeferredValue, useState } from 'react';
import { ActivityIndicator, Platform, Pressable, TextInput, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -8,6 +8,7 @@ import Explorer from '~/components/explorer/Explorer';
import Empty from '~/components/layout/Empty';
import FiltersBar from '~/components/search/filters/FiltersBar';
import { useFiltersSearch } from '~/hooks/useFiltersSearch';
import { useSortBy } from '~/hooks/useSortBy';
import { tw, twStyle } from '~/lib/tailwind';
import { SearchStackScreenProps } from '~/navigation/SearchStack';
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
@ -20,35 +21,36 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
const isFocused = useIsFocused();
const [search, setSearch] = useState('');
const deferredSearch = useDeferredValue(search);
const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length;
const isAndroid = Platform.OS === 'android';
const order = useSortBy();
const objects = usePathsExplorerQuery({
order,
arg: {
take: 30,
filters: searchStore.mergedFilters,
},
enabled: isFocused && searchStore.mergedFilters.length > 1, // only fetch when screen is focused & filters are applied
suspense: true,
order: null,
onSuccess: () => getExplorerStore().resetNewThumbnails()
});
useFiltersSearch(deferredSearch);
const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length;
const isAndroid = Platform.OS === 'android';
// Check if there are no objects or no search
const noObjects = objects.items?.length === 0 || !objects.items;
const noSearch = deferredSearch.length === 0 && appliedFiltersLength === 0;
useFiltersSearch(deferredSearch);
return (
<View
style={twStyle('flex-1 bg-app-header', {
style={twStyle('relative z-50 flex-1 bg-app-header', {
paddingTop: headerHeight + (isAndroid ? 15 : 0)
})}
>
{/* Header */}
<View style={tw`relative z-20 border-b border-app-cardborder bg-app-header`}>
<View style={tw`relative z-20 border-b border-app-cardborder bg-app-header pt-2`}>
{/* Search area input container */}
<View style={tw`flex-row items-center justify-between gap-4 px-5 pb-3`}>
{/* Back Button */}
@ -95,10 +97,11 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
getExplorerStore().toggleMenu = !explorerStore.toggleMenu;
}}
>
<DotsThreeOutline
<DotsThree
size={24}
weight='bold'
color={tw.color(
explorerStore.toggleMenu ? 'text-accent' : 'text-zinc-300'
explorerStore.toggleMenu ? 'text-accent' : 'text-ink-dull'
)}
/>
</Pressable>
@ -113,10 +116,9 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
isEmpty={noObjects}
emptyComponent={
<Empty
includeHeaderHeight
icon={noSearch ? 'Search' : 'FolderNoSpace'}
style={twStyle('flex-1 items-center justify-center border-0', {
marginBottom: headerHeight
})}
style={tw`flex-1 items-center justify-center border-0`}
textSize="text-md"
iconSize={100}
description={noSearch ? 'Add filters or type to search for files' : 'No files found'}

View file

@ -1,6 +1,6 @@
import { proxy, useSnapshot } from 'valtio';
import { proxySet } from 'valtio/utils';
import { resetStore } from '@sd/client';
import { resetStore, type Ordering } from '@sd/client';
export type ExplorerLayoutMode = 'list' | 'grid' | 'media';
@ -18,7 +18,12 @@ const state = {
// Using gridNumColumns instead of fixed size. We dynamically calculate the item size.
gridNumColumns: 3,
listItemSize: 65,
newThumbnails: proxySet() as Set<string>
newThumbnails: proxySet() as Set<string>,
// sorting
// we will display different sorting options based on the kind of explorer we are in
sortType: 'filePath' as 'filePath' | 'object' | 'ephemeral',
orderKey: 'name',
orderDirection: 'Asc' as 'Asc' | 'Desc'
};
export function flattenThumbnailKey(thumbKey: string[]) {

View file

@ -1,8 +1,20 @@
import { SearchFilterArgs } from '@sd/client';
import { proxy, useSnapshot } from 'valtio';
import { SearchFilterArgs } from '@sd/client';
import { IconName } from '~/components/icons/Icon';
export type SearchFilters = 'locations' | 'tags' | 'name' | 'extension' | 'hidden' | 'kind';
export type SortOptionsType = {
by:
| 'none'
| 'name'
| 'sizeInBytes'
| 'dateIndexed'
| 'dateCreated'
| 'dateModified'
| 'dateAccessed'
| 'dateTaken';
direction: 'Asc' | 'Desc';
};
export interface FilterItem {
id: number;
@ -32,8 +44,9 @@ export interface Filters {
interface State {
search: string;
filters: Filters;
sort: SortOptionsType;
appliedFilters: Partial<Filters>;
mergedFilters: SearchFilterArgs[],
mergedFilters: SearchFilterArgs[];
disableActionButtons: boolean;
}
@ -47,6 +60,10 @@ const initialState: State = {
hidden: false,
kind: []
},
sort: {
by: 'none',
direction: 'Asc'
},
appliedFilters: {},
mergedFilters: [],
disableActionButtons: true

View file

@ -15,7 +15,7 @@ libp2p = { version = "0.53.2", features = [
"autonat",
"macros",
] }
reqwest = { workspace = true, features = ["json"] }
reqwest = { workspace = true, features = ["json", "native-tls-vendored"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

Some files were not shown because too many files have changed in this diff Show more