Merge remote-tracking branch 'origin' into c

This commit is contained in:
ameer2468 2024-06-26 13:24:34 +03:00
parent f97303ff54
commit 0cc83a03db
1000 changed files with 61497 additions and 39766 deletions

View file

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

2
.gitattributes vendored
View file

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

File diff suppressed because one or more lines are too long

View file

@ -2,12 +2,13 @@ import client from '@actions/artifact';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as glob from '@actions/glob'; import * as glob from '@actions/glob';
import * as io from '@actions/io'; import * as io from '@actions/io';
import { exists } from '@actions/io/lib/io-util';
type OS = 'darwin' | 'windows' | 'linux'; type OS = 'darwin' | 'windows' | 'linux';
type Arch = 'x64' | 'arm64'; type Arch = 'x64' | 'arm64';
type TargetConfig = { bundle: string; ext: string }; type TargetConfig = { bundle: string; ext: string };
type BuildTarget = { type BuildTarget = {
updater: { bundle: string; bundleExt: string; archiveExt: string }; updater: false | { bundle: string; bundleExt: string; archiveExt: string };
standalone: Array<TargetConfig>; standalone: Array<TargetConfig>;
}; };
@ -29,15 +30,8 @@ const OS_TARGETS = {
standalone: [{ ext: 'msi', bundle: 'msi' }] standalone: [{ ext: 'msi', bundle: 'msi' }]
}, },
linux: { linux: {
updater: { updater: false,
bundle: 'appimage', standalone: [{ ext: 'deb', bundle: 'deb' }]
bundleExt: 'AppImage',
archiveExt: 'tar.gz'
},
standalone: [
{ ext: 'deb', bundle: 'deb' },
{ ext: 'AppImage', bundle: 'appimage' }
]
} }
} satisfies Record<OS, BuildTarget>; } satisfies Record<OS, BuildTarget>;
@ -50,19 +44,32 @@ const PROFILE = core.getInput('profile');
const BUNDLE_DIR = `target/${TARGET}/${PROFILE}/bundle`; const BUNDLE_DIR = `target/${TARGET}/${PROFILE}/bundle`;
const ARTIFACTS_DIR = '.artifacts'; const ARTIFACTS_DIR = '.artifacts';
const ARTIFACT_BASE = `Spacedrive-${OS}-${ARCH}`; const ARTIFACT_BASE = `Spacedrive-${OS}-${ARCH}`;
const FRONT_END_BUNDLE = 'apps/desktop/dist.tar.xz';
const UPDATER_ARTIFACT_NAME = `Spacedrive-Updater-${OS}-${ARCH}`; const UPDATER_ARTIFACT_NAME = `Spacedrive-Updater-${OS}-${ARCH}`;
const FRONTEND_ARCHIVE_NAME = `Spacedrive-frontend-${OS}-${ARCH}`;
async function globFiles(pattern: string) { async function globFiles(pattern: string) {
const globber = await glob.create(pattern); const globber = await glob.create(pattern);
return await globber.glob(); return await globber.glob();
} }
async function uploadUpdater({ bundle, bundleExt, archiveExt }: BuildTarget['updater']) { async function uploadFrontend() {
if (!(await exists(FRONT_END_BUNDLE))) {
console.error(`Frontend archive not found`);
return;
}
await client.uploadArtifact(FRONTEND_ARCHIVE_NAME, [FRONT_END_BUNDLE], 'apps/desktop');
}
async function uploadUpdater(updater: BuildTarget['updater']) {
if (!updater) return;
const { bundle, bundleExt, archiveExt } = updater;
const fullExt = `${bundleExt}.${archiveExt}`; const fullExt = `${bundleExt}.${archiveExt}`;
const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${fullExt}*`); const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${fullExt}*`);
const updaterPath = files.find((file) => file.endsWith(fullExt)); const updaterPath = files.find((file) => file.endsWith(fullExt));
if (!updaterPath) return console.error(`Updater path not found. Files: ${files}`); if (!updaterPath) throw new Error(`Updater path not found. Files: ${files}`);
const artifactPath = `${ARTIFACTS_DIR}/${UPDATER_ARTIFACT_NAME}.${archiveExt}`; const artifactPath = `${ARTIFACTS_DIR}/${UPDATER_ARTIFACT_NAME}.${archiveExt}`;
@ -81,7 +88,7 @@ async function uploadStandalone({ bundle, ext }: TargetConfig) {
const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${ext}*`); const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${ext}*`);
const standalonePath = files.find((file) => file.endsWith(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 artifactName = `${ARTIFACT_BASE}.${ext}`;
const artifactPath = `${ARTIFACTS_DIR}/${artifactName}`; const artifactPath = `${ARTIFACTS_DIR}/${artifactName}`;
@ -95,10 +102,10 @@ async function run() {
const { updater, standalone } = OS_TARGETS[OS]; const { updater, standalone } = OS_TARGETS[OS];
await uploadUpdater(updater); await Promise.all([
uploadUpdater(updater),
for (const config of standalone) { uploadFrontend(),
await uploadStandalone(config); ...standalone.map((config) => uploadStandalone(config))
} ]);
} }
run(); run();

View file

@ -7,7 +7,7 @@
"lint": "eslint . --cache" "lint": "eslint . --cache"
}, },
"dependencies": { "dependencies": {
"@actions/artifact": "^2.1.3", "@actions/artifact": "^2.1.7",
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/glob": "^0.4.0", "@actions/glob": "^0.4.0",
"@actions/io": "^1.1.3" "@actions/io": "^1.1.3"

View file

@ -8,14 +8,10 @@ inputs:
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 8.x.x version: 9.0.6
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View file

@ -17,10 +17,9 @@ runs:
steps: steps:
- name: Install Rust - name: Install Rust
id: toolchain id: toolchain
uses: dtolnay/rust-toolchain@stable uses: IronCoreLabs/rust-toolchain@v1
with: with:
target: ${{ inputs.target }} target: ${{ inputs.target }}
toolchain: '1.75'
components: clippy, rustfmt components: clippy, rustfmt
- name: Cache Rust Dependencies - name: Cache Rust Dependencies
@ -35,27 +34,6 @@ runs:
shell: bash shell: bash
run: echo '{}' | npx -y mustache - .cargo/config.toml.mustache .cargo/config.toml run: echo '{}' | npx -y mustache - .cargo/config.toml.mustache .cargo/config.toml
- name: Turn Off Debuginfo and bump opt-level
shell: bash
if: ${{ runner.os != 'Windows' }}
run: |
sed '/\[profile.dev]/a\
debug = 0
' Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml
sed '/\[profile.dev]/a\
opt-level=1
' Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml
- name: Turn Off Debuginfo and bump opt-level
if: ${{ runner.os == 'Windows' }}
shell: powershell
run: |
(Get-Content Cargo.toml) -replace '\[profile.dev\]', '[profile.dev]
debug = 0' | Set-Content Cargo.toml
(Get-Content Cargo.toml) -replace '\[profile.dev\]', '[profile.dev]
opt-level=1' | Set-Content Cargo.toml
- name: Restore cached Prisma codegen - name: Restore cached Prisma codegen
id: cache-prisma-restore id: cache-prisma-restore
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4

View file

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

View file

@ -40,18 +40,21 @@ jobs:
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
# - host: windows-latest # - host: windows-latest
# target: aarch64-pc-windows-msvc # target: aarch64-pc-windows-msvc
- host: ubuntu-20.04 - host: ubuntu-22.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
# - host: ubuntu-20.04 # - host: ubuntu-22.04
# target: x86_64-unknown-linux-musl # target: x86_64-unknown-linux-musl
# - host: ubuntu-20.04 # - host: uubuntu-22.04
# target: aarch64-unknown-linux-gnu # target: aarch64-unknown-linux-gnu
# - host: ubuntu-20.04 # - host: ubuntu-22.04
# target: aarch64-unknown-linux-musl # target: aarch64-unknown-linux-musl
# - host: ubuntu-20.04 # - host: ubuntu-22.04
# target: armv7-unknown-linux-gnueabihf # target: armv7-unknown-linux-gnueabihf
name: 'Make Cache' name: 'Make Cache'
runs-on: ${{ matrix.settings.host }} runs-on: ${{ matrix.settings.host }}
if: github.repository == 'spacedriveapp/spacedrive'
permissions: {}
timeout-minutes: 150 # 2.5 hours
steps: steps:
- name: Maximize build space - name: Maximize build space
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}

View file

@ -20,8 +20,10 @@ concurrency:
jobs: jobs:
typescript: typescript:
name: TypeScript name: Type and style check
runs-on: ubuntu-latest runs-on: ubuntu-22.04
timeout-minutes: 7
permissions: {}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -34,9 +36,20 @@ jobs:
- name: Perform typechecks - name: Perform typechecks
run: pnpm typecheck 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: eslint:
name: ESLint name: ESLint
runs-on: ubuntu-latest runs-on: ubuntu-22.04
permissions: {}
timeout-minutes: 5
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -52,6 +65,8 @@ jobs:
cypress: cypress:
name: Cypress name: Cypress
runs-on: macos-14 runs-on: macos-14
timeout-minutes: 45
permissions: {}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -67,6 +82,13 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} 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 - name: Setup Cypress
uses: cypress-io/github-action@v6 uses: cypress-io/github-action@v6
with: with:
@ -84,14 +106,16 @@ jobs:
command: env CI=true pnpm test:e2e command: env CI=true pnpm test:e2e
working-directory: apps/web working-directory: apps/web
- uses: actions/upload-artifact@v4 - name: Upload cypress screenshots
uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: cypress-screenshots name: cypress-screenshots
path: apps/web/cypress/screenshots path: apps/web/cypress/screenshots
if-no-files-found: ignore if-no-files-found: ignore
- uses: actions/upload-artifact@v4 - name: Upload cypress video's
uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: cypress-videos name: cypress-videos
@ -100,7 +124,10 @@ jobs:
rustfmt: rustfmt:
name: Rust Formatting name: Rust Formatting
runs-on: ubuntu-latest runs-on: ubuntu-22.04
timeout-minutes: 10
permissions:
contents: read
steps: steps:
- name: Maximize build space - name: Maximize build space
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
@ -118,12 +145,17 @@ jobs:
shell: powershell shell: powershell
run: | run: |
New-Item -ItemType Directory -Force -Path C:\spacedrive_target 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 New-Item -Path target -ItemType Junction -Value C:\spacedrive_target
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: dorny/paths-filter@v3 - name: Check if files have changed
uses: dorny/paths-filter@v3
continue-on-error: true continue-on-error: true
id: filter id: filter
with: with:
@ -151,11 +183,23 @@ jobs:
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
clippy: clippy:
name: Clippy (${{ matrix.platform }})
runs-on: ${{ matrix.platform }}
strategy: strategy:
fail-fast: true
matrix: matrix:
platform: [ubuntu-20.04, macos-14, 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: steps:
- name: Maximize build space - name: Maximize build space
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
@ -178,7 +222,8 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: dorny/paths-filter@v3 - name: Find files that have changed
uses: dorny/paths-filter@v3
continue-on-error: true continue-on-error: true
id: filter id: filter
with: with:
@ -196,6 +241,7 @@ jobs:
- 'extensions/*/**' - 'extensions/*/**'
- 'Cargo.toml' - 'Cargo.toml'
- 'Cargo.lock' - 'Cargo.lock'
- '.github/workflows/ci.yml'
- name: Setup System and Rust - name: Setup System and Rust
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true' if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
@ -205,22 +251,15 @@ jobs:
- name: Run Clippy - name: Run Clippy
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true' if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: actions-rs-plus/clippy-check@v2 uses: giraffate/clippy-action@v1
with: with:
args: --workspace --all-features --locked 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: Run tests
# name: Test (${{ matrix.platform }}) # if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
# runs-on: ${{ matrix.platform }} # run: cargo test --workspace --all-features --locked --target ${{ matrix.settings.target }}
# strategy:
# matrix:
# platform: [ubuntu-20.04, macos-latest, windows-latest]
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
#
# - name: Setup
# uses: ./.github/actions/setup
#
# - name: Test
# run: cargo test --workspace --all-features

View file

@ -1,8 +1,10 @@
name: Release name: Release
on: on:
pull_request:
paths:
- '.github/workflows/release.yml'
workflow_dispatch: 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 # From: https://github.com/rust-lang/rust-analyzer/blob/master/.github/workflows/release.yaml#L13-L21
env: env:
@ -32,17 +34,17 @@ jobs:
arch: x86_64 arch: x86_64
# - host: windows-latest # - host: windows-latest
# target: aarch64-pc-windows-msvc # target: aarch64-pc-windows-msvc
- host: ubuntu-20.04 - host: ubuntu-22.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
bundles: deb bundles: deb
os: linux os: linux
arch: x86_64 arch: x86_64
# - host: ubuntu-20.04 # - host: ubuntu-22.04
# target: x86_64-unknown-linux-musl # target: x86_64-unknown-linux-musl
# - host: ubuntu-20.04 # - host: ubuntu-22.04
# target: aarch64-unknown-linux-gnu # target: aarch64-unknown-linux-gnu
# bundles: deb # bundles: deb
# - host: ubuntu-20.04 # - host: ubuntu-22.04
# target: aarch64-unknown-linux-musl # target: aarch64-unknown-linux-musl
name: Desktop - Main ${{ matrix.settings.target }} name: Desktop - Main ${{ matrix.settings.target }}
runs-on: ${{ matrix.settings.host }} runs-on: ${{ matrix.settings.host }}
@ -101,8 +103,8 @@ jobs:
run: | run: |
pnpm tauri build --ci -v --target ${{ matrix.settings.target }} --bundles ${{ matrix.settings.bundles }},updater pnpm tauri build --ci -v --target ${{ matrix.settings.target }} --bundles ${{ matrix.settings.bundles }},updater
env: env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@ -112,15 +114,11 @@ jobs:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Build AppImage in Docker - name: Package frontend
if: ${{ runner.os == 'Linux' && ( matrix.settings.target == 'x86_64-unknown-linux-gnu' || matrix.settings.target == 'aarch64-unknown-linux-gnu' ) }} if: ${{ runner.os == 'Linux' }}
run: | run: |
set -euxo pipefail set -eux
docker run --rm -v $(pwd):/srv -e 'CI=true' -e 'TARGET=${{ matrix.settings.target }}' -w /srv debian:bookworm scripts/appimage/build_appimage.sh XZ_OPT='-T0 -7' tar -cJf apps/desktop/dist.tar.xz -C apps/desktop/dist .
cd 'target/${{ matrix.settings.target }}/release/bundle/appimage'
sudo chown "$(id -u):$(id -g)" -R .
tar -czf Updater.AppImage.tar.gz *.AppImage
pnpm tauri signer sign -k '${{ secrets.TAURI_PRIVATE_KEY }}' -p '${{ secrets.TAURI_KEY_PASSWORD }}' "$(pwd)/Updater.AppImage.tar.gz"
- name: Publish Artifacts - name: Publish Artifacts
uses: ./.github/actions/publish-artifacts uses: ./.github/actions/publish-artifacts

View file

@ -3,6 +3,10 @@ name: Server release
on: on:
release: release:
types: [published] types: [published]
pull_request:
paths:
- '.github/workflows/server.yml'
- 'apps/server/docker/*'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -27,16 +31,56 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 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 - name: Update buildah
shell: bash shell: bash
run: | run: |
set -euxo pipefail
wget -O- 'https://github.com/HeavenVolkoff/buildah-static/releases/latest/download/buildah-amd64.tar.gz' \ wget -O- 'https://github.com/HeavenVolkoff/buildah-static/releases/latest/download/buildah-amd64.tar.gz' \
| sudo tar -xzf- -C /usr/local/bin | 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 - name: Determine image name & tag
id: image_info id: image_info
shell: bash shell: bash
run: | run: |
set -euxo pipefail
if [ "$GITHUB_EVENT_NAME" == "release" ]; then if [ "$GITHUB_EVENT_NAME" == "release" ]; then
IMAGE_TAG="${GITHUB_REF##*/}" IMAGE_TAG="${GITHUB_REF##*/}"
else else
@ -52,10 +96,14 @@ jobs:
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT" echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
echo "repo_ref=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$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 - name: Build image
id: build-image id: build-image
# TODO: Change to stable version when available uses: redhat-actions/buildah-build@v2
uses: redhat-actions/buildah-build@c79846fb306beeba490e89fb75d2d1af95831e79
with: with:
tags: ${{ steps.image_info.outputs.tag }} ${{ github.event_name == 'release' && 'latest' || 'staging' }} tags: ${{ steps.image_info.outputs.tag }} ${{ github.event_name == 'release' && 'latest' || 'staging' }}
archs: amd64 archs: amd64
@ -69,9 +117,7 @@ jobs:
./apps/server/docker/Dockerfile ./apps/server/docker/Dockerfile
- name: Push image to ghcr.io - name: Push image to ghcr.io
# TODO: Restore redhat-actions/push-to-registry after PR is merged: uses: redhat-actions/push-to-registry@v2
# https://github.com/redhat-actions/push-to-registry/pull/93
uses: Eusebiotrigo/push-to-registry@5acfa470857b62a053884f7214581d55ffeb54ac
with: with:
tags: ${{ steps.build-image.outputs.tags }} tags: ${{ steps.build-image.outputs.tags }}
image: ${{ steps.build-image.outputs.image }} image: ${{ steps.build-image.outputs.image }}

3
.npmrc
View file

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

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

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

4
.vscode/launch.json vendored
View file

@ -11,9 +11,9 @@
"cargo": { "cargo": {
"args": [ "args": [
"build", "build",
"--profile=dev-debug",
"--manifest-path=./apps/desktop/src-tauri/Cargo.toml", "--manifest-path=./apps/desktop/src-tauri/Cargo.toml",
"--no-default-features", "--no-default-features"
"--features=ai-models"
], ],
"problemMatcher": "$rustc" "problemMatcher": "$rustc"
}, },

3
.vscode/tasks.json vendored
View file

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

View file

@ -89,14 +89,16 @@ To run the landing page:
If you encounter any issues, ensure that you are using the following versions of Rust, Node and Pnpm: If you encounter any issues, ensure that you are using the following versions of Rust, Node and Pnpm:
- Rust version: **1.75** - Rust version: **1.78**
- Node version: **18.18** - Node version: **18.18**
- Pnpm version: **8.15** - 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. 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. 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 ##### Mobile App
To run the mobile app: To run the mobile app:
@ -120,10 +122,6 @@ To run the mobile app:
- `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. - `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) - `pnpm mobile start` (runs the metro bundler only)
##### AppImage
Specific instructions on how to build an AppImage release are located [here](scripts/appimage/README.md)
### Pull Request ### Pull Request
Once you have finished making your changes, create a pull request (PR) to submit them. Once you have finished making your changes, create a pull request (PR) to submit them.
@ -169,27 +167,9 @@ Once that has completed, run `xcode-select --install` in the terminal to install
Also ensure that Rosetta is installed, as a few of our dependencies require it. You can install Rosetta with `softwareupdate --install-rosetta --agree-to-license`. 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`.
#### `ModuleNotFoundError: No module named 'distutils'` ### Translations
If you run into this issue, or some other error involving `node-gyp`: Check out the [i18n README](interface/locales/README.md) for more information on how to contribute to translations.
```
File "pnpm@8.15.6/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py", line 42, in <module>
import gyp # noqa: E402
^^^^^^^^^^
File "pnpm@8.15.6/node_modules/pnpm/dist/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 9, in <module>
import gyp.input
File "pnpm@8.15.6/node_modules/pnpm/dist/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 19, in <module>
from distutils.version import StrictVersion
```
Some pnpm dependencies require compilation steps that depend on Python to execute.
However, a recent change in Python version 3.12 broke the utility used to bridge these compilation steps to node/npm/pnpm.
Currently, there is no definitive solution to this issue due to it being fairly new. But there are two workarounds:
1. Downgrade your system Python version to 3.11 or lower.
2. Update pnpm to version v9.0.0-rc.0 (Release Candidate, not fully stable yet).
### Credits ### Credits

6054
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,18 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"core", "core",
"core/crates/*", "core/crates/*",
"crates/*", "crates/*",
"apps/cli", "apps/deps-generator",
"apps/p2p-relay", "apps/desktop/src-tauri",
"apps/desktop/src-tauri", "apps/desktop/crates/*",
"apps/desktop/crates/*", "apps/mobile/modules/sd-core/core",
"apps/mobile/modules/sd-core/core", "apps/mobile/modules/sd-core/android/crate",
"apps/mobile/modules/sd-core/android/crate", "apps/mobile/modules/sd-core/ios/crate",
"apps/mobile/modules/sd-core/ios/crate", "apps/server",
"apps/server",
] ]
exclude = ["crates/crypto"]
[workspace.package] [workspace.package]
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
@ -20,86 +20,78 @@ edition = "2021"
repository = "https://github.com/spacedriveapp/spacedrive" repository = "https://github.com/spacedriveapp/spacedrive"
[workspace.dependencies] [workspace.dependencies]
# First party dependencies
prisma-client-rust = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "f99d6f5566570f3ab1edecb7a172ad25b03d95af", features = [
"specta",
"sqlite-create-many",
"migrations",
"sqlite",
], default-features = false }
prisma-client-rust-cli = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "f99d6f5566570f3ab1edecb7a172ad25b03d95af", features = [
"specta",
"sqlite-create-many",
"migrations",
"sqlite",
], default-features = false }
prisma-client-rust-sdk = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "f99d6f5566570f3ab1edecb7a172ad25b03d95af", features = [
"sqlite",
], default-features = false }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-appender = "0.2.3"
rspc = { version = "0.1.4" }
specta = { version = "=2.0.0-rc.7" }
tauri-specta = { version = "=2.0.0-rc.4" }
swift-rs = { version = "1.0.6" }
# Third party dependencies used by one or more of our crates # Third party dependencies used by one or more of our crates
anyhow = "1.0.75" async-channel = "2.3"
async-channel = "2.0.0" async-trait = "0.1.80"
async-trait = "0.1.77" axum = "0.6.20" # Update blocked by hyper
axum = "=0.6.20" base64 = "0.22.1"
base64 = "0.21.5" base91 = "0.1.0"
blake3 = "1.5.0" blake3 = "1.5.0" # Update blocked by custom patch below
chrono = "0.4.31" chrono = "0.4.38"
clap = "4.4.7" directories = "5.0"
ed25519-dalek = "2.1.1"
futures = "0.3.30" futures = "0.3.30"
futures-concurrency = "7.4.3" futures-concurrency = "7.6"
globset = "^0.4.13" gix-ignore = "0.11.2"
hex = "0.4.3" globset = "0.4.14"
http = "0.2.9" http = "0.2" # Update blocked by axum
image = "0.24.7" hyper = "0.14" # Update blocked due to API breaking changes
itertools = "0.12.0" image = "0.25.1"
lending-stream = "1.0.0" itertools = "0.13.0"
normpath = "1.1.1" lending-stream = "1.0"
once_cell = "1.18.0" libc = "0.2"
pin-project-lite = "0.2.13" normpath = "1.2"
once_cell = "1.19"
pin-project-lite = "0.2.14"
rand = "0.8.5" rand = "0.8.5"
rand_chacha = "0.3.1" regex = "1.10"
regex = "1.10.2" reqwest = "0.11" # Update blocked by hyper
reqwest = "0.11.22" rmp = "0.8.14"
rmp-serde = "1.1.2" rmp-serde = "1.3.0"
rmpv = { version = "^1.0.1", features = ["with-serde"] } rmpv = { version = "1.3", features = ["with-serde"] }
rspc = "0.1.4"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
strum = "0.25" specta = "=2.0.0-rc.11"
strum_macros = "0.25" static_assertions = "1.1"
tempfile = "3.8.1" strum = "0.26"
thiserror = "1.0.50" strum_macros = "0.26"
tokio = "1.36.0" tempfile = "3.10"
tokio-stream = "0.1.14" thiserror = "1.0"
tokio-util = "0.7.10" tokio = "1.38"
uhlc = "=0.5.2" tokio-stream = "0.1.15"
uuid = "1.5.0" tokio-util = "0.7.11"
webp = "0.2.6" 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"
[workspace.dev-dependencies] [workspace.dependencies.prisma-client-rust]
tracing-test = { version = "^0.2.4" } git = "https://github.com/brendonovich/prisma-client-rust"
rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab"
features = ["migrations", "specta", "sqlite", "sqlite-create-many"]
default-features = false
[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
[workspace.dependencies.prisma-client-rust-sdk]
git = "https://github.com/brendonovich/prisma-client-rust"
rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab"
features = ["sqlite"]
default-features = false
[patch.crates-io] [patch.crates-io]
# Proper IOS Support # Proper IOS Support
if-watch = { git = "https://github.com/oscartbeaumont/if-watch.git", rev = "a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3" } if-watch = { git = "https://github.com/spacedriveapp/if-watch.git", rev = "a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3" }
# We hack it to the high heavens # We hack it to the high heavens
rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "f3347e2e8bfe3f37bfacc437ca329fe71cdcb048" } rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "ab12964b140991e0730c3423693533fba71efb03" }
# `cursor_position` method
tauri = { git = "https://github.com/spacedriveapp/tauri.git", rev = "8409af71a83d631ff9d1cd876c441a57511a1cbd" }
tao = { git = "https://github.com/spacedriveapp/tao", rev = "7880adbc090402c44fbcf006669458fa82623403" }
# Add `Control::open_stream_with_addrs` # Add `Control::open_stream_with_addrs`
libp2p = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" } libp2p = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" }
@ -107,13 +99,31 @@ libp2p-core = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev =
libp2p-swarm = { 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" } libp2p-stream = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" }
# aes = { git = "https://github.com/RustCrypto/block-ciphers", rev = "5837233f86419dbe75b8e3824349e30f6bc40b22" } blake3 = { git = "https://github.com/spacedriveapp/blake3.git", rev = "d3aab416c12a75c2bfabce33bcd594e428a79069" }
blake3 = { git = "https://github.com/brxken128/blake3", rev = "d3aab416c12a75c2bfabce33bcd594e428a79069" } # Due to image crate version bump
pdfium-render = { git = "https://github.com/fogodev/pdfium-render.git", rev = "e7aa1111f441c49e857cebda15b4e51b24356aaa" }
[profile.dev] [profile.dev]
# Make compilation faster on macOS # Make compilation faster on macOS
split-debuginfo = "unpacked" 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. # Set the settings for build scripts and proc-macros.
[profile.dev.build-override] [profile.dev.build-override]
@ -123,3 +133,18 @@ opt-level = 3
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 3 opt-level = 3
incremental = false incremental = false
# Set the default for dependencies, except workspace members.
[profile.dev-debug.package."*"]
inherits = "dev"
opt-level = 3
debug = "full"
incremental = false
# Optimize release builds
[profile.release]
panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
strip = true # Remove debug symbols

View file

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

View file

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

View file

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

View file

@ -8,10 +8,12 @@ repository = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
[dependencies] [dependencies]
anyhow = { workspace = true } # Workspace dependencies
clap = { workspace = true, features = ["derive"] } reqwest = { workspace = true, features = ["blocking", "native-tls-vendored"] }
reqwest = { workspace = true, features = ["blocking"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
# Specific Deps Generator dependencies
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
cargo_metadata = "0.18.1" cargo_metadata = "0.18.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

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

View file

@ -1,32 +1,15 @@
use std::path::{Path, PathBuf}; use std::path::Path;
use gtk::{ use gtk::{
gio::{ gio::{
content_type_guess, content_type_guess, prelude::AppInfoExt, prelude::FileExt, AppInfo, AppLaunchContext,
prelude::AppInfoExt, DesktopAppInfo, File as GioFile, ResourceError,
prelude::{AppLaunchContextExt, FileExt},
AppInfo, AppLaunchContext, DesktopAppInfo, File as GioFile, ResourceError,
}, },
glib::error::Error as GlibError, glib::error::Error as GlibError,
prelude::IsA,
}; };
use tokio::fs::File; use tokio::fs::File;
use tokio::io::AsyncReadExt; 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! { thread_local! {
static LAUNCH_CTX: AppLaunchContext = { static LAUNCH_CTX: AppLaunchContext = {
// TODO: Display supports requires GDK, which can only run on the main thread // 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" // "This is an Glib type conversion, it should never fail because GDKAppLaunchContext is a subclass of AppLaunchContext"
// )).unwrap_or_default(); // )).unwrap_or_default();
let ctx = AppLaunchContext::default();
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) { AppLaunchContext::default()
// 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
} }
} }

View file

@ -1,29 +1,13 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
env, env,
ffi::{CStr, OsStr, OsString}, ffi::{CStr, OsStr},
io, mem, mem,
os::unix::ffi::OsStrExt, os::unix::ffi::OsStrExt,
path::{Path, PathBuf}, path::PathBuf,
ptr, ptr,
}; };
fn version(version_str: &str) -> i32 {
let mut version_parts: Vec<i32> = version_str
.split('.')
.take(4) // Take up to 4 components
.map(|part| part.parse().unwrap_or(0))
.collect();
// Pad with zeros if needed
version_parts.resize_with(4, Default::default);
(version_parts[0] * 1_000_000_000)
+ (version_parts[1] * 1_000_000)
+ (version_parts[2] * 1_000)
+ version_parts[3]
}
pub fn get_current_user_home() -> Option<PathBuf> { pub fn get_current_user_home() -> Option<PathBuf> {
use libc::{getpwuid_r, getuid, passwd, ERANGE}; use libc::{getpwuid_r, getuid, passwd, ERANGE};
@ -192,74 +176,12 @@ pub fn normalize_environment() {
) )
.expect("PATH must be successfully normalized"); .expect("PATH must be successfully normalized");
if let Ok(appdir) = get_appdir() { if has_nvidia() {
println!("Running from APPIMAGE"); // Workaround for: https://github.com/tauri-apps/tauri/issues/9304
env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
// Workaround for https://github.com/AppImageCrafters/appimage-builder/issues/175
env::set_current_dir(appdir.join({
let appimage_libc_version = version(
std::env::var("APPDIR_LIBC_VERSION")
.expect("AppImage Libc version must be set")
.as_str(),
);
let system_lic_version = version({
#[cfg(target_env = "gnu")]
{
use libc::gnu_get_libc_version;
let ptr = unsafe { gnu_get_libc_version() };
if ptr.is_null() {
panic!("Couldn't read glic version");
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.expect("Couldn't read glic version")
}
#[cfg(not(target_env = "gnu"))]
{
// Use the same version as gcompat
// https://git.adelielinux.org/adelie/gcompat/-/blob/current/libgcompat/version.c
std::env::var("GLIBC_FAKE_VERSION").unwrap_or_else(|_| "2.8".to_string())
}
});
if system_lic_version < appimage_libc_version {
"runtime/compat"
} else {
"runtime/default"
}
}))
.expect("Failed to set current directory to $APPDIR");
// Bubblewrap does not work from inside appimage
env::set_var("WEBKIT_FORCE_SANDBOX", "0");
env::set_var("WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS", "1");
// FIX-ME: This is required because appimage-builder generates a broken GstRegistry, which breaks video playback
env::remove_var("GST_REGISTRY");
env::remove_var("GST_REGISTRY_UPDATE");
} }
} }
pub(crate) fn remove_prefix_from_pathlist(
env_name: &str,
prefix: &impl AsRef<Path>,
) -> Option<OsString> {
env::var_os(env_name).and_then(|value| {
let mut dirs = env::split_paths(&value)
.filter(|dir| !(dir.as_os_str().is_empty() || dir.starts_with(prefix)))
.peekable();
if dirs.peek().is_none() {
None
} else {
Some(env::join_paths(dirs).expect("Should not fail because we are only filtering a pathlist retrieved from the environmnet"))
}
})
}
// Check if snap by looking if SNAP is set and not empty and that the SNAP directory exists // Check if snap by looking if SNAP is set and not empty and that the SNAP directory exists
pub fn is_snap() -> bool { pub fn is_snap() -> bool {
if let Some(snap) = std::env::var_os("SNAP") { if let Some(snap) = std::env::var_os("SNAP") {
@ -271,21 +193,6 @@ pub fn is_snap() -> bool {
false false
} }
fn get_appdir() -> io::Result<PathBuf> {
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) {
if appdir.is_absolute() && appdir.is_dir() {
return Ok(appdir);
}
}
Err(io::Error::new(io::ErrorKind::NotFound, "AppDir not found"))
}
// Check if appimage by looking if APPDIR is set and is a valid directory
pub fn is_appimage() -> bool {
get_appdir().is_ok()
}
// Check if flatpak by looking if FLATPAK_ID is set and not empty and that the .flatpak-info file exists // 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 { pub fn is_flatpak() -> bool {
if let Some(flatpak_id) = std::env::var_os("FLATPAK_ID") { if let Some(flatpak_id) = std::env::var_os("FLATPAK_ID") {
@ -296,3 +203,31 @@ pub fn is_flatpak() -> bool {
false false
} }
fn has_nvidia() -> bool {
use wgpu::{
Backends, DeviceType, Dx12Compiler, Gles3MinorVersion, Instance, InstanceDescriptor,
InstanceFlags,
};
let instance = Instance::new(InstanceDescriptor {
flags: InstanceFlags::empty(),
backends: Backends::VULKAN | Backends::GL,
gles_minor_version: Gles3MinorVersion::Automatic,
dx12_shader_compiler: Dx12Compiler::default(),
});
for adapter in instance.enumerate_adapters(Backends::all()) {
let info = adapter.get_info();
match info.device_type {
DeviceType::DiscreteGpu | DeviceType::IntegratedGpu | DeviceType::VirtualGpu => {
// Nvidia PCI id
if info.vendor == 0x10de {
return true;
}
}
_ => {}
}
}
false
}

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import AppKit import AppKit
import SwiftRs
@objc @objc
public enum AppThemeType: Int { public enum AppThemeType: Int {
@ -7,6 +8,34 @@ public enum AppThemeType: Int {
case dark = 1 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") @_cdecl("lock_app_theme")
public func lockAppTheme(themeType: AppThemeType) { public func lockAppTheme(themeType: AppThemeType) {
var theme: NSAppearance? 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") @_cdecl("set_titlebar_style")
public func setTitlebarStyle(window: NSWindow, fullScreen: Bool) { 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) // this results in far less visual artifacts if we just manage it ourselves (the native taskbar re-appears when fullscreening/un-fullscreening)

View file

@ -9,11 +9,10 @@ pub enum AppThemeType {
Dark = 1 as Int, 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 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 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)); swift!(pub fn reload_webview(webview: &NSObject));
#[repr(C)] #[repr(C)]
@ -31,20 +30,3 @@ pub fn open_file_paths_with(file_urls: &[String], with_url: &str) {
let file_url = file_urls.join("\0"); let file_url = file_urls.join("\0");
unsafe { open_file_path_with(&file_url.as_str().into(), &with_url.into()) } unsafe { open_file_path_with(&file_url.as_str().into(), &with_url.into()) }
} }
// main!(|_| {
// unsafe { setup_disk_watcher() };
// print!("Waiting for disk events... ");
// Ok(())
// });
// #[no_mangle]
// pub extern "C" fn disk_event_callback(mounted: Bool, path: *const SRString) {
// let mounted_str = if mounted { "mounted" } else { "unmounted" };
// // Convert the raw pointer to a reference
// let path_ref = unsafe { &*path };
// let path_str = path_ref.to_string(); // Assuming SRString has a to_string method
// println!("Disk at path {} was {}", path_str, mounted_str);
// }

View file

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

View file

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

View file

@ -20,7 +20,10 @@
"@sd/ui": "workspace:*", "@sd/ui": "workspace:*",
"@t3-oss/env-core": "^0.7.1", "@t3-oss/env-core": "^0.7.1",
"@tanstack/react-query": "^4.36.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", "consistent-hash": "^1.2.2",
"immer": "^10.0.3", "immer": "^10.0.3",
"react": "^18.2.0", "react": "^18.2.0",
@ -31,7 +34,7 @@
"devDependencies": { "devDependencies": {
"@sd/config": "workspace:*", "@sd/config": "workspace:*",
"@sentry/vite-plugin": "^2.16.0", "@sentry/vite-plugin": "^2.16.0",
"@tauri-apps/cli": "^1.5.11", "@tauri-apps/cli": "next",
"@types/react": "^18.2.67", "@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"sass": "^1.72.0", "sass": "^1.72.0",

View file

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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "sd-desktop" name = "sd-desktop"
version = "0.2.11" version = "0.3.2"
description = "The universal file manager." description = "The universal file manager."
authors = ["Spacedrive Technology Inc <support@spacedrive.com>"] authors = ["Spacedrive Technology Inc <support@spacedrive.com>"]
default-run = "sd-desktop" default-run = "sd-desktop"
@ -10,59 +10,68 @@ edition = { workspace = true }
[dependencies] [dependencies]
# Spacedrive Sub-crates # Spacedrive Sub-crates
sd-core = { path = "../../../core", features = [ sd-core = { path = "../../../core", features = ["ffmpeg", "heif"] }
"ffmpeg",
"heif",
] }
sd-fda = { path = "../../../crates/fda" } sd-fda = { path = "../../../crates/fda" }
sd-prisma = { path = "../../../crates/prisma" } sd-prisma = { path = "../../../crates/prisma" }
# Workspace dependencies
axum = { workspace = true, features = ["headers", "query"] } axum = { workspace = true, features = ["headers", "query"] }
hyper = "0.14.28" directories = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
hyper = { workspace = true }
http = { workspace = true } http = { workspace = true }
prisma-client-rust = { workspace = true } prisma-client-rust = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
rspc = { workspace = true, features = ["tauri", "tracing"] } rspc = { workspace = true, features = ["tauri", "tracing"] }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true }
specta = { workspace = true } specta = { workspace = true }
strum = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["sync"] } tokio = { workspace = true, features = ["sync"] }
tracing = { workspace = true } tracing = { workspace = true }
tauri-specta = { workspace = true, features = ["typescript"] } thiserror = { workspace = true }
uuid = { workspace = true, features = ["serde"] } uuid = { workspace = true, features = ["serde"] }
thiserror.workspace = true
opener = { version = "0.6.1", features = ["reveal"] } # Specific Desktop dependencies
tauri = { version = "=1.5.3", features = [ # WARNING: Do NOT enable default features, as that vendors dbus (see below)
"macos-private-api", opener = { version = "0.7.1", features = ["reveal"], default-features = false }
"path-all", tauri = { version = "=2.0.0-beta.17", features = [
"protocol-all", "macos-private-api",
"os-all", "unstable",
"shell-all", "linux-libxdo",
"dialog-all", ] } # Update blocked by rspc
"linux-protocol-headers", tauri-plugin-updater = "2.0.0-beta"
"updater", tauri-plugin-dialog = "2.0.0-beta"
"window-all", tauri-plugin-os = "2.0.0-beta"
"native-tls-vendored", tauri-plugin-shell = "2.0.0-beta"
"tracing", tauri-runtime = { version = "=2.0.0-beta.15" } # Update blocked by tauri
] } tauri-specta = { version = "=2.0.0-rc.8", features = ["typescript"] }
directories = "5.0.1" tauri-utils = { version = "=2.0.0-beta.16" } # Update blocked by tauri
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
# Spacedrive Sub-crates
sd-desktop-linux = { path = "../crates/linux" } 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] [target.'cfg(target_os = "macos")'.dependencies]
# Spacedrive Sub-crates
sd-desktop-macos = { path = "../crates/macos" } sd-desktop-macos = { path = "../crates/macos" }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
# Spacedrive Sub-crates
sd-desktop-windows = { path = "../crates/windows" } sd-desktop-windows = { path = "../crates/windows" }
webview2-com = "0.19.1"
[build-dependencies] [build-dependencies]
tauri-build = "1.5.0" # Specific Desktop dependencies
tauri-build = "2.0.0-beta"
[features] [features]
default = ["custom-protocol"] default = ["custom-protocol"]
devtools = ["tauri/devtools"]
ai-models = ["sd-core/ai"] ai-models = ["sd-core/ai"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -373,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>) { fn inner_reveal_paths(paths: impl Iterator<Item = PathBuf>) {
for path in paths { 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) { if let Err(e) = opener::reveal(path) {
error!("Failed to open logs dir: {e:#?}"); error!("Failed to open logs dir: {e:#?}");
} }

View file

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

View file

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

View file

@ -95,15 +95,22 @@ pub async fn sd_server_plugin<R: Runtime>(
.expect("Error with HTTP server!"); // TODO: Panic handling .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") Ok(tauri::plugin::Builder::new("sd-server")
.js_init_script(format!( .js_init_script(script.to_owned())
r#"window.__SD_CUSTOM_SERVER_AUTH_TOKEN__ = "{auth_token}"; window.__SD_CUSTOM_URI_SERVER__ = [{}];"#, .on_page_load(move |webview, _payload| {
[listen_addra, listen_addrb, listen_addrc, listen_addrd] webview
.iter() .eval(&script)
.map(|addr| format!("'http://{addr}'")) .expect("Spacedrive server URL must be injected")
.collect::<Vec<_>>() })
.join(","),
))
.on_event(move |_app, e| { .on_event(move |_app, e| {
if let RunEvent::Exit { .. } = e { if let RunEvent::Exit { .. } = e {
block_in_place(|| { block_in_place(|| {

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react'; import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { CacheProvider, createCache, RspcProvider } from '@sd/client'; import { RspcProvider } from '@sd/client';
import { import {
createRoutes, createRoutes,
ErrorPage, ErrorPage,
@ -56,16 +56,14 @@ export default function App() {
return ( return (
<RspcProvider queryClient={queryClient}> <RspcProvider queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<CacheProvider cache={cache}> {startupError ? (
{startupError ? ( <ErrorPage
<ErrorPage message={startupError}
message={startupError} submessage="Error occurred starting up the Spacedrive core"
submessage="Error occurred starting up the Spacedrive core" />
/> ) : (
) : ( <AppInner />
<AppInner /> )}
)}
</CacheProvider>
</QueryClientProvider> </QueryClientProvider>
</RspcProvider> </RspcProvider>
); );
@ -74,9 +72,7 @@ export default function App() {
// we have a minimum delay between creating new tabs as react router can't handle creating tabs super fast // 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 TAB_CREATE_DELAY = 150;
const cache = createCache(); const routes = createRoutes(platform);
const routes = createRoutes(platform, cache);
type redirect = { pathname: string; search: string | undefined }; type redirect = { pathname: string; search: string | undefined };

View file

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

View file

@ -1,14 +1,14 @@
import { dialog, invoke, os, shell } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api/core';
import { confirm } from '@tauri-apps/api/dialog';
import { homeDir } from '@tauri-apps/api/path'; 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. // @ts-expect-error: Doesn't have a types package.
import ConsistentHash from 'consistent-hash'; import ConsistentHash from 'consistent-hash';
import { OperatingSystem, Platform } from '@sd/interface'; import { OperatingSystem, Platform } from '@sd/interface';
import { commands, events } from './commands'; import { commands, events } from './commands';
import { env } from './env'; import { env } from './env';
import { createUpdater } from './updater';
const customUriAuthToken = (window as any).__SD_CUSTOM_SERVER_AUTH_TOKEN__ as string | undefined; const customUriAuthToken = (window as any).__SD_CUSTOM_SERVER_AUTH_TOKEN__ as string | undefined;
const customUriServerUrl = (window as any).__SD_CUSTOM_URI_SERVER__ as string[] | undefined; const customUriServerUrl = (window as any).__SD_CUSTOM_URI_SERVER__ as string[] | undefined;
@ -16,12 +16,12 @@ const customUriServerUrl = (window as any).__SD_CUSTOM_URI_SERVER__ as string[]
const queryParams = customUriAuthToken ? `?token=${encodeURIComponent(customUriAuthToken)}` : ''; const queryParams = customUriAuthToken ? `?token=${encodeURIComponent(customUriAuthToken)}` : '';
async function getOs(): Promise<OperatingSystem> { async function getOs(): Promise<OperatingSystem> {
switch (await os.type()) { switch (await type()) {
case 'Linux': case 'linux':
return 'linux'; return 'linux';
case 'Windows_NT': case 'windows':
return 'windows'; return 'windows';
case 'Darwin': case 'macos':
return 'macOS'; return 'macOS';
default: default:
return 'unknown'; return 'unknown';
@ -45,9 +45,11 @@ function constructServerUrl(urlSuffix: string) {
export const platform = { export const platform = {
platform: 'tauri', platform: 'tauri',
getThumbnailUrlByThumbKey: (keyParts) => getThumbnailUrlByThumbKey: (thumbKey) =>
constructServerUrl( constructServerUrl(
`/thumbnail/${keyParts.map((i) => encodeURIComponent(i)).join('/')}.webp` `/thumbnail/${encodeURIComponent(
thumbKey.base_directory_str
)}/${encodeURIComponent(thumbKey.shard_hex)}/${encodeURIComponent(thumbKey.cas_id)}.webp`
), ),
getFileUrl: (libraryId, locationLocalId, filePathId) => getFileUrl: (libraryId, locationLocalId, filePathId) =>
constructServerUrl(`/file/${libraryId}/${locationLocalId}/${filePathId}`), constructServerUrl(`/file/${libraryId}/${locationLocalId}/${filePathId}`),
@ -64,15 +66,18 @@ export const platform = {
constructServerUrl( constructServerUrl(
`/remote/${encodeURIComponent(remote_identity)}/uri/${path}?token=${customUriAuthToken}` `/remote/${encodeURIComponent(remote_identity)}/uri/${path}?token=${customUriAuthToken}`
), ),
openLink: shell.open, openLink: shellOpen,
getOs, getOs,
openDirectoryPickerDialog: (opts) => { 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 if (opts?.multiple) return result as any; // Tauri don't properly type narrow on `multiple` argument
return result; return result;
}, },
openFilePickerDialog: () => dialog.open(), openFilePickerDialog: () =>
saveFilePickerDialog: (opts) => dialog.save(opts), dialogOpen({
multiple: true
}).then((result) => result?.map((r) => r.path) ?? null),
saveFilePickerDialog: (opts) => dialogSave(opts),
showDevtools: () => invoke('show_devtools'), showDevtools: () => invoke('show_devtools'),
confirm: (msg, cb) => confirm(msg).then(cb), confirm: (msg, cb) => confirm(msg).then(cb),
subscribeToDragAndDropEvents: (cb) => subscribeToDragAndDropEvents: (cb) =>
@ -82,7 +87,7 @@ export const platform = {
userHomeDir: homeDir, userHomeDir: homeDir,
auth: { auth: {
start(url) { start(url) {
open(url); return shellOpen(url);
} }
}, },
...commands, ...commands,

View file

@ -25,6 +25,15 @@ export default defineConfig(({ mode }) => {
server: { server: {
port: 8001 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: [ plugins: [
devtoolsPlugin, devtoolsPlugin,
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_AUTH_TOKEN &&

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 319 KiB

View file

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

View file

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

View file

@ -7,7 +7,7 @@ const tauriTarget = z.union([z.literal('linux'), z.literal('windows'), z.literal
const tauriArch = z.union([z.literal('x86_64'), z.literal('aarch64')]); const tauriArch = z.union([z.literal('x86_64'), z.literal('aarch64')]);
const extensions = { const extensions = {
linux: 'AppImage', linux: 'deb',
windows: 'msi', windows: 'msi',
darwin: 'dmg' darwin: 'dmg'
} as const satisfies Record<z.infer<typeof tauriTarget>, string>; } as const satisfies Record<z.infer<typeof tauriTarget>, string>;

View file

@ -1,8 +1,8 @@
import { z } from 'zod'; import { object, z } from 'zod';
import { env } from '~/env'; import { env } from '~/env';
import * as github from './github'; import * as github from './github';
import { createSlashCommand, createViewSubmission, USER_REF } from './utils'; import { createBlockActions, createSlashCommand, createViewSubmission, USER_REF } from './utils';
export const callbackId = 'createReleaseModal' as const; export const callbackId = 'createReleaseModal' as const;
export const fields = { export const fields = {
@ -17,7 +17,7 @@ export const fields = {
} as const; } as const;
export const COMMAND_NAME = '/release' as const; export const COMMAND_NAME = '/release' as const;
export const EVENT_SCHEMAS = [createSlashCommand(COMMAND_NAME), createViewSubmission()] as const; export const EVENT_SCHEMAS = [createSlashCommand(COMMAND_NAME), createViewSubmission(), createBlockActions()] as const;
export async function createModal( export async function createModal(
trigger_id: string, trigger_id: string,
@ -63,7 +63,7 @@ export async function createModal(
type: 'plain_text', type: 'plain_text',
text: 'View Commit' text: 'View Commit'
}, },
url: `${github.REPO_API}/commit/${commit}` url: `${github.REPO_API}/commits/${commit}`
}, },
text: { text: {
type: 'mrkdwn', type: 'mrkdwn',
@ -185,7 +185,18 @@ export async function handleSubmission(
const { tag, commit, responseUrl } = JSON.parse(privateMetadata); const { tag, commit, responseUrl } = JSON.parse(privateMetadata);
await fetch(`${github.REPO_API}/git/refs`, { const createTag = await fetch(`${github.REPO_API}/git/tags`, {
method: 'POST',
body: JSON.stringify({
tag,
message: tagline,
object: commit,
type: 'commit'
}),
headers: github.HEADERS
}).then((r) => r.json());
const getRef = await fetch(`${github.REPO_API}/git/refs`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
ref: `refs/tags/${tag}`, ref: `refs/tags/${tag}`,
@ -222,6 +233,7 @@ export async function handleSubmission(
headers: github.HEADERS headers: github.HEADERS
} }
); );
const [release] = await Promise.all([createRelease, dispatchWorkflowRun]); const [release] = await Promise.all([createRelease, dispatchWorkflowRun]);
await fetch(responseUrl, { await fetch(responseUrl, {
@ -267,7 +279,7 @@ export async function handleSubmission(
type: 'plain_text', type: 'plain_text',
text: 'View Commit' text: 'View Commit'
}, },
url: `https://github.com/${env.GITHUB_ORG}/${env.GITHUB_REPO}/commit/${commit}` url: `https://github.com/${env.GITHUB_ORG}/${env.GITHUB_REPO}/commits/${commit}`
} }
] ]
} }

