Merge remote-tracking branch 'origin' into eng-1188-image-conversion-ui

This commit is contained in:
ameer2468 2024-06-24 12:53:37 +03:00
commit 17d20969d2
1549 changed files with 124103 additions and 59392 deletions

View file

@ -1,20 +1,25 @@
{{#nativeDeps}}
[env]
PROTOC = { force = true, value = "{{{protoc}}}" }
FFMPEG_DIR = { force = true, value = "{{{nativeDeps}}}" }
{{#isLinux}}
ORT_LIB_LOCATION = { force = true, value = "{{{nativeDeps}}}/lib" }
{{/isLinux}}
OPENSSL_STATIC = { force = true, value = "1" }
OPENSSL_NO_VENDOR = { force = true, value = "0" }
OPENSSL_RUST_USE_NASM = { force = true, value = "1" }
{{/nativeDeps}}
{{#isMacOS}}
[target.x86_64-apple-darwin]
rustflags = ["-L", "{{{nativeDeps}}}/lib"]
rustflags = ["-L", "{{{nativeDeps}}}/lib", "-Csplit-debuginfo=unpacked"]
[target.x86_64-apple-darwin.heif]
rustc-link-search = ["{{{nativeDeps}}}/lib"]
rustc-link-lib = ["heif"]
[target.aarch64-apple-darwin]
rustflags = ["-L", "{{{nativeDeps}}}/lib"]
rustflags = ["-L", "{{{nativeDeps}}}/lib", "-Csplit-debuginfo=unpacked"]
[target.aarch64-apple-darwin.heif]
rustc-link-search = ["{{{nativeDeps}}}/lib"]
@ -23,6 +28,9 @@ rustc-link-lib = ["heif"]
{{#isWin}}
[target.x86_64-pc-windows-msvc]
{{#hasLLD}}
linker = "lld-link.exe"
{{/hasLLD}}
rustflags = ["-L", "{{{nativeDeps}}}\\lib"]
[target.x86_64-pc-windows-msvc.heif]
@ -32,14 +40,30 @@ rustc-link-lib = ["heif"]
{{#isLinux}}
[target.x86_64-unknown-linux-gnu]
rustflags = ["-L", "{{{nativeDeps}}}/lib", "-C", "link-arg=-Wl,-rpath=${ORIGIN}/../lib/spacedrive"]
{{#hasLLD}}
linker = "clang"
{{/hasLLD}}
rustflags = [
"-L", "{{{nativeDeps}}}/lib", "-C", "link-arg=-Wl,-rpath=${ORIGIN}/../lib/spacedrive",
{{#hasLLD}}
"-C", "link-arg=-fuse-ld={{{linker}}}",
{{/hasLLD}}
]
[target.x86_64-unknown-linux-gnu.heif]
rustc-link-search = ["{{{nativeDeps}}}/lib"]
rustc-link-lib = ["heif"]
[target.aarch64-unknown-linux-gnu]
rustflags = ["-L", "{{{nativeDeps}}}/lib", "-C", "link-arg=-Wl,-rpath=${ORIGIN}/../lib/spacedrive"]
{{#hasLLD}}
linker = "clang"
{{/hasLLD}}
rustflags = [
"-L", "{{{nativeDeps}}}/lib", "-C", "link-arg=-Wl,-rpath=${ORIGIN}/../lib/spacedrive",
{{#hasLLD}}
"-C", "link-arg=-fuse-ld={{{linker}}}",
{{/hasLLD}}
]
[target.aarch64-unknown-linux-gnu.heif]
rustc-link-search = ["{{{nativeDeps}}}/lib"]

View file

@ -11,9 +11,11 @@ codegen
Condvar
dashmap
davidmytton
dayjs
deel
elon
encryptor
Exif
Flac
graps
haden
@ -37,6 +39,7 @@ narkhede
naveen
neha
noco
Normalised
OSSC
poonen
rauch
@ -56,8 +59,11 @@ spacetunnel
specta
storedkey
stringly
thumbstrips
tobiaslutke
tokio
typecheck
uuid
vdfs
vijay
zacharysmith

View file

@ -64,7 +64,7 @@ indent_style = space
# Prisma
# https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model#formatting
[*.prisma]
indent_size = 4
indent_size = 2
indent_style = space
# YAML

2
.gitattributes vendored
View file

@ -1,2 +1,4 @@
pnpm-lock.yaml -diff
package-lock.json -diff
Cargo.lock -diff
.github/actions/publish-artifacts/dist/index.js -diff

View file

@ -0,0 +1,8 @@
module.exports = {
extends: [require.resolve('@sd/config/eslint/base.js')],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
},
ignorePatterns: ['dist/**/*']
};

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,14 @@
import * as artifact from '@actions/artifact';
import client from '@actions/artifact';
import * as core from '@actions/core';
import * as glob from '@actions/glob';
import * as io from '@actions/io';
import { exists } from '@actions/io/lib/io-util';
type OS = 'darwin' | 'windows' | 'linux';
type Arch = 'x64' | 'arm64';
type TargetConfig = { bundle: string; ext: string };
type BuildTarget = {
updater: TargetConfig;
updater: false | { bundle: string; bundleExt: string; archiveExt: string };
standalone: Array<TargetConfig>;
};
@ -15,26 +16,22 @@ const OS_TARGETS = {
darwin: {
updater: {
bundle: 'macos',
ext: 'app.tar.gz'
bundleExt: 'app',
archiveExt: 'tar.gz'
},
standalone: [{ ext: 'dmg', bundle: 'dmg' }]
},
windows: {
updater: {
bundle: 'msi',
ext: 'msi.zip'
bundleExt: 'msi',
archiveExt: 'zip'
},
standalone: [{ ext: 'msi', bundle: 'msi' }]
},
linux: {
updater: {
bundle: 'appimage',
ext: 'AppImage.tar.gz'
},
standalone: [
{ ext: 'deb', bundle: 'deb' },
{ ext: 'AppImage', bundle: 'appimage' }
]
updater: false,
standalone: [{ ext: 'deb', bundle: 'deb' }]
}
} satisfies Record<OS, BuildTarget>;
@ -47,22 +44,34 @@ const PROFILE = core.getInput('profile');
const BUNDLE_DIR = `target/${TARGET}/${PROFILE}/bundle`;
const ARTIFACTS_DIR = '.artifacts';
const ARTIFACT_BASE = `Spacedrive-${OS}-${ARCH}`;
const FRONT_END_BUNDLE = 'apps/desktop/dist.tar.xz';
const UPDATER_ARTIFACT_NAME = `Spacedrive-Updater-${OS}-${ARCH}`;
const client = artifact.create();
const FRONTEND_ARCHIVE_NAME = `Spacedrive-frontend-${OS}-${ARCH}`;
async function globFiles(pattern: string) {
const globber = await glob.create(pattern);
return await globber.glob();
}
async function uploadUpdater({ bundle, ext }: TargetConfig) {
const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${ext}*`);
async function uploadFrontend() {
if (!(await exists(FRONT_END_BUNDLE))) {
console.error(`Frontend archive not found`);
return;
}
const updaterPath = files.find((file) => file.endsWith(ext));
if (!updaterPath) return console.error(`Updater path not found. Files: ${files}`);
await client.uploadArtifact(FRONTEND_ARCHIVE_NAME, [FRONT_END_BUNDLE], 'apps/desktop');
}
const artifactPath = `${ARTIFACTS_DIR}/${UPDATER_ARTIFACT_NAME}.${ext}`;
async function uploadUpdater(updater: BuildTarget['updater']) {
if (!updater) return;
const { bundle, bundleExt, archiveExt } = updater;
const fullExt = `${bundleExt}.${archiveExt}`;
const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${fullExt}*`);
const updaterPath = files.find((file) => file.endsWith(fullExt));
if (!updaterPath) throw new Error(`Updater path not found. Files: ${files}`);
const artifactPath = `${ARTIFACTS_DIR}/${UPDATER_ARTIFACT_NAME}.${archiveExt}`;
// https://tauri.app/v1/guides/distribution/updater#update-artifacts
await io.cp(updaterPath, artifactPath);
@ -79,7 +88,7 @@ async function uploadStandalone({ bundle, ext }: TargetConfig) {
const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${ext}*`);
const standalonePath = files.find((file) => file.endsWith(ext));
if (!standalonePath) return console.error(`Standalone path not found. Files: ${files}`);
if (!standalonePath) throw new Error(`Standalone path not found. Files: ${files}`);
const artifactName = `${ARTIFACT_BASE}.${ext}`;
const artifactPath = `${ARTIFACTS_DIR}/${artifactName}`;
@ -93,10 +102,10 @@ async function run() {
const { updater, standalone } = OS_TARGETS[OS];
await uploadUpdater(updater);
for (const config of standalone) {
await uploadStandalone(config);
}
await Promise.all([
uploadUpdater(updater),
uploadFrontend(),
...standalone.map((config) => uploadStandalone(config))
]);
}
run();

View file

@ -1,16 +1,19 @@
{
"name": "@sd/publish-artifacts",
"private": true,
"scripts": {
"build": "ncc build index.ts --minify"
"build": "ncc build index.ts --minify",
"typecheck": "tsc -b",
"lint": "eslint . --cache"
},
"dependencies": {
"@actions/artifact": "^1.1.2",
"@actions/artifact": "^2.1.7",
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"@actions/glob": "^0.4.0",
"@actions/io": "^1.1.3"
},
"devDependencies": {
"@vercel/ncc": "^0.38.1"
"@vercel/ncc": "^0.38.1",
"@sd/config": "workspace:*"
}
}

View file

@ -6,6 +6,7 @@
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"noEmit": true
}
}

View file

@ -9,12 +9,12 @@ runs:
using: 'composite'
steps:
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8.x.x
version: 9.0.6
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
token: ${{ inputs.token }}
check-latest: true

View file

@ -8,20 +8,25 @@ inputs:
description: Whether to save the Rust cache
required: false
default: 'false'
restore-cache:
description: Whether to restore the Rust cache
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@stable
uses: IronCoreLabs/rust-toolchain@v1
with:
target: ${{ inputs.target }}
toolchain: '1.73'
components: clippy, rustfmt
- name: Cache Rust Dependencies
if: ${{ inputs.restore-cache == 'true' }}
uses: Swatinem/rust-cache@v2
with:
key: ${{ inputs.target }}
save-if: ${{ inputs.save-cache }}
shared-key: stable-cache
@ -31,7 +36,7 @@ runs:
- name: Restore cached Prisma codegen
id: cache-prisma-restore
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
key: prisma-1-${{ runner.os }}-${{ hashFiles('./core/prisma/*', './crates/sync-generator/*', './Cargo.*') }}
path: crates/prisma/src/**/*.rs
@ -40,12 +45,33 @@ runs:
working-directory: core
if: ${{ steps.cache-prisma-restore.outputs.cache-hit != 'true' }}
shell: bash
run: cargo prisma generate
run: |
set -euxo pipefail
cargo prisma generate
# Check if a new migration should be created due to changes in the schema
cargo prisma migrate dev -n test --create-only --skip-generate
_new_migrations="$(
git ls-files --others --exclude-standard \
| { grep '^prisma/migrations/' || true; } \
| xargs sh -euxc '[ "$#" -lt 2 ] || grep -L "$@" || true' sh 'This is an empty migration' \
| wc -l \
| awk '{$1=$1};1'
)"
if [ "$_new_migrations" -gt 0 ]; then
echo "::error file=core/prisma/schema.prisma,title=Missing migration::New migration should be generated due to changes in prisma schema"
case "$GITHUB_REF" in
"refs/heads/main" | "refs/heads/gh-readonly-queue/main/"* | "refs/tags/"*)
# Fail action if we are on main or a release tag, to avoid releasing an app with a broken db
exit 1
;;
esac
fi
- name: Save Prisma codegen
id: cache-prisma-save
if: ${{ inputs.save-cache == 'true' }}
uses: actions/cache/save@v3
if: ${{ steps.cache-prisma-restore.outputs.cache-hit != 'true' && (github.ref == 'refs/heads/main' || inputs.save-cache == 'true') }}
uses: actions/cache/save@v4
with:
key: ${{ steps.cache-prisma-restore.outputs.cache-primary-key }}
path: crates/prisma/src/**/*.rs

View file

@ -22,7 +22,7 @@ runs:
- name: Restore cached LLVM and Clang
if: ${{ runner.os == 'Windows' }}
id: cache-llvm-restore
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
key: llvm-15
path: C:/Program Files/LLVM
@ -37,15 +37,40 @@ runs:
- name: Save LLVM and Clang
if: ${{ runner.os == 'Windows' && inputs.save-cache == 'true' }}
id: cache-llvm-save
uses: actions/cache/save@v3
uses: actions/cache/save@v4
with:
key: ${{ steps.cache-llvm-restore.outputs.cache-primary-key }}
path: C:/Program Files/LLVM
- name: Install current Bash on macOS
shell: bash
if: runner.os == 'macOS'
run: brew install bash
- name: Install Nasm
if: ${{ runner.os != 'Linux' }}
uses: ilammy/setup-nasm@v1
- name: Install Mold (linker)
shell: bash
if: ${{ runner.os == 'Linux' }}
run: |
curl -L# 'https://github.com/rui314/mold/releases/download/v2.4.0/mold-2.4.0-x86_64-linux.tar.gz' \
| sudo tar -xzf- -C /usr/local
- name: Remove 32-bit libs and incompatible pre-installed pkgs from Runner
shell: bash
if: ${{ runner.os == 'Linux' }}
run: |
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-* || true
- name: Setup Rust and Dependencies
uses: ./.github/actions/setup-rust
with:
@ -71,4 +96,4 @@ runs:
pushd scripts
npm i --production
popd
node scripts/preprep.mjs
env NODE_ENV=debug node scripts/preprep.mjs

View file

@ -9,44 +9,58 @@ on:
- main
schedule:
- cron: '0 0 * * *'
pull_request:
paths:
- '.github/workflows/cache-factory.yaml'
workflow_dispatch:
# Cancel previous runs of the same workflow on the same branch.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# From: https://github.com/rust-lang/rust-analyzer/blob/master/.github/workflows/ci.yaml
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
jobs:
make_cache:
strategy:
fail-fast: true
matrix:
settings:
- host: macos-latest
- host: macos-13
target: x86_64-apple-darwin
- host: macos-latest
- host: macos-14
target: aarch64-apple-darwin
- host: windows-latest
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 }}
if: github.repository == 'spacedriveapp/spacedrive'
permissions: {}
timeout-minutes: 150 # 2.5 hours
steps:
- name: Maximize build space
if: ${{ runner.os == 'Linux' }}
uses: easimon/maximize-build-space@master
with:
swap-size-mb: 3072
swap-size-mb: 4096
root-reserve-mb: 6144
remove-dotnet: 'true'
remove-codeql: 'true'
@ -54,7 +68,7 @@ jobs:
remove-docker-images: 'true'
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
@ -70,11 +84,18 @@ jobs:
target: ${{ matrix.settings.target }}
save-cache: 'true'
- name: Clippy
run: cargo clippy --workspace --all-features --target ${{ matrix.settings.target }}
- name: Compile tests (debug)
run: cargo test --workspace --all-features --no-run --locked --target ${{ matrix.settings.target }}
- name: Compile tests (release)
run: cargo test --workspace --all-features --no-run --locked --release --target ${{ matrix.settings.target }}
# It's faster to `test` before `build` ¯\_(ツ)_/¯
- name: Compile (debug)
run: cargo test --workspace --all-features --no-run --target ${{ matrix.settings.target }}
run: cargo build --quiet --workspace --all-features --target ${{ matrix.settings.target }}
- name: Compile (release)
run: cargo test --workspace --all-features --no-run --release --target ${{ matrix.settings.target }}
run: cargo build --quiet --workspace --all-features --release --target ${{ matrix.settings.target }}
- name: Clippy
run: cargo clippy --workspace --all-features --target ${{ matrix.settings.target }}

View file

@ -6,6 +6,12 @@ on:
env:
SPACEDRIVE_CUSTOM_APT_FLAGS: --no-install-recommends
# From: https://github.com/rust-lang/rust-analyzer/blob/master/.github/workflows/ci.yaml
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
SD_AUTH: disabled
# Cancel previous runs of the same workflow on the same branch.
concurrency:
@ -14,11 +20,13 @@ concurrency:
jobs:
typescript:
name: TypeScript
runs-on: ubuntu-20.04
name: Type and style check
runs-on: ubuntu-22.04
timeout-minutes: 7
permissions: {}
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node.js, pnpm and dependencies
uses: ./.github/actions/setup-pnpm
@ -28,12 +36,23 @@ jobs:
- name: Perform typechecks
run: pnpm typecheck
- name: Perform style check
run: |-
set -eux
pnpm autoformat only-frontend
if [ -n "$(git diff --name-only --cached)" ]; then
echo "Some files are not correctly formatted. Please run 'pnpm autoformat' and commit the changes." >&2
exit 1
fi
eslint:
name: ESLint
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
permissions: {}
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node.js, pnpm and dependencies
uses: ./.github/actions/setup-pnpm
@ -43,9 +62,72 @@ jobs:
- name: Perform linting
run: pnpm lint
cypress:
name: Cypress
runs-on: macos-14
timeout-minutes: 45
permissions: {}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup System and Rust
uses: ./.github/actions/setup-system
with:
token: ${{ secrets.GITHUB_TOKEN }}
target: aarch64-apple-darwin
- name: Setup Node.js, pnpm and dependencies
uses: ./.github/actions/setup-pnpm
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cypress
run: |
set -euxo pipefail
pnpm exec cypress install
rm -rf /Users/runner/.cache/Cypress
ln -sf /Users/runner/Library/Caches/Cypress /Users/runner/.cache/Cypress
- name: Setup Cypress
uses: cypress-io/github-action@v6
with:
runTests: false
working-directory: .
- name: Download test data
run: pnpm test-data small
- name: E2E test
uses: cypress-io/github-action@v6
with:
build: npx cypress info
install: false
command: env CI=true pnpm test:e2e
working-directory: apps/web
- name: Upload cypress screenshots
uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-screenshots
path: apps/web/cypress/screenshots
if-no-files-found: ignore
- name: Upload cypress video's
uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-videos
path: apps/web/cypress/videos
if-no-files-found: ignore
rustfmt:
name: Rust Formatting
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Maximize build space
if: ${{ runner.os == 'Linux' }}
@ -58,17 +140,23 @@ jobs:
remove-haskell: 'true'
remove-docker-images: 'true'
- name: Checkout repository
uses: actions/checkout@v3
- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
shell: powershell
run: |
New-Item -ItemType Directory -Force -Path C:\spacedrive_target
- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
shell: powershell
run: |
New-Item -Path target -ItemType Junction -Value C:\spacedrive_target
- uses: dorny/paths-filter@v2
- name: Checkout repository
uses: actions/checkout@v4
- name: Check if files have changed
uses: dorny/paths-filter@v3
continue-on-error: true
id: filter
with:
filters: |
@ -85,19 +173,33 @@ jobs:
- 'Cargo.lock'
- name: Setup Rust and Prisma
if: steps.filter.outputs.changes == 'true'
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-rust
with:
restore-cache: 'false'
- name: Run rustfmt
if: steps.filter.outputs.changes == 'true'
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
run: cargo fmt --all -- --check
clippy:
name: Clippy (${{ matrix.platform }})
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: true
matrix:
platform: [ubuntu-20.04, macos-latest, windows-latest]
settings:
- host: macos-13
target: x86_64-apple-darwin
- host: macos-14
target: aarch64-apple-darwin
- host: windows-latest
target: x86_64-pc-windows-msvc
- host: ubuntu-22.04
target: x86_64-unknown-linux-gnu
name: Clippy (${{ matrix.settings.host }})
runs-on: ${{ matrix.settings.host }}
permissions:
contents: read
timeout-minutes: 45
steps:
- name: Maximize build space
if: ${{ runner.os == 'Linux' }}
@ -110,9 +212,6 @@ jobs:
remove-haskell: 'true'
remove-docker-images: 'true'
- name: Checkout repository
uses: actions/checkout@v3
- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
shell: powershell
@ -120,7 +219,12 @@ jobs:
New-Item -ItemType Directory -Force -Path C:\spacedrive_target
New-Item -Path target -ItemType Junction -Value C:\spacedrive_target
- uses: dorny/paths-filter@v2
- name: Checkout repository
uses: actions/checkout@v4
- name: Find files that have changed
uses: dorny/paths-filter@v3
continue-on-error: true
id: filter
with:
filters: |
@ -137,32 +241,25 @@ jobs:
- 'extensions/*/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'
- name: Setup System and Rust
if: steps.filter.outputs.changes == 'true'
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-system
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Run Clippy
if: steps.filter.outputs.changes == 'true'
uses: actions-rs/clippy-check@v1
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: giraffate/clippy-action@v1
with:
args: --workspace --all-features
token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
tool_name: 'Clippy (${{ matrix.settings.host }})'
filter_mode: diff_context
github_token: ${{ secrets.GITHUB_TOKEN }}
clippy_flags: --workspace --all-features --locked
fail_on_error: true
# test:
# name: Test (${{ matrix.platform }})
# runs-on: ${{ matrix.platform }}
# strategy:
# matrix:
# platform: [ubuntu-20.04, macos-latest, windows-latest]
# steps:
# - name: Checkout repository
# uses: actions/checkout@v3
#
# - name: Setup
# uses: ./.github/actions/setup
#
# - name: Test
# run: cargo test --workspace --all-features
# - name: Run tests
# if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
# run: cargo test --workspace --all-features --locked --target ${{ matrix.settings.target }}

View file

@ -18,6 +18,11 @@ on:
env:
SPACEDRIVE_CUSTOM_APT_FLAGS: --no-install-recommends
SPACEDRIVE_CI: '1'
# From: https://github.com/rust-lang/rust-analyzer/blob/master/.github/workflows/ci.yaml
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
# Cancel previous runs of the same workflow on the same branch.
concurrency:
@ -25,13 +30,28 @@ concurrency:
cancel-in-progress: true
jobs:
js:
name: JS
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js, pnpm and dependencies
uses: ./.github/actions/setup-pnpm
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Build mobile JS
run: pnpm mobile export
# Disabled until I can figure out why our app on x86_64 crashes on startup.
# android:
# name: Android
# runs-on: macos-12
# steps:
# - name: Checkout repository
# uses: actions/checkout@v3
# uses: actions/checkout@v4
#
# - name: Setup Java JDK
# uses: actions/setup-java@v3.10.0
@ -56,11 +76,11 @@ jobs:
# - name: Cache NDK
# uses: actions/cache@v3
# with:
# path: ${{ env.ANDROID_HOME }}/ndk/23.1.7779620
# key: ndk-23.1.7779620
# path: ${{ env.ANDROID_HOME }}/ndk/26.1.10909125
# key: ndk-26.1.10909125
#
# - name: Install NDK
# run: echo "y" | sudo ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;23.1.7779620"
# run: echo "y" | sudo ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;26.1.10909125"
#
# - name: Cache Gradle
# uses: gradle/gradle-build-action@v2
@ -85,7 +105,7 @@ jobs:
# arch: x86_64
# api-level: 30
# target: google_apis
# ndk: 23.1.7779620
# ndk: 26.1.10909125
# ram-size: 4096M
# emulator-boot-timeout: 12000
# force-avd-creation: false
@ -105,7 +125,7 @@ jobs:
# arch: x86_64
# api-level: 30
# target: google_apis
# ndk: 23.1.7779620
# ndk: 26.1.10909125
# ram-size: 4096M
# emulator-boot-timeout: 12000
# force-avd-creation: false
@ -114,19 +134,19 @@ jobs:
# script: |
# adb install -r apps/mobile/android/app/build/outputs/apk/release/app-release.apk
# adb wait-for-device
# bash ./apps/mobile/scripts/run-maestro-tests android
# ./apps/mobile/scripts/run-maestro-tests.sh android
ios:
name: iOS
runs-on: macos-12
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
# - name: Install Xcode
# uses: maxim-lobanov/setup-xcode@v1
# with:
# xcode-version: latest-stable
- name: Setup System and Rust
uses: ./.github/actions/setup-system
@ -143,15 +163,22 @@ jobs:
working-directory: ./apps/mobile
run: pnpm expo prebuild --platform ios --no-install
- name: Cache Pods
uses: actions/cache@v3
# Hermes doesn't work with Cocoapods 1.15.0
# https://forums.developer.apple.com/forums/thread/745518
- name: Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
path: |
./apps/mobile/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: pods-${{ hashFiles('./apps/mobile/ios/Podfile.lock') }}
restore-keys: pods-
version: latest
# - name: Cache Pods
# uses: actions/cache@v4
# with:
# path: |
# ./apps/mobile/ios/Pods
# ~/Library/Caches/CocoaPods
# ~/.cocoapods
# key: pods-${{ hashFiles('./apps/mobile/ios/Podfile.lock') }}
# restore-keys: pods-
- name: Install Pods
working-directory: ./apps/mobile/ios
@ -159,21 +186,22 @@ jobs:
- name: Build iOS
working-directory: ./apps/mobile/ios
run: xcodebuild -workspace ./Spacedrive.xcworkspace -scheme Spacedrive -configuration Release -sdk iphonesimulator -derivedDataPath build -arch x86_64
run: xcodebuild -workspace ./Spacedrive.xcworkspace -scheme Spacedrive -configuration Release -sdk iphonesimulator -derivedDataPath build -arch "$(uname -m)"
- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
brew tap facebook/fb
brew install facebook/fb/idb-companion
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
echo "${HOME}/.maestro/bin" >> $GITHUB_PATH
- name: Run Simulator
uses: futureware-tech/simulator-action@v2
id: run_simulator
uses: futureware-tech/simulator-action@v3
with:
model: 'iPhone 11'
model: 'iPhone 14'
os_version: 16
erase_before_boot: false
- name: Run Tests
run: |
xcrun simctl install booted apps/mobile/ios/build/Build/Products/Release-iphonesimulator/Spacedrive.app
bash ./apps/mobile/scripts/run-maestro-tests ios
run: ./apps/mobile/scripts/run-maestro-tests.sh ios ${{ steps.run_simulator.outputs.udid }}

View file

@ -1,20 +1,28 @@
name: Release
on:
pull_request:
paths:
- '.github/workflows/release.yml'
workflow_dispatch:
# NOTE: For Linux builds, we can only build with Ubuntu. It should be the oldest base system we intend to support. See PR-759 & https://tauri.app/v1/guides/building/linux for reference.
# From: https://github.com/rust-lang/rust-analyzer/blob/master/.github/workflows/release.yaml#L13-L21
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs:
desktop-main:
strategy:
matrix:
settings:
- host: macos-latest
- host: macos-13
target: x86_64-apple-darwin
bundles: app,dmg
os: darwin
arch: x86_64
- host: macos-latest
- host: macos-14
target: aarch64-apple-darwin
bundles: app,dmg
os: darwin
@ -26,17 +34,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,appimage
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 # no appimage for now unfortunetly
# - host: ubuntu-20.04
# bundles: deb
# - host: ubuntu-22.04
# target: aarch64-unknown-linux-musl
name: Desktop - Main ${{ matrix.settings.target }}
runs-on: ${{ matrix.settings.host }}
@ -52,9 +60,6 @@ jobs:
remove-haskell: 'true'
remove-docker-images: 'true'
- name: Checkout repository
uses: actions/checkout@v3
- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
shell: powershell
@ -62,12 +67,8 @@ jobs:
New-Item -ItemType Directory -Force -Path C:\spacedrive_target
New-Item -Path target -ItemType Junction -Value C:\spacedrive_target
- name: Remove 32-bit libs
if: ${{ runner.os == 'Linux' }}
run: |
dpkg -l | grep i386
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386"
sudo dpkg --remove-architecture i386
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Apple API key
if: ${{ runner.os == 'macOS' }}
@ -75,7 +76,7 @@ jobs:
mkdir -p ~/.appstoreconnect/private_keys/
cd ~/.appstoreconnect/private_keys/
echo ${{ secrets.APPLE_API_KEY_BASE64 }} >> AuthKey_${{ secrets.APPLE_API_KEY }}.p8.base64
base64 --decode AuthKey_${{ secrets.APPLE_API_KEY }}.p8.base64 -o AuthKey_${{ secrets.APPLE_API_KEY }}.p8
base64 --decode -i AuthKey_${{ secrets.APPLE_API_KEY }}.p8.base64 -o AuthKey_${{ secrets.APPLE_API_KEY }}.p8
rm AuthKey_${{ secrets.APPLE_API_KEY }}.p8.base64
- name: Install Codesigning Certificate
@ -102,8 +103,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 }}
@ -113,6 +114,12 @@ jobs:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Package frontend
if: ${{ runner.os == 'Linux' }}
run: |
set -eux
XZ_OPT='-T0 -7' tar -cJf apps/desktop/dist.tar.xz -C apps/desktop/dist .
- name: Publish Artifacts
uses: ./.github/actions/publish-artifacts
with:
@ -130,10 +137,11 @@ jobs:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Create Release
uses: softprops/action-gh-release@v1
# TODO: Change to stable version when available
uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981
with:
draft: true
files: '*/**'

View file

@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Trigger Algolia Crawler
run: |

View file

@ -3,12 +3,16 @@ name: Server release
on:
release:
types: [published]
pull_request:
paths:
- '.github/workflows/server.yml'
- 'apps/server/docker/*'
workflow_dispatch:
jobs:
build-server:
name: Build a docker image for spacedrive server
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
defaults:
run:
shell: bash
@ -25,18 +29,58 @@ jobs:
remove-docker-images: 'true'
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Update Podman & crun
run: |
sudo apt-get remove crun podman
brew install crun podman
- name: Update buildah
shell: bash
run: |
set -euxo pipefail
wget -O- 'https://github.com/HeavenVolkoff/buildah-static/releases/latest/download/buildah-amd64.tar.gz' \
| sudo tar -xzf- -C /usr/local/bin
- name: Install netavark
shell: bash
run: |
set -euxo pipefail
sudo mkdir -p /usr/local/lib/podman
sudo wget -O- 'https://github.com/containers/netavark/releases/latest/download/netavark.gz' \
| gunzip | sudo dd status=none of=/usr/local/lib/podman/netavark
sudo chmod +x /usr/local/lib/podman/netavark
- name: Install passt
shell: bash
working-directory: /tmp
run: |
set -euxo pipefail
deb="$(
curl -SsL https://passt.top/builds/latest/x86_64 \
| grep -oP 'passt[^\.<>'\''"]+\.deb' | sort -u | head -n1
)"
curl -SsJLO "https://passt.top/builds/latest/x86_64/${deb}"
sudo dpkg -i "${deb}"
- name: Basic sanity check
run: |
crun --version
podman version
buildah --version
- name: Determine image name & tag
id: image_info
shell: bash
run: |
set -euxo pipefail
if [ "$GITHUB_EVENT_NAME" == "release" ]; then
IMAGE_TAG="${GITHUB_REF##*/}"
else
@ -52,11 +96,16 @@ jobs:
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
echo "repo_ref=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64'
- name: Build image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
tags: ${{ steps.image_info.outputs.tag }} ${{ github.event_name == 'release' && 'production' || 'staging' }}
tags: ${{ steps.image_info.outputs.tag }} ${{ github.event_name == 'release' && 'latest' || 'staging' }}
archs: amd64
image: ${{ steps.image_info.outputs.name }}
layers: 'false'

6
.gitignore vendored
View file

@ -14,7 +14,6 @@ cli/turbo-new.exe
cli/turbo.exe
storybook-static/
.DS_Store
cache
.env*
vendor/
data
@ -82,3 +81,8 @@ spacedrive
.cargo/config
.cargo/config.toml
.github/scripts/deps
.vite-inspect
vite.config.ts.*
/test-data
/config.json

3
.npmrc
View file

@ -1,10 +1,9 @@
; make all engine requirements (e.g. node version) strictly kept
engine-strict=true
; tempfix for pnpm#5909: https://github.com/pnpm/pnpm/issues/5909#issuecomment-1397758156
prefer-symlinked-executables=false
; necessary for metro + mobile
strict-peer-dependencies=false
node-linker=hoisted
auto-install-peers=true
max-old-space-size=4096
enable-pre-post-scripts=true
package-manager-strict=false

2
.nvmrc
View file

@ -1 +1 @@
v18.17
v18.18

View file

@ -2,6 +2,11 @@
target/
dist/
# Mobile build artifacts
apps/mobile/.expo
apps/mobile/android/app/build
apps/mobile/modules/sd-core/android/build
# macOS/iOS product/cache
.build/
Pods/
@ -20,10 +25,13 @@ apps/desktop/src/index.tsx
apps/desktop/src/commands.ts
# Import only file, which order is relevant
interface/components/TextViewer/prism.tsx
interface/components/TextViewer/prism-lazy.ts
.next/
.contentlayer/
# Stops from constant package.json changes showing up in commits
package*.json
# Dont format locales json
interface/locales

View file

@ -1,12 +1,13 @@
{
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"oscartbeaumont.rspc-vscode",
"editorconfig.editorconfig",
"bradlc.vscode-tailwindcss",
"prisma.prisma",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
"tauri-apps.tauri-vscode", // Tauri is a framework for building lightweight, secure applications with web technologies
"rust-lang.rust-analyzer", // Provides Rust language support
"oscartbeaumont.rspc-vscode", // RSPC is a Rust version of trpc.
"editorconfig.editorconfig", // EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs
"bradlc.vscode-tailwindcss", // Provides Tailwind CSS IntelliSense
"prisma.prisma", // Prisma is an open-source database toolkit
"dbaeumer.vscode-eslint", // Integrates ESLint JavaScript into VS Code
"esbenp.prettier-vscode", // Code formatter using prettier,
"lokalise.i18n-ally" // i18n-ally is an all-in-one i18n (internationalization) extension for VS Code
]
}

115
.vscode/i18n-ally-reviews.yml vendored Normal file
View file

@ -0,0 +1,115 @@
# Review comments generated by i18n-ally. Please commit this file.
reviews:
about_vision_text:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: OS2GadFYJi0w8WbQ1KpUe
type: approve
comment: 疑似翻译腔。这个地方不太好译。
time: '2024-04-16T02:03:55.931Z'
all_jobs_have_been_cleared:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: hwThsx7VP-THpRXov2MB6
type: comment
comment: 要不要把“清除”改为“完成”?
time: '2024-04-16T10:56:22.929Z'
archive_info:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: pW79_SMSNiOyRj94kdSZO
type: comment
comment: 不太通顺。“位置”是否要加定语修饰?
time: '2024-04-16T11:03:10.218Z'
changelog_page_description:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: JN3YruMypxX5wuaMjD8Hu
type: comment
comment: 口语化显得更自然些。
time: '2024-04-16T11:05:27.478Z'
clouds:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: ebAW-cnfA4llVgee6CRmF
type: comment
comment: 一个字太少。
time: '2024-04-16T11:06:06.594Z'
coordinates:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: HJLIcCmrHV1ZwCsAJOSiS
type: comment
comment: 有可能应该改成“地理坐标”。
time: '2024-04-16T11:07:21.331Z'
create_library_description:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: N01f9vhjfYidHDnkhVV4o
type: comment
comment: >-
“libraries are
databases”这一句并不容易翻译这里把英文原文放上去的方式我觉得并不妥当但是我想不到更好的译法了。定语往后放到谓语的位置。同时添加必要的助词。
time: '2024-04-16T11:13:48.568Z'
create_new_library_description:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: Wb89DhKwsCB9vGBDUIgsj
type: comment
comment: 见“create_library_description”。
time: '2024-04-16T11:14:21.837Z'
creating_your_library:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: 6q9xmFoeVizgSTBbBey9O
type: comment
comment: “您的库”是典型的翻译腔。
time: '2024-04-16T11:15:52.949Z'
delete_warning:
locales:
zh-CN:
comments:
- user:
name: Heavysnowjakarta
email: heavysnowjakarta@gmail.com
id: 5oa5lvp8PkJDRceIenfne
type: comment
comment: 我不确定 `{{type}}` 是中文还是英文。如果是英文,前面应该加空格。
time: '2024-04-16T11:24:52.250Z'

1
.vscode/launch.json vendored
View file

@ -11,6 +11,7 @@
"cargo": {
"args": [
"build",
"--profile=dev-debug",
"--manifest-path=./apps/desktop/src-tauri/Cargo.toml",
"--no-default-features"
],

26
.vscode/settings.json vendored
View file

@ -10,9 +10,11 @@
"dotenv",
"dotenvy",
"fontsource",
"ianvs",
"ipfs",
"Keepsafe",
"nodestate",
"normalise",
"overscan",
"pathctx",
"prismjs",
@ -26,7 +28,6 @@
"tailwindcss",
"tanstack",
"titlebar",
"ianvs",
"tsparticles",
"unlisten",
"upsert",
@ -43,6 +44,7 @@
"tw\\.[^`]+`([^`]*)`", // tw.xxx`...`
"tw\\(.*?\\).*?`([^`]*)", // tw(....)`...`
"tw`([^`]*)", // tw`...` (mobile)
"twStyle(([^)]*)", // twStyle(....) (mobile)
"twStyle\\(([^)]*)\\)", // twStyle(....) (mobile)
["styled\\([^,)]+,([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] // styled(....)`...` (mobile)
],
@ -77,10 +79,28 @@
"*.tsx": "${capture}.ts",
".npmrc": ".nvmrc, .yarnrc.yml",
".gitignore": ".eslintignore, .prettierignore",
"README.md": "CONTRIBUTING.md, CODE_OF_CONDUCT.md",
"README.md": "CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md",
"Cargo.toml": "Cargo.lock",
".eslintrc.js": ".eslintcache",
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, pnpm-workspace.yaml, .pnp.cjs, .pnp.loader.mjs",
"tsconfig.json": "tsconfig.*.json"
}
},
"rust-analyzer.linkedProjects": [],
"rust-analyzer.cargo.extraEnv": {},
"rust-analyzer.check.targets": null,
"rust-analyzer.showUnlinkedFileNotification": false,
"i18n-ally.localesPaths": [
"interface/locales",
"apps/mobile/ios/Pods/RCT-Folly/folly/lang",
"apps/mobile/ios/Pods/boost/boost/locale",
"apps/mobile/ios/Pods/boost/boost/predef/language"
],
"i18n-ally.enabledParsers": ["ts", "json"],
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/common.json",
"i18n-ally.enabledFrameworks": ["react"],
"i18n-ally.keystyle": "flat",
// You need to add this to your locale settings file "i18n-ally.translate.google.apiKey": "xxx"
"i18n-ally.translate.engines": ["google"]
}

View file

@ -39,21 +39,31 @@ To make changes locally, follow these steps:
1. Clone the repository: `git clone https://github.com/spacedriveapp/spacedrive`
2. Navigate to the project directory: `cd spacedrive`
3. For Linux or MacOS users, run: `./scripts/setup.sh`
- This will install FFmpeg and any other required dependencies for Spacedrive to build.
4. For Windows users, run the following command in PowerShell: `.\scripts\setup.ps1`
- This will install pnpm, LLVM, FFmpeg, and any other required dependencies for Spacedrive to build.
5. Install dependencies: `pnpm i`
6. Prepare the build: `pnpm prep` (This will run all necessary codegen and build required dependencies)
3. Configure your system environment for Spacedrive development
1. For Linux users, run: `./scripts/setup.sh`
> This [script](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh#L133) will check if Rust and pnpm are installed then proceed to install Clang, NASM, LLVM, libvips, Gstreamer's Plugins, FFmpeg, Perl, [Tauri essentials](https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-linux) and any other required dependencies for Spacedrive to build.
2. For macOS users, run: `./scripts/setup.sh`
> This [script](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh#L108) will check if Rust, pnpm and Xcode are installed and proceed to use Homebrew to install NASM, [Tauri essentials](https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-macos) and install any other required dependencies for Spacedrive to build.
3. For Windows users, run in PowerShell: `.\scripts\setup.ps1`
> This [script](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.ps1#L81) will install pnpm, LLVM, FFmpeg, C++ build tools, NASM, Rust + Cargo, Rust tools, Edge Webview 2, Strawberry Perl, [Tauri essentials](https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-windows) and any other required dependencies for Spacedrive to build.
4. Install dependencies: `pnpm i`
5. Prepare the build: `pnpm prep` (This will run all necessary codegen and build required dependencies)
To quickly run only the desktop app after `prep`, you can use:
- `pnpm tauri dev`
If necessary, the [webview devtools](https://tauri.app/v1/guides/debugging/application/#webview-console) can be opened by pressing `Ctrl + Shift + I` (Linux and Windows) and `Command + Option + I` (Mac) in the desktop app.
If necessary, the [webview devtools](https://tauri.app/v1/guides/debugging/application/#webview-console) can be opened by pressing `Ctrl + Shift + I` (Linux and Windows) or `Command + Option + I` (macOS) in the desktop app.
Also, the react-devtools can be launched using `pnpm dlx react-devtools`.
However, it must be executed before starting the desktop app for it qto connect.
However, it must be executed before starting the desktop app for it to connect.
You can download a bundle with sample files to test the app by running:
- `pnpm test-data`
Only for Linux and macOS (Requires curl and tar).
The test files will be located in a directory called `test-data` in the root of the spacedrive repo.
To run the web app:
@ -65,32 +75,52 @@ You can launch these individually if you'd prefer:
- `cargo run -p sd-server` (server)
- `pnpm web dev` (web interface)
To run the e2e tests for the web app:
- `pnpm web test:e2e`
If you are developing a new test, you can execute Cypress in interactive mode with:
- `pnpm web test:interactive`
To run the landing page:
- `pnpm landing dev`
If you encounter any issues, ensure that you are using the following versions of Rust, Node and Pnpm:
- Rust version: **1.73.0**
- Node version: **18.17**
- Pnpm version: **8.0.0**
- Rust version: **1.78**
- Node version: **18.18**
- Pnpm version: **9.1.1**
After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.
Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours.
After you finish making your changes and committed them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code.
##### Mobile App
To run the mobile app:
- Install Java JDK <= 17 for Android
- Java 21 is not compatible: https://github.com/react-native-async-storage/async-storage/issues/1057#issuecomment-1925963956
- Install [Android Studio](https://developer.android.com/studio) for Android and [Xcode](https://apps.apple.com/au/app/xcode/id497799835) for iOS development.
- Run `./scripts/setup.sh mobile`
- This will set up most of the dependencies required to build the mobile app.
- Make sure you have [NDK 23.1.7779620 and CMake](https://developer.android.com/studio/projects/install-ndk#default-version) installed in Android Studio.
- Make sure you have [NDK 26.1.10909125 and CMake](https://developer.android.com/studio/projects/install-ndk#default-version) installed in Android Studio.
- Run the following commands:
- `pnpm android` (runs on Android Emulator)
- `pnpm ios` (runs on iOS Emulator)
- `pnpm start` (runs the metro bundler)
- `pnpm mobile android` (runs on Android Emulator)
- In order to have locations working on Android, you must run the following command once the application has been installed for the first time. Otherwise, locations will not work.
- `adb shell appops set --uid com.spacedrive.app MANAGE_EXTERNAL_STORAGE allow`
- Run the following commands to access the logs from `sd-core`.
- `adb shell`
- Then `run-as com.spacedrive.app` to access the app's directory on device.
- Run `cd files/logs` and then select the logs with the timestamp of when you ran the app. Ex: `sd.log.2023-11-28`.
- You can view the logs using `tail -f [log-name]`. Ex: `tail -f sd.log.2023-11-28`.
- `pnpm mobile ios` (runs on iOS Emulator)
- `xcrun simctl launch --console booted com.spacedrive.app` allows you to view the console output of the iOS app from `tracing`. However, the application must be built in `debug` mode for this.
- `pnpm mobile start` (runs the metro bundler only)
### Pull Request
@ -118,7 +148,7 @@ This error occurs when Xcode is not installed or when the Xcode command line too
To resolve this issue:
- Install Xcode from the Mac App Store.
- Install Xcode from the macOS App Store or directly from [here](https://xcodereleases.com/) (requires Apple Account).
- Run `xcode-select -s /Applications/Xcode.app/Contents/Developer`.
This command will use Xcode's developer tools instead of macOS's default tools.
@ -131,12 +161,16 @@ error: terminated(1): /us/bin/xcrun --sdk macos --show-sdk-platform-path output
xcrun: error: unable to lookup item 'PlatformPath' from command line tools installation xcrun: error: unable to lookup item 'PlatformPath' in SDK '/Library/Developer /CommandLineTools/SDKs/MacOSX.sdk'
```
Ensure that MacOS is fully updated, and that you have XCode installed (via the app store).
Ensure that macOS is fully updated, and that you have Xcode installed (via the app store).
Once that has completed, run `xcode-select --install` in the terminal to install the command line tools. If they are already installed, ensure that you update MacOS to the latest version available.
Once that has completed, run `xcode-select --install` in the terminal to install the command line tools. If they are already installed, ensure that you update macOS to the latest version available.
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.
### Credits
This CONTRIBUTING.md file was inspired by the [github/docs CONTRIBUTING.md](https://github.com/github/docs/blob/main/CONTRIBUTING.md) file, and we extend our gratitude to the original author.

7572
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,7 @@ members = [
"core",
"core/crates/*",
"crates/*",
# "crates/p2p/tunnel",
# "crates/p2p/tunnel/utils",
"apps/cli",
"apps/deps-generator",
"apps/desktop/src-tauri",
"apps/desktop/crates/*",
"apps/mobile/modules/sd-core/core",
@ -14,6 +12,7 @@ members = [
"apps/mobile/modules/sd-core/ios/crate",
"apps/server",
]
exclude = ["crates/crypto"]
[workspace.package]
license = "AGPL-3.0-only"
@ -21,44 +20,131 @@ edition = "2021"
repository = "https://github.com/spacedriveapp/spacedrive"
[workspace.dependencies]
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", branch = "spacedrive", features = [
"rspc",
"sqlite-create-many",
"migrations",
"sqlite",
], default-features = false }
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", branch = "spacedrive", features = [
"rspc",
"sqlite-create-many",
"migrations",
"sqlite",
], default-features = false }
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust", branch = "spacedrive", features = [
"sqlite",
], default-features = false }
# Third party dependencies used by one or more of our crates
async-channel = "2.3"
async-trait = "0.1.80"
axum = "0.6.20" # Update blocked by hyper
base64 = "0.22.1"
base91 = "0.1.0"
blake3 = "1.5.0" # Update blocked by custom patch below
chrono = "0.4.38"
directories = "5.0"
ed25519-dalek = "2.1.1"
futures = "0.3.30"
futures-concurrency = "7.6"
gix-ignore = "0.11.2"
globset = "0.4.14"
http = "0.2" # Update blocked by axum
hyper = "0.14" # Update blocked due to API breaking changes
image = "0.25.1"
itertools = "0.13.0"
lending-stream = "1.0"
libc = "0.2"
normpath = "1.2"
once_cell = "1.19"
pin-project-lite = "0.2.14"
rand = "0.8.5"
regex = "1.10"
reqwest = "0.11" # Update blocked by hyper
rmp = "0.8.14"
rmp-serde = "1.3.0"
rmpv = { version = "1.3", features = ["with-serde"] }
rspc = "0.1.4"
serde = "1.0"
serde_json = "1.0"
specta = "=2.0.0-rc.11"
static_assertions = "1.1"
strum = "0.26"
strum_macros = "0.26"
tempfile = "3.10"
thiserror = "1.0"
tokio = "1.38"
tokio-stream = "0.1.15"
tokio-util = "0.7.11"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-test = "0.2.5"
uhlc = "0.6.0" # Must follow version used by specta
uuid = "1.8"
webp = "0.3.0"
tracing = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e" } # To work with tracing-appender
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e", features = [
"env-filter",
] } # To work with tracing-appender
tracing-appender = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e" } # Unreleased changes for rolling log deletion
[workspace.dependencies.prisma-client-rust]
git = "https://github.com/brendonovich/prisma-client-rust"
rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab"
features = ["migrations", "specta", "sqlite", "sqlite-create-many"]
default-features = false
rspc = { version = "0.1.4" }
specta = { version = "1.0.5" }
tauri-specta = { version = "1.0.2" }
[workspace.dependencies.prisma-client-rust-cli]
git = "https://github.com/brendonovich/prisma-client-rust"
rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab"
features = ["migrations", "specta", "sqlite", "sqlite-create-many"]
default-features = false
swift-rs = { version = "1.0.6" }
tokio = { version = "1.34.0" }
uuid = { version = "1.5.0", features = ["v4", "serde"] }
serde = { version = "1.0" }
serde_json = { version = "1.0" }
[workspace.dependencies.prisma-client-rust-sdk]
git = "https://github.com/brendonovich/prisma-client-rust"
rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab"
features = ["sqlite"]
default-features = false
[patch.crates-io]
# Proper IOS Support
if-watch = { git = "https://github.com/oscartbeaumont/if-watch.git", rev = "f732786057e57250e863a9ea0b1874e4cc9907c2" }
if-watch = { git = "https://github.com/spacedriveapp/if-watch.git", rev = "a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3" }
# Beta features
specta = { git = "https://github.com/oscartbeaumont/specta", rev = "4bc6e46fc8747cd8d8a07597c1fe13c52aa16a41" }
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "adebce542049b982dd251466d4144f4d57e92177" }
tauri-specta = { git = "https://github.com/oscartbeaumont/tauri-specta", rev = "c964bef228a90a66effc18cefcba6859c45a8e08" }
# We hack it to the high heavens
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" }
libp2p-core = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" }
libp2p-swarm = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" }
libp2p-stream = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" }
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"
opt-level = 0
debug = 0
strip = "none"
lto = false
codegen-units = 256
incremental = true
[profile.dev-debug]
inherits = "dev"
# Enables debugger
split-debuginfo = "none"
opt-level = 0
debug = "full"
strip = "none"
lto = "off"
codegen-units = 256
incremental = true
# Set the settings for build scripts and proc-macros.
[profile.dev.build-override]
opt-level = 3
# Set the default for dependencies, except workspace members.
[profile.dev.package."*"]
opt-level = 3
incremental = false
# Set the default for dependencies, except workspace members.
[profile.dev-debug.package."*"]
inherits = "dev"
opt-level = 3
debug = "full"
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

View file

@ -87,22 +87,31 @@ This project is using what I'm calling the **"PRRTT"** stack (Prisma, Rust, Reac
### Apps:
- `desktop`: A [Tauri](https://tauri.studio) app.
- `desktop`: A [Tauri](https://tauri.app) app.
- `mobile`: A [React Native](https://reactnative.dev/) app.
- `web`: A [React](https://reactjs.org) webapp.
- `landing`: A [React](https://reactjs.org) app using Vite SSR & Vite pages.
- `landing`: A [React](https://reactjs.org) app using [Next.js](https://nextjs.org).
- `server`: A [Rust](https://www.rust-lang.org) server for the webapp. (planned)
- `cli`: A [Rust](https://www.rust-lang.org) command line interface. (planned)
- `storybook`: A [React](https://reactjs.org) storybook for the UI components.
### Core:
- `core`: The [Rust](https://www.rust-lang.org) core, referred to internally as `sdcore`. Contains filesystem, database and networking logic. Can be deployed in a variety of host applications.
- `crates`: Shared Rust libraries used by the core and other Rust applications.
### Interface:
- `interface`: The complete user interface in React (used by apps `desktop`, `web`)
### Packages:
- `assets`: Shared assets (images, fonts, etc).
- `client`: A [TypeScript](https://www.typescriptlang.org/) client library to handle dataflow via RPC between UI and the Rust core.
- `config`: `eslint` configurations (includes `eslint-config-next`, `eslint-config-prettier` and all `tsconfig.json` configs used throughout the monorepo).
- `ui`: A [React](https://reactjs.org) Shared component library.
- `interface`: The complete user interface in React (used by apps `desktop`, `web` and `landing`)
- `config`: `eslint` configurations (includes `eslint-config-next`, `eslint-config-prettier` and all `tsconfig.json` configs used throughout the monorepo.
- `macos`: A [Swift](https://developer.apple.com/swift/) Native binary for MacOS system extensions.
- `macos`: A [Swift](https://developer.apple.com/swift/) Native binary for MacOS system extensions (planned).
- `ios`: A [Swift](https://developer.apple.com/swift/) Native binary (planned).
- `windows`: A [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) Native binary (planned).
- `android`: A [Kotlin](https://kotlinlang.org/) Native binary (planned).

View file

@ -1,14 +0,0 @@
[package]
name = "sd-cli"
version = "0.1.0"
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
[dependencies]
indoc = "2.0.4"
clap = { version = "4.4.7", features = ["derive"] }
anyhow = "1.0.75"
hex = "0.4.3"
sd-crypto = { path = "../../crates/crypto" }
tokio = { workspace = true, features = ["io-util", "rt-multi-thread"] }

View file

@ -1,4 +0,0 @@
# CLI
Basic CLI for interacting with encrypted files.
Will be expanded to a general Spacedrive CLI in the future.

View file

@ -1,85 +0,0 @@
use anyhow::{Context, Result};
use clap::Parser;
use indoc::printdoc;
use sd_crypto::header::file::FileHeader;
use std::path::PathBuf;
use tokio::fs::File;
#[derive(Parser)]
struct Args {
#[arg(help = "the file path to get details for")]
path: PathBuf,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let mut reader = File::open(args.path).await.context("unable to open file")?;
let (header, aad) = FileHeader::from_reader(&mut reader).await?;
print_crypto_details(&header, &aad);
Ok(())
}
fn print_crypto_details(header: &FileHeader, aad: &[u8]) {
printdoc! {"
Header version: {version}
Encryption algorithm: {algorithm}
AAD (hex): {hex}
",
version = header.version,
algorithm = header.algorithm,
hex = hex::encode(aad)
};
header.keyslots.iter().enumerate().for_each(|(i, k)| {
printdoc! {"
Keyslot {index}:
Version: {version}
Algorithm: {algorithm}
Hashing algorithm: {hashing_algorithm}
Salt (hex): {salt}
Master Key (hex, encrypted): {master}
Master key nonce (hex): {nonce}
",
index = i + i,
version = k.version,
algorithm = k.algorithm,
hashing_algorithm = k.hashing_algorithm,
salt = hex::encode(&*k.salt),
master = hex::encode(&*k.master_key),
nonce = hex::encode(k.nonce)
};
});
header.metadata.iter().for_each(|m| {
printdoc! {"
Metadata:
Version: {version}
Algorithm: {algorithm}
Encrypted size: {size}
Nonce (hex): {nonce}
",
version = m.version,
algorithm = m.algorithm,
size = m.metadata.len(),
nonce = hex::encode(m.metadata_nonce)
}
});
header.preview_media.iter().for_each(|p| {
printdoc! {"
Preview Media:
Version: {version}
Algorithm: {algorithm}
Encrypted size: {size}
Nonce (hex): {nonce}
",
version = p.version,
algorithm = p.algorithm,
size = p.media.len(),
nonce = hex::encode(p.media_nonce)
};
});
}

View file

@ -0,0 +1,19 @@
[package]
name = "sd-deps-generator"
version = "0.0.0"
authors = ["Jake Robinson <jake@spacedrive.com>"]
description = "A tool to compile all Spacedrive dependencies and their respective licenses"
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
[dependencies]
# Workspace dependencies
reqwest = { workspace = true, features = ["blocking", "native-tls-vendored"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
# Specific Deps Generator dependencies
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
cargo_metadata = "0.18.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -6,9 +6,11 @@ repository = { workspace = true }
edition = { workspace = true }
[dependencies]
libc = "0.2"
libc = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
[target.'cfg(target_os = "linux")'.dependencies]
wgpu = { version = "0.20.0", default-features = false }
# WARNING: gtk should follow the same version used by tauri
gtk = "=0.15"
# 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,33 +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();
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) {
// Remove AppImage paths from environment variables to avoid external applications attempting to use the AppImage's libraries
// https://github.com/AppImage/AppImageKit/blob/701b711f42250584b65a88f6427006b1d160164d/src/AppRun.c#L168-L194
ctx.unsetenv("PYTHONHOME");
ctx.unsetenv("GTK_DATA_PREFIX");
ctx.unsetenv("GTK_THEME");
ctx.unsetenv("GDK_BACKEND");
ctx.unsetenv("GTK_EXE_PREFIX");
ctx.unsetenv("GTK_IM_MODULE_FILE");
ctx.unsetenv("GDK_PIXBUF_MODULE_FILE");
remove_prefix_from_env_in_ctx(&ctx, "PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "LD_LIBRARY_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "PYTHONPATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "XDG_DATA_DIRS", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "PERLLIB", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GSETTINGS_SCHEMA_DIR", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "QT_PLUGIN_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH_1_0", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GTK_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GIO_EXTRA_MODULES", &appdir);
}
ctx
AppLaunchContext::default()
}
}

View file

@ -1,10 +1,10 @@
use std::{
collections::HashSet,
env,
ffi::{CStr, OsStr, OsString},
ffi::{CStr, OsStr},
mem,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
path::PathBuf,
ptr,
};
@ -175,23 +175,11 @@ 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"))
}
})
if has_nvidia() {
// Workaround for: https://github.com/tauri-apps/tauri/issues/9304
env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
}
// Check if snap by looking if SNAP is set and not empty and that the SNAP directory exists
@ -205,15 +193,6 @@ pub fn is_snap() -> bool {
false
}
// Check if appimage by looking if APPDIR is set and is a valid directory
pub fn is_appimage() -> bool {
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) {
appdir.is_absolute() && appdir.is_dir()
} else {
false
}
}
// Check if flatpak by looking if FLATPAK_ID is set and not empty and that the .flatpak-info file exists
pub fn is_flatpak() -> bool {
if let Some(flatpak_id) = std::env::var_os("FLATPAK_ID") {
@ -224,3 +203,31 @@ pub fn is_flatpak() -> bool {
false
}
fn has_nvidia() -> bool {
use wgpu::{
Backends, DeviceType, Dx12Compiler, Gles3MinorVersion, Instance, InstanceDescriptor,
InstanceFlags,
};
let instance = Instance::new(InstanceDescriptor {
flags: InstanceFlags::empty(),
backends: Backends::VULKAN | Backends::GL,
gles_minor_version: Gles3MinorVersion::Automatic,
dx12_shader_compiler: Dx12Compiler::default(),
});
for adapter in instance.enumerate_adapters(Backends::all()) {
let info = adapter.get_info();
match info.device_type {
DeviceType::DiscreteGpu | DeviceType::IntegratedGpu | DeviceType::VirtualGpu => {
// Nvidia PCI id
if info.vendor == 0x10de {
return true;
}
}
_ => {}
}
}
false
}

View file

@ -4,4 +4,4 @@ mod app_info;
mod env;
pub use app_info::{list_apps_associated_with_ext, open_file_path, open_files_path_with};
pub use env::{get_current_user_home, is_appimage, is_flatpak, is_snap, normalize_environment};
pub use env::{get_current_user_home, is_flatpak, is_snap, normalize_environment};

View file

@ -5,10 +5,8 @@ license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
[dependencies]
[target.'cfg(target_os = "macos")'.dependencies]
swift-rs = { workspace = true, features = ["serde"] }
swift-rs = { version = "1.0.6", features = ["serde"] }
[target.'cfg(target_os = "macos")'.build-dependencies]
swift-rs = { workspace = true, features = ["build"] }
swift-rs = { version = "1.0.6", features = ["build"] }

View file

@ -1,4 +1,5 @@
import AppKit
import SwiftRs
@objc
public enum AppThemeType: Int {
@ -7,6 +8,34 @@ public enum AppThemeType: Int {
case dark = 1
}
var activity: NSObjectProtocol?
@_cdecl("disable_app_nap")
public func disableAppNap(reason: SRString) -> Bool {
// Check if App Nap is already disabled
guard activity == nil else {
return false
}
activity = ProcessInfo.processInfo.beginActivity(
options: .userInitiatedAllowingIdleSystemSleep,
reason: reason.toString()
)
return true
}
@_cdecl("enable_app_nap")
public func enableAppNap() -> Bool {
// Check if App Nap is already enabled
guard let pinfo = activity else {
return false
}
ProcessInfo.processInfo.endActivity(pinfo)
activity = nil
return true
}
@_cdecl("lock_app_theme")
public func lockAppTheme(themeType: AppThemeType) {
var theme: NSAppearance?
@ -30,20 +59,6 @@ public func lockAppTheme(themeType: AppThemeType) {
}
}
@_cdecl("blur_window_background")
public func blurWindowBackground(window: NSWindow) {
let windowContent = window.contentView!
let blurryView = NSVisualEffectView()
blurryView.material = .sidebar
blurryView.state = .followsWindowActiveState
blurryView.blendingMode = .behindWindow
blurryView.wantsLayer = true
window.contentView = blurryView
blurryView.addSubview(windowContent)
}
@_cdecl("set_titlebar_style")
public func setTitlebarStyle(window: NSWindow, fullScreen: Bool) {
// this results in far less visual artifacts if we just manage it ourselves (the native taskbar re-appears when fullscreening/un-fullscreening)

View file

@ -1,4 +1,5 @@
#![cfg(target_os = "macos")]
use swift_rs::{swift, Bool, Int, SRData, SRObjectArray, SRString};
pub type NSObject = *mut std::ffi::c_void;
@ -8,11 +9,10 @@ pub enum AppThemeType {
Dark = 1 as Int,
}
swift!(pub fn disable_app_nap(reason: &SRString) -> Bool);
swift!(pub fn enable_app_nap() -> Bool);
swift!(pub fn lock_app_theme(theme_type: Int));
swift!(pub fn blur_window_background(window: &NSObject));
swift!(pub fn set_titlebar_style(window: &NSObject, is_fullscreen: Bool));
// swift!(pub fn setup_disk_watcher(window: &NSObject, transparent: Bool, large: Bool));
// swift!(pub fn disk_event_callback(mounted: Bool, path: &SRString));
swift!(pub fn reload_webview(webview: &NSObject));
#[repr(C)]
@ -30,20 +30,3 @@ pub fn open_file_paths_with(file_urls: &[String], with_url: &str) {
let file_url = file_urls.join("\0");
unsafe { open_file_path_with(&file_url.as_str().into(), &with_url.into()) }
}
// main!(|_| {
// unsafe { setup_disk_watcher() };
// print!("Waiting for disk events... ");
// Ok(())
// });
// #[no_mangle]
// pub extern "C" fn disk_event_callback(mounted: Bool, path: *const SRString) {
// let mounted_str = if mounted { "mounted" } else { "unmounted" };
// // Convert the raw pointer to a reference
// let path_ref = unsafe { &*path };
// let path_str = path_ref.to_string(); // Assuming SRString has a to_string method
// println!("Disk at path {} was {}", path_str, mounted_str);
// }

View file

@ -6,10 +6,10 @@ repository = { workspace = true }
edition = { workspace = true }
[dependencies]
thiserror = "1.0.50"
normpath = "1.1.1"
libc = "0.2"
libc = { workspace = true }
normpath = { workspace = true }
thiserror = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.51"
version = "0.57"
features = ["Win32_UI_Shell", "Win32_Foundation", "Win32_System_Com"]

View file

@ -10,6 +10,7 @@ use normpath::PathExt;
use windows::{
core::{HSTRING, PCWSTR},
Win32::{
Foundation::E_FAIL,
System::Com::{
CoInitializeEx, CoUninitialize, IDataObject, COINIT_APARTMENTTHREADED,
COINIT_DISABLE_OLE1DDE,
@ -97,11 +98,15 @@ pub fn open_file_path_with(path: impl AsRef<Path>, url: &str) -> Result<()> {
ensure_com_initialized();
let path = path.as_ref();
let ext = path.extension().ok_or(Error::OK)?;
let ext = path
.extension()
.ok_or(Error::new(E_FAIL, "No file extension"))?;
for handler in list_apps_associated_with_ext(ext)?.iter() {
let name = unsafe { handler.GetName()?.to_string()? };
if name == url {
let path = path.normalize_virtually().map_err(|_| Error::OK)?;
let path = path
.normalize_virtually()
.map_err(|e| Error::new(E_FAIL, e.to_string()))?;
let wide_path = path
.as_os_str()
.encode_wide()
@ -116,5 +121,8 @@ pub fn open_file_path_with(path: impl AsRef<Path>, url: &str) -> Result<()> {
}
}
Err(Error::OK)
Err(Error::new(
E_FAIL,
"No available handler for the given path",
))
}

View file

@ -1,5 +1,6 @@
{
"name": "@sd/desktop",
"type": "module",
"private": true,
"scripts": {
"vite": "vite",
@ -11,33 +12,34 @@
"lint": "eslint src --cache"
},
"dependencies": {
"@remix-run/router": "^1.11.0",
"@rspc/client": "=0.0.0-main-799eec5d",
"@rspc/tauri": "=0.0.0-main-799eec5d",
"@oscartbeaumont-sd/rspc-client": "=0.0.0-main-dc31e5b2",
"@oscartbeaumont-sd/rspc-tauri": "=0.0.0-main-dc31e5b2",
"@remix-run/router": "=1.13.1",
"@sd/client": "workspace:*",
"@sd/interface": "workspace:*",
"@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.3",
"@tauri-apps/plugin-os": "2.0.0-beta.3",
"@tauri-apps/plugin-shell": "2.0.0-beta.3",
"consistent-hash": "^1.2.2",
"immer": "^10.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "6.18.0",
"react-router-dom": "=6.20.1",
"sonner": "^1.0.3"
},
"devDependencies": {
"@sd/config": "workspace:*",
"@sentry/vite-plugin": "^2.9.0",
"@tauri-apps/cli": "^1.5.6",
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "^4.1.1",
"sass": "^1.69.5",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vite-plugin-html": "^3.2.0",
"vite-plugin-svgr": "^3.3.0",
"vite-tsconfig-paths": "^4.2.1"
"@sentry/vite-plugin": "^2.16.0",
"@tauri-apps/cli": "next",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"sass": "^1.72.0",
"typescript": "^5.4.2",
"vite": "^5.1.6",
"vite-tsconfig-paths": "^4.3.2"
}
}

View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "sd-desktop"
version = "0.1.2"
version = "0.3.2"
description = "The universal file manager."
authors = ["Spacedrive Technology Inc <support@spacedrive.com>"]
default-run = "sd-desktop"
@ -9,55 +9,69 @@ repository = { workspace = true }
edition = { workspace = true }
[dependencies]
tauri = { version = "1.5.2", features = [
"dialog-all",
"linux-protocol-headers",
"macos-private-api",
"os-all",
"path-all",
"protocol-all",
"shell-all",
"updater",
"window-all",
"native-tls-vendored",
] }
# Spacedrive Sub-crates
sd-core = { path = "../../../core", features = ["ffmpeg", "heif"] }
sd-fda = { path = "../../../crates/fda" }
sd-prisma = { path = "../../../crates/prisma" }
rspc = { workspace = true, features = ["tauri"] }
sd-core = { path = "../../../core", features = [
"ffmpeg",
"location-watcher",
"heif",
] }
# Workspace dependencies
axum = { workspace = true, features = ["headers", "query"] }
directories = { workspace = true }
futures = { workspace = true }
hyper = { workspace = true }
http = { workspace = true }
prisma-client-rust = { workspace = true }
rand = { workspace = true }
rspc = { workspace = true, features = ["tauri", "tracing"] }
serde = { workspace = true }
serde_json = { workspace = true }
specta = { workspace = true }
strum = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["sync"] }
tracing = { workspace = true }
serde = "1.0.190"
http = "0.2.9"
opener = { version = "0.6.1", features = ["reveal"] }
specta = { workspace = true }
tauri-specta = { workspace = true, features = ["typescript"] }
uuid = { version = "1.5.0", features = ["serde"] }
futures = "0.3"
axum = { version = "0.6.20", features = ["headers", "query"] }
rand = "0.8.5"
thiserror = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
prisma-client-rust = { workspace = true }
sd-prisma = { path = "../../../crates/prisma" }
tauri-plugin-window-state = "0.1.0"
# Specific Desktop dependencies
# WARNING: Do NOT enable default features, as that vendors dbus (see below)
opener = { version = "0.7.1", features = ["reveal"], default-features = false }
tauri = { version = "=2.0.0-beta.17", features = [
"macos-private-api",
"unstable",
"linux-libxdo",
] } # Update blocked by rspc
tauri-plugin-updater = "2.0.0-beta"
tauri-plugin-dialog = "2.0.0-beta"
tauri-plugin-os = "2.0.0-beta"
tauri-plugin-shell = "2.0.0-beta"
tauri-runtime = { version = "=2.0.0-beta.15" } # Update blocked by tauri
tauri-specta = { version = "=2.0.0-rc.8", features = ["typescript"] }
tauri-utils = { version = "=2.0.0-beta.16" } # Update blocked by tauri
[target.'cfg(target_os = "linux")'.dependencies]
# Spacedrive Sub-crates
sd-desktop-linux = { path = "../crates/linux" }
webkit2gtk = { version = "0.18.2", features = ["v2_2"] }
# Specific Desktop dependencies
# WARNING: dbus must NOT be vendored, as that breaks the app on Linux,X11,Nvidia
dbus = { version = "0.9.7", features = ["stdfd"] }
# 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]
# Spacedrive Sub-crates
sd-desktop-macos = { path = "../crates/macos" }
[target.'cfg(target_os = "windows")'.dependencies]
# Spacedrive Sub-crates
sd-desktop-windows = { path = "../crates/windows" }
webview2-com = "0.19.1"
[build-dependencies]
tauri-build = "1.5.0"
# Specific Desktop dependencies
tauri-build = "2.0.0-beta"
[features]
default = ["custom-protocol"]
devtools = ["tauri/devtools"]
ai-models = ["sd-core/ai"]
custom-protocol = ["tauri/custom-protocol"]

View file

@ -1,3 +1,12 @@
fn main() {
#[cfg(all(not(target_os = "windows"), feature = "ai-models"))]
// This is required because libonnxruntime.so is incorrectly built with the Initial Executable (IE) thread-Local storage access model by zig
// https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter8-20.html
// https://github.com/ziglang/zig/issues/16152
// https://github.com/ziglang/zig/pull/17702
// Due to this, the linker will fail to dlopen libonnxruntime.so because it runs out of the static TLS space reserved after initial load
// To workaround this problem libonnxruntime.so is added as a dependency to the binaries, which makes the linker allocate its TLS space during initial load
println!("cargo:rustc-link-lib=onnxruntime");
tauri_build::build();
}

View file

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

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

@ -1,3 +1,6 @@
use sd_core::Node;
use sd_prisma::prisma::{file_path, location};
use std::{
collections::{BTreeSet, HashMap, HashSet},
hash::{Hash, Hasher},
@ -6,10 +9,6 @@ use std::{
};
use futures::future::join_all;
use sd_core::{
prisma::{file_path, location},
Node,
};
use serde::Serialize;
use specta::Type;
use tauri::async_runtime::spawn_blocking;
@ -55,7 +54,7 @@ pub async fn open_file_paths(
};
open_result
.map(|_| OpenFilePathResult::AllGood(id))
.map(|()| OpenFilePathResult::AllGood(id))
.unwrap_or_else(|err| {
error!("Failed to open logs dir: {err}");
OpenFilePathResult::OpenError(id, err.to_string())
@ -309,7 +308,11 @@ pub async fn open_file_path_with(
error!("{e:#?}");
});
#[allow(unreachable_code)]
#[cfg(not(any(
target_os = "windows",
target_os = "linux",
target_os = "macos"
)))]
Err(())
})
.collect::<Result<Vec<_>, _>>()
@ -331,7 +334,7 @@ pub async fn open_ephemeral_file_with(paths_and_urls: Vec<PathAndUrl>) -> Result
#[cfg(target_os = "macos")]
if let Some(path) = path.to_str().map(str::to_string) {
if let Err(e) = spawn_blocking(move || {
sd_desktop_macos::open_file_paths_with(&[path], &url)
sd_desktop_macos::open_file_paths_with(&[path], &url);
})
.await
{
@ -370,21 +373,6 @@ pub async fn open_ephemeral_file_with(paths_and_urls: Vec<PathAndUrl>) -> Result
fn inner_reveal_paths(paths: impl Iterator<Item = PathBuf>) {
for path in paths {
#[cfg(target_os = "linux")]
if sd_desktop_linux::is_appimage() {
// This is a workaround for the app, when package inside an AppImage, crashing when using opener::reveal.
if let Err(e) = sd_desktop_linux::open_file_path(if path.is_file() {
path.parent().unwrap_or(&path)
} else {
&path
}) {
error!("Failed to open logs dir: {e:#?}");
}
} else if let Err(e) = opener::reveal(path) {
error!("Failed to open logs dir: {e:#?}");
}
#[cfg(not(target_os = "linux"))]
if let Err(e) = opener::reveal(path) {
error!("Failed to open logs dir: {e:#?}");
}
@ -444,7 +432,7 @@ pub async fn reveal_items(
.location()
.find_many(vec![
// TODO(N): This will fall apart with removable media and is making an invalid assumption that the `Node` is fixed for an `Instance`.
location::instance_id::equals(Some(library.config().instance_id)),
location::instance_id::equals(Some(library.config().await.instance_id)),
location::id::in_vec(locations),
])
.select(location::select!({ path }))
@ -452,7 +440,7 @@ pub async fn reveal_items(
.await
.unwrap_or_default()
.into_iter()
.flat_map(|location| location.path.map(Into::into)),
.filter_map(|location| location.path.map(Into::into)),
);
}

View file

@ -3,52 +3,55 @@
windows_subsystem = "windows"
)]
use std::{fs, path::PathBuf, sync::Arc, time::Duration};
use std::{fs, path::PathBuf, process::Command, sync::Arc, time::Duration};
use menu::{set_enabled, MenuEvent};
use sd_core::{Node, NodeError};
use tauri::{
api::path, ipc::RemoteDomainAccessScope, window::PlatformWebview, AppHandle, Manager,
WindowEvent,
};
use sd_fda::DiskAccess;
use serde::{Deserialize, Serialize};
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};
use tokio::task::block_in_place;
use tokio::time::sleep;
use tracing::error;
mod tauri_plugins;
mod theme;
mod file;
mod menu;
mod tauri_plugins;
mod theme;
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();
}
#[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")
}
// If this errors, we don't have FDA and we need to re-prompt for it
async fn request_fda_macos() {
DiskAccess::request_fda().expect("Unable to request full disk access");
}
#[tauri::command(async)]
#[specta::specta]
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");
@ -58,12 +61,12 @@ fn reload_webview_inner(webview: PlatformWebview) {
#[cfg(target_os = "macos")]
{
unsafe {
sd_desktop_macos::reload_webview(&(webview.inner() as _));
sd_desktop_macos::reload_webview(&webview.inner().cast());
}
}
#[cfg(target_os = "linux")]
{
use webkit2gtk::traits::WebViewExt;
use webkit2gtk::WebViewExt;
webview.inner().reload();
}
@ -81,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)]
@ -98,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(())
}
@ -136,14 +125,53 @@ async fn open_logs_dir(node: tauri::State<'_, Arc<Node>>) -> Result<(), ()> {
})
}
// TODO(@Oscar): A helper like this should probs exist in tauri-specta
macro_rules! tauri_handlers {
($($name:path),+) => {{
#[cfg(debug_assertions)]
tauri_specta::ts::export(specta::collect_types![$($name),+], "../src/commands.ts").unwrap();
#[tauri::command(async)]
#[specta::specta]
async fn open_trash_in_os_explorer() -> Result<(), ()> {
#[cfg(target_os = "macos")]
{
let full_path = format!("{}/.Trash/", std::env::var("HOME").unwrap());
tauri::generate_handler![$($name),+]
}};
Command::new("open")
.arg(full_path)
.spawn()
.map_err(|err| error!("Error opening trash: {err:#?}"))?
.wait()
.map_err(|err| error!("Error opening trash: {err:#?}"))?;
Ok(())
}
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.arg("shell:RecycleBinFolder")
.spawn()
.map_err(|err| error!("Error opening trash: {err:#?}"))?
.wait()
.map_err(|err| error!("Error opening trash: {err:#?}"))?;
return Ok(());
}
#[cfg(target_os = "linux")]
{
Command::new("xdg-open")
.arg("trash://")
.spawn()
.map_err(|err| error!("Error opening trash: {err:#?}"))?
.wait()
.map_err(|err| error!("Error opening trash: {err:#?}"))?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, specta::Type, tauri_specta::Event)]
#[serde(tag = "type")]
pub enum DragAndDropEvent {
Hovered { paths: Vec<String>, x: f64, y: f64 },
Dropped { paths: Vec<String>, x: f64, y: f64 },
Cancelled,
}
const CLIENT_ID: &str = "2abb241e-40b8-4517-a3e3-5594375c8fbb";
@ -153,169 +181,168 @@ 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");
let (invoke_handler, register_events) = {
let builder = ts::builder()
.events(collect_events![DragAndDropEvent])
.commands(tauri_specta::collect_commands![
app_ready,
reset_spacedrive,
open_logs_dir,
refresh_menu_bar,
reload_webview,
set_menu_bar_item_state,
request_fda_macos,
open_trash_in_os_explorer,
file::open_file_paths,
file::open_ephemeral_files,
file::get_file_path_open_with_apps,
file::get_ephemeral_files_open_with_apps,
file::open_file_path_with,
file::open_ephemeral_file_with,
file::reveal_items,
theme::lock_app_theme,
updater::check_for_update,
updater::install_update
])
.config(specta::ts::ExportConfig::default().formatter(specta::ts::formatter::prettier));
#[cfg(debug_assertions)]
let data_dir = data_dir.join("dev");
#[cfg(debug_assertions)]
let builder = builder.path("../src/commands.ts");
// 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 {
api_url: "https://app.spacedrive.com".to_string(),
client_id: CLIENT_ID.to_string(),
},
)
.await,
),
Err(err) => (None, Err(NodeError::Logger(err))),
builder.build().unwrap()
};
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) = if let Some((node, router)) = node_router {
(node, router)
} else {
panic!("Unable to get the node or router");
};
let app = app
.plugin(rspc::integrations::tauri::plugin(router, {
let node = node.clone();
move || node.clone()
}))
.plugin(sd_server_plugin(node.clone()).unwrap()) // TODO: Handle `unwrap`
.manage(node.clone());
// 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")]
let app = app.on_window_event(|event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
if event.window().label() == "main" {
AppHandle::hide(&event.window().app_handle()).expect("Window should hide on macOS");
api.prevent_close();
}
}
});
let app = app
.plugin(updater::plugin())
.plugin(tauri_plugin_window_state::Builder::default().build())
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);
app.windows().iter().for_each(|(_, window)| {
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");
let data_dir = app
.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 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::*;
handle.plugin(rspc::integrations::tauri::plugin(router, {
let node = node.clone();
move || node.clone()
}))?;
handle.plugin(sd_server_plugin(node.clone()).await.unwrap())?; // TODO: Handle `unwrap`
handle.manage(node.clone());
let nswindow = window.ns_window().unwrap();
unsafe { set_titlebar_style(&nswindow, true) };
unsafe { blur_window_background(&nswindow) };
let menu_handle = window.menu_handle();
tokio::spawn({
let libraries = node.libraries.clone();
let menu_handle = menu_handle.clone();
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!("cleaning localStorage");
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")]
{
unsafe {
sd_desktop_macos::set_titlebar_style(
&window.ns_window().expect("NSWindows must exist on macOS"),
false,
);
sd_desktop_macos::disable_app_nap(
&"File indexer needs to run unimpeded".into(),
);
};
}
});
}
});
// 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(|event| {
if let WindowEvent::Resized(_) = event.event() {
let (_state, command) = if event
.window()
.is_fullscreen()
.expect("Can't get fullscreen state")
{
(true, "window_fullscreened")
} else {
(false, "window_not_fullscreened")
};
.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
window
.app_handle()
.hide()
.expect("Window should hide on macOS");
api.prevent_close();
}
WindowEvent::Resized(_) => {
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())
.invoke_handler(tauri_handlers![
app_ready,
reset_spacedrive,
open_logs_dir,
refresh_menu_bar,
reload_webview,
set_menu_bar_item_state,
file::open_file_paths,
file::open_ephemeral_files,
file::get_file_path_open_with_apps,
file::get_ephemeral_files_open_with_apps,
file::open_file_path_with,
file::open_ephemeral_file_with,
file::reveal_items,
theme::lock_app_theme,
// TODO: move to plugin w/tauri-specta
updater::check_for_update,
updater::install_update
])
.build(tauri::generate_context!())?;
.build(tauri::generate_context!())?
.run(|_, _| {});
app.run(|_, _| {});
Ok(())
}

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(super) 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")
.disabled(),
)
.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+Alt+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(crate) 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" => {
let window = event.window();
pub fn handle_menu_event(event: MenuEvent, app: &AppHandle) {
let webview = app
.get_webview_window("main")
.expect("unable to find window");
#[cfg(debug_assertions)]
if window.is_devtools_open() {
window.close_devtools();
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 {
window.close().unwrap();
webview.open_devtools();
}
#[cfg(not(debug_assertions))]
window.close().unwrap();
}
"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(super) 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

@ -1,7 +1,8 @@
use std::{
io,
net::{Ipv4Addr, TcpListener},
net::Ipv4Addr,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use axum::{
@ -12,11 +13,14 @@ use axum::{
response::Response,
RequestPartsExt,
};
use http::Method;
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 tokio::task::block_in_place;
use thiserror::Error;
use tokio::{net::TcpListener, task::block_in_place};
use tracing::info;
/// Inject `window.__SD_ERROR__` so the frontend can render core startup errors.
@ -30,12 +34,24 @@ pub fn sd_error_plugin<R: Runtime>(err: NodeError) -> TauriPlugin<R> {
.build()
}
#[derive(Error, Debug)]
pub enum SdServerPluginError {
#[error("hyper error")]
HyperError(#[from] hyper::Error),
#[error("io error")]
IoError(#[from] std::io::Error),
}
/// Right now Tauri doesn't support async custom URI protocols so we ship an Axum server.
/// I began the upstream work on this: https://github.com/tauri-apps/wry/pull/872
/// Related to https://github.com/tauri-apps/tauri/issues/3725 & https://bugs.webkit.org/show_bug.cgi?id=146351#c5
///
/// The server is on a random port w/ a localhost bind address and requires a random on startup auth token which is injected into the webview so this *should* be secure enough.
pub fn sd_server_plugin<R: Runtime>(node: Arc<Node>) -> io::Result<TauriPlugin<R>> {
///
/// 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>,
) -> Result<TauriPlugin<R>, SdServerPluginError> {
let auth_token: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(15)
@ -49,20 +65,28 @@ pub fn sd_server_plugin<R: Runtime>(node: Arc<Node>) -> io::Result<TauriPlugin<R
))
.fallback(|| async { "404 Not Found: We're past the event horizon..." });
let port = std::env::var("SD_PORT")
.ok()
.and_then(|port| port.parse().ok())
.unwrap_or(0); // randomise port
// Only allow current device to access it
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, port))?;
let listen_addr = listener.local_addr()?;
let listenera = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let listen_addra = listenera.local_addr()?;
let listenerb = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let listen_addrb = listenerb.local_addr()?;
let listenerc = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let listen_addrc = listenerc.local_addr()?;
let listenerd = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let listen_addrd = listenerd.local_addr()?;
// let listen_addr = listener.local_addr()?; // We get it from a listener so `0` is turned into a random port
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
info!("Internal server listening on: http://{:?}", listen_addr);
info!("Internal server listening on: http://{listen_addra:?} http://{listen_addrb:?} http://{listen_addrc:?} http://{listen_addrd:?}");
let server = axum::Server::builder(CombinedIncoming {
a: AddrIncoming::from_listener(listenera)?,
b: AddrIncoming::from_listener(listenerb)?,
c: AddrIncoming::from_listener(listenerc)?,
d: AddrIncoming::from_listener(listenerd)?,
});
tokio::spawn(async move {
axum::Server::from_tcp(listener)
.expect("error creating HTTP server!")
server
.serve(app.into_make_service())
.with_graceful_shutdown(async {
rx.recv().await;
@ -71,10 +95,22 @@ pub fn sd_server_plugin<R: Runtime>(node: Arc<Node>) -> io::Result<TauriPlugin<R
.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(","),
);
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__ = "http://{listen_addr}";"#
))
.js_init_script(script.to_owned())
.on_page_load(move |webview, _payload| {
webview
.eval(&script)
.expect("Spacedrive server URL must be injected")
})
.on_event(move |_app, e| {
if let RunEvent::Exit { .. } = e {
block_in_place(|| {
@ -88,7 +124,7 @@ pub fn sd_server_plugin<R: Runtime>(node: Arc<Node>) -> io::Result<TauriPlugin<R
#[derive(Deserialize)]
struct QueryParams {
token: String,
token: Option<String>,
}
async fn auth_middleware<B>(
@ -100,16 +136,19 @@ async fn auth_middleware<B>(
where
B: Send,
{
let req = if query.token != auth_token {
let req = if query.token.as_ref() != Some(&auth_token) {
let (mut parts, body) = request.into_parts();
let auth: TypedHeader<Authorization<Bearer>> = parts
.extract()
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// We don't check auth for OPTIONS requests cause the CORS middleware will handle it
if parts.method != Method::OPTIONS {
let auth: TypedHeader<Authorization<Bearer>> = parts
.extract()
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
if auth.token() != auth_token {
return Err(StatusCode::UNAUTHORIZED);
if auth.token() != auth_token {
return Err(StatusCode::UNAUTHORIZED);
}
}
Request::from_parts(parts, body)
@ -119,3 +158,38 @@ where
Ok(next.run(req).await)
}
struct CombinedIncoming {
a: AddrIncoming,
b: AddrIncoming,
c: AddrIncoming,
d: AddrIncoming,
}
impl Accept for CombinedIncoming {
type Conn = <AddrIncoming as Accept>::Conn;
type Error = <AddrIncoming as Accept>::Error;
fn poll_accept(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Self::Conn, Self::Error>>> {
if let Poll::Ready(Some(value)) = Pin::new(&mut self.a).poll_accept(cx) {
return Poll::Ready(Some(value));
}
if let Poll::Ready(Some(value)) = Pin::new(&mut self.b).poll_accept(cx) {
return Poll::Ready(Some(value));
}
if let Poll::Ready(Some(value)) = Pin::new(&mut self.c).poll_accept(cx) {
return Poll::Ready(Some(value));
}
if let Poll::Ready(Some(value)) = Pin::new(&mut self.d).poll_accept(cx) {
return Poll::Ready(Some(value));
}
Poll::Pending
}
}

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(|b| b.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,24 +44,25 @@ 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()
.map(|update| UpdateEvent::UpdateAvailable { update })
.unwrap_or(UpdateEvent::NoUpdateAvailable),
.map_or(UpdateEvent::NoUpdateAvailable, |update| {
UpdateEvent::UpdateAvailable { update }
}),
)
.ok();
@ -80,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())?;
@ -97,17 +98,14 @@ pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("sd-updater")
.on_page_load(|window, _| {
#[cfg(target_os = "linux")]
let updater_available = {
let env = window.env();
let updater_available = false;
env.appimage.is_some()
};
#[cfg(not(target_os = "linux"))]
let updater_available = true;
if updater_available {
window
.eval("window.__SD_UPDATER__ = true")
.eval("window.__SD_UPDATER__ = true;")
.expect("Failed to inject updater JS");
}
})

View file

@ -1,90 +1,22 @@
{
"package": {
"productName": "Spacedrive"
},
"$schema": "https://github.com/tauri-apps/tauri/raw/tauri-v2.0.0-beta.17/core/tauri-config-schema/schema.json",
"productName": "Spacedrive",
"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": {
"appimage": {
"bundleMediaFramework": true
},
"active": true,
"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": "File explorer from the future.",
"longDescription": "Cross-platform universal file explorer, powered by an open-source virtual distributed filesystem.",
"deb": {
"depends": ["libc6"]
},
"macOS": {
"frameworks": ["../../.deps/Spacedrive.framework"],
"minimumSystemVersion": "10.15",
"exceptionDomain": "",
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"wix": {
"bannerPath": "icons/WindowsBanner.bmp",
"dialogImagePath": "icons/WindowsDialogImage.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",
"hiddenTitle": true,
"width": 1400,
"height": 725,
"height": 750,
"minWidth": 768,
"minHeight": 500,
"resizable": true,
@ -92,14 +24,70 @@
"alwaysOnTop": false,
"focus": false,
"visible": false,
"fileDropEnabled": true,
"dragDropEnabled": true,
"decorations": true,
"transparent": true,
"center": true
"center": true,
"windowEffects": {
"effects": ["sidebar"],
"state": "followsWindowActiveState",
"radius": 9
}
}
],
"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", "dbus"]
}
},
"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,26 +1,29 @@
import { createMemoryHistory } from '@remix-run/router';
import { QueryClientProvider } from '@tanstack/react-query';
import { listen } from '@tauri-apps/api/event';
import { appWindow } from '@tauri-apps/api/window';
import { useEffect, useMemo, useRef, useState } from 'react';
import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { RspcProvider } from '@sd/client';
import {
createRoutes,
ErrorPage,
KeybindEvent,
PlatformProvider,
routes,
SpacedriveInterface,
SpacedriveInterfaceRoot,
SpacedriveRouterProvider,
TabsContext
} from '@sd/interface';
import { RouteTitleContext } from '@sd/interface/hooks/useRouteTitle';
import { getSpacedropState } from '@sd/interface/hooks/useSpacedropState';
import '@sd/ui/style/style.scss';
import * as commands from './commands';
import { useLocale } from '@sd/interface/hooks';
import { commands } from './commands';
import { platform } from './platform';
import { queryClient } from './query';
import { createMemoryRouterWithHistory } from './router';
import { createUpdater } from './updater';
// TODO: Bring this back once upstream is fixed up.
// const client = hooks.createClient({
@ -45,32 +48,23 @@ export default function App() {
document.dispatchEvent(new KeybindEvent(input.payload as string));
});
const dropEventListener = appWindow.onFileDropEvent((event) => {
if (event.payload.type === 'drop') {
getSpacedropState().droppedFiles = event.payload.paths;
}
});
return () => {
keybindListener.then((unlisten) => unlisten());
dropEventListener.then((unlisten) => unlisten());
};
}, []);
return (
<RspcProvider queryClient={queryClient}>
<PlatformProvider platform={platform}>
<QueryClientProvider client={queryClient}>
{startupError ? (
<ErrorPage
message={startupError}
submessage="Error occurred starting up the Spacedrive core"
/>
) : (
<AppInner />
)}
</QueryClientProvider>
</PlatformProvider>
<QueryClientProvider client={queryClient}>
{startupError ? (
<ErrorPage
message={startupError}
submessage="Error occurred starting up the Spacedrive core"
/>
) : (
<AppInner />
)}
</QueryClientProvider>
</RspcProvider>
);
}
@ -78,14 +72,36 @@ export default function App() {
// we have a minimum delay between creating new tabs as react router can't handle creating tabs super fast
const TAB_CREATE_DELAY = 150;
const routes = createRoutes(platform);
type redirect = { pathname: string; search: string | undefined };
function AppInner() {
function createTab() {
const [tabs, setTabs] = useState(() => [createTab()]);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const selectedTab = tabs[selectedTabIndex]!;
function createTab(redirect?: redirect) {
const history = createMemoryHistory();
const router = createMemoryRouterWithHistory({ routes, history });
const id = Math.random().toString();
// for "Open in new tab"
if (redirect) {
router.navigate({
pathname: redirect.pathname,
search: redirect.search
});
}
const dispose = router.subscribe((event) => {
// we don't care about non-idle events as those are artifacts of form mutations + suspense
if (event.navigation.state !== 'idle') return;
setTabs((routers) => {
const index = routers.findIndex((r) => r.router === router);
const index = routers.findIndex((r) => r.id === id);
if (index === -1) return routers;
const routerAtIndex = routers[index]!;
@ -104,56 +120,70 @@ function AppInner() {
});
return {
id,
router,
history,
dispose,
element: document.createElement('div'),
currentIndex: 0,
maxIndex: 0,
title: 'New Tab'
};
}
const [tabs, setTabs] = useState(() => [createTab()]);
const [tabIndex, setTabIndex] = useState(0);
const tab = tabs[tabIndex]!;
const createTabPromise = useRef(Promise.resolve());
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current;
if (!div) return;
div.appendChild(selectedTab.element);
return () => {
while (div.firstChild) {
div.removeChild(div.firstChild);
}
};
}, [selectedTab.element]);
return (
<RouteTitleContext.Provider
value={useMemo(
() => ({
setTitle(title) {
setTabs((oldTabs) => {
const tabs = [...oldTabs];
const tab = tabs[tabIndex];
if (!tab) return tabs;
setTitle(id, title) {
setTabs((tabs) => {
const tabIndex = tabs.findIndex((t) => t.id === id);
if (tabIndex === -1) return tabs;
tabs[tabIndex] = { ...tab, title };
tabs[tabIndex] = { ...tabs[tabIndex]!, title };
return tabs;
return [...tabs];
});
}
}),
[tabIndex]
[]
)}
>
<TabsContext.Provider
value={{
tabIndex,
setTabIndex,
tabIndex: selectedTabIndex,
setTabIndex: setSelectedTabIndex,
tabs: tabs.map(({ router, title }) => ({ router, title })),
createTab() {
createTab(redirect?: redirect) {
createTabPromise.current = createTabPromise.current.then(
() =>
new Promise((res) => {
setTabs((tabs) => {
const newTabs = [...tabs, createTab()];
startTransition(() => {
setTabs((tabs) => {
const newTab = createTab(redirect);
const newTabs = [...tabs, newTab];
setTabIndex(newTabs.length - 1);
setSelectedTabIndex(newTabs.length - 1);
return newTabs;
return newTabs;
});
});
setTimeout(res, TAB_CREATE_DELAY);
@ -161,30 +191,63 @@ function AppInner() {
);
},
removeTab(index: number) {
setTabs((tabs) => {
const tab = tabs[index];
if (!tab) return tabs;
startTransition(() => {
setTabs((tabs) => {
const tab = tabs[index];
if (!tab) return tabs;
tab.dispose();
tab.dispose();
tabs.splice(index, 1);
tabs.splice(index, 1);
setTabIndex(tabs.length - 1);
setSelectedTabIndex(Math.min(selectedTabIndex, tabs.length - 1));
return [...tabs];
return [...tabs];
});
});
}
}}
>
<SpacedriveInterface
routing={{
router: tab.router,
routerKey: tabIndex,
currentIndex: tab.currentIndex,
maxIndex: tab.maxIndex
}}
/>
<PlatformUpdaterProvider>
<SpacedriveInterfaceRoot>
{tabs.map((tab, index) =>
createPortal(
<SpacedriveRouterProvider
key={tab.id}
routing={{
routes,
visible: selectedTabIndex === tabs.indexOf(tab),
router: tab.router,
currentIndex: tab.currentIndex,
tabId: tab.id,
maxIndex: tab.maxIndex
}}
/>,
tab.element
)
)}
<div ref={ref} />
</SpacedriveInterfaceRoot>
</PlatformUpdaterProvider>
</TabsContext.Provider>
</RouteTitleContext.Provider>
);
}
function PlatformUpdaterProvider(props: PropsWithChildren) {
const { t } = useLocale();
return (
<PlatformProvider
platform={useMemo(
() => ({
...platform,
updater: window.__SD_UPDATER__ ? createUpdater(t) : undefined
}),
[t]
)}
>
{props.children}
</PlatformProvider>
);
}

View file

@ -1,82 +1,232 @@
/* eslint-disable */
/** tauri-specta globals **/
import { invoke as TAURI_INVOKE } from '@tauri-apps/api/core';
import * as TAURI_API_EVENT from '@tauri-apps/api/event';
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.
declare global {
interface Window {
__TAURI_INVOKE__<T>(cmd: string, args?: Record<string, unknown>): Promise<T>;
}
export const commands = {
async appReady(): Promise<void> {
await TAURI_INVOKE('app_ready');
},
async resetSpacedrive(): Promise<void> {
await TAURI_INVOKE('reset_spacedrive');
},
async openLogsDir(): Promise<Result<null, null>> {
try {
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>> {
try {
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<void> {
await TAURI_INVOKE('reload_webview');
},
async setMenuBarItemState(event: MenuEvent, enabled: boolean): Promise<void> {
await TAURI_INVOKE('set_menu_bar_item_state', { event, enabled });
},
async requestFdaMacos(): Promise<void> {
await TAURI_INVOKE('request_fda_macos');
},
async openTrashInOsExplorer(): Promise<Result<null, null>> {
try {
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 };
}
},
async openFilePaths(
library: string,
ids: number[]
): Promise<Result<OpenFilePathResult[], null>> {
try {
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<EphemeralFileOpenResult[], null>> {
try {
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 };
}
},
async getFilePathOpenWithApps(
library: string,
ids: number[]
): Promise<Result<OpenWithApplication[], null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('get_file_path_open_with_apps', { library, ids })
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async getEphemeralFilesOpenWithApps(
paths: string[]
): Promise<Result<OpenWithApplication[], null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('get_ephemeral_files_open_with_apps', { paths })
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async openFilePathWith(
library: string,
fileIdsAndUrls: [number, string][]
): Promise<Result<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('open_file_path_with', { library, fileIdsAndUrls })
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async openEphemeralFileWith(pathsAndUrls: [string, string][]): Promise<Result<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('open_ephemeral_file_with', { pathsAndUrls })
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: e as any };
}
},
async revealItems(library: string, items: RevealItem[]): Promise<Result<null, null>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('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<void> {
await TAURI_INVOKE('lock_app_theme', { themeType });
},
async checkForUpdate(): Promise<Result<Update | null, string>> {
try {
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>> {
try {
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 };
}
}
};
export const events = __makeEvents__<{
dragAndDropEvent: DragAndDropEvent;
}>({
dragAndDropEvent: 'drag-and-drop-event'
});
/** user-defined types **/
export type AppThemeType = 'Auto' | 'Light' | 'Dark';
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>>;
once: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: T extends null
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
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: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg)
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case 'listen':
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case 'once':
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case 'emit':
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
}
});
}
}
);
}
// Function avoids 'window not defined' in SSR
const invoke = () => window.__TAURI_INVOKE__;
export function appReady() {
return invoke()<null>("app_ready")
}
export function resetSpacedrive() {
return invoke()<null>("reset_spacedrive")
}
export function openLogsDir() {
return invoke()<null>("open_logs_dir")
}
export function refreshMenuBar() {
return invoke()<null>("refresh_menu_bar")
}
export function reloadWebview() {
return invoke()<null>("reload_webview")
}
export function setMenuBarItemState(id: string, enabled: boolean) {
return invoke()<null>("set_menu_bar_item_state", { id,enabled })
}
export function openFilePaths(library: string, ids: number[]) {
return invoke()<OpenFilePathResult[]>("open_file_paths", { library,ids })
}
export function openEphemeralFiles(paths: string[]) {
return invoke()<EphemeralFileOpenResult[]>("open_ephemeral_files", { paths })
}
export function getFilePathOpenWithApps(library: string, ids: number[]) {
return invoke()<OpenWithApplication[]>("get_file_path_open_with_apps", { library,ids })
}
export function getEphemeralFilesOpenWithApps(paths: string[]) {
return invoke()<OpenWithApplication[]>("get_ephemeral_files_open_with_apps", { paths })
}
export function openFilePathWith(library: string, fileIdsAndUrls: ([number, string])[]) {
return invoke()<null>("open_file_path_with", { library,fileIdsAndUrls })
}
export function openEphemeralFileWith(pathsAndUrls: ([string, string])[]) {
return invoke()<null>("open_ephemeral_file_with", { pathsAndUrls })
}
export function revealItems(library: string, items: RevealItem[]) {
return invoke()<null>("reveal_items", { library,items })
}
export function lockAppTheme(themeType: AppThemeType) {
return invoke()<null>("lock_app_theme", { themeType })
}
export function checkForUpdate() {
return invoke()<Update | null>("check_for_update")
}
export function installUpdate() {
return invoke()<null>("install_update")
}
export type Update = { version: string; body: string | null }
export type OpenWithApplication = { url: string; name: string }
export type AppThemeType = "Auto" | "Light" | "Dark"
export type EphemeralFileOpenResult = { t: "Ok"; c: string } | { t: "Err"; c: string }
export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string }
export type RevealItem = { Location: { id: number } } | { FilePath: { id: number } } | { Ephemeral: { path: string } }

View file

@ -6,5 +6,7 @@ export const env = createEnv({
client: {
VITE_LANDING_ORIGIN: z.string().default('https://www.spacedrive.com')
},
runtimeEnv: import.meta.env
runtimeEnv: import.meta.env,
skipValidation: false,
emptyStringAsUndefined: true
});

View file

@ -1,4 +1,4 @@
import { tauriLink } from '@rspc/tauri/v2';
import { tauriLink } from '@oscartbeaumont-sd/rspc-tauri/v2';
globalThis.isDev = import.meta.env.DEV;
globalThis.rspcLinks = [
@ -8,3 +8,6 @@ globalThis.rspcLinks = [
// }),
tauriLink()
];
globalThis.onHotReload = (func: () => void) => {
if (import.meta.hot) import.meta.hot.dispose(func);
};

View file

@ -1,65 +1,95 @@
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';
import * as commands from './commands';
import { commands, events } from './commands';
import { env } from './env';
import { createUpdater } from './updater';
const customUriAuthToken = (window as any).__SD_CUSTOM_SERVER_AUTH_TOKEN__ as string | undefined;
let customUriServerUrl = (window as any).__SD_CUSTOM_URI_SERVER__ as string | undefined;
if (customUriServerUrl === undefined || customUriServerUrl === '')
console.warn("'window.__SD_CUSTOM_URI_SERVER__' may have not been injected correctly!");
if (customUriServerUrl && !customUriServerUrl?.endsWith('/')) {
customUriServerUrl += '/';
}
const customUriServerUrl = (window as any).__SD_CUSTOM_URI_SERVER__ as string[] | undefined;
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';
}
}
let hr: typeof ConsistentHash | undefined;
function constructServerUrl(urlSuffix: string) {
if (!hr) {
if (!customUriServerUrl)
throw new Error("'window.__SD_CUSTOM_URI_SERVER__' was not injected correctly!");
hr = new ConsistentHash();
customUriServerUrl.forEach((url) => hr.add(url));
}
// Randomly switch between servers to avoid HTTP connection limits
return hr.get(urlSuffix) + urlSuffix + queryParams;
}
export const platform = {
platform: 'tauri',
getThumbnailUrlByThumbKey: (keyParts) =>
`${customUriServerUrl}thumbnail/${keyParts
.map((i) => encodeURIComponent(i))
.join('/')}.webp${queryParams}`,
getThumbnailUrlByThumbKey: (thumbKey) =>
constructServerUrl(
`/thumbnail/${encodeURIComponent(
thumbKey.base_directory_str
)}/${encodeURIComponent(thumbKey.shard_hex)}/${encodeURIComponent(thumbKey.cas_id)}.webp`
),
getFileUrl: (libraryId, locationLocalId, filePathId) =>
`${customUriServerUrl}file/${libraryId}/${locationLocalId}/${filePathId}${queryParams}`,
constructServerUrl(`/file/${libraryId}/${locationLocalId}/${filePathId}`),
getFileUrlByPath: (path) =>
`${customUriServerUrl}local-file-by-path/${encodeURIComponent(path)}${queryParams}`,
openLink: shell.open,
constructServerUrl(`/local-file-by-path/${encodeURIComponent(path)}`),
getRemoteRspcEndpoint: (remote_identity) => ({
url: `${customUriServerUrl?.[0]
?.replace('https', 'wss')
?.replace('http', 'ws')}/remote/${encodeURIComponent(
remote_identity
)}/rspc/ws?token=${customUriAuthToken}`
}),
constructRemoteRspcPath: (remote_identity, path) =>
constructServerUrl(
`/remote/${encodeURIComponent(remote_identity)}/uri/${path}?token=${customUriAuthToken}`
),
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) =>
events.dragAndDropEvent.listen((e) => {
cb(e.payload);
}),
userHomeDir: homeDir,
updater: window.__SD_UPDATER__ ? createUpdater() : undefined,
auth: {
start(url) {
open(url);
return shellOpen(url);
}
},
...commands,
landingApiOrigin: env.VITE_LANDING_ORIGIN
} satisfies Platform;
} satisfies Omit<Platform, 'updater'>;

View file

@ -1,9 +1,10 @@
import { listen } from '@tauri-apps/api/event';
import { proxy, useSnapshot } from 'valtio';
import { UpdateStore } from '@sd/interface';
import { useLocale } from '@sd/interface/hooks';
import { toast, ToastId } from '@sd/ui';
import * as commands from './commands';
import { commands } from './commands';
declare global {
interface Window {
@ -12,7 +13,7 @@ declare global {
}
}
export function createUpdater() {
export function createUpdater(t: ReturnType<typeof useLocale>['t']) {
if (!window.__SD_UPDATER__) return;
const updateStore = proxy<UpdateStore>({
@ -21,15 +22,20 @@ export function createUpdater() {
listen<UpdateStore>('updater', (e) => {
Object.assign(updateStore, e.payload);
console.log(updateStore);
});
const onInstallCallbacks = new Set<() => void>();
async function checkForUpdate() {
const update = await commands.checkForUpdate();
const result = await commands.checkForUpdate();
if (!update) return null;
if (result.status === 'error') {
console.error('UPDATER ERROR', result.error);
// TODO: Show some UI?
return null;
}
if (!result.data) return null;
const update = result.data;
let id: ToastId | null = null;
@ -41,11 +47,13 @@ export function createUpdater() {
toast.info(
(_id) => {
const { t } = useLocale();
id = _id;
return {
title: 'New Update Available',
body: `Version ${update.version}`
title: t('new_update_available'),
body: t('version', { version: update.version })
};
},
{
@ -54,7 +62,7 @@ export function createUpdater() {
},
duration: 10 * 1000,
action: {
label: 'Update',
label: t('update'),
onClick: installUpdate
}
}
@ -71,11 +79,11 @@ export function createUpdater() {
const promise = commands.installUpdate();
toast.promise(promise, {
loading: 'Downloading Update',
success: 'Update Downloaded. Restart Spacedrive to install',
loading: t('downloading_update'),
success: t('update_downloaded'),
error: (e: any) => (
<>
<p>Failed to download update</p>
<p>{t('failed_to_download_update')}</p>
<p className="text-gray-300">Error: {e.toString()}</p>
</>
)
@ -88,23 +96,32 @@ export function createUpdater() {
async function runJustUpdatedCheck(onViewChangelog: () => void) {
const version = window.__SD_DESKTOP_VERSION__;
const lastVersion = localStorage.getItem(SD_VERSION_LOCALSTORAGE);
if (!lastVersion) return;
if (lastVersion !== version) {
localStorage.setItem(SD_VERSION_LOCALSTORAGE, version);
let tagline = null;
const { frontmatter } = await fetch(
`${import.meta.env.VITE_LANDING_ORIGIN}/api/releases/${version}`
).then((r) => r.json());
try {
const request = await fetch(
`${import.meta.env.VITE_LANDING_ORIGIN}/api/releases/${version}`
);
const { frontmatter } = await request.json();
tagline = frontmatter?.tagline;
} catch (error) {
console.warn('Failed to fetch release info');
console.error(error);
}
toast.success(
{
title: `Updated successfully, you're on version ${version}`,
body: frontmatter?.tagline
title: t('updated_successfully', { version }),
body: tagline
},
{
duration: 10 * 1000,
action: {
label: 'View Changes',
label: t('view_changes'),
onClick: onViewChangelog
}
}

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 &&

View file

@ -98,7 +98,7 @@ export const Document = defineDocumentType(() => ({
.replace(/^.+?(\/)/, '')
.split('/')
.slice(-1)[0]
)
)
},
section: {
type: 'string',

View file

@ -10,31 +10,32 @@
"typecheck": "contentlayer build && tsc -b"
},
"dependencies": {
"@docsearch/react": "3",
"@docsearch/react": "^3.5.2",
"@octokit/webhooks": "^12.0.3",
"@phosphor-icons/react": "^2.0.13",
"@phosphor-icons/react": "^2.0.14",
"@radix-ui/react-dialog": "^1.0.5",
"@react-three/drei": "^9.88.13",
"@react-three/fiber": "^8.15.10",
"@react-three/fiber": "^8.15.11",
"@sd/assets": "workspace:*",
"@sd/ui": "workspace:*",
"@t3-oss/env-nextjs": "^0.7.1",
"@tsparticles/react": "^3.0.0",
"clsx": "^2.0.0",
"contentlayer": "^0.3.4",
"dayjs": "^1.11.10",
"framer-motion": "^10.16.4",
"framer-motion": "^10.16.5",
"image-size": "^1.0.2",
"katex": "^0.16.9",
"markdown-to-jsx": "^7.3.2",
"next": "13.5.6",
"next-contentlayer": "^0.3.4",
"next-plausible": "^3.11.2",
"next-plausible": "^3.11.3",
"react": "18.2.0",
"react-burger-menu": "^3.0.9",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-github-btn": "^1.4.0",
"react-tsparticles": "^2.12.2",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-external-links": "^2.1.0",
@ -45,25 +46,25 @@
"remark-math": "^5.1.1",
"remark-mdx-images": "^2.0.0",
"tailwind-merge": "^1.14.0",
"three": "^0.158.0",
"tsparticles": "^2.12.0",
"three": "^0.161.0",
"tsparticles": "^3.3.0",
"unist-util-visit": "^5.0.0",
"zod": "~3.22.4"
},
"devDependencies": {
"@next/bundle-analyzer": "^13.5.6",
"@octokit/openapi-types": "^19.0.2",
"@octokit/openapi-types": "^20.0.0",
"@sd/config": "workspace:*",
"@svgr/webpack": "^8.1.0",
"@types/node": "~18.17.19",
"@types/react": "^18.2.34",
"@types/react-burger-menu": "^2.8.5",
"@types/react-dom": "^18.2.14",
"@types/three": "^0.158.2",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"sharp": "^0.32.6",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2"
"@types/node": ">18.18.x",
"@types/react": "^18.2.67",
"@types/react-burger-menu": "^2.8.7",
"@types/react-dom": "^18.2.22",
"@types/three": "^0.162.0",
"autoprefixer": "^10.4.18",
"postcss": "^8.4.36",
"sharp": "^0.33.2",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View file

@ -0,0 +1,68 @@
---
author: Jamie Pine
title: Introducing Spacedrive Alpha v0.2 - Drag & drop, AI Labels, Advanced Search, 11 Languages, Tabs, and Spacedrop!
tags: [News, Updates, Release]
date: 2024-02-15
image: images/spacedrive-feb-2024-release.png
imageAlt: Spacedrive Feb 2024 Release Image
---
It's been an exciting couple of months since we launched Spacedrive. We're thrilled to share that we've hit **149,000** unique installations, with an average runtime of **54 minutes**. The enthusiasm and feedback from our community have been incredible, and your requests for new features have not gone unnoticed.
Our team has been working tirelessly to turn your most-wanted features into reality. Let's take a look at what's new in v0.2...
## 🫳 Drag and drop
<Video url="/videos/Spacedrive_DragAndDrop.webm" />
The Explorer is the most intricate user interface we have ever designed. Now, with drag and drop, it is one big step closer to having everything you need from a file explorer. In an upcoming release, we'll be adding support for dragging files and folders into and out of Spacedrive from the OS.
## ✨ AI Labeling
<Video url="/videos/Spacedrive_ImageLabler.webm" />
Our first of many AI powered features to come, we create labels for images using a lightweight object detection model running on-device. Currenly the Yolo model family offers limited labels and are not very accurate, but we will make more models available in following updates, such as the [Recognize Anything Model](https://github.com/xinyu1205/recognize-anything).
## Advanced search
We've added a new search bar to the top of the Explorer, which allows you to search for files and folders by name, content, and metadata. You can also use the search bar to filter by file type, date, and more.
## Tabs
<Video url="/videos/Spacedrive_Tabs.webm" />
You can open multiple tabs in the Explorer, and switch between them using the tab bar at the top of the window.
## Now in 11 languages!
We've expanded our language options to include English, Chinese (Simplified and Traditional), German, French, Dutch, Italian, Russian, Spanish, and Turkish. Some translations were created with AI assistance, so feel free to submit corrections on [GitHub](https://github.com/spacedriveapp/spacedrive).
## Updated roadmap
![Spacedrive Roadmap](/images/roadmap.jpg)
We've updated our roadmap to reflect the progress we've made since launch, and to give you a better idea of what's coming next. From today, we are aiming for a monthly major release cycle, with minor releases in between. Check out the [Roadmap →](/roadmap)
## And so much more...
We've also shipped these major features since the launch:
- Multi-select in the Explorer
- A bar for Quick View to view surrounding media
- CPU usage control slider
- Improved virtual filesystem
- Way. More. Docs. [See Docs →](/docs/product/getting-started/introduction)
## Up Next?
We've got big plans for the next major release:
- **Connect devices**:
One of the biggest technologies we've been tackling is multi-device connections and database sync. This has been a cornerstone of Spacedrive from the start, but yet to make it into production. So, we're speeding up the process by introducing an encrypted, cloud-assisted sync instead of peer-to-peer sync using CRDTs, which we will ship later.
- **iOS and Android**:
![Spacedrive Mobile App Design Preview](/images/mobile_design_preview.jpg)
With sync in place, we'll be releasing the alpha versions of our mobile apps for iOS and Android, if you are already a user of Spacedrive you're invited to join, we'll reach out via email with an invite.
- **Column View**:
A hugely demanded explorer view, for good reason. Column view is the most popular view for managing files, but also the hardest to engineer! Along with column view will come upgraded list view, with tree support.
This blog post marks the first of regular updates from the Spacedrive team. We'll be sharing our progress, new features, and more once a month. If you have any questions or feedback, please reach out to us on [Twitter](https://twitter.com/spacedriveapp) or [Discord](https://discord.gg/3YjJUZ6).

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.

After

Width:  |  Height:  |  Size: 114 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.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 278 KiB

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