Merge remote-tracking branch 'origin' into c

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

View file

@ -11,9 +11,11 @@ codegen
Condvar
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
View file

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

File diff suppressed because one or more lines are too long

View file

@ -2,12 +2,13 @@ import client from '@actions/artifact';
import * as core from '@actions/core';
import * as 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();

View file

@ -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"

View file

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

View file

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

View file

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

View file

@ -40,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' }}

View file

@ -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 }}

View file

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

View file

@ -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
View file

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

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

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

4
.vscode/launch.json vendored
View file

@ -11,9 +11,9 @@
"cargo": {
"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
View file

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

View file

@ -89,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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -8,10 +8,12 @@ repository = { workspace = true }
edition = { workspace = true }
[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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -6,10 +6,11 @@ repository = { workspace = true }
edition = { workspace = true }
[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"] }

View file

@ -1,32 +1,15 @@
use std::path::{Path, PathBuf};
use std::path::Path;
use gtk::{
gio::{
content_type_guess,
prelude::AppInfoExt,
prelude::{AppLaunchContextExt, FileExt},
AppInfo, AppLaunchContext, DesktopAppInfo, File as GioFile, ResourceError,
content_type_guess, prelude::AppInfoExt, prelude::FileExt, AppInfo, AppLaunchContext,
DesktopAppInfo, File as GioFile, ResourceError,
},
glib::error::Error as GlibError,
prelude::IsA,
};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::env::remove_prefix_from_pathlist;
fn remove_prefix_from_env_in_ctx(
ctx: &impl IsA<AppLaunchContext>,
env_name: &str,
prefix: &impl AsRef<Path>,
) {
if let Some(value) = remove_prefix_from_pathlist(env_name, prefix) {
ctx.setenv(env_name, value);
} else {
ctx.unsetenv(env_name);
}
}
thread_local! {
static LAUNCH_CTX: AppLaunchContext = {
// TODO: Display supports requires GDK, which can only run on the main thread
@ -36,33 +19,8 @@ thread_local! {
// "This is an Glib type conversion, it should never fail because GDKAppLaunchContext is a subclass of AppLaunchContext"
// )).unwrap_or_default();
let ctx = AppLaunchContext::default();
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) {
// Remove AppImage paths from environment variables to avoid external applications attempting to use the AppImage's libraries
// https://github.com/AppImage/AppImageKit/blob/701b711f42250584b65a88f6427006b1d160164d/src/AppRun.c#L168-L194
ctx.unsetenv("PYTHONHOME");
ctx.unsetenv("GTK_DATA_PREFIX");
ctx.unsetenv("GTK_THEME");
ctx.unsetenv("GDK_BACKEND");
ctx.unsetenv("GTK_EXE_PREFIX");
ctx.unsetenv("GTK_IM_MODULE_FILE");
ctx.unsetenv("GDK_PIXBUF_MODULE_FILE");
remove_prefix_from_env_in_ctx(&ctx, "PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "LD_LIBRARY_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "PYTHONPATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "XDG_DATA_DIRS", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "PERLLIB", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GSETTINGS_SCHEMA_DIR", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "QT_PLUGIN_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH_1_0", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GTK_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GIO_EXTRA_MODULES", &appdir);
}
ctx
AppLaunchContext::default()
}
}

View file

@ -1,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
}

View file

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

View file

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

View file

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

View file

@ -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);
// }

View file

@ -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"]

View file

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

View file

@ -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",

View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "sd-desktop"
version = "0.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"]

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -373,21 +373,6 @@ pub async fn open_ephemeral_file_with(paths_and_urls: Vec<PathAndUrl>) -> Result
fn inner_reveal_paths(paths: impl Iterator<Item = PathBuf>) {
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:#?}");
}

View file

@ -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
}
}

View file

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

View file

@ -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(|| {

View file

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

View file

@ -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}}"
]
}
}
}

View file

@ -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 };

View file

@ -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)

View file

@ -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,

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 319 KiB

View file

@ -12,6 +12,7 @@ export type Platform = {
version?: string;
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 },

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { z } from 'zod';
import { object, z } from 'zod';
import { env } from '~/env';
import * 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}`
}
]
}

View file

@ -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 });

View file

@ -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);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 382 KiB

View file

@ -110,20 +110,23 @@ export const items = [
},
{
when: '0.3 Alpha',
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',

View file

@ -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',

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -1,7 +1,7 @@
[package]
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"

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