View file

@ -16,6 +16,7 @@ export async function POST(req: Request) {
if (!isValid.valid) return new Response(isValid.error, { status: 400 }); if (!isValid.valid) return new Response(isValid.error, { status: 400 });
const parsedBody = BODY.safeParse(Object.fromEntries([...new URLSearchParams(body)])); const parsedBody = BODY.safeParse(Object.fromEntries([...new URLSearchParams(body)]));
if (!parsedBody.success) { if (!parsedBody.success) {
console.log(parsedBody.error); console.log(parsedBody.error);
return new Response('Unexpected request', { status: 400 }); return new Response('Unexpected request', { status: 400 });

View file

@ -42,11 +42,91 @@ export function createViewSubmission() {
export function createSlashCommand<T extends string>(command: T) { export function createSlashCommand<T extends string>(command: T) {
return z.object({ return z.object({
command: z.literal(command), token: z.string(),
team_id: z.string(),
team_domain: z.string(),
channel_id: z.string(), channel_id: z.string(),
text: z.string().transform((s) => s.split(' ')), channel_name: z.string(),
user_id: z.string(), user_id: z.string(),
trigger_id: z.string(), user_name: z.string(),
response_url: z.string() command: z.literal(command),
text: z.string().transform((s) => s.split(' ')),
api_app_id: z.string(),
is_enterprise_install: z.union([z.literal('false'), z.literal('true')]).transform((v) => v === 'true'),
response_url: z.string(),
trigger_id: z.string()
}); });
} }
const BLOCK_ACTIONS_INNER = z.object({
api_app_id: z.string(),
token: z.string(),
container: z.object({
type: z.string(),
message_ts: z.string(),
channel_id: z.string(),
is_ephemeral: z.boolean()
}),
trigger_id: z.string(),
team: z.object({
id: z.string(),
domain: z.string()
}),
enterprise: z.any().nullable(),
is_enterprise_install: z.boolean(),
channel: z.object({
id: z.string(),
name: z.string()
}),
message: z.object({
subtype: z.string(),
text: z.string(),
type: z.string(),
ts: z.string(),
bot_id: z.string(),
blocks: z.array(
z.object({
type: z.string(),
block_id: z.string(),
text: z.object({
type: z.string(),
text: z.string(),
verbatim: z.boolean()
}).optional(),
elements: z.array(
z.object({
type: z.string(),
action_id: z.string(),
text: z.object({
type: z.string(),
text: z.string(),
emoji: z.boolean()
}),
url: z.string().optional()
})
).optional()
})
)
}),
state: z.object({
values: z.record(z.record(z.any()))
}),
response_url: z.string(),
actions: z.array(
z.object({
action_id: z.string(),
block_id: z.string(),
text: z.object({
type: z.string(),
text: z.string(),
emoji: z.boolean()
}),
type: z.string(),
action_ts: z.string()
})
)
});
export function createBlockActions() {
return createInteraction('block_actions', BLOCK_ACTIONS_INNER);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 382 KiB

View file

@ -110,20 +110,23 @@ export const items = [
}, },
{ {
when: '0.3 Alpha', when: '0.3 Alpha',
subtext: 'April 2024', subtext: 'May 2024',
title: 'Connect devices & Library sync', title: 'Connect devices & sync',
description: 'Automatically synchronized libraries across all your devices.' description: 'Automatically synchronized libraries across all your devices.'
}, },
{
title: 'Mobile app TestFlight & Play Store Beta',
description: 'Access your library on the go, with a mobile app for iOS and Android.'
},
{
title: 'Spacedrive in every language, i18n',
description: 'Spacedrive will be available in every language.'
},
{ {
title: 'Key manager', title: 'Key manager',
description: description:
'View, mount, unmount and hide keys. Mounted keys can be used to instantly encrypt and decrypt any files on your node.' 'View, mount, unmount and hide keys. Mounted keys can be used to instantly encrypt and decrypt any files on your node.'
}, },
{
title: 'Mobile app TestFlight',
description:
'Access your library on the go, with a mobile app for iOS and Android in alpha via TestFlight and Play Store.'
},
{ {
title: 'Advanced media analysis', title: 'Advanced media analysis',
description: 'Transcribe audio, identify faces, video scenes and more.' description: 'Transcribe audio, identify faces, video scenes and more.'
@ -140,7 +143,7 @@ export const items = [
}, },
{ {
when: '0.4 Alpha', when: '0.4 Alpha',
subtext: 'May 2024', subtext: 'June 2024',
title: 'AI search', title: 'AI search',
description: description:
'Search the contents of your files, including images, audio and video with a deep understanding of context and content.' 'Search the contents of your files, including images, audio and video with a deep understanding of context and content.'
@ -156,6 +159,11 @@ export const items = [
description: description:
'Backup and sync from anywhere with a Spacedrive Cloud account. Paid plans for additional storage and cloud features.' 'Backup and sync from anywhere with a Spacedrive Cloud account. Paid plans for additional storage and cloud features.'
}, },
{
title: 'CLI',
description:
'Access Spacedrive from the command line, with a rich set of commands to manage your library and devices.'
},
{ {
title: 'Web portal', title: 'Web portal',
description: description:
@ -177,7 +185,7 @@ export const items = [
}, },
{ {
when: '0.5 Beta', when: '0.5 Beta',
subtext: 'June 2024', subtext: 'August 2024',
title: 'Encrypted vault(s)', title: 'Encrypted vault(s)',
description: description:
'Effortlessly manage & encrypt sensitive files. Encrypt individual files or create flexible-size vaults.' 'Effortlessly manage & encrypt sensitive files. Encrypt individual files or create flexible-size vaults.'
@ -185,7 +193,7 @@ export const items = [
{ {
title: 'Extensions', title: 'Extensions',
description: description:
'Build tools on top of Spacedrive, extend functionality and integrate third party services. Extension directory on spacedrive.com/extensions.' 'Build tools on top of Spacedrive, extend functionality and integrate third party services.'
}, },
{ {
when: '1.0 Release', when: '1.0 Release',

View file

@ -31,39 +31,6 @@ export const teamMembers: Array<TeamMemberProps> = [
github: 'https://github.com/fogodev' github: 'https://github.com/fogodev'
} }
}, },
{
name: 'Nik Elšnik',
location: 'Slovenia',
role: 'TypeScript Engineer - Lead Interface',
imageUrl: '/images/team/nikec.jpg',
socials: {
github: 'https://github.com/niikeec',
twitter: 'https://x.com/nikec_'
}
},
{
name: 'Brendan Allan',
location: 'Western Australia',
role: 'Rust Engineer - Database & Sync',
imageUrl: '/images/team/brendan.jpg',
socials: {
twitter: 'https://x.com/brendonovichdev',
twitch: 'https://twitch.tv/brendonovich',
github: 'https://github.com/brendonovich'
}
},
{
name: 'Oscar Beaumont',
location: 'Western Australia',
role: 'Rust Engineer - P2P & Networking',
imageUrl: '/images/team/oscar.jpg',
socials: {
twitter: 'https://x.com/oscartbeaumont',
twitch: 'https://twitch.tv/oscartbeaumont',
github: 'https://github.com/oscartbeaumont'
}
},
{ {
name: 'Mihail Dounaev', name: 'Mihail Dounaev',
location: 'Finland', location: 'Finland',
@ -74,15 +41,6 @@ export const teamMembers: Array<TeamMemberProps> = [
dribbble: 'https://dribbble.com/mmmint' dribbble: 'https://dribbble.com/mmmint'
} }
}, },
{
name: 'Jake Robinson',
location: 'United Kingdom',
role: 'Rust Engineer - Cryptographer',
imageUrl: '/images/team/jake.jpg',
socials: {
github: 'https://github.com/brxken128'
}
},
{ {
name: 'Ameer Al Ashhab', name: 'Ameer Al Ashhab',
location: 'Jordan', location: 'Jordan',

View file

@ -1,34 +0,0 @@
import Image from 'next/image';
import { tw } from '@sd/ui';
const AppFrameOuter = tw.div`relative m-auto flex w-full max-w-7xl rounded-lg transition-opacity px-4`;
const AppFrameInner = tw.div`z-30 flex w-full rounded-lg border-t border-app-line/50 backdrop-blur`;
const AppImage = () => {
return (
<div className="w-screen">
<div className="relative mx-auto max-w-full sm:w-full sm:max-w-[1400px]">
<div className="bloom burst bloom-one" />
<div className="bloom burst bloom-three" />
<div className="bloom burst bloom-two" />
</div>
<div className="fade-in-app-embed relative z-30 mt-8 h-[255px] w-full px-1 text-center sm:mt-16 sm:h-[428px] md:h-[428px] lg:h-[628px]">
<AppFrameOuter>
<AppFrameInner>
<Image
className="rounded-lg"
alt="spacedrive"
src="/images/app/1.webp"
loading="eager"
width={1278}
height={626}
quality={100}
/>
</AppFrameInner>
</AppFrameOuter>
</div>
</div>
);
};
export default AppImage;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -1,7 +1,7 @@
[package] [package]
name = "sd-mobile-android" name = "sd-mobile-android"
version = "0.1.0" version = "0.1.0"
rust-version = "1.64.0" rust-version = "1.64"
license = { workspace = true } license = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
@ -14,8 +14,8 @@ crate-type = ["cdylib"]
# Spacedrive Sub-crates # Spacedrive Sub-crates
sd-mobile-core = { path = "../../core" } sd-mobile-core = { path = "../../core" }
# FFI # Workspace dependencies
jni = "0.21.1"
# Other
tracing = { workspace = true } tracing = { workspace = true }
# Specific Mobile Android dependencies
jni = "0.21.1"

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