Merge remote-tracking branch 'origin' into eng-1188-image-conversion-ui
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -1,2 +1,4 @@
|
|||
pnpm-lock.yaml -diff
|
||||
package-lock.json -diff
|
||||
Cargo.lock -diff
|
||||
.github/actions/publish-artifacts/dist/index.js -diff
|
||||
|
|
8
.github/actions/publish-artifacts/.eslintrc.cjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
extends: [require.resolve('@sd/config/eslint/base.js')],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
},
|
||||
ignorePatterns: ['dist/**/*']
|
||||
};
|
96
.github/actions/publish-artifacts/dist/index.js
vendored
59
.github/actions/publish-artifacts/index.ts
vendored
|
@ -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();
|
||||
|
|
11
.github/actions/publish-artifacts/package.json
vendored
|
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
6
.github/actions/setup-pnpm/action.yml
vendored
|
@ -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
|
||||
|
|
38
.github/actions/setup-rust/action.yaml
vendored
|
@ -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
|
||||
|
|
31
.github/actions/setup-system/action.yml
vendored
|
@ -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
|
||||
|
|
47
.github/workflows/cache-factory.yaml
vendored
|
@ -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 }}
|
||||
|
|
175
.github/workflows/ci.yml
vendored
|
@ -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 }}
|
||||
|
|
82
.github/workflows/mobile-ci.yml
vendored
|
@ -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 }}
|
||||
|
|
54
.github/workflows/release.yml
vendored
|
@ -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: '*/**'
|
||||
|
|
2
.github/workflows/search-index.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Trigger Algolia Crawler
|
||||
run: |
|
||||
|
|
55
.github/workflows/server.yml
vendored
|
@ -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
|
@ -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
|
@ -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,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
|
||||
|
|
17
.vscode/extensions.json
vendored
|
@ -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
|
@ -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
|
@ -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
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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
160
Cargo.toml
|
@ -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
|
||||
|
|
19
README.md
|
@ -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).
|
||||
|
|
|
@ -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"] }
|
|
@ -1,4 +0,0 @@
|
|||
# CLI
|
||||
|
||||
Basic CLI for interacting with encrypted files.
|
||||
Will be expanded to a general Spacedrive CLI in the future.
|
|
@ -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)
|
||||
};
|
||||
});
|
||||
}
|
19
apps/deps-generator/Cargo.toml
Normal 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"
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 70 KiB |
|
@ -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"] }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
// }
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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",
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
1
apps/desktop/src-tauri/.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
gen/
|
||||
WixTools
|
||||
*.dll
|
||||
*.dll.*
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
30
apps/desktop/src-tauri/capabilities/default.json
Normal 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"
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 70 KiB |
|
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,8 +181,50 @@ 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("./"))
|
||||
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 builder = builder.path("../src/commands.ts");
|
||||
|
||||
builder.build().unwrap()
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(invoke_handler)
|
||||
.setup(move |app| {
|
||||
// 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);
|
||||
|
||||
let data_dir = app
|
||||
.path()
|
||||
.data_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("./"))
|
||||
.join("spacedrive");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -164,61 +234,38 @@ async fn main() -> tauri::Result<()> {
|
|||
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,
|
||||
Node::new(data_dir, sd_core::Env::new(CLIENT_ID)).await,
|
||||
),
|
||||
Err(err) => (None, Err(NodeError::Logger(err))),
|
||||
};
|
||||
|
||||
let app = tauri::Builder::default();
|
||||
|
||||
let (node_router, app) = match result {
|
||||
Ok((node, router)) => (Some((node, router)), app),
|
||||
let handle = app.handle();
|
||||
let (node, router) = match result {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
error!("Error starting up the node: {err:#?}");
|
||||
(None, app.plugin(sd_error_plugin(err)))
|
||||
handle.plugin(sd_error_plugin(err))?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let (node, router) = if let Some((node, router)) = node_router {
|
||||
(node, router)
|
||||
} else {
|
||||
panic!("Unable to get the node or router");
|
||||
};
|
||||
let should_clear_localstorage = node.libraries.get_all().await.is_empty();
|
||||
|
||||
let app = app
|
||||
.plugin(rspc::integrations::tauri::plugin(router, {
|
||||
handle.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());
|
||||
}))?;
|
||||
handle.plugin(sd_server_plugin(node.clone()).await.unwrap())?; // TODO: Handle `unwrap`
|
||||
handle.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();
|
||||
handle.windows().iter().for_each(|(_, window)| {
|
||||
if should_clear_localstorage {
|
||||
println!("cleaning localStorage");
|
||||
for webview in window.webviews() {
|
||||
webview.eval("localStorage.clear();").ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let app = app
|
||||
.plugin(updater::plugin())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.setup(move |app| {
|
||||
let app = app.handle();
|
||||
|
||||
app.windows().iter().for_each(|(_, window)| {
|
||||
tokio::spawn({
|
||||
let window = window.clone();
|
||||
async move {
|
||||
|
@ -234,88 +281,68 @@ async fn main() -> tauri::Result<()> {
|
|||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
window.set_decorations(true).unwrap();
|
||||
window.set_decorations(false).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use sd_desktop_macos::*;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Configure IPC for custom protocol
|
||||
app.ipc_scope().configure_remote_access(
|
||||
RemoteDomainAccessScope::new("localhost")
|
||||
.allow_on_scheme("spacedrive")
|
||||
.add_window("main"),
|
||||
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(),
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
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")
|
||||
{
|
||||
})
|
||||
})
|
||||
.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(())
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Menu::new(app)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::menu::{AboutMetadataBuilder, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
|
||||
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(),
|
||||
))
|
||||
.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);
|
||||
|
||||
let file_menu = Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_file", "New File")
|
||||
.accelerator("CmdOrCtrl+N")
|
||||
.disabled(), // TODO(brxken128): add keybind handling here
|
||||
.separator()
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::NewLibrary, "New Library")
|
||||
.accelerator("Cmd+Shift+T")
|
||||
.build(app)?,
|
||||
)
|
||||
.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;
|
||||
|
||||
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 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"),
|
||||
// .item(
|
||||
// &SubmenuBuilder::new(app, "Libraries")
|
||||
// // TODO: Implement this
|
||||
// .items(&[])
|
||||
// .build()?,
|
||||
// )
|
||||
.separator()
|
||||
.hide()
|
||||
.hide_others()
|
||||
.show_all()
|
||||
.separator()
|
||||
.quit()
|
||||
.build()?;
|
||||
|
||||
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()?;
|
||||
|
||||
let edit_menu = SubmenuBuilder::new(app, "Edit")
|
||||
.copy()
|
||||
.cut()
|
||||
.paste()
|
||||
.redo()
|
||||
.undo()
|
||||
.select_all()
|
||||
.build()?;
|
||||
|
||||
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.add_native_item(MenuItem::Separator).add_item(
|
||||
CustomMenuItem::new("toggle_devtools", "Toggle Developer Tools")
|
||||
.accelerator("CmdOrCtrl+Shift+Alt+I"),
|
||||
let view_menu = view_menu.separator().item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::ToggleDeveloperTools, "Toggle Developer Tools")
|
||||
.accelerator("CmdOrCtrl+Shift+Alt+I")
|
||||
.build(app)?,
|
||||
);
|
||||
|
||||
let window_menu = Menu::new()
|
||||
.add_native_item(MenuItem::Minimize)
|
||||
.add_native_item(MenuItem::Zoom)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_window", "New Window")
|
||||
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")
|
||||
.disabled(),
|
||||
.build(app)?,
|
||||
)
|
||||
.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"),
|
||||
);
|
||||
.close_window()
|
||||
.fullscreen()
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::ReloadWebview, "Reload Webview")
|
||||
.accelerator("CmdOrCtrl+Shift+R")
|
||||
.build(app)?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
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 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:#?}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,9 +136,11 @@ 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();
|
||||
|
||||
// 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
|
||||
|
@ -111,6 +149,7 @@ where
|
|||
if auth.token() != auth_token {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
Request::from_parts(parts, body)
|
||||
} else {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,90 +1,22 @@
|
|||
{
|
||||
"package": {
|
||||
"productName": "Spacedrive"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://localhost:8001",
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..."
|
||||
},
|
||||
"tauri": {
|
||||
"macOSPrivateApi": true,
|
||||
"bundle": {
|
||||
"appimage": {
|
||||
"bundleMediaFramework": true
|
||||
},
|
||||
"active": true,
|
||||
"targets": ["deb", "msi", "dmg", "updater"],
|
||||
"$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",
|
||||
"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
|
||||
}
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:8001",
|
||||
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop...",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"macOSPrivateApi": 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}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,21 +48,13 @@ 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
|
||||
|
@ -70,7 +65,6 @@ export default function App() {
|
|||
<AppInner />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</PlatformProvider>
|
||||
</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,63 +120,78 @@ 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) => {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const newTabs = [...tabs, createTab()];
|
||||
const newTab = createTab(redirect);
|
||||
const newTabs = [...tabs, newTab];
|
||||
|
||||
setTabIndex(newTabs.length - 1);
|
||||
setSelectedTabIndex(newTabs.length - 1);
|
||||
|
||||
return newTabs;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(res, TAB_CREATE_DELAY);
|
||||
})
|
||||
);
|
||||
},
|
||||
removeTab(index: number) {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const tab = tabs[index];
|
||||
if (!tab) return tabs;
|
||||
|
@ -169,22 +200,54 @@ function AppInner() {
|
|||
|
||||
tabs.splice(index, 1);
|
||||
|
||||
setTabIndex(tabs.length - 1);
|
||||
setSelectedTabIndex(Math.min(selectedTabIndex, tabs.length - 1));
|
||||
|
||||
return [...tabs];
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SpacedriveInterface
|
||||
<PlatformUpdaterProvider>
|
||||
<SpacedriveInterfaceRoot>
|
||||
{tabs.map((tab, index) =>
|
||||
createPortal(
|
||||
<SpacedriveRouterProvider
|
||||
key={tab.id}
|
||||
routing={{
|
||||
routes,
|
||||
visible: selectedTabIndex === tabs.indexOf(tab),
|
||||
router: tab.router,
|
||||
routerKey: tabIndex,
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 } }
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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'>;
|
||||
|
|
|
@ -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(
|
||||
try {
|
||||
const request = await fetch(
|
||||
`${import.meta.env.VITE_LANDING_ORIGIN}/api/releases/${version}`
|
||||
).then((r) => r.json());
|
||||
);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
68
apps/landing/posts/alpha-zero-two-release.mdx
Normal 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).
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 817 B After Width: | Height: | Size: 741 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
apps/landing/public/images/agent.webp
Normal file
After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 524 KiB After Width: | Height: | Size: 360 KiB |
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 278 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
BIN
apps/landing/public/images/mobile_design_preview.jpg
Normal file
After Width: | Height: | Size: 893 KiB |
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 278 KiB |
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 1.4 MiB |