Merge remote-tracking branch 'origin' into c
|
@ -11,9 +11,11 @@ codegen
|
|||
Condvar
|
||||
dashmap
|
||||
davidmytton
|
||||
dayjs
|
||||
deel
|
||||
elon
|
||||
encryptor
|
||||
Exif
|
||||
Flac
|
||||
graps
|
||||
haden
|
||||
|
@ -59,7 +61,9 @@ storedkey
|
|||
stringly
|
||||
thumbstrips
|
||||
tobiaslutke
|
||||
tokio
|
||||
typecheck
|
||||
uuid
|
||||
vdfs
|
||||
vijay
|
||||
zacharysmith
|
||||
|
|
2
.gitattributes
vendored
|
@ -1,2 +1,4 @@
|
|||
pnpm-lock.yaml -diff
|
||||
package-lock.json -diff
|
||||
Cargo.lock -diff
|
||||
.github/actions/publish-artifacts/dist/index.js -diff
|
||||
|
|
39
.github/actions/publish-artifacts/dist/index.js
vendored
43
.github/actions/publish-artifacts/index.ts
vendored
|
@ -2,12 +2,13 @@ import client from '@actions/artifact';
|
|||
import * as core from '@actions/core';
|
||||
import * as glob from '@actions/glob';
|
||||
import * as io from '@actions/io';
|
||||
import { exists } from '@actions/io/lib/io-util';
|
||||
|
||||
type OS = 'darwin' | 'windows' | 'linux';
|
||||
type Arch = 'x64' | 'arm64';
|
||||
type TargetConfig = { bundle: string; ext: string };
|
||||
type BuildTarget = {
|
||||
updater: { bundle: string; bundleExt: string; archiveExt: string };
|
||||
updater: false | { bundle: string; bundleExt: string; archiveExt: string };
|
||||
standalone: Array<TargetConfig>;
|
||||
};
|
||||
|
||||
|
@ -29,15 +30,8 @@ const OS_TARGETS = {
|
|||
standalone: [{ ext: 'msi', bundle: 'msi' }]
|
||||
},
|
||||
linux: {
|
||||
updater: {
|
||||
bundle: 'appimage',
|
||||
bundleExt: 'AppImage',
|
||||
archiveExt: 'tar.gz'
|
||||
},
|
||||
standalone: [
|
||||
{ ext: 'deb', bundle: 'deb' },
|
||||
{ ext: 'AppImage', bundle: 'appimage' }
|
||||
]
|
||||
updater: false,
|
||||
standalone: [{ ext: 'deb', bundle: 'deb' }]
|
||||
}
|
||||
} satisfies Record<OS, BuildTarget>;
|
||||
|
||||
|
@ -50,19 +44,32 @@ const PROFILE = core.getInput('profile');
|
|||
const BUNDLE_DIR = `target/${TARGET}/${PROFILE}/bundle`;
|
||||
const ARTIFACTS_DIR = '.artifacts';
|
||||
const ARTIFACT_BASE = `Spacedrive-${OS}-${ARCH}`;
|
||||
const FRONT_END_BUNDLE = 'apps/desktop/dist.tar.xz';
|
||||
const UPDATER_ARTIFACT_NAME = `Spacedrive-Updater-${OS}-${ARCH}`;
|
||||
const FRONTEND_ARCHIVE_NAME = `Spacedrive-frontend-${OS}-${ARCH}`;
|
||||
|
||||
async function globFiles(pattern: string) {
|
||||
const globber = await glob.create(pattern);
|
||||
return await globber.glob();
|
||||
}
|
||||
|
||||
async function uploadUpdater({ bundle, 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 files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${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}`;
|
||||
|
||||
|
@ -81,7 +88,7 @@ async function uploadStandalone({ bundle, ext }: TargetConfig) {
|
|||
const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${ext}*`);
|
||||
|
||||
const standalonePath = files.find((file) => file.endsWith(ext));
|
||||
if (!standalonePath) return console.error(`Standalone path not found. Files: ${files}`);
|
||||
if (!standalonePath) throw new Error(`Standalone path not found. Files: ${files}`);
|
||||
|
||||
const artifactName = `${ARTIFACT_BASE}.${ext}`;
|
||||
const artifactPath = `${ARTIFACTS_DIR}/${artifactName}`;
|
||||
|
@ -95,10 +102,10 @@ async function run() {
|
|||
|
||||
const { updater, standalone } = OS_TARGETS[OS];
|
||||
|
||||
await uploadUpdater(updater);
|
||||
|
||||
for (const config of standalone) {
|
||||
await uploadStandalone(config);
|
||||
}
|
||||
await Promise.all([
|
||||
uploadUpdater(updater),
|
||||
uploadFrontend(),
|
||||
...standalone.map((config) => uploadStandalone(config))
|
||||
]);
|
||||
}
|
||||
run();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lint": "eslint . --cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/artifact": "^2.1.3",
|
||||
"@actions/artifact": "^2.1.7",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/glob": "^0.4.0",
|
||||
"@actions/io": "^1.1.3"
|
||||
|
|
6
.github/actions/setup-pnpm/action.yml
vendored
|
@ -8,14 +8,10 @@ inputs:
|
|||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8.x.x
|
||||
version: 9.0.6
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
|
24
.github/actions/setup-rust/action.yaml
vendored
|
@ -17,10 +17,9 @@ runs:
|
|||
steps:
|
||||
- name: Install Rust
|
||||
id: toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: IronCoreLabs/rust-toolchain@v1
|
||||
with:
|
||||
target: ${{ inputs.target }}
|
||||
toolchain: '1.75'
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Cache Rust Dependencies
|
||||
|
@ -35,27 +34,6 @@ runs:
|
|||
shell: bash
|
||||
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
|
||||
id: cache-prisma-restore
|
||||
uses: actions/cache/restore@v4
|
||||
|
|
10
.github/actions/setup-system/action.yml
vendored
|
@ -62,12 +62,14 @@ runs:
|
|||
shell: bash
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
dpkg -l | grep i386
|
||||
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386"
|
||||
sudo dpkg --remove-architecture i386
|
||||
set -eux
|
||||
if dpkg -l | grep i386; then
|
||||
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386" || true
|
||||
sudo dpkg --remove-architecture i386 || true
|
||||
fi
|
||||
|
||||
# https://github.com/actions/runner-images/issues/9546#issuecomment-2014940361
|
||||
sudo apt-get remove libunwind-*
|
||||
sudo apt-get remove libunwind-* || true
|
||||
|
||||
- name: Setup Rust and Dependencies
|
||||
uses: ./.github/actions/setup-rust
|
||||
|
|
13
.github/workflows/cache-factory.yaml
vendored
|
@ -40,18 +40,21 @@ jobs:
|
|||
target: x86_64-pc-windows-msvc
|
||||
# - host: windows-latest
|
||||
# target: aarch64-pc-windows-msvc
|
||||
- host: ubuntu-20.04
|
||||
- host: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
# - host: ubuntu-20.04
|
||||
# - host: ubuntu-22.04
|
||||
# target: x86_64-unknown-linux-musl
|
||||
# - host: ubuntu-20.04
|
||||
# - host: uubuntu-22.04
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
# - host: ubuntu-20.04
|
||||
# - host: ubuntu-22.04
|
||||
# target: aarch64-unknown-linux-musl
|
||||
# - host: ubuntu-20.04
|
||||
# - host: ubuntu-22.04
|
||||
# target: armv7-unknown-linux-gnueabihf
|
||||
name: 'Make Cache'
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
if: github.repository == 'spacedriveapp/spacedrive'
|
||||
permissions: {}
|
||||
timeout-minutes: 150 # 2.5 hours
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
|
|
95
.github/workflows/ci.yml
vendored
|
@ -20,8 +20,10 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
typescript:
|
||||
name: TypeScript
|
||||
runs-on: ubuntu-latest
|
||||
name: Type and style check
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 7
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
@ -34,9 +36,20 @@ jobs:
|
|||
- name: Perform typechecks
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Perform style check
|
||||
run: |-
|
||||
set -eux
|
||||
pnpm autoformat only-frontend
|
||||
if [ -n "$(git diff --name-only --cached)" ]; then
|
||||
echo "Some files are not correctly formatted. Please run 'pnpm autoformat' and commit the changes." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions: {}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
@ -52,6 +65,8 @@ jobs:
|
|||
cypress:
|
||||
name: Cypress
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 45
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
@ -67,6 +82,13 @@ jobs:
|
|||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Cypress
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
pnpm exec cypress install
|
||||
rm -rf /Users/runner/.cache/Cypress
|
||||
ln -sf /Users/runner/Library/Caches/Cypress /Users/runner/.cache/Cypress
|
||||
|
||||
- name: Setup Cypress
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
|
@ -84,14 +106,16 @@ jobs:
|
|||
command: env CI=true pnpm test:e2e
|
||||
working-directory: apps/web
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- name: Upload cypress screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: apps/web/cypress/screenshots
|
||||
if-no-files-found: ignore
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- name: Upload cypress video's
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: cypress-videos
|
||||
|
@ -100,7 +124,10 @@ jobs:
|
|||
|
||||
rustfmt:
|
||||
name: Rust Formatting
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
|
@ -118,12 +145,17 @@ jobs:
|
|||
shell: powershell
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path C:\spacedrive_target
|
||||
- name: Symlink target to C:\
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path target -ItemType Junction -Value C:\spacedrive_target
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
- name: Check if files have changed
|
||||
uses: dorny/paths-filter@v3
|
||||
continue-on-error: true
|
||||
id: filter
|
||||
with:
|
||||
|
@ -151,11 +183,23 @@ jobs:
|
|||
run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.platform }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
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:
|
||||
- name: Maximize build space
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
|
@ -178,7 +222,8 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
- name: Find files that have changed
|
||||
uses: dorny/paths-filter@v3
|
||||
continue-on-error: true
|
||||
id: filter
|
||||
with:
|
||||
|
@ -196,6 +241,7 @@ jobs:
|
|||
- 'extensions/*/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
- name: Setup System and Rust
|
||||
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||
|
@ -205,22 +251,15 @@ jobs:
|
|||
|
||||
- name: Run Clippy
|
||||
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||
uses: actions-rs-plus/clippy-check@v2
|
||||
uses: giraffate/clippy-action@v1
|
||||
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: Test (${{ matrix.platform }})
|
||||
# runs-on: ${{ matrix.platform }}
|
||||
# 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
|
||||
# - name: Run tests
|
||||
# if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||
# run: cargo test --workspace --all-features --locked --target ${{ matrix.settings.target }}
|
||||
|
|
28
.github/workflows/release.yml
vendored
|
@ -1,8 +1,10 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/release.yml'
|
||||
workflow_dispatch:
|
||||
# NOTE: For Linux builds, we can only build with Ubuntu. It should be the oldest base system we intend to support. See PR-759 & https://tauri.app/v1/guides/building/linux for reference.
|
||||
|
||||
# From: https://github.com/rust-lang/rust-analyzer/blob/master/.github/workflows/release.yaml#L13-L21
|
||||
env:
|
||||
|
@ -32,17 +34,17 @@ jobs:
|
|||
arch: x86_64
|
||||
# - host: windows-latest
|
||||
# target: aarch64-pc-windows-msvc
|
||||
- host: ubuntu-20.04
|
||||
- host: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
bundles: deb
|
||||
os: linux
|
||||
arch: x86_64
|
||||
# - host: ubuntu-20.04
|
||||
# - host: ubuntu-22.04
|
||||
# target: x86_64-unknown-linux-musl
|
||||
# - host: ubuntu-20.04
|
||||
# - host: ubuntu-22.04
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
# bundles: deb
|
||||
# - host: ubuntu-20.04
|
||||
# - host: ubuntu-22.04
|
||||
# target: aarch64-unknown-linux-musl
|
||||
name: Desktop - Main ${{ matrix.settings.target }}
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
|
@ -101,8 +103,8 @@ jobs:
|
|||
run: |
|
||||
pnpm tauri build --ci -v --target ${{ matrix.settings.target }} --bundles ${{ matrix.settings.bundles }},updater
|
||||
env:
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
|
@ -112,15 +114,11 @@ jobs:
|
|||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Build AppImage in Docker
|
||||
if: ${{ runner.os == 'Linux' && ( matrix.settings.target == 'x86_64-unknown-linux-gnu' || matrix.settings.target == 'aarch64-unknown-linux-gnu' ) }}
|
||||
- name: Package frontend
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
docker run --rm -v $(pwd):/srv -e 'CI=true' -e 'TARGET=${{ matrix.settings.target }}' -w /srv debian:bookworm scripts/appimage/build_appimage.sh
|
||||
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"
|
||||
set -eux
|
||||
XZ_OPT='-T0 -7' tar -cJf apps/desktop/dist.tar.xz -C apps/desktop/dist .
|
||||
|
||||
- name: Publish Artifacts
|
||||
uses: ./.github/actions/publish-artifacts
|
||||
|
|
56
.github/workflows/server.yml
vendored
|
@ -3,6 +3,10 @@ name: Server release
|
|||
on:
|
||||
release:
|
||||
types: [published]
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/server.yml'
|
||||
- 'apps/server/docker/*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
@ -27,16 +31,56 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Homebrew
|
||||
id: set-up-homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
|
||||
- name: Update Podman & crun
|
||||
run: |
|
||||
sudo apt-get remove crun podman
|
||||
brew install crun podman
|
||||
|
||||
- name: Update buildah
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
wget -O- 'https://github.com/HeavenVolkoff/buildah-static/releases/latest/download/buildah-amd64.tar.gz' \
|
||||
| sudo tar -xzf- -C /usr/local/bin
|
||||
|
||||
- name: Install netavark
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
sudo mkdir -p /usr/local/lib/podman
|
||||
sudo wget -O- 'https://github.com/containers/netavark/releases/latest/download/netavark.gz' \
|
||||
| gunzip | sudo dd status=none of=/usr/local/lib/podman/netavark
|
||||
sudo chmod +x /usr/local/lib/podman/netavark
|
||||
|
||||
- name: Install passt
|
||||
shell: bash
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
deb="$(
|
||||
curl -SsL https://passt.top/builds/latest/x86_64 \
|
||||
| grep -oP 'passt[^\.<>'\''"]+\.deb' | sort -u | head -n1
|
||||
)"
|
||||
|
||||
curl -SsJLO "https://passt.top/builds/latest/x86_64/${deb}"
|
||||
sudo dpkg -i "${deb}"
|
||||
|
||||
- name: Basic sanity check
|
||||
run: |
|
||||
crun --version
|
||||
podman version
|
||||
buildah --version
|
||||
|
||||
- name: Determine image name & tag
|
||||
id: image_info
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
if [ "$GITHUB_EVENT_NAME" == "release" ]; then
|
||||
IMAGE_TAG="${GITHUB_REF##*/}"
|
||||
else
|
||||
|
@ -52,10 +96,14 @@ jobs:
|
|||
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
echo "repo_ref=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64'
|
||||
|
||||
- name: Build image
|
||||
id: build-image
|
||||
# TODO: Change to stable version when available
|
||||
uses: redhat-actions/buildah-build@c79846fb306beeba490e89fb75d2d1af95831e79
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
tags: ${{ steps.image_info.outputs.tag }} ${{ github.event_name == 'release' && 'latest' || 'staging' }}
|
||||
archs: amd64
|
||||
|
@ -69,9 +117,7 @@ jobs:
|
|||
./apps/server/docker/Dockerfile
|
||||
|
||||
- name: Push image to ghcr.io
|
||||
# TODO: Restore redhat-actions/push-to-registry after PR is merged:
|
||||
# https://github.com/redhat-actions/push-to-registry/pull/93
|
||||
uses: Eusebiotrigo/push-to-registry@5acfa470857b62a053884f7214581d55ffeb54ac
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
|
|
3
.npmrc
|
@ -1,10 +1,9 @@
|
|||
; make all engine requirements (e.g. node version) strictly kept
|
||||
engine-strict=true
|
||||
; tempfix for pnpm#5909: https://github.com/pnpm/pnpm/issues/5909#issuecomment-1397758156
|
||||
prefer-symlinked-executables=false
|
||||
; necessary for metro + mobile
|
||||
strict-peer-dependencies=false
|
||||
node-linker=hoisted
|
||||
auto-install-peers=true
|
||||
max-old-space-size=4096
|
||||
enable-pre-post-scripts=true
|
||||
package-manager-strict=false
|
||||
|
|
115
.vscode/i18n-ally-reviews.yml
vendored
Normal file
|
@ -0,0 +1,115 @@
|
|||
# Review comments generated by i18n-ally. Please commit this file.
|
||||
|
||||
reviews:
|
||||
about_vision_text:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: OS2GadFYJi0w8WbQ1KpUe
|
||||
type: approve
|
||||
comment: 疑似翻译腔。这个地方不太好译。
|
||||
time: '2024-04-16T02:03:55.931Z'
|
||||
all_jobs_have_been_cleared:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: hwThsx7VP-THpRXov2MB6
|
||||
type: comment
|
||||
comment: 要不要把“清除”改为“完成”?
|
||||
time: '2024-04-16T10:56:22.929Z'
|
||||
archive_info:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: pW79_SMSNiOyRj94kdSZO
|
||||
type: comment
|
||||
comment: 不太通顺。“位置”是否要加定语修饰?
|
||||
time: '2024-04-16T11:03:10.218Z'
|
||||
changelog_page_description:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: JN3YruMypxX5wuaMjD8Hu
|
||||
type: comment
|
||||
comment: 口语化显得更自然些。
|
||||
time: '2024-04-16T11:05:27.478Z'
|
||||
clouds:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: ebAW-cnfA4llVgee6CRmF
|
||||
type: comment
|
||||
comment: 一个字太少。
|
||||
time: '2024-04-16T11:06:06.594Z'
|
||||
coordinates:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: HJLIcCmrHV1ZwCsAJOSiS
|
||||
type: comment
|
||||
comment: 有可能应该改成“地理坐标”。
|
||||
time: '2024-04-16T11:07:21.331Z'
|
||||
create_library_description:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: N01f9vhjfYidHDnkhVV4o
|
||||
type: comment
|
||||
comment: >-
|
||||
“libraries are
|
||||
databases”这一句并不容易翻译,这里把英文原文放上去的方式我觉得并不妥当,但是我想不到更好的译法了。定语往后放到谓语的位置。同时添加必要的助词。
|
||||
time: '2024-04-16T11:13:48.568Z'
|
||||
create_new_library_description:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: Wb89DhKwsCB9vGBDUIgsj
|
||||
type: comment
|
||||
comment: 见“create_library_description”。
|
||||
time: '2024-04-16T11:14:21.837Z'
|
||||
creating_your_library:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: 6q9xmFoeVizgSTBbBey9O
|
||||
type: comment
|
||||
comment: “您的库”是典型的翻译腔。
|
||||
time: '2024-04-16T11:15:52.949Z'
|
||||
delete_warning:
|
||||
locales:
|
||||
zh-CN:
|
||||
comments:
|
||||
- user:
|
||||
name: Heavysnowjakarta
|
||||
email: heavysnowjakarta@gmail.com
|
||||
id: 5oa5lvp8PkJDRceIenfne
|
||||
type: comment
|
||||
comment: 我不确定 `{{type}}` 是中文还是英文。如果是英文,前面应该加空格。
|
||||
time: '2024-04-16T11:24:52.250Z'
|
4
.vscode/launch.json
vendored
|
@ -11,9 +11,9 @@
|
|||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--profile=dev-debug",
|
||||
"--manifest-path=./apps/desktop/src-tauri/Cargo.toml",
|
||||
"--no-default-features",
|
||||
"--features=ai-models"
|
||||
"--no-default-features"
|
||||
],
|
||||
"problemMatcher": "$rustc"
|
||||
},
|
||||
|
|
3
.vscode/tasks.json
vendored
|
@ -56,8 +56,7 @@
|
|||
"command": "run",
|
||||
"args": [
|
||||
"--manifest-path=./apps/desktop/src-tauri/Cargo.toml",
|
||||
"--no-default-features",
|
||||
"--features=ai-models"
|
||||
"--no-default-features"
|
||||
],
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "short"
|
||||
|
|
|
@ -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:
|
||||
|
||||
- Rust version: **1.75**
|
||||
- Rust version: **1.78**
|
||||
- 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.
|
||||
|
||||
Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours.
|
||||
|
||||
After you finish making your changes and committed them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code.
|
||||
|
||||
##### Mobile App
|
||||
|
||||
To run the mobile app:
|
||||
|
@ -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.
|
||||
- `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
|
||||
|
||||
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`.
|
||||
|
||||
#### `ModuleNotFoundError: No module named 'distutils'`
|
||||
### Translations
|
||||
|
||||
If you run into this issue, or some other error involving `node-gyp`:
|
||||
|
||||
```
|
||||
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).
|
||||
Check out the [i18n README](interface/locales/README.md) for more information on how to contribute to translations.
|
||||
|
||||
### Credits
|
||||
|
||||
|
|
6054
Cargo.lock
generated
189
Cargo.toml
|
@ -1,18 +1,18 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"core",
|
||||
"core/crates/*",
|
||||
"crates/*",
|
||||
"apps/cli",
|
||||
"apps/p2p-relay",
|
||||
"apps/desktop/src-tauri",
|
||||
"apps/desktop/crates/*",
|
||||
"apps/mobile/modules/sd-core/core",
|
||||
"apps/mobile/modules/sd-core/android/crate",
|
||||
"apps/mobile/modules/sd-core/ios/crate",
|
||||
"apps/server",
|
||||
"core",
|
||||
"core/crates/*",
|
||||
"crates/*",
|
||||
"apps/deps-generator",
|
||||
"apps/desktop/src-tauri",
|
||||
"apps/desktop/crates/*",
|
||||
"apps/mobile/modules/sd-core/core",
|
||||
"apps/mobile/modules/sd-core/android/crate",
|
||||
"apps/mobile/modules/sd-core/ios/crate",
|
||||
"apps/server",
|
||||
]
|
||||
exclude = ["crates/crypto"]
|
||||
|
||||
[workspace.package]
|
||||
license = "AGPL-3.0-only"
|
||||
|
@ -20,86 +20,78 @@ edition = "2021"
|
|||
repository = "https://github.com/spacedriveapp/spacedrive"
|
||||
|
||||
[workspace.dependencies]
|
||||
# First party dependencies
|
||||
prisma-client-rust = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "f99d6f5566570f3ab1edecb7a172ad25b03d95af", features = [
|
||||
"specta",
|
||||
"sqlite-create-many",
|
||||
"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
|
||||
anyhow = "1.0.75"
|
||||
async-channel = "2.0.0"
|
||||
async-trait = "0.1.77"
|
||||
axum = "=0.6.20"
|
||||
base64 = "0.21.5"
|
||||
blake3 = "1.5.0"
|
||||
chrono = "0.4.31"
|
||||
clap = "4.4.7"
|
||||
async-channel = "2.3"
|
||||
async-trait = "0.1.80"
|
||||
axum = "0.6.20" # Update blocked by hyper
|
||||
base64 = "0.22.1"
|
||||
base91 = "0.1.0"
|
||||
blake3 = "1.5.0" # Update blocked by custom patch below
|
||||
chrono = "0.4.38"
|
||||
directories = "5.0"
|
||||
ed25519-dalek = "2.1.1"
|
||||
futures = "0.3.30"
|
||||
futures-concurrency = "7.4.3"
|
||||
globset = "^0.4.13"
|
||||
hex = "0.4.3"
|
||||
http = "0.2.9"
|
||||
image = "0.24.7"
|
||||
itertools = "0.12.0"
|
||||
lending-stream = "1.0.0"
|
||||
normpath = "1.1.1"
|
||||
once_cell = "1.18.0"
|
||||
pin-project-lite = "0.2.13"
|
||||
futures-concurrency = "7.6"
|
||||
gix-ignore = "0.11.2"
|
||||
globset = "0.4.14"
|
||||
http = "0.2" # Update blocked by axum
|
||||
hyper = "0.14" # Update blocked due to API breaking changes
|
||||
image = "0.25.1"
|
||||
itertools = "0.13.0"
|
||||
lending-stream = "1.0"
|
||||
libc = "0.2"
|
||||
normpath = "1.2"
|
||||
once_cell = "1.19"
|
||||
pin-project-lite = "0.2.14"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
regex = "1.10.2"
|
||||
reqwest = "0.11.22"
|
||||
rmp-serde = "1.1.2"
|
||||
rmpv = { version = "^1.0.1", features = ["with-serde"] }
|
||||
regex = "1.10"
|
||||
reqwest = "0.11" # Update blocked by hyper
|
||||
rmp = "0.8.14"
|
||||
rmp-serde = "1.3.0"
|
||||
rmpv = { version = "1.3", features = ["with-serde"] }
|
||||
rspc = "0.1.4"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
strum = "0.25"
|
||||
strum_macros = "0.25"
|
||||
tempfile = "3.8.1"
|
||||
thiserror = "1.0.50"
|
||||
tokio = "1.36.0"
|
||||
tokio-stream = "0.1.14"
|
||||
tokio-util = "0.7.10"
|
||||
uhlc = "=0.5.2"
|
||||
uuid = "1.5.0"
|
||||
webp = "0.2.6"
|
||||
specta = "=2.0.0-rc.11"
|
||||
static_assertions = "1.1"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
tempfile = "3.10"
|
||||
thiserror = "1.0"
|
||||
tokio = "1.38"
|
||||
tokio-stream = "0.1.15"
|
||||
tokio-util = "0.7.11"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing-test = "0.2.5"
|
||||
uhlc = "0.6.0" # Must follow version used by specta
|
||||
uuid = "1.8"
|
||||
webp = "0.3.0"
|
||||
|
||||
[workspace.dev-dependencies]
|
||||
tracing-test = { version = "^0.2.4" }
|
||||
[workspace.dependencies.prisma-client-rust]
|
||||
git = "https://github.com/brendonovich/prisma-client-rust"
|
||||
rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab"
|
||||
features = ["migrations", "specta", "sqlite", "sqlite-create-many"]
|
||||
default-features = false
|
||||
|
||||
[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]
|
||||
# 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
|
||||
rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "f3347e2e8bfe3f37bfacc437ca329fe71cdcb048" }
|
||||
|
||||
# `cursor_position` method
|
||||
tauri = { git = "https://github.com/spacedriveapp/tauri.git", rev = "8409af71a83d631ff9d1cd876c441a57511a1cbd" }
|
||||
tao = { git = "https://github.com/spacedriveapp/tao", rev = "7880adbc090402c44fbcf006669458fa82623403" }
|
||||
rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "ab12964b140991e0730c3423693533fba71efb03" }
|
||||
|
||||
# Add `Control::open_stream_with_addrs`
|
||||
libp2p = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev = "a005656df7e82059a0eb2e333ebada4731d23f8c" }
|
||||
|
@ -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-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]
|
||||
# Make compilation faster on macOS
|
||||
split-debuginfo = "unpacked"
|
||||
opt-level = 0
|
||||
debug = 0
|
||||
strip = "none"
|
||||
lto = false
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
|
||||
[profile.dev-debug]
|
||||
inherits = "dev"
|
||||
# Enables debugger
|
||||
split-debuginfo = "none"
|
||||
opt-level = 0
|
||||
debug = "full"
|
||||
strip = "none"
|
||||
lto = "off"
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
|
||||
# Set the settings for build scripts and proc-macros.
|
||||
[profile.dev.build-override]
|
||||
|
@ -123,3 +133,18 @@ opt-level = 3
|
|||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
incremental = false
|
||||
|
||||
# Set the default for dependencies, except workspace members.
|
||||
[profile.dev-debug.package."*"]
|
||||
inherits = "dev"
|
||||
opt-level = 3
|
||||
debug = "full"
|
||||
incremental = false
|
||||
|
||||
# Optimize release builds
|
||||
[profile.release]
|
||||
panic = "abort" # Strip expensive panic clean-up logic
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
lto = true # Enables link to optimizations
|
||||
opt-level = "s" # Optimize for binary size
|
||||
strip = true # Remove debug symbols
|
||||
|
|
|
@ -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"
|
|
@ -1,4 +0,0 @@
|
|||
# CLI
|
||||
|
||||
Basic CLI for interacting with encrypted files.
|
||||
Will be expanded to a general Spacedrive CLI in the future.
|
|
@ -1,85 +0,0 @@
|
|||
use anyhow::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)
|
||||
// };
|
||||
// });
|
||||
// }
|
|
@ -8,10 +8,12 @@ repository = { workspace = true }
|
|||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
reqwest = { workspace = true, features = ["blocking"] }
|
||||
# Workspace dependencies
|
||||
reqwest = { workspace = true, features = ["blocking", "native-tls-vendored"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# Specific Deps Generator dependencies
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
cargo_metadata = "0.18.1"
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 70 KiB |
|
@ -6,10 +6,11 @@ repository = { workspace = true }
|
|||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
libc = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
wgpu = { version = "0.20.0", default-features = false }
|
||||
# WARNING: gtk should follow the same version used by tauri
|
||||
# https://github.com/tauri-apps/tauri/blob/441eb4f4a5f9af206752c2e287975eb8d5ccfd01/core/tauri/Cargo.toml#L95
|
||||
gtk = { version = "0.15", features = [ "v3_20" ] }
|
||||
# https://github.com/tauri-apps/tauri/blob/tauri-v2.0.0-beta.17/core/tauri/Cargo.toml#L85C1-L85C51
|
||||
gtk = { version = "0.18", features = ["v3_24"] }
|
||||
|
|
|
@ -1,32 +1,15 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use gtk::{
|
||||
gio::{
|
||||
content_type_guess,
|
||||
prelude::AppInfoExt,
|
||||
prelude::{AppLaunchContextExt, FileExt},
|
||||
AppInfo, AppLaunchContext, DesktopAppInfo, File as GioFile, ResourceError,
|
||||
content_type_guess, prelude::AppInfoExt, prelude::FileExt, AppInfo, AppLaunchContext,
|
||||
DesktopAppInfo, File as GioFile, ResourceError,
|
||||
},
|
||||
glib::error::Error as GlibError,
|
||||
prelude::IsA,
|
||||
};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::env::remove_prefix_from_pathlist;
|
||||
|
||||
fn remove_prefix_from_env_in_ctx(
|
||||
ctx: &impl IsA<AppLaunchContext>,
|
||||
env_name: &str,
|
||||
prefix: &impl AsRef<Path>,
|
||||
) {
|
||||
if let Some(value) = remove_prefix_from_pathlist(env_name, prefix) {
|
||||
ctx.setenv(env_name, value);
|
||||
} else {
|
||||
ctx.unsetenv(env_name);
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LAUNCH_CTX: AppLaunchContext = {
|
||||
// TODO: Display supports requires GDK, which can only run on the main thread
|
||||
|
@ -36,33 +19,8 @@ thread_local! {
|
|||
// "This is an Glib type conversion, it should never fail because GDKAppLaunchContext is a subclass of AppLaunchContext"
|
||||
// )).unwrap_or_default();
|
||||
|
||||
let ctx = AppLaunchContext::default();
|
||||
|
||||
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) {
|
||||
// Remove AppImage paths from environment variables to avoid external applications attempting to use the AppImage's libraries
|
||||
// https://github.com/AppImage/AppImageKit/blob/701b711f42250584b65a88f6427006b1d160164d/src/AppRun.c#L168-L194
|
||||
ctx.unsetenv("PYTHONHOME");
|
||||
ctx.unsetenv("GTK_DATA_PREFIX");
|
||||
ctx.unsetenv("GTK_THEME");
|
||||
ctx.unsetenv("GDK_BACKEND");
|
||||
ctx.unsetenv("GTK_EXE_PREFIX");
|
||||
ctx.unsetenv("GTK_IM_MODULE_FILE");
|
||||
ctx.unsetenv("GDK_PIXBUF_MODULE_FILE");
|
||||
|
||||
remove_prefix_from_env_in_ctx(&ctx, "PATH", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "LD_LIBRARY_PATH", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "PYTHONPATH", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "XDG_DATA_DIRS", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "PERLLIB", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "GSETTINGS_SCHEMA_DIR", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "QT_PLUGIN_PATH", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH_1_0", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "GTK_PATH", &appdir);
|
||||
remove_prefix_from_env_in_ctx(&ctx, "GIO_EXTRA_MODULES", &appdir);
|
||||
}
|
||||
|
||||
ctx
|
||||
AppLaunchContext::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
ffi::{CStr, OsStr, OsString},
|
||||
io, mem,
|
||||
ffi::{CStr, OsStr},
|
||||
mem,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
ptr,
|
||||
};
|
||||
|
||||
fn version(version_str: &str) -> i32 {
|
||||
let mut version_parts: Vec<i32> = version_str
|
||||
.split('.')
|
||||
.take(4) // Take up to 4 components
|
||||
.map(|part| part.parse().unwrap_or(0))
|
||||
.collect();
|
||||
|
||||
// Pad with zeros if needed
|
||||
version_parts.resize_with(4, Default::default);
|
||||
|
||||
(version_parts[0] * 1_000_000_000)
|
||||
+ (version_parts[1] * 1_000_000)
|
||||
+ (version_parts[2] * 1_000)
|
||||
+ version_parts[3]
|
||||
}
|
||||
|
||||
pub fn get_current_user_home() -> Option<PathBuf> {
|
||||
use libc::{getpwuid_r, getuid, passwd, ERANGE};
|
||||
|
||||
|
@ -192,74 +176,12 @@ pub fn normalize_environment() {
|
|||
)
|
||||
.expect("PATH must be successfully normalized");
|
||||
|
||||
if let Ok(appdir) = get_appdir() {
|
||||
println!("Running from APPIMAGE");
|
||||
|
||||
// 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");
|
||||
if has_nvidia() {
|
||||
// Workaround for: https://github.com/tauri-apps/tauri/issues/9304
|
||||
env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_prefix_from_pathlist(
|
||||
env_name: &str,
|
||||
prefix: &impl AsRef<Path>,
|
||||
) -> Option<OsString> {
|
||||
env::var_os(env_name).and_then(|value| {
|
||||
let mut dirs = env::split_paths(&value)
|
||||
.filter(|dir| !(dir.as_os_str().is_empty() || dir.starts_with(prefix)))
|
||||
.peekable();
|
||||
|
||||
if dirs.peek().is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(env::join_paths(dirs).expect("Should not fail because we are only filtering a pathlist retrieved from the environmnet"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check if snap by looking if SNAP is set and not empty and that the SNAP directory exists
|
||||
pub fn is_snap() -> bool {
|
||||
if let Some(snap) = std::env::var_os("SNAP") {
|
||||
|
@ -271,21 +193,6 @@ pub fn is_snap() -> bool {
|
|||
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
|
||||
pub fn is_flatpak() -> bool {
|
||||
if let Some(flatpak_id) = std::env::var_os("FLATPAK_ID") {
|
||||
|
@ -296,3 +203,31 @@ pub fn is_flatpak() -> bool {
|
|||
|
||||
false
|
||||
}
|
||||
|
||||
fn has_nvidia() -> bool {
|
||||
use wgpu::{
|
||||
Backends, DeviceType, Dx12Compiler, Gles3MinorVersion, Instance, InstanceDescriptor,
|
||||
InstanceFlags,
|
||||
};
|
||||
|
||||
let instance = Instance::new(InstanceDescriptor {
|
||||
flags: InstanceFlags::empty(),
|
||||
backends: Backends::VULKAN | Backends::GL,
|
||||
gles_minor_version: Gles3MinorVersion::Automatic,
|
||||
dx12_shader_compiler: Dx12Compiler::default(),
|
||||
});
|
||||
for adapter in instance.enumerate_adapters(Backends::all()) {
|
||||
let info = adapter.get_info();
|
||||
match info.device_type {
|
||||
DeviceType::DiscreteGpu | DeviceType::IntegratedGpu | DeviceType::VirtualGpu => {
|
||||
// Nvidia PCI id
|
||||
if info.vendor == 0x10de {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
|
|
@ -4,4 +4,4 @@ mod app_info;
|
|||
mod env;
|
||||
|
||||
pub use app_info::{list_apps_associated_with_ext, open_file_path, open_files_path_with};
|
||||
pub use env::{get_current_user_home, is_appimage, is_flatpak, is_snap, normalize_environment};
|
||||
pub use env::{get_current_user_home, is_flatpak, is_snap, normalize_environment};
|
||||
|
|
|
@ -5,10 +5,8 @@ license = { workspace = true }
|
|||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
swift-rs = { workspace = true, features = ["serde"] }
|
||||
swift-rs = { version = "1.0.6", features = ["serde"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
swift-rs = { workspace = true, features = ["build"] }
|
||||
swift-rs = { version = "1.0.6", features = ["build"] }
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import AppKit
|
||||
import SwiftRs
|
||||
|
||||
@objc
|
||||
public enum AppThemeType: Int {
|
||||
|
@ -7,6 +8,34 @@ public enum AppThemeType: Int {
|
|||
case dark = 1
|
||||
}
|
||||
|
||||
var activity: NSObjectProtocol?
|
||||
|
||||
@_cdecl("disable_app_nap")
|
||||
public func disableAppNap(reason: SRString) -> Bool {
|
||||
// Check if App Nap is already disabled
|
||||
guard activity == nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
activity = ProcessInfo.processInfo.beginActivity(
|
||||
options: .userInitiatedAllowingIdleSystemSleep,
|
||||
reason: reason.toString()
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@_cdecl("enable_app_nap")
|
||||
public func enableAppNap() -> Bool {
|
||||
// Check if App Nap is already enabled
|
||||
guard let pinfo = activity else {
|
||||
return false
|
||||
}
|
||||
|
||||
ProcessInfo.processInfo.endActivity(pinfo)
|
||||
activity = nil
|
||||
return true
|
||||
}
|
||||
|
||||
@_cdecl("lock_app_theme")
|
||||
public func lockAppTheme(themeType: AppThemeType) {
|
||||
var theme: NSAppearance?
|
||||
|
@ -30,20 +59,6 @@ public func lockAppTheme(themeType: AppThemeType) {
|
|||
}
|
||||
}
|
||||
|
||||
@_cdecl("blur_window_background")
|
||||
public func blurWindowBackground(window: NSWindow) {
|
||||
let windowContent = window.contentView!
|
||||
let blurryView = NSVisualEffectView()
|
||||
|
||||
blurryView.material = .sidebar
|
||||
blurryView.state = .followsWindowActiveState
|
||||
blurryView.blendingMode = .behindWindow
|
||||
blurryView.wantsLayer = true
|
||||
|
||||
window.contentView = blurryView
|
||||
blurryView.addSubview(windowContent)
|
||||
}
|
||||
|
||||
@_cdecl("set_titlebar_style")
|
||||
public func setTitlebarStyle(window: NSWindow, fullScreen: Bool) {
|
||||
// this results in far less visual artifacts if we just manage it ourselves (the native taskbar re-appears when fullscreening/un-fullscreening)
|
||||
|
|
|
@ -9,11 +9,10 @@ pub enum AppThemeType {
|
|||
Dark = 1 as Int,
|
||||
}
|
||||
|
||||
swift!(pub fn disable_app_nap(reason: &SRString) -> Bool);
|
||||
swift!(pub fn enable_app_nap() -> Bool);
|
||||
swift!(pub fn lock_app_theme(theme_type: Int));
|
||||
swift!(pub fn blur_window_background(window: &NSObject));
|
||||
swift!(pub fn set_titlebar_style(window: &NSObject, is_fullscreen: Bool));
|
||||
// swift!(pub fn setup_disk_watcher(window: &NSObject, transparent: Bool, large: Bool));
|
||||
// swift!(pub fn disk_event_callback(mounted: Bool, path: &SRString));
|
||||
swift!(pub fn reload_webview(webview: &NSObject));
|
||||
|
||||
#[repr(C)]
|
||||
|
@ -31,20 +30,3 @@ pub fn open_file_paths_with(file_urls: &[String], with_url: &str) {
|
|||
let file_url = file_urls.join("\0");
|
||||
unsafe { open_file_path_with(&file_url.as_str().into(), &with_url.into()) }
|
||||
}
|
||||
|
||||
// main!(|_| {
|
||||
// unsafe { setup_disk_watcher() };
|
||||
// print!("Waiting for disk events... ");
|
||||
// Ok(())
|
||||
// });
|
||||
|
||||
// #[no_mangle]
|
||||
// pub extern "C" fn disk_event_callback(mounted: Bool, path: *const SRString) {
|
||||
// let mounted_str = if mounted { "mounted" } else { "unmounted" };
|
||||
|
||||
// // Convert the raw pointer to a reference
|
||||
// let path_ref = unsafe { &*path };
|
||||
// let path_str = path_ref.to_string(); // Assuming SRString has a to_string method
|
||||
|
||||
// println!("Disk at path {} was {}", path_str, mounted_str);
|
||||
// }
|
||||
|
|
|
@ -6,10 +6,10 @@ repository = { workspace = true }
|
|||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
libc = { workspace = true }
|
||||
normpath = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.51"
|
||||
version = "0.57"
|
||||
features = ["Win32_UI_Shell", "Win32_Foundation", "Win32_System_Com"]
|
||||
|
|
|
@ -10,6 +10,7 @@ use normpath::PathExt;
|
|||
use windows::{
|
||||
core::{HSTRING, PCWSTR},
|
||||
Win32::{
|
||||
Foundation::E_FAIL,
|
||||
System::Com::{
|
||||
CoInitializeEx, CoUninitialize, IDataObject, COINIT_APARTMENTTHREADED,
|
||||
COINIT_DISABLE_OLE1DDE,
|
||||
|
@ -97,11 +98,15 @@ pub fn open_file_path_with(path: impl AsRef<Path>, url: &str) -> Result<()> {
|
|||
ensure_com_initialized();
|
||||
let path = path.as_ref();
|
||||
|
||||
let ext = path.extension().ok_or(Error::OK)?;
|
||||
let ext = path
|
||||
.extension()
|
||||
.ok_or(Error::new(E_FAIL, "No file extension"))?;
|
||||
for handler in list_apps_associated_with_ext(ext)?.iter() {
|
||||
let name = unsafe { handler.GetName()?.to_string()? };
|
||||
if name == url {
|
||||
let path = path.normalize_virtually().map_err(|_| Error::OK)?;
|
||||
let path = path
|
||||
.normalize_virtually()
|
||||
.map_err(|e| Error::new(E_FAIL, e.to_string()))?;
|
||||
let wide_path = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
|
@ -116,5 +121,8 @@ pub fn open_file_path_with(path: impl AsRef<Path>, url: &str) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
Err(Error::OK)
|
||||
Err(Error::new(
|
||||
E_FAIL,
|
||||
"No available handler for the given path",
|
||||
))
|
||||
}
|
||||
|
|
|
@ -20,7 +20,10 @@
|
|||
"@sd/ui": "workspace:*",
|
||||
"@t3-oss/env-core": "^0.7.1",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tauri-apps/api": "1.5.1",
|
||||
"@tauri-apps/api": "next",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-os": "2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-beta.3",
|
||||
"consistent-hash": "^1.2.2",
|
||||
"immer": "^10.0.3",
|
||||
"react": "^18.2.0",
|
||||
|
@ -31,7 +34,7 @@
|
|||
"devDependencies": {
|
||||
"@sd/config": "workspace:*",
|
||||
"@sentry/vite-plugin": "^2.16.0",
|
||||
"@tauri-apps/cli": "^1.5.11",
|
||||
"@tauri-apps/cli": "next",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"sass": "^1.72.0",
|
||||
|
|
1
apps/desktop/src-tauri/.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
gen/
|
||||
WixTools
|
||||
*.dll
|
||||
*.dll.*
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "sd-desktop"
|
||||
version = "0.2.11"
|
||||
version = "0.3.2"
|
||||
description = "The universal file manager."
|
||||
authors = ["Spacedrive Technology Inc <support@spacedrive.com>"]
|
||||
default-run = "sd-desktop"
|
||||
|
@ -10,59 +10,68 @@ edition = { workspace = true }
|
|||
|
||||
[dependencies]
|
||||
# Spacedrive Sub-crates
|
||||
sd-core = { path = "../../../core", features = [
|
||||
"ffmpeg",
|
||||
"heif",
|
||||
] }
|
||||
sd-core = { path = "../../../core", features = ["ffmpeg", "heif"] }
|
||||
sd-fda = { path = "../../../crates/fda" }
|
||||
sd-prisma = { path = "../../../crates/prisma" }
|
||||
|
||||
# Workspace dependencies
|
||||
axum = { workspace = true, features = ["headers", "query"] }
|
||||
hyper = "0.14.28"
|
||||
directories = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hyper = { workspace = true }
|
||||
http = { workspace = true }
|
||||
prisma-client-rust = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rspc = { workspace = true, features = ["tauri", "tracing"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tracing = { workspace = true }
|
||||
tauri-specta = { workspace = true, features = ["typescript"] }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
thiserror.workspace = true
|
||||
|
||||
opener = { version = "0.6.1", features = ["reveal"] }
|
||||
tauri = { version = "=1.5.3", features = [
|
||||
"macos-private-api",
|
||||
"path-all",
|
||||
"protocol-all",
|
||||
"os-all",
|
||||
"shell-all",
|
||||
"dialog-all",
|
||||
"linux-protocol-headers",
|
||||
"updater",
|
||||
"window-all",
|
||||
"native-tls-vendored",
|
||||
"tracing",
|
||||
] }
|
||||
directories = "5.0.1"
|
||||
# Specific Desktop dependencies
|
||||
# WARNING: Do NOT enable default features, as that vendors dbus (see below)
|
||||
opener = { version = "0.7.1", features = ["reveal"], default-features = false }
|
||||
tauri = { version = "=2.0.0-beta.17", features = [
|
||||
"macos-private-api",
|
||||
"unstable",
|
||||
"linux-libxdo",
|
||||
] } # Update blocked by rspc
|
||||
tauri-plugin-updater = "2.0.0-beta"
|
||||
tauri-plugin-dialog = "2.0.0-beta"
|
||||
tauri-plugin-os = "2.0.0-beta"
|
||||
tauri-plugin-shell = "2.0.0-beta"
|
||||
tauri-runtime = { version = "=2.0.0-beta.15" } # Update blocked by tauri
|
||||
tauri-specta = { version = "=2.0.0-rc.8", features = ["typescript"] }
|
||||
tauri-utils = { version = "=2.0.0-beta.16" } # Update blocked by tauri
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Spacedrive Sub-crates
|
||||
sd-desktop-linux = { path = "../crates/linux" }
|
||||
webkit2gtk = { version = "0.18.2", features = ["v2_2"] }
|
||||
|
||||
# Specific Desktop dependencies
|
||||
# WARNING: dbus must NOT be vendored, as that breaks the app on Linux,X11,Nvidia
|
||||
dbus = { version = "0.9.7", features = ["stdfd"] }
|
||||
# https://github.com/tauri-apps/tauri/blob/tauri-v2.0.0-beta.17/core/tauri/Cargo.toml#L86
|
||||
webkit2gtk = { version = "=2.0.1", features = ["v2_38"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# Spacedrive Sub-crates
|
||||
sd-desktop-macos = { path = "../crates/macos" }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# Spacedrive Sub-crates
|
||||
sd-desktop-windows = { path = "../crates/windows" }
|
||||
webview2-com = "0.19.1"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = "1.5.0"
|
||||
# Specific Desktop dependencies
|
||||
tauri-build = "2.0.0-beta"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
devtools = ["tauri/devtools"]
|
||||
ai-models = ["sd-core/ai"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
|
30
apps/desktop/src-tauri/capabilities/default.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"app:default",
|
||||
"event:default",
|
||||
"image:default",
|
||||
"menu:default",
|
||||
"path:default",
|
||||
"resources:default",
|
||||
"window:default",
|
||||
"tray:default",
|
||||
"webview:default",
|
||||
"window:default",
|
||||
"shell:allow-open",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"dialog:allow-confirm",
|
||||
"os:allow-os-type",
|
||||
"window:allow-close",
|
||||
"window:allow-create",
|
||||
"window:allow-maximize",
|
||||
"window:allow-minimize",
|
||||
"window:allow-toggle-maximize",
|
||||
"window:allow-start-dragging",
|
||||
"webview:allow-internal-toggle-devtools"
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 70 KiB |
|
@ -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>) {
|
||||
for path in paths {
|
||||
#[cfg(target_os = "linux")]
|
||||
if sd_desktop_linux::is_appimage() {
|
||||
// This is a workaround for the app, when package inside an AppImage, crashing when using opener::reveal.
|
||||
if let Err(e) = sd_desktop_linux::open_file_path(if path.is_file() {
|
||||
path.parent().unwrap_or(&path)
|
||||
} else {
|
||||
&path
|
||||
}) {
|
||||
error!("Failed to open logs dir: {e:#?}");
|
||||
}
|
||||
} else if let Err(e) = opener::reveal(path) {
|
||||
error!("Failed to open logs dir: {e:#?}");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if let Err(e) = opener::reveal(path) {
|
||||
error!("Failed to open logs dir: {e:#?}");
|
||||
}
|
||||
|
|
|
@ -3,24 +3,17 @@
|
|||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex, PoisonError},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{fs, path::PathBuf, process::Command, sync::Arc, time::Duration};
|
||||
|
||||
use menu::{set_enabled, MenuEvent};
|
||||
use sd_core::{Node, NodeError};
|
||||
|
||||
use sd_fda::DiskAccess;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
api::path, ipc::RemoteDomainAccessScope, window::PlatformWebview, AppHandle, FileDropEvent,
|
||||
Manager, Window, WindowEvent,
|
||||
};
|
||||
use tauri::{async_runtime::block_on, webview::PlatformWebview, AppHandle, Manager, WindowEvent};
|
||||
use tauri_plugins::{sd_error_plugin, sd_server_plugin};
|
||||
use tauri_specta::{collect_events, ts, Event};
|
||||
use tauri_specta::{collect_events, ts};
|
||||
use tokio::task::block_in_place;
|
||||
use tokio::time::sleep;
|
||||
use tracing::error;
|
||||
|
||||
|
@ -33,35 +26,32 @@ mod updater;
|
|||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
async fn app_ready(app_handle: AppHandle) {
|
||||
let window = app_handle.get_window("main").unwrap();
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
// 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() {
|
||||
DiskAccess::request_fda().expect("Unable to request full disk access");
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
async fn set_menu_bar_item_state(_window: tauri::Window, _id: String, _enabled: bool) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
_window
|
||||
.menu_handle()
|
||||
.get_item(&_id)
|
||||
.set_enabled(_enabled)
|
||||
.expect("Unable to modify menu item");
|
||||
}
|
||||
async fn set_menu_bar_item_state(window: tauri::Window, event: MenuEvent, enabled: bool) {
|
||||
let menu = window
|
||||
.menu()
|
||||
.expect("unable to get menu for current window");
|
||||
|
||||
set_enabled(&menu, event, enabled);
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
async fn reload_webview(app_handle: AppHandle) {
|
||||
app_handle
|
||||
.get_window("main")
|
||||
.get_webview_window("main")
|
||||
.expect("Error getting window handle")
|
||||
.with_webview(reload_webview_inner)
|
||||
.expect("Error while reloading webview");
|
||||
|
@ -76,7 +66,7 @@ fn reload_webview_inner(webview: PlatformWebview) {
|
|||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use webkit2gtk::traits::WebViewExt;
|
||||
use webkit2gtk::WebViewExt;
|
||||
|
||||
webview.inner().reload();
|
||||
}
|
||||
|
@ -94,8 +84,10 @@ fn reload_webview_inner(webview: PlatformWebview) {
|
|||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
async fn reset_spacedrive(app_handle: AppHandle) {
|
||||
let data_dir = path::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("./"))
|
||||
let data_dir = app_handle
|
||||
.path()
|
||||
.data_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("./"))
|
||||
.join("spacedrive");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -111,25 +103,9 @@ async fn reset_spacedrive(app_handle: AppHandle) {
|
|||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
async fn refresh_menu_bar(
|
||||
_node: tauri::State<'_, Arc<Node>>,
|
||||
_app_handle: AppHandle,
|
||||
) -> Result<(), ()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let menu_handles: Vec<tauri::window::MenuHandle> = _app_handle
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|x| x.1.menu_handle())
|
||||
.collect();
|
||||
|
||||
let has_library = !_node.libraries.get_all().await.is_empty();
|
||||
|
||||
for menu in menu_handles {
|
||||
menu::set_library_locked_menu_items_enabled(menu, has_library);
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_menu_bar(node: tauri::State<'_, Arc<Node>>, app: AppHandle) -> Result<(), ()> {
|
||||
let has_library = !node.libraries.get_all().await.is_empty();
|
||||
menu::refresh_menu_bar(&app, has_library);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -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)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum DragAndDropEvent {
|
||||
|
@ -157,11 +174,6 @@ pub enum DragAndDropEvent {
|
|||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DragAndDropState {
|
||||
windows: HashMap<tauri::Window, tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
const CLIENT_ID: &str = "2abb241e-40b8-4517-a3e3-5594375c8fbb";
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -169,46 +181,8 @@ async fn main() -> tauri::Result<()> {
|
|||
#[cfg(target_os = "linux")]
|
||||
sd_desktop_linux::normalize_environment();
|
||||
|
||||
let data_dir = path::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("./"))
|
||||
.join("spacedrive");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let data_dir = data_dir.join("dev");
|
||||
|
||||
// The `_guard` must be assigned to variable for flushing remaining logs on main exit through Drop
|
||||
let (_guard, result) = match Node::init_logger(&data_dir) {
|
||||
Ok(guard) => (
|
||||
Some(guard),
|
||||
Node::new(data_dir, sd_core::Env::new(CLIENT_ID)).await,
|
||||
),
|
||||
Err(err) => (None, Err(NodeError::Logger(err))),
|
||||
};
|
||||
|
||||
let app = tauri::Builder::default();
|
||||
|
||||
let (node_router, app) = match result {
|
||||
Ok((node, router)) => (Some((node, router)), app),
|
||||
Err(err) => {
|
||||
error!("Error starting up the node: {err:#?}");
|
||||
(None, app.plugin(sd_error_plugin(err)))
|
||||
}
|
||||
};
|
||||
|
||||
let (node, router) = node_router.expect("Unable to get the node or router");
|
||||
|
||||
let should_clear_localstorage = node.libraries.get_all().await.is_empty();
|
||||
|
||||
let app = app
|
||||
.plugin(rspc::integrations::tauri::plugin(router, {
|
||||
let node = node.clone();
|
||||
move || node.clone()
|
||||
}))
|
||||
.plugin(sd_server_plugin(node.clone()).await.unwrap()) // TODO: Handle `unwrap`
|
||||
.manage(node.clone());
|
||||
|
||||
let specta_builder = {
|
||||
let specta_builder = ts::builder()
|
||||
let (invoke_handler, register_events) = {
|
||||
let builder = ts::builder()
|
||||
.events(collect_events![DragAndDropEvent])
|
||||
.commands(tauri_specta::collect_commands![
|
||||
app_ready,
|
||||
|
@ -218,6 +192,7 @@ async fn main() -> tauri::Result<()> {
|
|||
reload_webview,
|
||||
set_menu_bar_item_state,
|
||||
request_fda_macos,
|
||||
open_trash_in_os_explorer,
|
||||
file::open_file_paths,
|
||||
file::open_ephemeral_files,
|
||||
file::get_file_path_open_with_apps,
|
||||
|
@ -226,224 +201,148 @@ async fn main() -> tauri::Result<()> {
|
|||
file::open_ephemeral_file_with,
|
||||
file::reveal_items,
|
||||
theme::lock_app_theme,
|
||||
// TODO: move to plugin w/tauri-specta
|
||||
updater::check_for_update,
|
||||
updater::install_update
|
||||
])
|
||||
.config(specta::ts::ExportConfig::default().formatter(specta::ts::formatter::prettier));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let specta_builder = specta_builder.path("../src/commands.ts");
|
||||
let builder = builder.path("../src/commands.ts");
|
||||
|
||||
specta_builder.into_plugin()
|
||||
builder.build().unwrap()
|
||||
};
|
||||
|
||||
let file_drop_status = Arc::new(Mutex::new(DragAndDropState::default()));
|
||||
let app = app
|
||||
.plugin(updater::plugin())
|
||||
// .plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(specta_builder)
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(invoke_handler)
|
||||
.setup(move |app| {
|
||||
let app = app.handle();
|
||||
// We need a the app handle to determine the data directory now.
|
||||
// This means all the setup code has to be within `setup`, however it doesn't support async so we `block_on`.
|
||||
block_in_place(|| {
|
||||
block_on(async move {
|
||||
register_events(app);
|
||||
|
||||
println!("setup");
|
||||
let data_dir = app
|
||||
.path()
|
||||
.data_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("./"))
|
||||
.join("spacedrive");
|
||||
|
||||
app.windows().iter().for_each(|(_, window)| {
|
||||
if should_clear_localstorage {
|
||||
println!("bruh?");
|
||||
window.eval("localStorage.clear();").ok();
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
let data_dir = data_dir.join("dev");
|
||||
|
||||
tokio::spawn({
|
||||
let window = window.clone();
|
||||
async move {
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
if !window.is_visible().unwrap_or(true) {
|
||||
// This happens if the JS bundle crashes and hence doesn't send ready event.
|
||||
println!(
|
||||
"Window did not emit `app_ready` event fast enough. Showing window..."
|
||||
);
|
||||
window.show().expect("Main window should show");
|
||||
// The `_guard` must be assigned to variable for flushing remaining logs on main exit through Drop
|
||||
let (_guard, result) = match Node::init_logger(&data_dir) {
|
||||
Ok(guard) => (
|
||||
Some(guard),
|
||||
Node::new(data_dir, sd_core::Env::new(CLIENT_ID)).await,
|
||||
),
|
||||
Err(err) => (None, Err(NodeError::Logger(err))),
|
||||
};
|
||||
|
||||
let handle = app.handle();
|
||||
let (node, router) = match result {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
error!("Error starting up the node: {err:#?}");
|
||||
handle.plugin(sd_error_plugin(err))?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
window.set_decorations(true).unwrap();
|
||||
let should_clear_localstorage = node.libraries.get_all().await.is_empty();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use sd_desktop_macos::{blur_window_background, set_titlebar_style};
|
||||
handle.plugin(rspc::integrations::tauri::plugin(router, {
|
||||
let node = node.clone();
|
||||
move || node.clone()
|
||||
}))?;
|
||||
handle.plugin(sd_server_plugin(node.clone()).await.unwrap())?; // TODO: Handle `unwrap`
|
||||
handle.manage(node.clone());
|
||||
|
||||
let nswindow = window.ns_window().unwrap();
|
||||
|
||||
unsafe { set_titlebar_style(&nswindow, false) };
|
||||
unsafe { blur_window_background(&nswindow) };
|
||||
|
||||
tokio::spawn({
|
||||
let libraries = node.libraries.clone();
|
||||
let menu_handle = window.menu_handle();
|
||||
async move {
|
||||
if libraries.get_all().await.is_empty() {
|
||||
menu::set_library_locked_menu_items_enabled(menu_handle, false);
|
||||
handle.windows().iter().for_each(|(_, window)| {
|
||||
if should_clear_localstorage {
|
||||
println!("cleaning localStorage");
|
||||
for webview in window.webviews() {
|
||||
webview.eval("localStorage.clear();").ok();
|
||||
}
|
||||
}
|
||||
|
||||
tokio::spawn({
|
||||
let window = window.clone();
|
||||
async move {
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
if !window.is_visible().unwrap_or(true) {
|
||||
// This happens if the JS bundle crashes and hence doesn't send ready event.
|
||||
println!(
|
||||
"Window did not emit `app_ready` event fast enough. Showing window..."
|
||||
);
|
||||
window.show().expect("Main window should show");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
window.set_decorations(false).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
unsafe {
|
||||
sd_desktop_macos::set_titlebar_style(
|
||||
&window.ns_window().expect("NSWindows must exist on macOS"),
|
||||
false,
|
||||
);
|
||||
sd_desktop_macos::disable_app_nap(
|
||||
&"File indexer needs to run unimpeded".into(),
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Configure IPC for custom protocol
|
||||
app.ipc_scope().configure_remote_access(
|
||||
RemoteDomainAccessScope::new("localhost")
|
||||
.allow_on_scheme("spacedrive")
|
||||
.add_window("main"),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
})
|
||||
.on_menu_event(menu::handle_menu_event)
|
||||
.on_window_event(move |event| match event.event() {
|
||||
.on_window_event(move |window, event| match event {
|
||||
// macOS expected behavior is for the app to not exit when the main window is closed.
|
||||
// Instead, the window is hidden and the dock icon remains so that on user click it should show the window again.
|
||||
#[cfg(target_os = "macos")]
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
// TODO: make this multi-window compatible in the future
|
||||
event
|
||||
.window()
|
||||
window
|
||||
.app_handle()
|
||||
.hide()
|
||||
.expect("Window should hide on macOS");
|
||||
api.prevent_close();
|
||||
}
|
||||
WindowEvent::FileDrop(drop) => {
|
||||
let window = event.window();
|
||||
let mut file_drop_status = file_drop_status
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
|
||||
match drop {
|
||||
FileDropEvent::Hovered(paths) => {
|
||||
// Look this shouldn't happen but let's be sure we don't leak threads.
|
||||
if file_drop_status.windows.contains_key(window) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We setup a thread to keep emitting the updated position of the cursor
|
||||
// It will be killed when the `FileDropEvent` is finished or cancelled.
|
||||
let paths = paths.clone();
|
||||
file_drop_status.windows.insert(window.clone(), {
|
||||
let window = window.clone();
|
||||
tokio::spawn(async move {
|
||||
let (mut last_x, mut last_y) = (0.0, 0.0);
|
||||
loop {
|
||||
let (x, y) = mouse_position(&window);
|
||||
|
||||
let x_diff = difference(x, last_x);
|
||||
let y_diff = difference(y, last_y);
|
||||
|
||||
// If the mouse hasn't moved much we will "debounce" the event
|
||||
if x_diff > 28.0 || y_diff > 28.0 {
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
|
||||
DragAndDropEvent::Hovered {
|
||||
paths: paths
|
||||
.iter()
|
||||
.filter_map(|x| x.to_str().map(|x| x.to_string()))
|
||||
.collect(),
|
||||
x,
|
||||
y,
|
||||
}
|
||||
.emit(&window)
|
||||
.ok();
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(125)).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
FileDropEvent::Dropped(paths) => {
|
||||
if let Some(handle) = file_drop_status.windows.remove(window) {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
let (x, y) = mouse_position(window);
|
||||
DragAndDropEvent::Dropped {
|
||||
paths: paths
|
||||
.iter()
|
||||
.filter_map(|x| x.to_str().map(|x| x.to_string()))
|
||||
.collect(),
|
||||
x,
|
||||
y,
|
||||
}
|
||||
.emit(window)
|
||||
.ok();
|
||||
}
|
||||
FileDropEvent::Cancelled => {
|
||||
if let Some(handle) = file_drop_status.windows.remove(window) {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
DragAndDropEvent::Cancelled.emit(window).ok();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
WindowEvent::Resized(_) => {
|
||||
let (_state, command) = if event
|
||||
.window()
|
||||
.is_fullscreen()
|
||||
.expect("Can't get fullscreen state")
|
||||
{
|
||||
(true, "window_fullscreened")
|
||||
} else {
|
||||
(false, "window_not_fullscreened")
|
||||
};
|
||||
let (_state, command) =
|
||||
if window.is_fullscreen().expect("Can't get fullscreen state") {
|
||||
(true, "window_fullscreened")
|
||||
} else {
|
||||
(false, "window_not_fullscreened")
|
||||
};
|
||||
|
||||
event
|
||||
.window()
|
||||
window
|
||||
.emit("keybind", command)
|
||||
.expect("Unable to emit window event");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let nswindow = event.window().ns_window().unwrap();
|
||||
let nswindow = window.ns_window().unwrap();
|
||||
unsafe { sd_desktop_macos::set_titlebar_style(&nswindow, _state) };
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.menu(menu::get_menu())
|
||||
.menu(menu::setup_menu)
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
// TODO: Bring back Tauri Plugin Window State - it was buggy so we removed it.
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(updater::plugin())
|
||||
.manage(updater::State::default())
|
||||
.build(tauri::generate_context!())?;
|
||||
.build(tauri::generate_context!())?
|
||||
.run(|_, _| {});
|
||||
|
||||
app.run(|_, _| {});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Get the mouse position relative to the window
|
||||
fn mouse_position(window: &Window) -> (f64, f64) {
|
||||
// We apply the OS scaling factor.
|
||||
// Tauri/Webkit *should* be responsible for this but it would seem it is bugged on the current webkit/tauri/wry/tao version.
|
||||
// Using newer Webkit did fix this automatically but I can't for the life of me work out how to get the right glibc versions in CI so we can't ship it.
|
||||
let scale_factor = window.scale_factor().unwrap();
|
||||
|
||||
let window_pos = window.outer_position().unwrap();
|
||||
let cursor_pos = window.cursor_position().unwrap();
|
||||
|
||||
(
|
||||
(cursor_pos.x - window_pos.x as f64) / scale_factor,
|
||||
(cursor_pos.y - window_pos.y as f64) / scale_factor,
|
||||
)
|
||||
}
|
||||
|
||||
// The distance between two numbers as a positive integer.
|
||||
fn difference(a: f64, b: f64) -> f64 {
|
||||
let x = a - b;
|
||||
if x < 0.0 {
|
||||
x * -1.0
|
||||
} else {
|
||||
x
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,191 +1,271 @@
|
|||
use tauri::{Manager, Menu, WindowMenuEvent, Wry};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::{AboutMetadata, CustomMenuItem, MenuItem, Submenu};
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItemKind},
|
||||
AppHandle, Manager, Wry,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
pub fn get_menu() -> Menu {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
custom_menu_bar()
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Menu::new()
|
||||
}
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Type, Deserialize, strum::EnumString, strum::AsRefStr, strum::Display,
|
||||
)]
|
||||
pub enum MenuEvent {
|
||||
NewLibrary,
|
||||
NewFile,
|
||||
NewDirectory,
|
||||
AddLocation,
|
||||
OpenOverview,
|
||||
OpenSearch,
|
||||
OpenSettings,
|
||||
ReloadExplorer,
|
||||
SetLayoutGrid,
|
||||
SetLayoutList,
|
||||
SetLayoutMedia,
|
||||
ToggleDeveloperTools,
|
||||
NewWindow,
|
||||
ReloadWebview,
|
||||
}
|
||||
|
||||
// update this whenever you add something which requires a valid library to use
|
||||
#[cfg(target_os = "macos")]
|
||||
const LIBRARY_LOCKED_MENU_IDS: [&str; 12] = [
|
||||
"new_window",
|
||||
"open_overview",
|
||||
"open_search",
|
||||
"open_settings",
|
||||
"reload_explorer",
|
||||
"layout_grid",
|
||||
"layout_list",
|
||||
"layout_media",
|
||||
"new_file",
|
||||
"new_directory",
|
||||
"new_library", // disabled because the first one should at least be done via onboarding
|
||||
"add_location",
|
||||
/// Menu items which require a library to be open to use.
|
||||
/// They will be disabled/enabled automatically.
|
||||
const LIBRARY_LOCKED_MENU_IDS: &[MenuEvent] = &[
|
||||
MenuEvent::NewWindow,
|
||||
MenuEvent::OpenOverview,
|
||||
MenuEvent::OpenSearch,
|
||||
MenuEvent::OpenSettings,
|
||||
MenuEvent::ReloadExplorer,
|
||||
MenuEvent::SetLayoutGrid,
|
||||
MenuEvent::SetLayoutList,
|
||||
MenuEvent::SetLayoutMedia,
|
||||
MenuEvent::NewFile,
|
||||
MenuEvent::NewDirectory,
|
||||
MenuEvent::NewLibrary,
|
||||
MenuEvent::AddLocation,
|
||||
];
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn custom_menu_bar() -> Menu {
|
||||
let app_menu = Menu::new()
|
||||
.add_native_item(MenuItem::About(
|
||||
"Spacedrive".to_string(),
|
||||
AboutMetadata::new()
|
||||
.authors(vec!["Spacedrive Technology Inc.".to_string()])
|
||||
.license("AGPL-3.0-only")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.website("https://spacedrive.com/")
|
||||
.website_label("Spacedrive.com"),
|
||||
))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("new_library", "New Library").disabled()) // TODO(brxken128): add keybind handling here
|
||||
.add_submenu(Submenu::new(
|
||||
"Library",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("library_<uuid>", "Library 1").disabled())
|
||||
.add_item(CustomMenuItem::new("library_<uuid2>", "Library 2").disabled()), // TODO: enumerate libraries and make this a library selector
|
||||
))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Hide)
|
||||
.add_native_item(MenuItem::HideOthers)
|
||||
.add_native_item(MenuItem::ShowAll)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Quit);
|
||||
pub fn setup_menu(app: &AppHandle) -> tauri::Result<Menu<Wry>> {
|
||||
app.on_menu_event(move |app, event| {
|
||||
if let Ok(event) = MenuEvent::from_str(&event.id().0) {
|
||||
handle_menu_event(event, app);
|
||||
} else {
|
||||
println!("Unknown menu event: {}", event.id().0);
|
||||
}
|
||||
});
|
||||
|
||||
let file_menu = Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_file", "New File")
|
||||
.accelerator("CmdOrCtrl+N")
|
||||
.disabled(), // TODO(brxken128): add keybind handling here
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_directory", "New Directory")
|
||||
.accelerator("CmdOrCtrl+D")
|
||||
.disabled(), // TODO(brxken128): add keybind handling here
|
||||
)
|
||||
.add_item(CustomMenuItem::new("add_location", "Add Location").disabled()); // TODO(brxken128): add keybind handling here;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Menu::new(app)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::menu::{AboutMetadataBuilder, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
|
||||
let edit_menu = Menu::new()
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Cut)
|
||||
.add_native_item(MenuItem::Paste)
|
||||
.add_native_item(MenuItem::Redo)
|
||||
.add_native_item(MenuItem::Undo)
|
||||
.add_native_item(MenuItem::SelectAll);
|
||||
let app_menu = SubmenuBuilder::new(app, "Spacedrive")
|
||||
.about(Some(
|
||||
AboutMetadataBuilder::new()
|
||||
.authors(Some(vec!["Spacedrive Technology Inc.".to_string()]))
|
||||
.license(Some(env!("CARGO_PKG_VERSION")))
|
||||
.version(Some(env!("CARGO_PKG_VERSION")))
|
||||
.website(Some("https://spacedrive.com/"))
|
||||
.website_label(Some("Spacedrive.com"))
|
||||
.build(),
|
||||
))
|
||||
.separator()
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::NewLibrary, "New Library")
|
||||
.accelerator("Cmd+Shift+T")
|
||||
.build(app)?,
|
||||
)
|
||||
// .item(
|
||||
// &SubmenuBuilder::new(app, "Libraries")
|
||||
// // TODO: Implement this
|
||||
// .items(&[])
|
||||
// .build()?,
|
||||
// )
|
||||
.separator()
|
||||
.hide()
|
||||
.hide_others()
|
||||
.show_all()
|
||||
.separator()
|
||||
.quit()
|
||||
.build()?;
|
||||
|
||||
let view_menu = Menu::new()
|
||||
.add_item(CustomMenuItem::new("open_overview", "Overview").accelerator("CmdOrCtrl+."))
|
||||
.add_item(CustomMenuItem::new("open_search", "Search").accelerator("CmdOrCtrl+F"))
|
||||
.add_item(CustomMenuItem::new("open_settings", "Settings").accelerator("CmdOrCtrl+Comma"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("reload_explorer", "Reload explorer").accelerator("CmdOrCtrl+R"),
|
||||
)
|
||||
.add_submenu(Submenu::new(
|
||||
"Layout",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("layout_grid", "Grid (Default)").disabled())
|
||||
.add_item(CustomMenuItem::new("layout_list", "List").disabled())
|
||||
.add_item(CustomMenuItem::new("layout_media", "Media").disabled()),
|
||||
));
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("command_pallete", "Command Pallete")
|
||||
// .accelerator("CmdOrCtrl+P"),
|
||||
// )
|
||||
let file_menu = SubmenuBuilder::new(app, "File")
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::NewFile, "New File")
|
||||
.accelerator("CmdOrCtrl+N")
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::NewDirectory, "New Directory")
|
||||
.accelerator("CmdOrCtrl+D")
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::AddLocation, "Add Location")
|
||||
// .accelerator("") // TODO
|
||||
.build(app)?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let view_menu = view_menu.add_native_item(MenuItem::Separator).add_item(
|
||||
CustomMenuItem::new("toggle_devtools", "Toggle Developer Tools")
|
||||
.accelerator("CmdOrCtrl+Shift+Alt+I"),
|
||||
);
|
||||
let edit_menu = SubmenuBuilder::new(app, "Edit")
|
||||
.copy()
|
||||
.cut()
|
||||
.paste()
|
||||
.redo()
|
||||
.undo()
|
||||
.select_all()
|
||||
.build()?;
|
||||
|
||||
let window_menu = Menu::new()
|
||||
.add_native_item(MenuItem::Minimize)
|
||||
.add_native_item(MenuItem::Zoom)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_window", "New Window")
|
||||
.accelerator("CmdOrCtrl+Shift+N")
|
||||
.disabled(),
|
||||
)
|
||||
.add_item(CustomMenuItem::new("close_window", "Close Window").accelerator("CmdOrCtrl+W"))
|
||||
.add_native_item(MenuItem::EnterFullScreen)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("reload_app", "Reload Webview").accelerator("CmdOrCtrl+Shift+R"),
|
||||
let view_menu = SubmenuBuilder::new(app, "View")
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::OpenOverview, "Open Overview")
|
||||
.accelerator("CmdOrCtrl+.")
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::OpenSearch, "Search")
|
||||
.accelerator("CmdOrCtrl+F")
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::OpenSettings, "Settings")
|
||||
.accelerator("CmdOrCtrl+Comma")
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::ReloadExplorer, "Open Explorer")
|
||||
.accelerator("CmdOrCtrl+R")
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&SubmenuBuilder::new(app, "Layout")
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::SetLayoutGrid, "Grid (Default)")
|
||||
// .accelerator("") // TODO
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::SetLayoutList, "List")
|
||||
// .accelerator("") // TODO
|
||||
.build(app)?,
|
||||
)
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::SetLayoutMedia, "Media")
|
||||
// .accelerator("") // TODO
|
||||
.build(app)?,
|
||||
)
|
||||
.build()?,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let view_menu = view_menu.separator().item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::ToggleDeveloperTools, "Toggle Developer Tools")
|
||||
.accelerator("CmdOrCtrl+Shift+Alt+I")
|
||||
.build(app)?,
|
||||
);
|
||||
|
||||
Menu::new()
|
||||
.add_submenu(Submenu::new("Spacedrive", app_menu))
|
||||
.add_submenu(Submenu::new("File", file_menu))
|
||||
.add_submenu(Submenu::new("Edit", edit_menu))
|
||||
.add_submenu(Submenu::new("View", view_menu))
|
||||
.add_submenu(Submenu::new("Window", window_menu))
|
||||
let view_menu = view_menu.build()?;
|
||||
|
||||
let window_menu = SubmenuBuilder::new(app, "Window")
|
||||
.minimize()
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::NewWindow, "New Window")
|
||||
.accelerator("CmdOrCtrl+Shift+N")
|
||||
.build(app)?,
|
||||
)
|
||||
.close_window()
|
||||
.fullscreen()
|
||||
.item(
|
||||
&MenuItemBuilder::with_id(MenuEvent::ReloadWebview, "Reload Webview")
|
||||
.accelerator("CmdOrCtrl+Shift+R")
|
||||
.build(app)?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&app_menu)
|
||||
.item(&file_menu)
|
||||
.item(&edit_menu)
|
||||
.item(&view_menu)
|
||||
.item(&window_menu)
|
||||
.build()?;
|
||||
|
||||
for event in LIBRARY_LOCKED_MENU_IDS {
|
||||
set_enabled(&menu, *event, false);
|
||||
}
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_menu_event(event: WindowMenuEvent<Wry>) {
|
||||
match event.menu_item_id() {
|
||||
"quit" => {
|
||||
let app = event.window().app_handle();
|
||||
app.exit(0);
|
||||
}
|
||||
"reload_explorer" => event.window().emit("keybind", "reload_explorer").unwrap(),
|
||||
"open_settings" => event.window().emit("keybind", "open_settings").unwrap(),
|
||||
"open_overview" => event.window().emit("keybind", "open_overview").unwrap(),
|
||||
"close_window" => {
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::AppHandle::hide(&event.window().app_handle()).unwrap();
|
||||
pub fn handle_menu_event(event: MenuEvent, app: &AppHandle) {
|
||||
let webview = app
|
||||
.get_webview_window("main")
|
||||
.expect("unable to find window");
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let window = event.window();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if window.is_devtools_open() {
|
||||
window.close_devtools();
|
||||
} else {
|
||||
window.close().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
window.close().unwrap();
|
||||
match event {
|
||||
// TODO: Use Tauri Specta with frontend instead of this
|
||||
MenuEvent::NewLibrary => webview.emit("keybind", "new_library").unwrap(),
|
||||
MenuEvent::NewFile => webview.emit("keybind", "new_file").unwrap(),
|
||||
MenuEvent::NewDirectory => webview.emit("keybind", "new_directory").unwrap(),
|
||||
MenuEvent::AddLocation => webview.emit("keybind", "add_location").unwrap(),
|
||||
MenuEvent::OpenOverview => webview.emit("keybind", "open_overview").unwrap(),
|
||||
MenuEvent::OpenSearch => webview.emit("keybind", "open_search".to_string()).unwrap(),
|
||||
MenuEvent::OpenSettings => webview.emit("keybind", "open_settings").unwrap(),
|
||||
MenuEvent::ReloadExplorer => webview.emit("keybind", "reload_explorer").unwrap(),
|
||||
MenuEvent::SetLayoutGrid => webview.emit("keybind", "set_layout_grid").unwrap(),
|
||||
MenuEvent::SetLayoutList => webview.emit("keybind", "set_layout_list").unwrap(),
|
||||
MenuEvent::SetLayoutMedia => webview.emit("keybind", "set_layout_media").unwrap(),
|
||||
MenuEvent::ToggleDeveloperTools =>
|
||||
{
|
||||
#[cfg(feature = "devtools")]
|
||||
if webview.is_devtools_open() {
|
||||
webview.close_devtools();
|
||||
} else {
|
||||
webview.open_devtools();
|
||||
}
|
||||
}
|
||||
"open_search" => event
|
||||
.window()
|
||||
.emit("keybind", "open_search".to_string())
|
||||
.unwrap(),
|
||||
"reload_app" => {
|
||||
event
|
||||
.window()
|
||||
MenuEvent::NewWindow => {
|
||||
// TODO: Implement this
|
||||
}
|
||||
MenuEvent::ReloadWebview => {
|
||||
webview
|
||||
.with_webview(crate::reload_webview_inner)
|
||||
.expect("Error while reloading webview");
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
"toggle_devtools" => {
|
||||
let window = event.window();
|
||||
|
||||
if window.is_devtools_open() {
|
||||
window.close_devtools();
|
||||
} else {
|
||||
window.open_devtools();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// If any are explicitly marked with `.disabled()` in the `custom_menu_bar()` function, this won't have an effect.
|
||||
/// We include them in the locked menu IDs anyway for future-proofing, in-case someone forgets.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_library_locked_menu_items_enabled(handle: tauri::window::MenuHandle, enabled: bool) {
|
||||
LIBRARY_LOCKED_MENU_IDS
|
||||
.iter()
|
||||
.try_for_each(|id| handle.get_item(id).set_enabled(enabled))
|
||||
.expect("Unable to disable menu items (there are no libraries present, so certain options should be hidden)");
|
||||
// Enable/disable all items in `LIBRARY_LOCKED_MENU_IDS`
|
||||
pub fn refresh_menu_bar(app: &AppHandle, enabled: bool) {
|
||||
let menu = app
|
||||
.get_window("main")
|
||||
.expect("unable to find window")
|
||||
.menu()
|
||||
.expect("unable to get menu for current window");
|
||||
|
||||
for event in LIBRARY_LOCKED_MENU_IDS {
|
||||
set_enabled(&menu, *event, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled(menu: &Menu<Wry>, event: MenuEvent, enabled: bool) {
|
||||
let result = match menu.get(event.as_ref()) {
|
||||
Some(MenuItemKind::MenuItem(i)) => i.set_enabled(enabled),
|
||||
Some(MenuItemKind::Submenu(i)) => i.set_enabled(enabled),
|
||||
Some(MenuItemKind::Predefined(_)) => return,
|
||||
Some(MenuItemKind::Check(i)) => i.set_enabled(enabled),
|
||||
Some(MenuItemKind::Icon(i)) => i.set_enabled(enabled),
|
||||
None => {
|
||||
error!("Unable to get menu item: {event:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Error setting menu item state: {e:#?}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,15 +95,22 @@ pub async fn sd_server_plugin<R: Runtime>(
|
|||
.expect("Error with HTTP server!"); // TODO: Panic handling
|
||||
});
|
||||
|
||||
let script = format!(
|
||||
r#"window.__SD_CUSTOM_SERVER_AUTH_TOKEN__ = "{auth_token}"; window.__SD_CUSTOM_URI_SERVER__ = [{}];"#,
|
||||
[listen_addra, listen_addrb, listen_addrc, listen_addrd]
|
||||
.iter()
|
||||
.map(|addr| format!("'http://{addr}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
|
||||
Ok(tauri::plugin::Builder::new("sd-server")
|
||||
.js_init_script(format!(
|
||||
r#"window.__SD_CUSTOM_SERVER_AUTH_TOKEN__ = "{auth_token}"; window.__SD_CUSTOM_URI_SERVER__ = [{}];"#,
|
||||
[listen_addra, listen_addrb, listen_addrc, listen_addrd]
|
||||
.iter()
|
||||
.map(|addr| format!("'http://{addr}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
))
|
||||
.js_init_script(script.to_owned())
|
||||
.on_page_load(move |webview, _payload| {
|
||||
webview
|
||||
.eval(&script)
|
||||
.expect("Spacedrive server URL must be injected")
|
||||
})
|
||||
.on_event(move |_app, e| {
|
||||
if let RunEvent::Exit { .. } = e {
|
||||
block_in_place(|| {
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
use tauri::{plugin::TauriPlugin, Manager, Runtime};
|
||||
use tauri_plugin_updater::{Update as TauriPluginUpdate, UpdaterExt};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, specta::Type, serde::Serialize)]
|
||||
pub struct Update {
|
||||
pub version: String,
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
fn new(update: &tauri::updater::UpdateResponse<impl tauri::Runtime>) -> Self {
|
||||
fn new(update: &TauriPluginUpdate) -> Self {
|
||||
Self {
|
||||
version: update.latest_version().to_string(),
|
||||
body: update.body().map(ToString::to_string),
|
||||
version: update.version.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +20,12 @@ pub struct State {
|
|||
install_lock: Mutex<()>,
|
||||
}
|
||||
|
||||
async fn get_update(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<tauri::updater::UpdateResponse<impl tauri::Runtime>, String> {
|
||||
tauri::updater::builder(app)
|
||||
async fn get_update(app: tauri::AppHandle) -> Result<Option<TauriPluginUpdate>, String> {
|
||||
app.updater_builder()
|
||||
.header("X-Spacedrive-Version", "stable")
|
||||
.map_err(|e| e.to_string())?
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
|
@ -45,19 +44,19 @@ pub enum UpdateEvent {
|
|||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn check_for_update(app: tauri::AppHandle) -> Result<Option<Update>, String> {
|
||||
app.emit_all("updater", UpdateEvent::Loading).ok();
|
||||
app.emit("updater", UpdateEvent::Loading).ok();
|
||||
|
||||
let update = match get_update(app.clone()).await {
|
||||
Ok(update) => update,
|
||||
Err(e) => {
|
||||
app.emit_all("updater", UpdateEvent::Error(e.clone())).ok();
|
||||
app.emit("updater", UpdateEvent::Error(e.clone())).ok();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let update = update.is_update_available().then(|| Update::new(&update));
|
||||
let update = update.map(|update| Update::new(&update));
|
||||
|
||||
app.emit_all(
|
||||
app.emit(
|
||||
"updater",
|
||||
update
|
||||
.clone()
|
||||
|
@ -81,11 +80,12 @@ pub async fn install_update(
|
|||
Err(_) => return Err("Update already installing".into()),
|
||||
};
|
||||
|
||||
app.emit_all("updater", UpdateEvent::Installing).ok();
|
||||
app.emit("updater", UpdateEvent::Installing).ok();
|
||||
|
||||
get_update(app.clone())
|
||||
.await?
|
||||
.download_and_install()
|
||||
.ok_or_else(|| "No update required".to_string())?
|
||||
.download_and_install(|_, _| {}, || {})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
|
@ -98,11 +98,8 @@ pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
|
|||
tauri::plugin::Builder::new("sd-updater")
|
||||
.on_page_load(|window, _| {
|
||||
#[cfg(target_os = "linux")]
|
||||
let updater_available = {
|
||||
let env = window.env();
|
||||
let updater_available = false;
|
||||
|
||||
env.appimage.is_some()
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let updater_available = true;
|
||||
|
||||
|
|
|
@ -1,86 +1,16 @@
|
|||
{
|
||||
"package": {
|
||||
"productName": "Spacedrive"
|
||||
},
|
||||
"$schema": "https://github.com/tauri-apps/tauri/raw/tauri-v2.0.0-beta.17/core/tauri-config-schema/schema.json",
|
||||
"productName": "Spacedrive",
|
||||
"identifier": "com.spacedrive.desktop",
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://localhost:8001",
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..."
|
||||
"devUrl": "http://localhost:8001",
|
||||
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop...",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"tauri": {
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"macOSPrivateApi": true,
|
||||
"bundle": {
|
||||
"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": [
|
||||
{
|
||||
"title": "Spacedrive",
|
||||
|
@ -94,14 +24,70 @@
|
|||
"alwaysOnTop": false,
|
||||
"focus": false,
|
||||
"visible": false,
|
||||
"fileDropEnabled": true,
|
||||
"dragDropEnabled": true,
|
||||
"decorations": true,
|
||||
"transparent": true,
|
||||
"center": true
|
||||
"center": true,
|
||||
"windowEffects": {
|
||||
"effects": ["sidebar"],
|
||||
"state": "followsWindowActiveState",
|
||||
"radius": 9
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src spacedrive: webkit-pdfjs-viewer: asset: https://asset.localhost blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||
"csp": "default-src webkit-pdfjs-viewer: asset: https://asset.localhost blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "msi", "dmg", "updater"],
|
||||
"publisher": "Spacedrive Technology Inc.",
|
||||
"copyright": "Spacedrive Technology Inc.",
|
||||
"category": "Productivity",
|
||||
"shortDescription": "Spacedrive",
|
||||
"longDescription": "Cross-platform universal file explorer, powered by an open-source virtual distributed filesystem.",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"files": {
|
||||
"/usr/share/spacedrive/models/yolov8s.onnx": "../../.deps/models/yolov8s.onnx"
|
||||
},
|
||||
"depends": ["libc6", "libxdo3", "dbus"]
|
||||
}
|
||||
},
|
||||
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15",
|
||||
"exceptionDomain": null,
|
||||
"entitlements": null,
|
||||
"frameworks": ["../../.deps/Spacedrive.framework"]
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"webviewInstallMode": { "type": "embedBootstrapper", "silent": true },
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"wix": {
|
||||
"dialogImagePath": "icons/WindowsDialogImage.bmp",
|
||||
"bannerPath": "icons/WindowsBanner.bmp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEZBMURCMkU5NEU3NDAyOEMKUldTTUFuUk82YklkK296dlkxUGkrTXhCT3ZMNFFVOWROcXNaS0RqWU1kMUdRV2tDdFdIS0Y3YUsK",
|
||||
"endpoints": [
|
||||
"https://spacedrive.com/api/releases/tauri/{{version}}/{{target}}/{{arch}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|||
import { listen } from '@tauri-apps/api/event';
|
||||
import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { CacheProvider, createCache, RspcProvider } from '@sd/client';
|
||||
import { RspcProvider } from '@sd/client';
|
||||
import {
|
||||
createRoutes,
|
||||
ErrorPage,
|
||||
|
@ -56,16 +56,14 @@ export default function App() {
|
|||
return (
|
||||
<RspcProvider queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CacheProvider cache={cache}>
|
||||
{startupError ? (
|
||||
<ErrorPage
|
||||
message={startupError}
|
||||
submessage="Error occurred starting up the Spacedrive core"
|
||||
/>
|
||||
) : (
|
||||
<AppInner />
|
||||
)}
|
||||
</CacheProvider>
|
||||
{startupError ? (
|
||||
<ErrorPage
|
||||
message={startupError}
|
||||
submessage="Error occurred starting up the Spacedrive core"
|
||||
/>
|
||||
) : (
|
||||
<AppInner />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</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
|
||||
const TAB_CREATE_DELAY = 150;
|
||||
|
||||
const cache = createCache();
|
||||
|
||||
const routes = createRoutes(platform, cache);
|
||||
const routes = createRoutes(platform);
|
||||
|
||||
type redirect = { pathname: string; search: string | undefined };
|
||||
|
||||
|
|
|
@ -1,79 +1,65 @@
|
|||
/** tauri-specta globals **/
|
||||
|
||||
import { invoke as TAURI_INVOKE } from '@tauri-apps/api';
|
||||
import { invoke as TAURI_INVOKE } from '@tauri-apps/api/core';
|
||||
import * as TAURI_API_EVENT from '@tauri-apps/api/event';
|
||||
import { type WebviewWindowHandle as __WebviewWindowHandle__ } from '@tauri-apps/api/window';
|
||||
import { type WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow';
|
||||
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
export const commands = {
|
||||
async appReady(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|app_ready');
|
||||
async appReady(): Promise<void> {
|
||||
await TAURI_INVOKE('app_ready');
|
||||
},
|
||||
async resetSpacedrive(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|reset_spacedrive');
|
||||
async resetSpacedrive(): Promise<void> {
|
||||
await TAURI_INVOKE('reset_spacedrive');
|
||||
},
|
||||
async openLogsDir(): Promise<__Result__<null, null>> {
|
||||
async openLogsDir(): Promise<Result<null, null>> {
|
||||
try {
|
||||
return { status: 'ok', data: await TAURI_INVOKE('plugin:tauri-specta|open_logs_dir') };
|
||||
return { status: 'ok', data: await TAURI_INVOKE('open_logs_dir') };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async refreshMenuBar(): Promise<__Result__<null, null>> {
|
||||
async refreshMenuBar(): Promise<Result<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|refresh_menu_bar')
|
||||
};
|
||||
return { status: 'ok', data: await TAURI_INVOKE('refresh_menu_bar') };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async reloadWebview(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|reload_webview');
|
||||
async reloadWebview(): Promise<void> {
|
||||
await TAURI_INVOKE('reload_webview');
|
||||
},
|
||||
async setMenuBarItemState(id: string, enabled: boolean): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|set_menu_bar_item_state', { id, enabled });
|
||||
async setMenuBarItemState(event: MenuEvent, enabled: boolean): Promise<void> {
|
||||
await TAURI_INVOKE('set_menu_bar_item_state', { event, enabled });
|
||||
},
|
||||
async requestFdaMacos(): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|request_fda_macos');
|
||||
async requestFdaMacos(): Promise<void> {
|
||||
await TAURI_INVOKE('request_fda_macos');
|
||||
},
|
||||
async openTrashInOsExplorer(): Promise<Result<null, null>> {
|
||||
try {
|
||||
return { status: 'ok', data: await TAURI_INVOKE('open_trash_in_os_explorer') };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openFilePaths(
|
||||
library: string,
|
||||
ids: number[]
|
||||
): Promise<
|
||||
__Result__<
|
||||
(
|
||||
| { t: 'NoLibrary' }
|
||||
| { t: 'NoFile'; c: number }
|
||||
| { t: 'OpenError'; c: [number, string] }
|
||||
| { t: 'AllGood'; c: number }
|
||||
| { t: 'Internal'; c: string }
|
||||
)[],
|
||||
null
|
||||
>
|
||||
> {
|
||||
): Promise<Result<OpenFilePathResult[], null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_file_paths', { library, ids })
|
||||
};
|
||||
return { status: 'ok', data: await TAURI_INVOKE('open_file_paths', { library, ids }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openEphemeralFiles(
|
||||
paths: string[]
|
||||
): Promise<__Result__<({ t: 'Ok'; c: string } | { t: 'Err'; c: string })[], null>> {
|
||||
async openEphemeralFiles(paths: string[]): Promise<Result<EphemeralFileOpenResult[], null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_ephemeral_files', { paths })
|
||||
};
|
||||
return { status: 'ok', data: await TAURI_INVOKE('open_ephemeral_files', { paths }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
|
@ -82,14 +68,11 @@ export const commands = {
|
|||
async getFilePathOpenWithApps(
|
||||
library: string,
|
||||
ids: number[]
|
||||
): Promise<__Result__<{ url: string; name: string }[], null>> {
|
||||
): Promise<Result<OpenWithApplication[], null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|get_file_path_open_with_apps', {
|
||||
library,
|
||||
ids
|
||||
})
|
||||
data: await TAURI_INVOKE('get_file_path_open_with_apps', { library, ids })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
|
@ -98,13 +81,11 @@ export const commands = {
|
|||
},
|
||||
async getEphemeralFilesOpenWithApps(
|
||||
paths: string[]
|
||||
): Promise<__Result__<{ url: string; name: string }[], null>> {
|
||||
): Promise<Result<OpenWithApplication[], null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|get_ephemeral_files_open_with_apps', {
|
||||
paths
|
||||
})
|
||||
data: await TAURI_INVOKE('get_ephemeral_files_open_with_apps', { paths })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
|
@ -114,63 +95,50 @@ export const commands = {
|
|||
async openFilePathWith(
|
||||
library: string,
|
||||
fileIdsAndUrls: [number, string][]
|
||||
): Promise<__Result__<null, null>> {
|
||||
): Promise<Result<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_file_path_with', {
|
||||
library,
|
||||
fileIdsAndUrls
|
||||
})
|
||||
data: await TAURI_INVOKE('open_file_path_with', { library, fileIdsAndUrls })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openEphemeralFileWith(pathsAndUrls: [string, string][]): Promise<__Result__<null, null>> {
|
||||
async openEphemeralFileWith(pathsAndUrls: [string, string][]): Promise<Result<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|open_ephemeral_file_with', {
|
||||
pathsAndUrls
|
||||
})
|
||||
data: await TAURI_INVOKE('open_ephemeral_file_with', { pathsAndUrls })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async revealItems(library: string, items: RevealItem[]): Promise<__Result__<null, null>> {
|
||||
async revealItems(library: string, items: RevealItem[]): Promise<Result<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|reveal_items', { library, items })
|
||||
};
|
||||
return { status: 'ok', data: await TAURI_INVOKE('reveal_items', { library, items }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async lockAppTheme(themeType: AppThemeType): Promise<null> {
|
||||
return await TAURI_INVOKE('plugin:tauri-specta|lock_app_theme', { themeType });
|
||||
async lockAppTheme(themeType: AppThemeType): Promise<void> {
|
||||
await TAURI_INVOKE('lock_app_theme', { themeType });
|
||||
},
|
||||
async checkForUpdate(): Promise<
|
||||
__Result__<{ version: string; body: string | null } | null, string>
|
||||
> {
|
||||
async checkForUpdate(): Promise<Result<Update | null, string>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('plugin:tauri-specta|check_for_update')
|
||||
};
|
||||
return { status: 'ok', data: await TAURI_INVOKE('check_for_update') };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async installUpdate(): Promise<__Result__<null, string>> {
|
||||
async installUpdate(): Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: 'ok', data: await TAURI_INVOKE('plugin:tauri-specta|install_update') };
|
||||
return { status: 'ok', data: await TAURI_INVOKE('install_update') };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
|
@ -181,7 +149,7 @@ export const commands = {
|
|||
export const events = __makeEvents__<{
|
||||
dragAndDropEvent: DragAndDropEvent;
|
||||
}>({
|
||||
dragAndDropEvent: 'plugin:tauri-specta:drag-and-drop-event'
|
||||
dragAndDropEvent: 'drag-and-drop-event'
|
||||
});
|
||||
|
||||
/** user-defined types **/
|
||||
|
@ -191,10 +159,34 @@ export type DragAndDropEvent =
|
|||
| { type: 'Hovered'; paths: string[]; x: number; y: number }
|
||||
| { type: 'Dropped'; paths: string[]; x: number; y: number }
|
||||
| { type: 'Cancelled' };
|
||||
export type EphemeralFileOpenResult = { t: 'Ok'; c: string } | { t: 'Err'; c: string };
|
||||
export type MenuEvent =
|
||||
| 'NewLibrary'
|
||||
| 'NewFile'
|
||||
| 'NewDirectory'
|
||||
| 'AddLocation'
|
||||
| 'OpenOverview'
|
||||
| 'OpenSearch'
|
||||
| 'OpenSettings'
|
||||
| 'ReloadExplorer'
|
||||
| 'SetLayoutGrid'
|
||||
| 'SetLayoutList'
|
||||
| 'SetLayoutMedia'
|
||||
| 'ToggleDeveloperTools'
|
||||
| 'NewWindow'
|
||||
| 'ReloadWebview';
|
||||
export type OpenFilePathResult =
|
||||
| { t: 'NoLibrary' }
|
||||
| { t: 'NoFile'; c: number }
|
||||
| { t: 'OpenError'; c: [number, string] }
|
||||
| { t: 'AllGood'; c: number }
|
||||
| { t: 'Internal'; c: string };
|
||||
export type OpenWithApplication = { url: string; name: string };
|
||||
export type RevealItem =
|
||||
| { Location: { id: number } }
|
||||
| { FilePath: { id: number } }
|
||||
| { Ephemeral: { path: string } };
|
||||
export type Update = { version: string };
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
|
@ -204,13 +196,13 @@ type __EventObj__<T> = {
|
|||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||
};
|
||||
|
||||
type __Result__<T, E> = { status: 'ok'; data: T } | { status: 'error'; error: E };
|
||||
export type Result<T, E> = { status: 'ok'; data: T } | { status: 'error'; error: E };
|
||||
|
||||
function __makeEvents__<T extends Record<string, any>>(mappings: Record<keyof T, string>) {
|
||||
return new Proxy(
|
||||
{} as unknown as {
|
||||
[K in keyof T]: __EventObj__<T[K]> & {
|
||||
(handle: __WebviewWindowHandle__): __EventObj__<T[K]>;
|
||||
(handle: __WebviewWindow__): __EventObj__<T[K]>;
|
||||
};
|
||||
},
|
||||
{
|
||||
|
@ -218,7 +210,7 @@ function __makeEvents__<T extends Record<string, any>>(mappings: Record<keyof T,
|
|||
const name = mappings[event as keyof T];
|
||||
|
||||
return new Proxy((() => {}) as any, {
|
||||
apply: (_, __, [window]: [__WebviewWindowHandle__]) => ({
|
||||
apply: (_, __, [window]: [__WebviewWindow__]) => ({
|
||||
listen: (arg: any) => window.listen(name, arg),
|
||||
once: (arg: any) => window.once(name, arg),
|
||||
emit: (arg: any) => window.emit(name, arg)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { dialog, invoke, os, shell } from '@tauri-apps/api';
|
||||
import { confirm } from '@tauri-apps/api/dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { homeDir } from '@tauri-apps/api/path';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { confirm, open as dialogOpen, save as dialogSave } from '@tauri-apps/plugin-dialog';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { open as shellOpen } from '@tauri-apps/plugin-shell';
|
||||
// @ts-expect-error: Doesn't have a types package.
|
||||
import ConsistentHash from 'consistent-hash';
|
||||
import { OperatingSystem, Platform } from '@sd/interface';
|
||||
|
||||
import { commands, events } from './commands';
|
||||
import { env } from './env';
|
||||
import { createUpdater } from './updater';
|
||||
|
||||
const customUriAuthToken = (window as any).__SD_CUSTOM_SERVER_AUTH_TOKEN__ as string | undefined;
|
||||
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)}` : '';
|
||||
|
||||
async function getOs(): Promise<OperatingSystem> {
|
||||
switch (await os.type()) {
|
||||
case 'Linux':
|
||||
switch (await type()) {
|
||||
case 'linux':
|
||||
return 'linux';
|
||||
case 'Windows_NT':
|
||||
case 'windows':
|
||||
return 'windows';
|
||||
case 'Darwin':
|
||||
case 'macos':
|
||||
return 'macOS';
|
||||
default:
|
||||
return 'unknown';
|
||||
|
@ -45,9 +45,11 @@ function constructServerUrl(urlSuffix: string) {
|
|||
|
||||
export const platform = {
|
||||
platform: 'tauri',
|
||||
getThumbnailUrlByThumbKey: (keyParts) =>
|
||||
getThumbnailUrlByThumbKey: (thumbKey) =>
|
||||
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) =>
|
||||
constructServerUrl(`/file/${libraryId}/${locationLocalId}/${filePathId}`),
|
||||
|
@ -64,15 +66,18 @@ export const platform = {
|
|||
constructServerUrl(
|
||||
`/remote/${encodeURIComponent(remote_identity)}/uri/${path}?token=${customUriAuthToken}`
|
||||
),
|
||||
openLink: shell.open,
|
||||
openLink: shellOpen,
|
||||
getOs,
|
||||
openDirectoryPickerDialog: (opts) => {
|
||||
const result = dialog.open({ directory: true, ...opts });
|
||||
const result = dialogOpen({ directory: true, ...opts });
|
||||
if (opts?.multiple) return result as any; // Tauri don't properly type narrow on `multiple` argument
|
||||
return result;
|
||||
},
|
||||
openFilePickerDialog: () => dialog.open(),
|
||||
saveFilePickerDialog: (opts) => dialog.save(opts),
|
||||
openFilePickerDialog: () =>
|
||||
dialogOpen({
|
||||
multiple: true
|
||||
}).then((result) => result?.map((r) => r.path) ?? null),
|
||||
saveFilePickerDialog: (opts) => dialogSave(opts),
|
||||
showDevtools: () => invoke('show_devtools'),
|
||||
confirm: (msg, cb) => confirm(msg).then(cb),
|
||||
subscribeToDragAndDropEvents: (cb) =>
|
||||
|
@ -82,7 +87,7 @@ export const platform = {
|
|||
userHomeDir: homeDir,
|
||||
auth: {
|
||||
start(url) {
|
||||
open(url);
|
||||
return shellOpen(url);
|
||||
}
|
||||
},
|
||||
...commands,
|
||||
|
|
|
@ -25,6 +25,15 @@ export default defineConfig(({ mode }) => {
|
|||
server: {
|
||||
port: 8001
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
treeshake: 'recommended',
|
||||
external: [
|
||||
// Don't bundle Fda video for non-macOS platforms
|
||||
process.platform !== 'darwin' && /^@sd\/assets\/videos\/Fda.mp4$/
|
||||
].filter(Boolean)
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
devtoolsPlugin,
|
||||
process.env.SENTRY_AUTH_TOKEN &&
|
||||
|
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 817 B After Width: | Height: | Size: 741 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
apps/landing/public/images/agent.webp
Normal file
After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 524 KiB After Width: | Height: | Size: 360 KiB |
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 278 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 278 KiB |
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 3.4 MiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 978 KiB |
BIN
apps/landing/public/images/tags.webp
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
apps/landing/public/library_switcher.webp
Normal file
After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 513 KiB After Width: | Height: | Size: 319 KiB |
BIN
apps/landing/public/videos/Spacedrive_QuickPreview.webm
Normal file
|
@ -12,6 +12,7 @@ export type Platform = {
|
|||
version?: string;
|
||||
links?: Array<{ name: string; arch: string }>;
|
||||
disabled?: boolean;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export const platforms = {
|
||||
|
@ -36,8 +37,9 @@ export const platforms = {
|
|||
name: 'Linux',
|
||||
os: 'linux',
|
||||
icon: LinuxLogo,
|
||||
version: 'AppImage',
|
||||
links: [{ name: 'x86_64', arch: 'x86_64' }]
|
||||
version: 'deb',
|
||||
links: [{ name: 'x86_64', arch: 'x86_64' }],
|
||||
note: 'Supports Ubuntu 22.04+, Debian Bookworm+, Linux Mint 21+, PopOS 22.04+'
|
||||
},
|
||||
docker: { name: 'Docker', icon: Docker },
|
||||
android: { name: 'Android', icon: AndroidLogo, version: '10+', disabled: true },
|
||||
|
|
|
@ -22,13 +22,17 @@ export function Downloads({ latestVersion }: Props) {
|
|||
|
||||
const plausible = usePlausible();
|
||||
|
||||
const formattedVersion = (() => {
|
||||
const [formattedVersion, note] = (() => {
|
||||
const platform = selectedPlatform ?? currentPlatform;
|
||||
|
||||
if (!platform?.version) return;
|
||||
if (platform.name === 'Linux') return platform.version;
|
||||
|
||||
return `${platform.name} ${platform.version}`;
|
||||
return platform
|
||||
? [
|
||||
platform.version &&
|
||||
(platform.name === 'Linux'
|
||||
? platform.version
|
||||
: `${platform.name} ${platform.version}`),
|
||||
platform.note
|
||||
]
|
||||
: [];
|
||||
})();
|
||||
|
||||
return (
|
||||
|
@ -95,6 +99,12 @@ export function Downloads({ latestVersion }: Props) {
|
|||
{formattedVersion}
|
||||
</>
|
||||
)}
|
||||
{note && (
|
||||
<>
|
||||
<span className="mx-2 opacity-50">|</span>
|
||||
{note}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{/* Platform icons */}
|
||||
<div className="relative z-10 mt-5 flex gap-3">
|
||||
|
|
|
@ -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 extensions = {
|
||||
linux: 'AppImage',
|
||||
linux: 'deb',
|
||||
windows: 'msi',
|
||||
darwin: 'dmg'
|
||||
} as const satisfies Record<z.infer<typeof tauriTarget>, string>;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import { object, z } from 'zod';
|
||||
import { env } from '~/env';
|
||||
|
||||
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 fields = {
|
||||
|
@ -17,7 +17,7 @@ export const fields = {
|
|||
} 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(
|
||||
trigger_id: string,
|
||||
|
@ -63,7 +63,7 @@ export async function createModal(
|
|||
type: 'plain_text',
|
||||
text: 'View Commit'
|
||||
},
|
||||
url: `${github.REPO_API}/commit/${commit}`
|
||||
url: `${github.REPO_API}/commits/${commit}`
|
||||
},
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
|
@ -185,7 +185,18 @@ export async function handleSubmission(
|
|||
|
||||
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',
|
||||
body: JSON.stringify({
|
||||
ref: `refs/tags/${tag}`,
|
||||
|
@ -222,6 +233,7 @@ export async function handleSubmission(
|
|||
headers: github.HEADERS
|
||||
}
|
||||
);
|
||||
|
||||
const [release] = await Promise.all([createRelease, dispatchWorkflowRun]);
|
||||
|
||||
await fetch(responseUrl, {
|
||||
|
@ -267,7 +279,7 @@ export async function handleSubmission(
|
|||
type: 'plain_text',
|
||||
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}`
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ export async function POST(req: Request) {
|
|||
if (!isValid.valid) return new Response(isValid.error, { status: 400 });
|
||||
|
||||
const parsedBody = BODY.safeParse(Object.fromEntries([...new URLSearchParams(body)]));
|
||||
|
||||
if (!parsedBody.success) {
|
||||
console.log(parsedBody.error);
|
||||
return new Response('Unexpected request', { status: 400 });
|
||||
|
|
|
@ -42,11 +42,91 @@ export function createViewSubmission() {
|
|||
|
||||
export function createSlashCommand<T extends string>(command: T) {
|
||||
return z.object({
|
||||
command: z.literal(command),
|
||||
token: z.string(),
|
||||
team_id: z.string(),
|
||||
team_domain: z.string(),
|
||||
channel_id: z.string(),
|
||||
text: z.string().transform((s) => s.split(' ')),
|
||||
channel_name: z.string(),
|
||||
user_id: z.string(),
|
||||
trigger_id: z.string(),
|
||||
response_url: z.string()
|
||||
user_name: 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);
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 513 KiB After Width: | Height: | Size: 319 KiB |
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 382 KiB |
|
@ -110,20 +110,23 @@ export const items = [
|
|||
},
|
||||
{
|
||||
when: '0.3 Alpha',
|
||||
subtext: 'April 2024',
|
||||
title: 'Connect devices & Library sync',
|
||||
subtext: 'May 2024',
|
||||
title: 'Connect devices & sync',
|
||||
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',
|
||||
description:
|
||||
'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',
|
||||
description: 'Transcribe audio, identify faces, video scenes and more.'
|
||||
|
@ -140,7 +143,7 @@ export const items = [
|
|||
},
|
||||
{
|
||||
when: '0.4 Alpha',
|
||||
subtext: 'May 2024',
|
||||
subtext: 'June 2024',
|
||||
title: 'AI search',
|
||||
description:
|
||||
'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:
|
||||
'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',
|
||||
description:
|
||||
|
@ -177,7 +185,7 @@ export const items = [
|
|||
},
|
||||
{
|
||||
when: '0.5 Beta',
|
||||
subtext: 'June 2024',
|
||||
subtext: 'August 2024',
|
||||
title: 'Encrypted vault(s)',
|
||||
description:
|
||||
'Effortlessly manage & encrypt sensitive files. Encrypt individual files or create flexible-size vaults.'
|
||||
|
@ -185,7 +193,7 @@ export const items = [
|
|||
{
|
||||
title: 'Extensions',
|
||||
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',
|
||||
|
|
|
@ -31,39 +31,6 @@ export const teamMembers: Array<TeamMemberProps> = [
|
|||
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',
|
||||
location: 'Finland',
|
||||
|
@ -74,15 +41,6 @@ export const teamMembers: Array<TeamMemberProps> = [
|
|||
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',
|
||||
location: 'Jordan',
|
||||
|
|
|
@ -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;
|
Before Width: | Height: | Size: 380 KiB After Width: | Height: | Size: 313 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 62 KiB |
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "sd-mobile-android"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.64.0"
|
||||
rust-version = "1.64"
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
@ -14,8 +14,8 @@ crate-type = ["cdylib"]
|
|||
# Spacedrive Sub-crates
|
||||
sd-mobile-core = { path = "../../core" }
|
||||
|
||||
# FFI
|
||||
jni = "0.21.1"
|
||||
|
||||
# Other
|
||||
# Workspace dependencies
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Specific Mobile Android dependencies
|
||||
jni = "0.21.1"
|
||||
|
|