Merge branch 'main' into mob-70-update-folder-names-when-renamed-on-ios

This commit is contained in:
Arnab Chakraborty 2024-04-26 11:52:36 -04:00
commit 4a55a2fae1
442 changed files with 39814 additions and 21179 deletions

View file

@ -7,7 +7,7 @@ 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: { bundle: string; bundleExt: string; archiveExt: string };
standalone: Array<TargetConfig>;
};
@ -31,7 +31,7 @@ const OS_TARGETS = {
linux: {
updater: {
bundle: 'appimage',
bundleExt: "AppImage",
bundleExt: 'AppImage',
archiveExt: 'tar.gz'
},
standalone: [
@ -57,8 +57,8 @@ async function globFiles(pattern: string) {
return await globber.glob();
}
async function uploadUpdater({ bundle, bundleExt, archiveExt }: BuildTarget["updater"]) {
const fullExt = `${bundleExt}.${archiveExt}`
async function uploadUpdater({ bundle, bundleExt, archiveExt }: BuildTarget['updater']) {
const fullExt = `${bundleExt}.${archiveExt}`;
const files = await globFiles(`${BUNDLE_DIR}/${bundle}/*.${fullExt}*`);
const updaterPath = files.find((file) => file.endsWith(fullExt));

View file

@ -11,7 +11,7 @@ runs:
- 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

@ -67,11 +67,32 @@ runs:
working-directory: core
if: ${{ steps.cache-prisma-restore.outputs.cache-hit != 'true' }}
shell: bash
run: cargo prisma generate
run: |
set -euxo pipefail
cargo prisma generate
# Check if a new migration should be created due to changes in the schema
cargo prisma migrate dev -n test --create-only --skip-generate
_new_migrations="$(
git ls-files --others --exclude-standard \
| { grep '^prisma/migrations/' || true; } \
| xargs sh -euxc '[ "$#" -lt 2 ] || grep -L "$@" || true' sh 'This is an empty migration' \
| wc -l \
| awk '{$1=$1};1'
)"
if [ "$_new_migrations" -gt 0 ]; then
echo "::error file=core/prisma/schema.prisma,title=Missing migration::New migration should be generated due to changes in prisma schema"
case "$GITHUB_REF" in
"refs/heads/main" | "refs/heads/gh-readonly-queue/main/"* | "refs/tags/"*)
# Fail action if we are on main or a release tag, to avoid releasing an app with a broken db
exit 1
;;
esac
fi
- name: Save Prisma codegen
id: cache-prisma-save
if: ${{ steps.cache-prisma-restore.outputs.cache-hit != 'true' }}
if: ${{ steps.cache-prisma-restore.outputs.cache-hit != 'true' && (github.ref == 'refs/heads/main' || inputs.save-cache == 'true') }}
uses: actions/cache/save@v4
with:
key: ${{ steps.cache-prisma-restore.outputs.cache-primary-key }}

View file

@ -42,6 +42,11 @@ runs:
key: ${{ steps.cache-llvm-restore.outputs.cache-primary-key }}
path: C:/Program Files/LLVM
- name: Install current Bash on macOS
shell: bash
if: runner.os == 'macOS'
run: brew install bash
- name: Install Nasm
if: ${{ runner.os != 'Linux' }}
uses: ilammy/setup-nasm@v1
@ -53,6 +58,17 @@ runs:
curl -L# 'https://github.com/rui314/mold/releases/download/v2.4.0/mold-2.4.0-x86_64-linux.tar.gz' \
| sudo tar -xzf- -C /usr/local
- name: Remove 32-bit libs and incompatible pre-installed pkgs from Runner
shell: bash
if: ${{ runner.os == 'Linux' }}
run: |
dpkg -l | grep i386
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386"
sudo dpkg --remove-architecture i386
# https://github.com/actions/runner-images/issues/9546#issuecomment-2014940361
sudo apt-get remove libunwind-*
- name: Setup Rust and Dependencies
uses: ./.github/actions/setup-rust
with:
@ -78,4 +94,4 @@ runs:
pushd scripts
npm i --production
popd
node scripts/preprep.mjs
env NODE_ENV=debug node scripts/preprep.mjs

View file

@ -11,6 +11,7 @@ env:
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
SD_AUTH: disabled
# Cancel previous runs of the same workflow on the same branch.
concurrency:
@ -66,17 +67,28 @@ 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:
runTests: false
working-directory: .
- name: Download test data
run: pnpm test-data small
- name: E2E test
uses: cypress-io/github-action@v6
with:
build: npx cypress info
install: false
command: pnpm test:e2e
command: env CI=true pnpm test:e2e
working-directory: apps/web
- uses: actions/upload-artifact@v4
@ -119,6 +131,7 @@ jobs:
uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
continue-on-error: true
id: filter
with:
filters: |
@ -135,13 +148,13 @@ jobs:
- 'Cargo.lock'
- name: Setup Rust and Prisma
if: steps.filter.outputs.changes == 'true'
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-rust
with:
restore-cache: 'false'
- name: Run rustfmt
if: steps.filter.outputs.changes == 'true'
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
run: cargo fmt --all -- --check
clippy:
@ -173,6 +186,7 @@ jobs:
uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
continue-on-error: true
id: filter
with:
filters: |
@ -191,16 +205,16 @@ jobs:
- 'Cargo.lock'
- name: Setup System and Rust
if: steps.filter.outputs.changes == 'true'
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-system
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Run Clippy
if: steps.filter.outputs.changes == 'true'
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: actions-rs-plus/clippy-check@v2
with:
args: --workspace --all-features
args: --workspace --all-features --locked
# test:
# name: Test (${{ matrix.platform }})

View file

@ -138,15 +138,15 @@ jobs:
ios:
name: iOS
runs-on: macos-14
runs-on: macos-12
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
# - name: Install Xcode
# uses: maxim-lobanov/setup-xcode@v1
# with:
# xcode-version: latest-stable
- name: Setup System and Rust
uses: ./.github/actions/setup-system
@ -189,9 +189,6 @@ jobs:
run: xcodebuild -workspace ./Spacedrive.xcworkspace -scheme Spacedrive -configuration Release -sdk iphonesimulator -derivedDataPath build -arch "$(uname -m)"
- name: Install Maestro
env:
# Workaround: https://github.com/mobile-dev-inc/maestro/issues/1585
MAESTRO_VERSION: '1.33.1'
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
brew tap facebook/fb
@ -199,16 +196,12 @@ jobs:
echo "${HOME}/.maestro/bin" >> $GITHUB_PATH
- name: Run Simulator
id: run_simulator
uses: futureware-tech/simulator-action@v3
with:
model: 'iPhone 15'
os_version: 17
model: 'iPhone 14'
os_version: 16
erase_before_boot: false
- name: Run Tests
env:
# https://github.com/expo/expo/blob/339fa68/apps/bare-expo/scripts/start-ios-e2e-test.ts#L12
MAESTRO_DRIVER_STARTUP_TIMEOUT: 120000
run: |
xcrun simctl install booted apps/mobile/ios/build/Build/Products/Release-iphonesimulator/Spacedrive.app
./apps/mobile/scripts/run-maestro-tests.sh ios
run: ./apps/mobile/scripts/run-maestro-tests.sh ios ${{ steps.run_simulator.outputs.udid }}

View file

@ -68,13 +68,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove 32-bit libs
if: ${{ runner.os == 'Linux' }}
run: |
dpkg -l | grep i386
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386"
sudo dpkg --remove-architecture i386
- name: Install Apple API key
if: ${{ runner.os == 'macOS' }}
run: |

View file

@ -30,13 +30,38 @@ jobs:
- 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: Determine image name & tag
id: image_info
shell: bash
run: |
set -euxo pipefail
if [ "$GITHUB_EVENT_NAME" == "release" ]; then
IMAGE_TAG="${GITHUB_REF##*/}"
else
@ -54,8 +79,7 @@ jobs:
- 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 +93,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
.gitignore vendored
View file

@ -83,3 +83,6 @@ spacedrive
.github/scripts/deps
.vite-inspect
vite.config.ts.*
/test-data
/config.json

View file

@ -32,3 +32,6 @@ interface/components/TextViewer/prism-lazy.ts
# Stops from constant package.json changes showing up in commits
package*.json
# Dont format locales json
interface/locales

View file

@ -7,6 +7,7 @@
"bradlc.vscode-tailwindcss", // Provides Tailwind CSS IntelliSense
"prisma.prisma", // Prisma is an open-source database toolkit
"dbaeumer.vscode-eslint", // Integrates ESLint JavaScript into VS Code
"esbenp.prettier-vscode" // Code formatter using prettier
"esbenp.prettier-vscode", // Code formatter using prettier,
"lokalise.i18n-ally" // i18n-ally is an all-in-one i18n (internationalization) extension for VS Code
]
}

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

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

16
.vscode/settings.json vendored
View file

@ -88,5 +88,19 @@
"rust-analyzer.linkedProjects": [],
"rust-analyzer.cargo.extraEnv": {},
"rust-analyzer.check.targets": null,
"rust-analyzer.showUnlinkedFileNotification": false
"rust-analyzer.showUnlinkedFileNotification": false,
"i18n-ally.localesPaths": [
"interface/locales",
"apps/mobile/ios/Pods/RCT-Folly/folly/lang",
"apps/mobile/ios/Pods/boost/boost/locale",
"apps/mobile/ios/Pods/boost/boost/predef/language"
],
"i18n-ally.enabledParsers": ["ts", "json"],
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/common.json",
"i18n-ally.enabledFrameworks": ["react"],
"i18n-ally.keystyle": "flat",
// You need to add this to your locale settings file "i18n-ally.translate.google.apiKey": "xxx"
"i18n-ally.translate.engines": ["google"]
}

View file

@ -58,6 +58,13 @@ To quickly run only the desktop app after `prep`, you can use:
Also, the react-devtools can be launched using `pnpm dlx react-devtools`.
However, it must be executed before starting the desktop app for it to connect.
You can download a bundle with sample files to test the app by running:
- `pnpm test-data`
Only for Linux and macOS (Requires curl and tar).
The test files will be located in a directory called `test-data` in the root of the spacedrive repo.
To run the web app:
- `pnpm dev:web`
@ -68,15 +75,23 @@ You can launch these individually if you'd prefer:
- `cargo run -p sd-server` (server)
- `pnpm web dev` (web interface)
To run the e2e tests for the web app:
- `pnpm web test:e2e`
If you are developing a new test, you can execute Cypress in interactive mode with:
- `pnpm web test:interactive`
To run the landing page:
- `pnpm landing dev`
If you encounter any issues, ensure that you are using the following versions of Rust, Node and Pnpm:
- Rust version: **1.75.0**
- Node version: **18.17**
- Pnpm version: **8.0.0**
- Rust version: **1.75**
- Node version: **18.18**
- Pnpm version: **9.0.6**
After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.

199
Cargo.lock generated
View file

@ -1106,15 +1106,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bincode"
version = "2.0.0-rc.3"
@ -6463,7 +6454,9 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_macros 0.10.0",
"phf_shared 0.10.0",
"proc-macro-hack",
]
[[package]]
@ -6540,6 +6533,20 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
@ -8124,8 +8131,9 @@ dependencies = [
"reqwest",
"rmp-serde",
"rmpv",
"sd-core-file-path-helper",
"sd-core-prisma-helpers",
"sd-core-sync",
"sd-file-path-helper",
"sd-prisma",
"sd-sync",
"sd-utils",
@ -8179,7 +8187,7 @@ dependencies = [
[[package]]
name = "sd-core"
version = "0.2.5"
version = "0.2.13"
dependencies = [
"aovec",
"async-channel",
@ -8229,11 +8237,14 @@ dependencies = [
"sd-ai",
"sd-cache",
"sd-cloud-api",
"sd-core-file-path-helper",
"sd-core-heavy-lifting",
"sd-core-indexer-rules",
"sd-core-prisma-helpers",
"sd-core-sync",
"sd-crypto",
"sd-ffmpeg",
"sd-file-ext",
"sd-file-path-helper",
"sd-images",
"sd-media-metadata",
"sd-p2p",
@ -8265,10 +8276,99 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"tracing-test",
"trash",
"uuid",
"webp",
]
[[package]]
name = "sd-core-file-path-helper"
version = "0.1.0"
dependencies = [
"chrono",
"prisma-client-rust",
"regex",
"sd-core-prisma-helpers",
"sd-prisma",
"sd-utils",
"serde",
"thiserror",
"tokio",
"tracing",
"winapi-util",
]
[[package]]
name = "sd-core-heavy-lifting"
version = "0.1.0"
dependencies = [
"async-channel",
"async-trait",
"blake3",
"chrono",
"futures",
"futures-concurrency",
"globset",
"itertools 0.12.0",
"lending-stream",
"prisma-client-rust",
"rmp-serde",
"rmpv",
"rspc",
"sd-core-file-path-helper",
"sd-core-indexer-rules",
"sd-core-prisma-helpers",
"sd-core-sync",
"sd-file-ext",
"sd-prisma",
"sd-sync",
"sd-task-system",
"sd-utils",
"serde",
"serde_json",
"specta",
"static_assertions",
"strum",
"tempfile",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
"tracing-test",
"uuid",
]
[[package]]
name = "sd-core-indexer-rules"
version = "0.1.0"
dependencies = [
"chrono",
"futures-concurrency",
"globset",
"prisma-client-rust",
"rmp-serde",
"rspc",
"sd-prisma",
"sd-utils",
"serde",
"specta",
"tempfile",
"thiserror",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "sd-core-prisma-helpers"
version = "0.1.0"
dependencies = [
"prisma-client-rust",
"sd-cache",
"sd-prisma",
"serde",
]
[[package]]
name = "sd-core-sync"
version = "0.0.0"
@ -8295,7 +8395,7 @@ dependencies = [
"aes-gcm-siv",
"argon2",
"balloon-hash",
"bincode 2.0.0-rc.3",
"bincode",
"blake3",
"chacha20poly1305",
"cmov",
@ -8336,7 +8436,7 @@ dependencies = [
[[package]]
name = "sd-desktop"
version = "0.2.5"
version = "0.2.13"
dependencies = [
"axum",
"directories 5.0.1",
@ -8357,7 +8457,6 @@ dependencies = [
"specta",
"tauri",
"tauri-build",
"tauri-plugin-window-state",
"tauri-specta",
"thiserror",
"tokio",
@ -8424,27 +8523,11 @@ dependencies = [
"tokio",
]
[[package]]
name = "sd-file-path-helper"
version = "0.1.0"
dependencies = [
"chrono",
"prisma-client-rust",
"regex",
"sd-prisma",
"sd-utils",
"serde",
"thiserror",
"tokio",
"tracing",
"winapi-util",
]
[[package]]
name = "sd-images"
version = "0.0.0"
dependencies = [
"bincode 2.0.0-rc.3",
"bincode",
"image",
"libheif-rs",
"libheif-sys",
@ -8601,11 +8684,13 @@ name = "sd-server"
version = "0.1.0"
dependencies = [
"axum",
"base64 0.21.7",
"http",
"include_dir",
"mime_guess",
"rspc",
"sd-core",
"secstr",
"tempfile",
"tokio",
"tracing",
@ -8703,6 +8788,15 @@ dependencies = [
"zbus",
]
[[package]]
name = "secstr"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04f657244f605c4cf38f6de5993e8bd050c8a303f86aeabff142d5c7c113e12"
dependencies = [
"libc",
]
[[package]]
name = "security-framework"
version = "2.9.2"
@ -9460,6 +9554,7 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"phf 0.10.1",
"strum_macros",
]
@ -9817,21 +9912,6 @@ dependencies = [
"tauri-utils 1.5.1",
]
[[package]]
name = "tauri-plugin-window-state"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa47eaa4047a7b51064caff32f0c6282e2c5adc6ceacdd493ecf1b01fa4b0eaa"
dependencies = [
"bincode 1.3.3",
"bitflags 2.4.1",
"log",
"serde",
"serde_json",
"tauri",
"thiserror",
]
[[package]]
name = "tauri-runtime"
version = "0.14.1"
@ -10456,6 +10536,22 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "trash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1a7a9a17d3b004898be42be29a4c18d5a4cf008b5cdf72d69b1945dfcb158a"
dependencies = [
"chrono",
"libc",
"log",
"objc",
"once_cell",
"scopeguard",
"url",
"windows 0.44.0",
]
[[package]]
name = "treediff"
version = "4.0.2"
@ -11192,6 +11288,15 @@ dependencies = [
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows"
version = "0.48.0"

View file

@ -59,9 +59,12 @@ chrono = "0.4.31"
clap = "4.4.7"
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"
@ -73,6 +76,7 @@ rmp-serde = "1.1.2"
rmpv = { version = "^1.0.1", features = ["with-serde"] }
serde = "1.0"
serde_json = "1.0"
static_assertions = "1.1.0"
strum = "0.25"
strum_macros = "0.25"
tempfile = "3.8.1"
@ -84,6 +88,9 @@ uhlc = "=0.5.2"
uuid = "1.5.0"
webp = "0.2.6"
[workspace.dev-dependencies]
tracing-test = { version = "^0.2.4" }
[patch.crates-io]
# Proper IOS Support
if-watch = { git = "https://github.com/oscartbeaumont/if-watch.git", rev = "a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3" }

View file

@ -6,6 +6,7 @@ repository = { workspace = true }
edition = { workspace = true }
[dependencies]
# Spacedrive Sub-crates
sd-crypto = { path = "../../crates/crypto" }
anyhow = { workspace = true }

View file

@ -1,6 +1,6 @@
[package]
name = "sd-desktop"
version = "0.2.5"
version = "0.2.13"
description = "The universal file manager."
authors = ["Spacedrive Technology Inc <support@spacedrive.com>"]
default-run = "sd-desktop"
@ -9,6 +9,7 @@ repository = { workspace = true }
edition = { workspace = true }
[dependencies]
# Spacedrive Sub-crates
sd-core = { path = "../../../core", features = [
"ffmpeg",
"heif",
@ -33,20 +34,19 @@ 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",
"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"
tauri-plugin-window-state = "0.1.1"
[target.'cfg(target_os = "linux")'.dependencies]
sd-desktop-linux = { path = "../crates/linux" }
@ -63,6 +63,6 @@ webview2-com = "0.19.1"
tauri-build = "1.5.0"
[features]
default = ["ai-models", "custom-protocol"]
default = ["custom-protocol"]
ai-models = ["sd-core/ai"]
custom-protocol = ["tauri/custom-protocol"]

View file

@ -7,6 +7,7 @@ use std::{
collections::HashMap,
fs,
path::PathBuf,
process::Command,
sync::{Arc, Mutex, PoisonError},
time::Duration,
};
@ -149,6 +150,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("~/.local/share/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 {
@ -218,6 +260,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,
@ -241,7 +284,7 @@ async fn main() -> tauri::Result<()> {
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(tauri_plugin_window_state::Builder::default().build())
.plugin(specta_builder)
.setup(move |app| {
let app = app.handle();

View file

@ -12,6 +12,8 @@
"macOSPrivateApi": true,
"bundle": {
"active": true,
"publisher": "Spacedrive Technology Inc.",
"category": "Productivity",
"targets": ["deb", "msi", "dmg", "updater"],
"identifier": "com.spacedrive.desktop",
"icon": [
@ -24,7 +26,7 @@
"resources": {},
"externalBin": [],
"copyright": "Spacedrive Technology Inc.",
"shortDescription": "File explorer from the future.",
"shortDescription": "Spacedrive",
"longDescription": "Cross-platform universal file explorer, powered by an open-source virtual distributed filesystem.",
"deb": {
"files": {

View file

@ -78,18 +78,28 @@ const cache = createCache();
const routes = createRoutes(platform, cache);
type redirect = { pathname: string; search: string | undefined };
function AppInner() {
const [tabs, setTabs] = useState(() => [createTab()]);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const selectedTab = tabs[selectedTabIndex]!;
function createTab() {
function createTab(redirect?: redirect) {
const history = createMemoryHistory();
const router = createMemoryRouterWithHistory({ routes, history });
const id = Math.random().toString();
// for "Open in new tab"
if (redirect) {
router.navigate({
pathname: redirect.pathname,
search: redirect.search
});
}
const dispose = router.subscribe((event) => {
// we don't care about non-idle events as those are artifacts of form mutations + suspense
if (event.navigation.state !== 'idle') return;
@ -165,13 +175,13 @@ function AppInner() {
tabIndex: selectedTabIndex,
setTabIndex: setSelectedTabIndex,
tabs: tabs.map(({ router, title }) => ({ router, title })),
createTab() {
createTab(redirect?: redirect) {
createTabPromise.current = createTabPromise.current.then(
() =>
new Promise((res) => {
startTransition(() => {
setTabs((tabs) => {
const newTab = createTab();
const newTab = createTab(redirect);
const newTabs = [...tabs, newTab];
setSelectedTabIndex(newTabs.length - 1);

View file

@ -41,6 +41,17 @@ export const commands = {
async requestFdaMacos(): Promise<null> {
return await TAURI_INVOKE('plugin:tauri-specta|request_fda_macos');
},
async openTrashInOsExplorer(): Promise<__Result__<null, null>> {
try {
return {
status: 'ok',
data: await TAURI_INVOKE('plugin:tauri-specta|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[]

View file

@ -56,7 +56,7 @@
"@octokit/openapi-types": "^20.0.0",
"@sd/config": "workspace:*",
"@svgr/webpack": "^8.1.0",
"@types/node": ">18.x",
"@types/node": ">18.18.x",
"@types/react": "^18.2.67",
"@types/react-burger-menu": "^2.8.7",
"@types/react-dom": "^18.2.22",

View file

@ -20,7 +20,7 @@ export function DockerDialog({
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-app/80 backdrop-blur-sm radix-state-closed:animate-out radix-state-closed:fade-out-0 radix-state-open:animate-in radix-state-open:fade-in-0" />
<Dialog.Content className="fixed left-[50%] top-[50%] z-50 w-[500px] translate-x-[-50%] translate-y-[-50%] overflow-hidden rounded-md border border-app-line bg-app shadow-lg outline-none duration-200 radix-state-closed:animate-out radix-state-closed:fade-out-0 radix-state-closed:zoom-out-95 radix-state-closed:slide-out-to-left-1/2 radix-state-closed:slide-out-to-top-[48%] radix-state-open:animate-in radix-state-open:fade-in-0 radix-state-open:zoom-in-95 radix-state-open:slide-in-from-left-1/2 radix-state-open:slide-in-from-top-[48%]">
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-[500px] translate-x-[-1/2] translate-y-[-1/2] overflow-hidden rounded-md border border-app-line bg-app shadow-lg outline-none duration-200 radix-state-closed:animate-out radix-state-closed:fade-out-0 radix-state-closed:zoom-out-95 radix-state-closed:slide-out-to-left-1/2 radix-state-closed:slide-out-to-top-[48%] radix-state-open:animate-in radix-state-open:fade-in-0 radix-state-open:zoom-in-95 radix-state-open:slide-in-from-left-1/2 radix-state-open:slide-in-from-top-[48%]">
<div className="p-3 pt-0">
<h2 className="py-2 text-center text-lg font-semibold text-ink">Docker</h2>
{/* Link */}

View file

@ -36,7 +36,7 @@ export const platforms = {
name: 'Linux',
os: 'linux',
icon: LinuxLogo,
version: 'AppImage',
version: 'deb',
links: [{ name: 'x86_64', arch: 'x86_64' }]
},
docker: { name: 'Docker', icon: Docker },
@ -96,7 +96,7 @@ export function Platform({ platform, ...props }: ComponentProps<'a'> & PlatformP
<Tooltip label={platform.name}>
<Outer {...props}>
<Icon
className={`h-[24px] w-[24px] text-white ${
className={`size-[24px] text-white ${
platform.disabled ? 'opacity-20' : 'opacity-90'
}`}
weight="fill"

View file

@ -30,7 +30,7 @@ export async function Footer() {
/>
<div className="m-auto grid min-h-64 max-w-[100rem] grid-cols-2 gap-6 p-8 pb-20 pt-10 text-white sm:grid-cols-2 lg:grid-cols-6">
<div className="col-span-2">
<Image alt="Spacedrive logo" src={Logo} className="mb-5 h-10 w-10" />
<Image alt="Spacedrive logo" src={Logo} className="mb-5 size-10" />
<h1 className="mb-1 text-xl font-bold">Spacedrive</h1>
<p className="text-sm text-gray-350 opacity-50">
@ -38,31 +38,31 @@ export async function Footer() {
</p>
<div className="mb-10 mt-12 flex flex-row space-x-3">
<FooterLink link="https://x.com/spacedriveapp">
<Twitter className="h-6 w-6" />
<Twitter className="size-6" />
</FooterLink>
<FooterLink aria-label="discord" link="https://discord.gg/gTaF2Z44f5">
<Discord className="h-6 w-6" />
<Discord className="size-6" />
</FooterLink>
<FooterLink
aria-label="instagram"
link="https://instagram.com/spacedriveapp"
>
<Instagram className="h-6 w-6" />
<Instagram className="size-6" />
</FooterLink>
<FooterLink aria-label="github" link="https://github.com/spacedriveapp">
<Github className="h-6 w-6" />
<Github className="size-6" />
</FooterLink>
<FooterLink
aria-label="open collective"
link="https://opencollective.com/spacedrive"
>
<Opencollective className="h-6 w-6" />
<Opencollective className="size-6" />
</FooterLink>
<FooterLink
aria-label="twitch stream"
link="https://twitch.tv/jamiepinelive"
>
<Twitch className="h-6 w-6" />
<Twitch className="size-6" />
</FooterLink>
</div>
</div>
@ -141,8 +141,8 @@ export async function Footer() {
</div>
</div>
<div className="absolute top-0 flex h-1 w-full flex-row items-center justify-center opacity-100">
<div className="h-[1px] w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-[1px] w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
<div className="h-px w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-px w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
</div>
</footer>
);

View file

@ -20,7 +20,7 @@ export function MobileDropdown() {
<Dropdown.Root
button={
<Button aria-label="mobile-menu" className="hover:!bg-transparent" size="icon">
<DotsThreeVertical weight="bold" className="h-6 w-6 " />
<DotsThreeVertical weight="bold" className="size-6 " />
</Button>
}
className="right-4 top-2 block text-white lg:hidden"

View file

@ -12,7 +12,7 @@ export function NavBar() {
<div className="navbar-blur fixed z-[55] h-16 w-full !bg-black/10 px-2 transition">
<div className="relative m-auto flex h-full max-w-[100rem] items-center p-5">
<Link href="/" className="absolute flex flex-row items-center">
<Image alt="Spacedrive logo" src={Logo} className="z-30 mr-3 h-8 w-8" />
<Image alt="Spacedrive logo" src={Logo} className="z-30 mr-3 size-8" />
<h3 className="text-xl font-bold text-white">Spacedrive</h3>
</Link>
@ -40,7 +40,7 @@ export function NavBar() {
target="_blank"
rel="noreferrer"
>
<Discord className="h-6 w-6 text-white opacity-100 duration-300 hover:opacity-50" />
<Discord className="size-6 text-white opacity-100 duration-300 hover:opacity-50" />
</Link>
<Link
aria-label="github"
@ -48,13 +48,13 @@ export function NavBar() {
target="_blank"
rel="noreferrer"
>
<Github className="h-6 w-6 text-white opacity-100 duration-300 hover:opacity-50" />
<Github className="size-6 text-white opacity-100 duration-300 hover:opacity-50" />
</Link>
</div>
</div>
<div className="absolute bottom-0 flex h-1 w-full flex-row items-center justify-center pt-4 opacity-100">
<div className="h-[1px] w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-[1px] w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
<div className="h-px w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-px w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
</div>
</div>
);

View file

@ -24,7 +24,7 @@ export function NewBanner(props: NewBannerProps) {
<Newspaper weight="fill" className="text-white " size={20} />
<p className="font-regular truncate text-white">{headline}</p>
</div>
<div role="separator" className="h-22 mx-4 w-[1px] bg-zinc-700/70" />
<div role="separator" className="h-22 mx-4 w-px bg-zinc-700/70" />
<span className="font-regular shrink-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent decoration-primary-600">
{link} <span aria-hidden="true">&rarr;</span>
</span>

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

@ -13,7 +13,7 @@ export function Breadcrumbs() {
<div className="flex flex-row items-center gap-1">
{slug.map((item, index) => (
<Fragment key={index}>
{index > 0 && <CaretRight className="h-4 w-4" />}
{index > 0 && <CaretRight className="size-4" />}
<span className="px-1 text-sm">{toTitleCase(item)}</span>
</Fragment>
))}

View file

@ -36,7 +36,7 @@ export function OpenMobileSidebarButton() {
return (
<Button className="ml-1 !border-none !px-2" onClick={() => menu.setOpen((o) => !o)}>
<List weight="bold" className="h-6 w-6" />
<List weight="bold" className="size-6" />
</Button>
);
}
@ -57,7 +57,7 @@ export function MobileSidebarWrapper({ children }: PropsWithChildren) {
onClick={() => menu.setOpen((o) => !o)}
className="-ml-0.5 mb-3 !border-none !px-1"
>
<X weight="bold" className="h-6 w-6" />
<X weight="bold" className="size-6" />
</Button>
{children}
</div>

View file

@ -58,7 +58,7 @@ export async function Sidebar() {
slug={section.slug}
>
<div className="mr-4 rounded-lg border-t border-gray-400/20 bg-gray-500 p-1">
<Icon weight="bold" className="h-4 w-4 text-white opacity-80" />
<Icon weight="bold" className="size-4 text-white opacity-80" />
</div>
{toTitleCase(section.slug)}
</SectionLink>

View file

@ -16,7 +16,7 @@ export default function NotFound() {
<Markdown classNames="flex w-full justify-center">
<div className="m-auto flex flex-col items-center ">
<div className="h-32" />
<SmileyXEyes className="mb-3 h-44 w-44" />
<SmileyXEyes className="mb-3 size-44" />
<h1 className="mb-2 text-center">
In the quantum realm this page potentially exists.
</h1>

View file

@ -1,3 +1,4 @@
import { ArrowUp } from '@phosphor-icons/react/dist/ssr';
import Image from 'next/image';
import CyclingImage from '~/components/CyclingImage';
import { toTitleCase } from '~/utils/util';
@ -77,34 +78,40 @@ export default async function Page() {
src="/images/app/gradient.webp"
/>
<div className="relative m-auto mt-10 flex w-full max-w-7xl overflow-hidden rounded-lg transition-transform duration-700 ease-in-out hover:-translate-y-4 hover:scale-[1.02] md:mt-0">
<div className="z-30 flex w-full rounded-lg border-t border-app-line/50 backdrop-blur">
<CyclingImage
loading="eager"
width={1278}
height={626}
alt="spacedrive app"
className="rounded-lg"
images={[
'/images/app/1.webp',
'/images/app/2.webp',
'/images/app/3.webp',
'/images/app/4.webp',
'/images/app/5.webp',
'/images/app/10.webp',
'/images/app/6.webp',
'/images/app/7.webp',
'/images/app/8.webp',
'/images/app/9.webp'
]}
/>
<Image
loading="eager"
className="pointer-events-none absolute opacity-100 transition-opacity duration-1000 ease-in-out hover:opacity-0 md:w-auto"
width={2278}
height={626}
alt="l"
src="/images/app/gradient-overlay.png"
/>
<div className="flex flex-col items-center justify-center">
<div className="z-30 flex w-full rounded-lg border-t border-app-line/50 backdrop-blur">
<CyclingImage
loading="eager"
width={1278}
height={626}
alt="spacedrive app"
className="rounded-lg"
images={[
'/images/app/1.webp',
'/images/app/2.webp',
'/images/app/3.webp',
'/images/app/4.webp',
'/images/app/5.webp',
'/images/app/10.webp',
'/images/app/6.webp',
'/images/app/7.webp',
'/images/app/8.webp',
'/images/app/9.webp'
]}
/>
<Image
loading="eager"
className="pointer-events-none absolute opacity-100 transition-opacity duration-1000 ease-in-out hover:opacity-0 md:w-auto"
width={2278}
height={626}
alt="l"
src="/images/app/gradient-overlay.png"
/>
</div>
<ArrowUp className="invisible size-7 pt-2 text-white/40 md:visible" />
<p className="invisible pt-2 text-xs text-white/40 md:visible">
Hover to see more
</p>
</div>
</div>
</div>

View file

@ -103,14 +103,14 @@ const PackageCard = ({ features, name, price, toggle, subTitle }: Props) => {
<p className="text-md mb-4 uppercase text-[#A7ADD2]">{name}</p>
{price && (
<>
<p className="text-2xl font-bold leading-[1] text-white">
<p className="text-2xl font-bold leading-none text-white">
${toggle ? price.yearly : price.monthly}
</p>
<p className="text-md text-[#A7ADD2]">per {duration}</p>
</>
)}
{subTitle && (
<p className="text-2xl font-bold leading-[1] text-white">{subTitle}</p>
<p className="text-2xl font-bold leading-none text-white">{subTitle}</p>
)}
</div>
</div>
@ -128,7 +128,7 @@ const PackageCard = ({ features, name, price, toggle, subTitle }: Props) => {
)}
{features.map((feature, index) => (
<div key={index} className="flex items-center justify-center gap-2.5">
<div className="flex h-5 w-5 items-center justify-center rounded-full border border-[#353252] bg-[#2A2741]">
<div className="flex size-5 items-center justify-center rounded-full border border-[#353252] bg-[#2A2741]">
<Check weight="bold" size={12} color="white" />
</div>
<p className="text-sm text-white">{feature}</p>

View file

@ -170,6 +170,11 @@ export const items = [
description:
'Automatically save versions of files when they change, with a timeline view and the ability to restore.'
},
{
title: 'Local Server Protection',
description:
"Protect local instances of Spacedrive's server from other clients on your network."
},
{
when: '0.5 Beta',
subtext: 'June 2024',

View file

@ -28,7 +28,7 @@ export default function Page() {
{items.map((item, i) => (
<Fragment key={i}>
{/* Using span so i can use the group-last-of-type selector */}
<span className="group flex max-w-[10rem] items-start justify-end gap-4 first:items-start">
<span className="group flex max-w-40 items-start justify-end gap-4 first:items-start">
<div
className={clsx(
'flex flex-col items-end',

View file

@ -76,32 +76,32 @@ export function TeamMember(props: TeamMemberProps) {
<div className="mt-3 flex flex-row space-x-2">
{props.socials?.twitter && (
<Link href={props.socials.twitter}>
<Twitter className="h-[20px] w-[20px]" />
<Twitter className="size-[20px]" />
</Link>
)}
{props.socials?.github && (
<Link href={props.socials.github}>
<Github className="h-[20px] w-[20px]" />
<Github className="size-[20px]" />
</Link>
)}
{props.socials?.gitlab && (
<Link href={props.socials.gitlab}>
<Gitlab className="h-[20px] w-[20px]" />
<Gitlab className="size-[20px]" />
</Link>
)}
{props.socials?.twitch && (
<Link href={props.socials.twitch}>
<Twitch className="h-[20px] w-[20px]" />
<Twitch className="size-[20px]" />
</Link>
)}
{props.socials?.dribbble && (
<Link href={props.socials.dribbble}>
<Dribbble className="h-[20px] w-[20px]" />
<Dribbble className="size-[20px]" />
</Link>
)}
{props.socials?.website && (
<Link href={props.socials.website}>
<Website className="h-[20px] w-[20px]" />
<Website className="size-[20px]" />
</Link>
)}
</div>

View file

@ -56,7 +56,7 @@ const BentoBoxes = () => {
lg:grid lg:grid-cols-6"
>
<BentoBox colSpan={4} className="p-6" bgUrl="images/bento/encrypt-bg.webp">
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 h-full w-full" />
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 size-full" />
<div className="relative z-20">
<Heading>Encryption</Heading>
<Text className="mx-auto max-w-[417px]">
@ -64,7 +64,7 @@ const BentoBoxes = () => {
unauthorized access and guaranteed protection.
</Text>
</div>
<div className="flex h-[80%] w-auto items-start justify-center">
<div className="flex h-4/5 w-auto items-start justify-center">
<Image
className="mx-auto"
alt="Encryption"
@ -77,7 +77,7 @@ const BentoBoxes = () => {
</div>
</BentoBox>
<BentoBox colSpan={2} className="p-6">
<div className="flex h-[75%] w-auto items-center justify-center">
<div className="flex h-3/4 w-auto items-center justify-center">
<Image
className="mx-auto mt-3 brightness-125"
alt="Powerful tags"
@ -88,9 +88,9 @@ const BentoBoxes = () => {
src="/images/bento/tags.webp"
/>
</div>
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 h-full w-full" />
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 size-full" />
<div className="relative z-[40] mt-2 md:mt-7">
<div className="relative z-40 mt-2 md:mt-7">
<Heading>Powerful tags</Heading>
<Text>
Create and apply tags to your files and folders, and instantly locate
@ -105,8 +105,8 @@ const BentoBoxes = () => {
Easily find your files and folders through our search
</Text>
</div>
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 h-full w-full" />
<div className="flex h-[80%] w-auto items-start justify-center">
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 size-full" />
<div className="flex h-4/5 w-auto items-start justify-center">
<Image
className="mx-auto brightness-110"
alt="Search"
@ -119,8 +119,8 @@ const BentoBoxes = () => {
</div>
</BentoBox>
<BentoBox colSpan={2} className="p-6">
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 h-full w-full" />
<div className="flex h-[80%] w-auto items-center justify-center">
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 size-full" />
<div className="flex h-4/5 w-auto items-center justify-center">
<Image
className="mx-auto brightness-125"
alt="Library"
@ -143,8 +143,8 @@ const BentoBoxes = () => {
Send files to other devices quickly and easily
</Text>
</div>
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 h-full w-full" />
<div className="flex h-[80%] w-auto items-center justify-center">
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 size-full" />
<div className="flex h-4/5 w-auto items-center justify-center">
<div
style={
{
@ -211,7 +211,7 @@ const BentoBoxes = () => {
Windows, macOS, Linux, iOS, Android, and the web. Spacedrive is everywhere.
</Text>
</div>
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 h-full w-full" />
<div className="bento-radial-gradient-fade absolute right-0 top-0 z-20 size-full" />
</BentoBox>
</MagicContainer>
);

View file

@ -61,7 +61,7 @@ export default function NavBar() {
<div className="navbar-blur fixed z-[55] h-16 w-full !bg-black/10 px-2 transition">
<div className="relative m-auto flex h-full max-w-[100rem] items-center p-5">
<Link href="/" className="absolute flex flex-row items-center">
<Image alt="Spacedrive logo" src={Logo} className="z-30 mr-3 h-8 w-8" />
<Image alt="Spacedrive logo" src={Logo} className="z-30 mr-3 size-8" />
<h3 className="text-xl font-bold text-white">Spacedrive</h3>
</Link>
@ -88,7 +88,7 @@ export default function NavBar() {
className="ml-[140px] hover:!bg-transparent"
size="icon"
>
<DotsThreeVertical weight="bold" className="h-6 w-6 " />
<DotsThreeVertical weight="bold" className="size-6 " />
</Button>
}
className="right-4 top-2 block h-6 w-44 text-white lg:hidden"
@ -145,7 +145,7 @@ export default function NavBar() {
target="_blank"
rel="noreferrer"
>
<Discord className="h-6 w-6 text-white opacity-100 duration-300 hover:opacity-50" />
<Discord className="size-6 text-white opacity-100 duration-300 hover:opacity-50" />
</Link>
<Link
aria-label="github"
@ -153,13 +153,13 @@ export default function NavBar() {
target="_blank"
rel="noreferrer"
>
<Github className="h-6 w-6 text-white opacity-100 duration-300 hover:opacity-50" />
<Github className="size-6 text-white opacity-100 duration-300 hover:opacity-50" />
</Link>
</div>
</div>
<div className="absolute bottom-0 flex h-1 w-full flex-row items-center justify-center pt-4 opacity-100">
<div className="h-[1px] w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-[1px] w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
<div className="h-px w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-px w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
</div>
</div>
);

View file

@ -24,7 +24,7 @@ const NewBanner: React.FC<NewBannerProps> = (props) => {
<Newspaper weight="fill" className="text-white " size={20} />
<p className="font-regular truncate text-white">{headline}</p>
</div>
<div role="separator" className="h-22 mx-4 w-[1px] bg-zinc-700/70" />
<div role="separator" className="h-22 mx-4 w-px bg-zinc-700/70" />
<Link
href={href}
className="font-regular shrink-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent decoration-primary-600"

View file

@ -14,7 +14,7 @@ const WormHole = () => {
className="absolute top-[-150px] w-full max-w-[450px] rotate-[300deg] sm:top-[-200px]
sm:max-w-[500px] md:top-[-200px] lg:top-auto lg:mr-[250px] lg:max-w-full lg:rotate-0"
>
<div className="absolute left-[200px] top-[50px] z-10 h-full w-full">
<div className="absolute left-[200px] top-[50px] z-10 size-full">
<Image
width={30}
height={45}
@ -24,7 +24,7 @@ const WormHole = () => {
src="/images/icons/heart.svg"
/>
</div>
<div className="absolute left-[200px] top-[50px] z-10 h-full w-full">
<div className="absolute left-[200px] top-[50px] z-10 size-full">
<Image
width={40}
height={45}
@ -36,8 +36,8 @@ const WormHole = () => {
</div>
<div
className="absolute top-[-100px] z-10
h-full w-full
sm:left-[200px] sm:top-[10px]"
size-full sm:left-[200px]
sm:top-[10px]"
>
<Image
width={40}
@ -50,8 +50,8 @@ const WormHole = () => {
</div>
<div
className="absolute left-[120px] top-[-50px]
z-10 h-full w-full
sm:left-[200px] sm:top-[-10px]"
z-10 size-full sm:left-[200px]
sm:top-[-10px]"
>
<Image
width={40}
@ -63,8 +63,8 @@ const WormHole = () => {
/>
</div>
<div
className="absolute left-[200px] top-[350px] z-10 h-full
w-full lg:left-[200px] lg:top-[300px]"
className="absolute left-[200px] top-[350px] z-10 size-full
lg:left-[200px] lg:top-[300px]"
>
<Image
width={40}
@ -75,7 +75,7 @@ const WormHole = () => {
src="/images/icons/video.svg"
/>
</div>
<div className="absolute left-[200px] top-[150px] z-10 h-full w-full">
<div className="absolute left-[200px] top-[150px] z-10 size-full">
<Image
width={40}
height={45}
@ -85,7 +85,7 @@ const WormHole = () => {
src="/images/icons/application.svg"
/>
</div>
<div className="absolute left-[120px] top-[50px] z-10 h-full w-full lg:left-[200px] lg:top-[120px]">
<div className="absolute left-[120px] top-[50px] z-10 size-full lg:left-[200px] lg:top-[120px]">
<Image
width={40}
height={45}
@ -95,7 +95,7 @@ const WormHole = () => {
src="/images/icons/collection.svg"
/>
</div>
<div className="absolute left-[120px] top-[50px] z-10 h-full w-full sm:left-[200px] sm:top-[300px] lg:left-[200px] lg:top-[420px]">
<div className="absolute left-[120px] top-[50px] z-10 size-full sm:left-[200px] sm:top-[300px] lg:left-[200px] lg:top-[420px]">
<Image
width={40}
height={45}
@ -108,7 +108,7 @@ const WormHole = () => {
<div
className="absolute
left-[60px] top-[-190px]
z-10 h-full w-full sm:left-[50px] sm:top-[50px] lg:left-[200px] lg:top-[490px]"
z-10 size-full sm:left-[50px] sm:top-[50px] lg:left-[200px] lg:top-[490px]"
>
<Image
width={40}
@ -121,7 +121,7 @@ const WormHole = () => {
</div>
<div
className="absolute left-[120px] top-[50px]
z-10 h-full w-full md:left-[200px] md:top-[350px]"
z-10 size-full md:left-[200px] md:top-[350px]"
>
<Image
width={40}
@ -132,7 +132,7 @@ const WormHole = () => {
src="/images/icons/database.svg"
/>
</div>
<div className="absolute left-[100px] top-[-200px] z-10 h-full w-full sm:left-[150px] sm:top-[50px]">
<div className="absolute left-[100px] top-[-200px] z-10 size-full sm:left-[150px] sm:top-[50px]">
<Image
width={40}
height={45}
@ -153,7 +153,7 @@ const WormHole = () => {
</div>
<div
className={clsx(
'worm-hole-border-gradient relative top-[100px] z-[20] flex w-full max-w-[500px] flex-col rounded-lg',
'worm-hole-border-gradient relative top-[100px] z-20 flex w-full max-w-[500px] flex-col rounded-lg',
'items-center justify-center gap-2 bg-gradient-to-r from-[#080710]/0 to-[#080710]/50 p-8 backdrop-blur-sm'
)}
>

View file

@ -21,7 +21,7 @@ export default function Notice({ text, type, title }: NoticeProps) {
)}
>
<div className="flex flex-row items-center gap-x-1">
<Icon className="my-0 h-5 w-5 text-white" />
<Icon className="my-0 size-5 text-white" />
<h5 className="m-0 text-sm font-bold uppercase text-white">{title || type}</h5>
</div>
<p className="mx-0 my-1 mb-0 text-white">

View file

@ -0,0 +1,38 @@
'use client';
import { Check } from '@phosphor-icons/react';
import { Copy } from '@phosphor-icons/react/dist/ssr';
import { FC, useRef, useState } from 'react';
const Pre: FC<{ children: React.ReactNode }> = ({ children }) => {
const textInput = useRef<HTMLDivElement | null>(null);
const [copied, setCopied] = useState(false);
const onCopy = () => {
setCopied(true);
navigator.clipboard.writeText(textInput.current!.textContent ?? '');
setTimeout(() => {
setCopied(false);
}, 2000);
};
return (
<div ref={textInput} className="relative">
<button
aria-label="Copy code"
type="button"
className="absolute right-2 top-2 z-10 rounded-md bg-app-box p-3 text-white/60 transition-colors duration-200 ease-in-out hover:bg-app-darkBox"
onClick={onCopy}
>
{copied ? (
<Check className="my-0 size-5 text-green-400/60" />
) : (
<Copy className="my-0 size-5 text-white" />
)}
</button>
<pre className="language-container">{children}</pre>
</div>
);
};
export default Pre;

View file

@ -3,6 +3,7 @@ import NextImage, { ImageProps } from 'next/image';
import { env } from '~/env';
import Notice from './Notice';
import Pre from './Pre';
import Video from './Video';
const Image = (props: ImageProps) => (
@ -15,8 +16,9 @@ const Image = (props: ImageProps) => (
export const BlogMDXComponents = {
img: Image, // we remap 'img' to 'Image'
pre: Pre,
Image,
Video
} as MDXComponents;
export const DocMDXComponents = { img: Image, Image, Notice, Video } as MDXComponents;
export const DocMDXComponents = { img: Image, Image, Notice, Video, pre: Pre } as MDXComponents;

View file

@ -74,6 +74,13 @@ pre[class*='language-'] {
border-radius: 0.3em;
}
pre.language-container {
padding: 1.1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.2em 0.3em;

View file

@ -22,6 +22,11 @@ module.exports = {
importNames: ['SafeAreaView'],
message: 'Import SafeAreaView from react-native-safe-area-context instead'
},
{
name: 'react-native',
importNames: ['Image', 'ImageProps', 'ImageBackground'],
message: 'Import it from expo-image instead'
},
{
name: 'react-native-toast-message',
message: 'Import it from components instead'

View file

@ -11,6 +11,7 @@ edition = { workspace = true }
crate-type = ["cdylib"]
[dependencies]
# Spacedrive Sub-crates
sd-mobile-core = { path = "../../core" }
# FFI

View file

@ -7,6 +7,7 @@ repository = { workspace = true }
edition = { workspace = true }
[dependencies]
# Spacedrive Sub-crates
sd-core = { path = "../../../../../core", features = [
"mobile",
], default-features = false }

View file

@ -74,10 +74,15 @@ pub fn handle_core_msg(
None => {
let _guard = Node::init_logger(&data_dir);
// TODO: probably don't unwrap
let new_node = Node::new(data_dir, sd_core::Env::new(CLIENT_ID))
.await
.unwrap();
let new_node = match Node::new(data_dir, sd_core::Env::new(CLIENT_ID)).await {
Ok(node) => node,
Err(err) => {
error!("failed to initialise node: {}", err);
callback(Err(query));
return;
}
};
node.replace(new_node.clone());
new_node
}

View file

@ -14,4 +14,5 @@ edition = { workspace = true }
crate-type = ["staticlib"]
[dependencies]
# Spacedrive Sub-crates
sd-mobile-core = { path = "../../core" }

View file

@ -41,6 +41,7 @@
"expo-av": "^13.10.5",
"expo-blur": "^12.9.2",
"expo-build-properties": "~0.11.1",
"expo-image": "^1.10.6",
"expo-linking": "~6.2.2",
"expo-media-library": "~15.9.1",
"expo-splash-screen": "~0.26.4",

View file

@ -2,23 +2,70 @@
set -eEuo pipefail
if [ "${CI:-}" = "true" ]; then
set -x
fi
# Script root
_root="$(CDPATH='' cd -- "$(dirname "$0")" && pwd -P)"
_test_dir="$(CDPATH='' cd -- "${_root}/../tests" && pwd -P)"
PLATFORM=${1:-}
PLATFORM="${1:-}"
DEVICE_ID=""
IOS_APP_BIN_PATH="${_root}/../ios/build/Build/Products/Release-iphonesimulator/Spacedrive.app"
case $PLATFORM in
ios | android) ;;
ios)
DEVICE_ID="${2:-}"
if [ -z "$DEVICE_ID" ]; then
echo "Empty IOS emulator UUID" >&2
exit 1
fi
if ! [ -e "$IOS_APP_BIN_PATH" ]; then
echo "Invalid IOS app binary path" >&2
exit 1
fi
;;
android)
echo 'Android tests are not implemented yet' >&2
exit 1
;;
*)
echo "Usage: run-maestro-tests.sh <android|ios>" >&2
exit 1
;;
esac
start_app() {
case $PLATFORM in
ios)
xcrun simctl bootstatus "$DEVICE_ID" -b
open -a Simulator --args -CurrentDeviceUDID "$DEVICE_ID"
xcrun simctl install "$DEVICE_ID" "${_root}/../ios/build/Build/Products/Release-iphonesimulator/Spacedrive.app"
# ¯\_(ツ)_/¯
sleep 10
;;
android)
echo 'Android tests are not implemented yet' >&2
exit 1
;;
esac
}
# https://stackoverflow.com/q/11027679#answer-59592881
# SYNTAX:
# catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND [ARG1[ ARG2[ ...[ ARGN]]]]
catch() {
{
IFS=$'\n' read -r -d '' "${1}"
IFS=$'\n' read -r -d '' "${2}"
(
IFS=$'\n' read -r -d '' _ERRNO_
return "$_ERRNO_"
)
} < <((printf '\0%s\0%d\0' "$( ( ( ({
shift 2
"${@}"
echo "${?}" 1>&3-
} | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
}
run_maestro_test() {
if [ $# -ne 1 ]; then
echo "Usage: run_maestro_test <test_file>" >&2
@ -26,15 +73,52 @@ run_maestro_test() {
fi
local i
local retry_failed=0
local retry_seconds
for i in {1..6}; do
if maestro test "$1"; then
_maestro_out=''
_maestro_err=''
# https://github.com/expo/expo/blob/339fa68/apps/bare-expo/scripts/start-ios-e2e-test.ts#L12
if catch _maestro_out _maestro_err \
env MAESTRO_DRIVER_STARTUP_TIMEOUT=120000 maestro --device "$DEVICE_ID" test "$1"; then
# Test succeeded
printf '%s' "$_maestro_out"
printf '%s' "$_maestro_err" >&2
return
else
elif echo "$_maestro_err" | grep 'TimeoutException'; then
# Test timed out
# Kill maestro processes
pgrep -fi maestro | xargs kill -KILL
# Restart app if necessary
case $PLATFORM in
ios)
if ! { xcrun simctl listapps booted | grep CFBundleIdentifier | grep Spacedrive; }; then
start_app
fi
;;
android)
echo 'Android tests are not implemented yet' >&2
exit 1
;;
esac
# Retry
retry_seconds=$((20 * i))
echo "Test $1 failed. Retrying in $retry_seconds seconds..."
echo "Test $1 timed out. Retrying in $retry_seconds seconds..."
sleep $retry_seconds
else
# Test failed
printf '%s' "$_maestro_out"
printf '%s' "$_maestro_err" >&2
if [ $retry_failed -eq 0 ]; then
retry_failed=1
echo "Test $1 failed. Retrying once more in 10 seconds..."
sleep 10
else
return 1
fi
fi
done
@ -57,6 +141,9 @@ else
)
fi
# Start Spacedrive in the device emulator
start_app
# Run onboarding first
onboardingFile="${_test_dir}/onboarding.yml"
if ! run_maestro_test "$onboardingFile"; then

View file

@ -39,7 +39,7 @@ import { useTheme } from './hooks/useTheme';
import { changeTwTheme, tw } from './lib/tailwind';
import RootNavigator from './navigation';
import OnboardingNavigator from './navigation/OnboardingNavigator';
import { P2P } from './screens/p2p';
import { P2P } from './screens/p2p/P2P';
import { currentLibraryStore } from './utils/nav';
LogBox.ignoreLogs(['Sending `onAnimatedValueUpdate` with no listeners registered.']);

View file

@ -17,7 +17,7 @@ export const ProgressBar = memo((props: ProgressBarProps) => {
return (
<View style={tw`h-1 overflow-hidden rounded-full bg-app-button`}>
<MotiView
style={tw`h-full w-[50%] bg-accent`}
style={tw`h-full w-1/2 bg-accent`}
from={{ left: '-50%' }}
animate={{ left: '100%' }}
transition={{ type: 'timing', duration: 1500, loop: true }}

View file

@ -18,7 +18,7 @@ import { Button } from '../primitive/Button';
import LibraryItem from './LibraryItem';
const iconStyle = tw`text-ink-faint`;
const iconSize = 28;
const iconSize = 24;
export const CATEGORIES_LIST = [
{ name: 'Albums', icon: <Images size={iconSize} style={iconStyle} /> },
{ name: 'Places', icon: <MapPin size={iconSize} style={iconStyle} /> },
@ -26,7 +26,7 @@ export const CATEGORIES_LIST = [
{ name: 'Projects', icon: <Briefcase size={iconSize} style={iconStyle} /> },
{ name: 'Favorites', icon: <Heart size={iconSize} style={iconStyle} /> },
{ name: 'Recents', icon: <Clock size={iconSize} style={iconStyle} /> },
{ name: 'Labels', icon: <Tag size={iconSize} style={iconStyle} /> },
// { name: 'Labels', icon: <Tag size={iconSize} style={iconStyle} /> },
{ name: 'Imports', icon: <ArchiveBox size={iconSize} style={iconStyle} /> }
];
const BrowseCategories = () => {

View file

@ -12,7 +12,7 @@ const Jobs = () => {
return (
<View style={tw`gap-3`}>
<View style={tw`w-full flex-row items-center justify-between px-5`}>
<Text style={tw`text-lg font-bold text-white`}>Jobs</Text>
<Text style={tw`text-lg font-bold text-white`}>Active Jobs</Text>
</View>
<Fade color="black" height="100%" width={30}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
@ -67,7 +67,7 @@ const Job = ({ progress, message, error }: JobProps) => {
backgroundColor={tw.color('ink-light/5')}
>
{(fill) => (
<View style={tw`flex-row items-end gap-[1px]`}>
<View style={tw`flex-row items-end gap-px`}>
<Text style={tw`text-lg font-bold text-white`}>
{error ? '0' : fill.toFixed(0)}
</Text>
@ -79,7 +79,7 @@ const Job = ({ progress, message, error }: JobProps) => {
</View>
)}
</AnimatedCircularProgress>
<Text style={tw`w-[60%] text-sm leading-5 text-ink-dull`}>{message}</Text>
<Text style={tw`w-3/5 text-sm leading-5 text-ink-dull`}>{message}</Text>
</View>
</Card>
);

View file

@ -12,12 +12,12 @@ interface CategoryProps {
const ListLibraryItem = ({ name, icon }: CategoryProps) => {
return (
<Card style={tw`flex-row items-center justify-between gap-2 py-3`}>
<View style={tw`flex-row items-center gap-2`}>
<View style={tw`flex-row items-center gap-2 px-2`}>
{icon}
<Text style={twStyle(`mt-0 text-sm text-white`)}>{name}</Text>
<Text style={twStyle(`text-sm text-white`)}>{name}</Text>
</View>
<View
style={tw`h-8 w-auto flex-row items-center justify-center rounded-full border border-app-lightborder/70 px-2`}
style={tw`h-10 w-10 flex-row items-center justify-center rounded-full border border-app-lightborder/70 px-2`}
>
<Text style={tw`text-xs font-medium text-ink-dull`}>
{Math.floor(Math.random() * 200)}

View file

@ -109,7 +109,7 @@ const useFormState = () => {
// Switch to the new library
currentLibraryStore.id = library.uuid;
} catch (e) {
toast({ type: 'error', text: 'Failed to create library' });
toast.error('Failed to create library');
resetOnboardingStore();
navigation.navigate('GetStarted');
}

View file

@ -0,0 +1,78 @@
import { DrawerContentScrollView } from '@react-navigation/drawer';
import { DrawerContentComponentProps } from '@react-navigation/drawer/lib/typescript/src/types';
import { AppLogo } from '@sd/assets/images';
import { Image } from 'expo-image';
import { CheckCircle } from 'phosphor-react-native';
import { useRef } from 'react';
import { Platform, Pressable, Text, View } from 'react-native';
import { JobManagerContextProvider, useLibraryQuery } from '@sd/client';
import Layout from '~/constants/Layout';
import { tw, twStyle } from '~/lib/tailwind';
import { PulseAnimation } from '../animation/lottie';
import { ModalRef } from '../layout/Modal';
import { JobManagerModal } from '../modal/job/JobManagerModal';
import { Button } from '../primitive/Button';
import DrawerLibraryManager from './DrawerLibraryManager';
import DrawerLocations from './DrawerLocations';
import DrawerTags from './DrawerTags';
const drawerHeight = Platform.select({
ios: Layout.window.height * 0.85,
android: Layout.window.height * 0.9
});
function JobIcon() {
const { data: isActive } = useLibraryQuery(['jobs.isActive']);
return isActive ? (
<PulseAnimation style={tw`h-[24px] w-[32px]`} speed={1.5} />
) : (
<CheckCircle color="white" size={24} />
);
}
// NOTE: `navigation` is not typed here...
const DrawerContent = ({ navigation, state }: DrawerContentComponentProps) => {
// const stackName = getStackNameFromState(state);
const modalRef = useRef<ModalRef>(null);
return (
<DrawerContentScrollView style={tw`flex-1 px-3 py-2`} scrollEnabled={false}>
<View style={twStyle('justify-between', { height: drawerHeight })}>
<View>
<View style={tw`flex flex-row items-center`}>
<Image source={AppLogo} style={tw`h-[40px] w-[40px]`} />
<Text style={tw`ml-2 text-lg font-bold text-ink`}>Spacedrive</Text>
</View>
<View style={tw`mt-6`} />
{/* Library Manager */}
<DrawerLibraryManager />
{/* Locations */}
<DrawerLocations />
{/* Tags */}
<DrawerTags />
</View>
<View style={tw`mt-3 flex w-full flex-row items-center gap-x-4`}>
{/* Job Manager */}
<JobManagerContextProvider>
<Pressable onPress={() => modalRef.current?.present()}>
<JobIcon />
</Pressable>
<JobManagerModal ref={modalRef} />
</JobManagerContextProvider>
<Button
onPress={() => {
alert('Todo');
}}
variant="gray"
>
<Text style={tw`text-xs font-medium text-white`}>Feedback</Text>
</Button>
</View>
</View>
</DrawerContentScrollView>
);
};
export default DrawerContent;

View file

@ -1,11 +1,11 @@
import { useDrawerStatus } from '@react-navigation/drawer';
import { useNavigation } from '@react-navigation/native';
import { MotiView } from 'moti';
import { CaretRight, Gear, Lock, Plus } from 'phosphor-react-native';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Alert, Pressable, Text, View } from 'react-native';
import { useClientContext } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
import { currentLibraryStore } from '~/utils/nav';
import { AnimatedHeight } from '../animation/layout';
@ -13,31 +13,33 @@ import { ModalRef } from '../layout/Modal';
import CreateLibraryModal from '../modal/CreateLibraryModal';
import { Divider } from '../primitive/Divider';
interface Props {
style?: string;
}
const BrowseLibraryManager = ({ style }: Props) => {
const DrawerLibraryManager = () => {
const [dropdownClosed, setDropdownClosed] = useState(true);
// Closes the dropdown when the drawer is closed
const isDrawerOpen = useDrawerStatus() === 'open';
useEffect(() => {
if (!isDrawerOpen) setDropdownClosed(true);
}, [isDrawerOpen]);
const { library: currentLibrary, libraries } = useClientContext();
const navigation = useNavigation<SettingsStackScreenProps<'Settings'>['navigation']>();
const navigation = useNavigation();
const modalRef = useRef<ModalRef>(null);
return (
<View style={twStyle(`w-full`, style)}>
<View>
<Pressable onPress={() => setDropdownClosed((v) => !v)}>
<View
style={twStyle(
'flex h-11 w-full flex-row items-center justify-between border bg-app-input px-3 shadow-sm',
'flex h-10 w-full flex-row items-center justify-between border bg-app-input px-3 shadow-sm',
dropdownClosed
? 'rounded-md border-app-inputborder'
: 'rounded-t-md border-app-inputborder border-b-app-inputborder'
: 'rounded-t-md border-b-0 border-app-inputborder'
)}
>
<Text style={tw`text-md font-semibold text-ink`}>
<Text style={tw`text-sm font-semibold text-ink`}>
{currentLibrary?.config.name}
</Text>
<MotiView
@ -48,10 +50,13 @@ const BrowseLibraryManager = ({ style }: Props) => {
</MotiView>
</View>
</Pressable>
<AnimatedHeight style={tw`absolute top-10 z-10 w-full`} hide={dropdownClosed}>
<View style={tw`w-full rounded-b-md border border-zinc-800 bg-zinc-900 p-2`}>
<AnimatedHeight hide={dropdownClosed}>
<View
style={tw`w-full rounded-b-md border border-app-inputborder bg-app-input p-2`}
>
{/* Libraries */}
{libraries.data?.map((library) => {
// console.log('library', library);
return (
<Pressable
key={library.uuid}
@ -80,10 +85,7 @@ const BrowseLibraryManager = ({ style }: Props) => {
{/* Create Library */}
<Pressable
style={tw`flex flex-row items-center px-1.5 py-[8px]`}
onPress={() => {
modalRef.current?.present();
setDropdownClosed(true);
}}
onPress={() => modalRef.current?.present()}
>
<Plus size={18} weight="bold" color="white" style={tw`mr-2`} />
<Text style={tw`text-sm font-semibold text-white`}>New Library</Text>
@ -92,8 +94,13 @@ const BrowseLibraryManager = ({ style }: Props) => {
{/* Manage Library */}
<Pressable
onPress={() => {
navigation.navigate('LibraryGeneralSettings');
setDropdownClosed(true);
navigation.navigate('Root', {
screen: 'Home',
params: {
screen: 'SettingsStack',
params: { screen: 'LibraryGeneralSettings' }
}
});
}}
>
<View style={tw`flex flex-row items-center px-1.5 py-[8px]`}>
@ -114,4 +121,4 @@ const BrowseLibraryManager = ({ style }: Props) => {
);
};
export default BrowseLibraryManager;
export default DrawerLibraryManager;

View file

@ -0,0 +1,130 @@
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types';
import { useNavigation } from '@react-navigation/native';
import { useRef } from 'react';
import { Pressable, Text, View } from 'react-native';
import {
arraysEqual,
byteSize,
Location,
useCache,
useLibraryQuery,
useNodes,
useOnlineLocations
} from '@sd/client';
import { ModalRef } from '~/components/layout/Modal';
import { tw, twStyle } from '~/lib/tailwind';
import FolderIcon from '../icons/FolderIcon';
import CollapsibleView from '../layout/CollapsibleView';
import ImportModal from '../modal/ImportModal';
import { Button } from '../primitive/Button';
type DrawerLocationItemProps = {
onPress: () => void;
location: Location;
};
const DrawerLocationItem: React.FC<DrawerLocationItemProps> = ({
location,
onPress
}: DrawerLocationItemProps) => {
const onlineLocations = useOnlineLocations();
const online = onlineLocations.some((l) => arraysEqual(location.pub_id, l));
return (
<Pressable onPress={onPress}>
<View
style={twStyle(
'h-auto w-full flex-row items-center justify-between rounded-md border border-app-inputborder/50 bg-app-darkBox p-2'
)}
>
<View style={tw`flex-row items-center gap-1`}>
<View style={tw`relative`}>
<FolderIcon size={20} />
<View
style={twStyle(
'z-5 absolute bottom-1 right-px h-1.5 w-1.5 rounded-full',
online ? 'bg-green-500' : 'bg-red-500'
)}
/>
</View>
<Text style={twStyle('text-xs font-medium text-ink')} numberOfLines={1}>
{location.name ?? ''}
</Text>
</View>
<View style={tw`rounded-md border border-app-lightborder bg-app-box px-1 py-0.5`}>
<Text style={tw`text-[11px] font-medium text-ink-dull`} numberOfLines={1}>
{`${byteSize(location.size_in_bytes)}`}
</Text>
</View>
</View>
</Pressable>
);
};
const DrawerLocations = () => {
const navigation = useNavigation<DrawerNavigationHelpers>();
const modalRef = useRef<ModalRef>(null);
const result = useLibraryQuery(['locations.list'], { keepPreviousData: true });
useNodes(result.data?.nodes);
const locations = useCache(result.data?.items);
return (
<>
<CollapsibleView
title="Locations"
titleStyle={tw`text-sm font-semibold text-ink`}
containerStyle={tw`mb-3 mt-6`}
>
<View style={tw`mt-2 flex-col justify-between gap-1`}>
{locations?.slice(0, 3).map((location) => (
<DrawerLocationItem
key={location.id}
location={location}
onPress={() =>
navigation.navigate('BrowseStack', {
screen: 'Location',
params: { id: location.id },
initial: false
})
}
/>
))}
</View>
<View style={tw`mt-2 flex-row flex-wrap gap-1`}>
{/* Add Location */}
<Button
style={twStyle(`py-0`, locations?.length > 3 ? 'w-[49%]' : 'w-full')}
onPress={() => modalRef.current?.present()}
variant="dashed"
>
<Text style={tw`p-2 text-center text-xs font-medium text-ink-dull`}>
+ Location
</Text>
</Button>
{/* See all locations */}
{locations?.length > 3 && (
<Button
onPress={() => {
navigation.navigate('BrowseStack', {
screen: 'Locations',
initial: false
});
}}
style={tw`w-[49%] py-0`}
variant="gray"
>
<Text style={tw`p-2 text-center text-xs font-medium text-ink`}>
View all
</Text>
</Button>
)}
</View>
</CollapsibleView>
<ImportModal ref={modalRef} />
</>
);
};
export default DrawerLocations;

View file

@ -0,0 +1,114 @@
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types';
import { useNavigation } from '@react-navigation/native';
import { useRef } from 'react';
import { ColorValue, Pressable, Text, View } from 'react-native';
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
import { ModalRef } from '~/components/layout/Modal';
import { tw, twStyle } from '~/lib/tailwind';
import CollapsibleView from '../layout/CollapsibleView';
import CreateTagModal from '../modal/tag/CreateTagModal';
import { Button } from '../primitive/Button';
type DrawerTagItemProps = {
tagName: string;
tagColor: ColorValue;
onPress: () => void;
};
const DrawerTagItem: React.FC<DrawerTagItemProps> = (props) => {
const { tagName, tagColor, onPress } = props;
return (
<Pressable onPress={onPress} testID="drawer-tag">
<View
style={twStyle(
'h-auto flex-row items-center gap-2 rounded-md border border-app-inputborder/50 bg-app-darkBox p-2'
)}
>
<View style={twStyle('h-4 w-4 rounded-full', { backgroundColor: tagColor })} />
<Text style={twStyle('text-xs font-medium text-ink')} numberOfLines={1}>
{tagName}
</Text>
</View>
</Pressable>
);
};
const DrawerTags = () => {
const tags = useLibraryQuery(['tags.list']);
const navigation = useNavigation<DrawerNavigationHelpers>();
useNodes(tags.data?.nodes);
const tagData = useCache(tags.data?.items);
const modalRef = useRef<ModalRef>(null);
return (
<CollapsibleView
title="Tags"
titleStyle={tw`text-sm font-semibold text-ink`}
containerStyle={tw`mb-3 mt-6`}
>
<View style={tw`mt-2 flex-row justify-between gap-1`}>
<TagColumn tags={tagData} dataAmount={[0, 2]} />
<TagColumn tags={tagData} dataAmount={[2, 4]} />
</View>
<View style={tw`mt-2 flex-row flex-wrap gap-1`}>
{/* Add Tag */}
<Button
style={twStyle(`py-0`, tagData?.length > 4 ? 'w-[49%]' : 'w-full')}
onPress={() => modalRef.current?.present()}
variant="dashed"
>
<Text style={tw`p-2 text-center text-xs font-medium text-ink-dull`}>+ Tag</Text>
</Button>
{/* See all tags */}
{tagData?.length > 4 && (
<Button
onPress={() => {
navigation.navigate('BrowseStack', {
screen: 'Tags',
initial: false
});
}}
style={tw`w-[49%] py-0`}
variant="gray"
>
<Text style={tw`p-2 text-center text-xs font-medium text-ink`}>
View all
</Text>
</Button>
)}
</View>
<CreateTagModal ref={modalRef} />
</CollapsibleView>
);
};
interface TagColumnProps {
tags?: Tag[];
dataAmount: [start: number, end: number];
}
const TagColumn = ({ tags, dataAmount }: TagColumnProps) => {
const navigation = useNavigation<DrawerNavigationHelpers>();
return (
<View style={tw`w-[49%] flex-col gap-1`}>
{tags?.slice(dataAmount[0], dataAmount[1]).map((tag: any) => (
<DrawerTagItem
key={tag.id}
tagName={tag.name!}
onPress={() =>
navigation.navigate('BrowseStack', {
screen: 'Tag',
params: { id: tag.id, color: tag.color }
})
}
tagColor={tag.color as ColorValue}
/>
))}
</View>
);
};
export default DrawerTags;

View file

@ -1,20 +1,18 @@
import { useNavigation } from '@react-navigation/native';
import { FlashList } from '@shopify/flash-list';
import { UseInfiniteQueryResult } from '@tanstack/react-query';
import { AnimatePresence, MotiView } from 'moti';
import { useState } from 'react';
import { ActivityIndicator, Pressable } from 'react-native';
import { isPath, SearchData, type ExplorerItem } from '@sd/client';
import Layout from '~/constants/Layout';
import { tw } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
import { useExplorerStore } from '~/stores/explorerStore';
import { useActionsModalStore } from '~/stores/modalStore';
import ScreenContainer from '../layout/ScreenContainer';
import FileItem from './FileItem';
import FileRow from './FileRow';
import Menu from './Menu';
import Menu from './menu/Menu';
type ExplorerProps = {
tabHeight?: boolean;
@ -27,14 +25,8 @@ type ExplorerProps = {
const Explorer = (props: ExplorerProps) => {
const navigation = useNavigation<BrowseStackScreenProps<'Location'>['navigation']>();
const explorerStore = useExplorerStore();
const [layoutMode, setLayoutMode] = useState<ExplorerLayoutMode>(getExplorerStore().layoutMode);
function changeLayoutMode(kind: ExplorerLayoutMode) {
// We need to keep layoutMode as a state to make sure flash-list re-renders.
setLayoutMode(kind);
getExplorerStore().layoutMode = kind;
}
const store = useExplorerStore();
const { modalRef, setData } = useActionsModalStore();
@ -52,45 +44,11 @@ const Explorer = (props: ExplorerProps) => {
return (
<ScreenContainer tabHeight={props.tabHeight} scrollview={false} style={'gap-0 py-0'}>
{/* Header */}
{/* Sort By */}
{/* <SortByMenu /> */}
<AnimatePresence>
{explorerStore.toggleMenu && (
<MotiView
from={{ translateY: -70 }}
animate={{ translateY: 0 }}
transition={{
type: 'timing',
duration: 300,
repeat: 0,
repeatReverse: false
}}
exit={{ translateY: -70 }}
>
<Menu
changeLayoutMode={(kind: ExplorerLayoutMode) => {
changeLayoutMode(kind);
}}
layoutMode={layoutMode}
/>
</MotiView>
)}
</AnimatePresence>
{/* Layout (Grid/List) */}
{/* {layoutMode === 'grid' ? (
<Pressable onPress={() => changeLayoutMode('list')}>
<SquaresFour color={tw.color('ink')} size={23} />
</Pressable>
) : (
<Pressable onPress={() => changeLayoutMode('grid')}>
<Rows color={tw.color('ink')} size={23} />
</Pressable>
)} */}
<Menu />
{/* Items */}
<FlashList
key={layoutMode}
numColumns={layoutMode === 'grid' ? getExplorerStore().gridNumColumns : 1}
key={store.layoutMode}
numColumns={store.layoutMode === 'grid' ? store.gridNumColumns : 1}
data={props.items ?? []}
keyExtractor={(item) =>
item.type === 'NonIndexedPath'
@ -101,15 +59,19 @@ const Explorer = (props: ExplorerProps) => {
}
renderItem={({ item }) => (
<Pressable onPress={() => handlePress(item)}>
{layoutMode === 'grid' ? <FileItem data={item} /> : <FileRow data={item} />}
{store.layoutMode === 'grid' ? (
<FileItem data={item} />
) : (
<FileRow data={item} />
)}
</Pressable>
)}
contentContainerStyle={tw`px-2 py-5`}
extraData={layoutMode}
extraData={store.layoutMode}
estimatedItemSize={
layoutMode === 'grid'
? Layout.window.width / getExplorerStore().gridNumColumns
: getExplorerStore().listItemSize
store.layoutMode === 'grid'
? Layout.window.width / store.gridNumColumns
: store.listItemSize
}
onEndReached={() => props.loadMore?.()}
onEndReachedThreshold={0.6}

View file

@ -23,7 +23,7 @@ const FileItem = ({ data }: FileItemProps) => {
})}
>
<FileThumb data={data} />
<View style={tw`mt-1 px-1.5 py-[1px]`}>
<View style={tw`mt-1 px-1.5 py-px`}>
<Text numberOfLines={1} style={tw`text-center text-xs font-medium text-white`}>
{filePath?.name}
{filePath?.extension && `.${filePath.extension}`}

View file

@ -1,7 +1,8 @@
import { DocumentDirectoryPath } from '@dr.pogodin/react-native-fs';
import { getIcon } from '@sd/assets/util';
import { Image } from 'expo-image';
import { useEffect, useLayoutEffect, useMemo, useState, type PropsWithChildren } from 'react';
import { Image, View } from 'react-native';
import { View } from 'react-native';
import {
getExplorerItemData,
getItemFilePath,

View file

@ -1,41 +0,0 @@
import { MonitorPlay, Rows, SlidersHorizontal, SquaresFour } from 'phosphor-react-native';
import { Pressable, View } from 'react-native';
import { tw } from '~/lib/tailwind';
import { ExplorerLayoutMode } from '~/stores/explorerStore';
interface MenuProps {
layoutMode: ExplorerLayoutMode;
changeLayoutMode: (kind: ExplorerLayoutMode) => void;
}
const Menu = ({ layoutMode, changeLayoutMode }: MenuProps) => {
return (
<View
style={tw`w-screen flex-row justify-between border-b border-app-cardborder bg-app-header px-7 py-4`}
>
<View style={tw`flex-row gap-3`}>
<Pressable hitSlop={24} onPress={() => changeLayoutMode('grid')}>
<SquaresFour
color={tw.color(layoutMode === 'grid' ? 'text-accent' : 'text-ink-dull')}
size={23}
/>
</Pressable>
<Pressable hitSlop={24} onPress={() => changeLayoutMode('list')}>
<Rows
color={tw.color(layoutMode === 'list' ? 'text-accent' : 'text-ink-dull')}
size={23}
/>
</Pressable>
<Pressable hitSlop={24} onPress={() => changeLayoutMode('media')}>
<MonitorPlay
color={tw.color(layoutMode === 'media' ? 'text-accent' : 'text-ink-dull')}
size={23}
/>
</Pressable>
</View>
<SlidersHorizontal style={tw`text-ink-dull`} />
</View>
);
};
export default Menu;

View file

@ -0,0 +1,72 @@
import { AnimatePresence, MotiView } from 'moti';
import { MonitorPlay, Rows, SlidersHorizontal, SquaresFour } from 'phosphor-react-native';
import { Pressable, View } from 'react-native';
import { toast } from '~/components/primitive/Toast';
import { tw } from '~/lib/tailwind';
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
import SortByMenu from './SortByMenu';
const Menu = () => {
const store = useExplorerStore();
return (
<AnimatePresence>
{store.toggleMenu && (
<MotiView
from={{ translateY: -70 }}
animate={{ translateY: 0 }}
transition={{
type: 'timing',
duration: 300,
repeat: 0,
repeatReverse: false
}}
exit={{ translateY: -70 }}
>
<View
style={tw`w-screen flex-row items-center justify-between border-b border-app-cardborder bg-app-header px-7 py-4`}
>
<View style={tw`flex-row gap-3`}>
<Pressable onPress={() => (getExplorerStore().layoutMode = 'grid')}>
<SquaresFour
color={tw.color(
store.layoutMode === 'grid'
? 'text-accent'
: 'text-ink-dull'
)}
size={23}
/>
</Pressable>
<Pressable onPress={() => (getExplorerStore().layoutMode = 'list')}>
<Rows
color={tw.color(
store.layoutMode === 'list'
? 'text-accent'
: 'text-ink-dull'
)}
size={23}
/>
</Pressable>
<Pressable
onPress={() => toast.error('Media view is not available yet...')}
// onPress={() => (getExplorerStore().layoutMode = 'media')}
>
<MonitorPlay
color={tw.color(
store.layoutMode === 'media'
? 'text-accent'
: 'text-ink-dull'
)}
size={23}
/>
</Pressable>
</View>
<SortByMenu />
</View>
</MotiView>
)}
</AnimatePresence>
);
};
export default Menu;

View file

@ -15,8 +15,8 @@ const sortOptions = {
type SortByType = keyof typeof sortOptions;
const ArrowUpIcon = () => <ArrowUp weight="bold" size={16} color={tw.color('ink')} />;
const ArrowDownIcon = () => <ArrowDown weight="bold" size={16} color={tw.color('ink')} />;
const ArrowUpIcon = () => <ArrowUp weight="bold" size={16} color={tw.color('ink-dull')} />;
const ArrowDownIcon = () => <ArrowDown weight="bold" size={16} color={tw.color('ink-dull')} />;
const SortByMenu = () => {
const [sortBy, setSortBy] = useState<SortByType>('name');
@ -26,7 +26,7 @@ const SortByMenu = () => {
<Menu
trigger={
<View style={tw`flex flex-row items-center`}>
<Text style={tw`mr-0.5 font-medium text-ink`}>{sortOptions[sortBy]}</Text>
<Text style={tw`mr-0.5 font-medium text-ink-dull`}>{sortOptions[sortBy]}</Text>
{sortDirection === 'asc' ? <ArrowUpIcon /> : <ArrowDownIcon />}
</View>
}

View file

@ -1,22 +1,21 @@
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types';
import { useNavigation } from '@react-navigation/native';
import { NativeStackHeaderProps } from '@react-navigation/native-stack';
import { ArrowLeft, DotsThreeOutline, MagnifyingGlass } from 'phosphor-react-native';
import { ArrowLeft, DotsThreeOutline, List, MagnifyingGlass } from 'phosphor-react-native';
import { Platform, Pressable, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { tw, twStyle } from '~/lib/tailwind';
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
import BrowseLibraryManager from '../browse/DrawerLibraryManager';
import { Icon } from '../icons/Icon';
import Search from '../search/Search';
type HeaderProps = {
title?: string; //title of the page
showLibrary?: boolean; //show the library manager
showSearch?: boolean; //show the search button
searchType?: 'explorer' | 'location' | 'categories'; //Temporary
showDrawer?: boolean; //show the drawer button
searchType?: 'explorer' | 'location' | 'categories' | 'tags'; //Temporary
navBack?: boolean; //navigate back to the previous screen
navBackHome?: boolean; //navigate back to the home screen of the stack
headerKind?: 'default' | 'location' | 'tag'; //kind of header
route?: never;
routeTitle?: never;
@ -33,16 +32,15 @@ type Props =
// Default header with search bar and button to open drawer
export default function Header({
title,
showLibrary,
searchType,
navBack,
route,
routeTitle,
navBackHome = false,
headerKind = 'default',
showSearch = true
showDrawer = false,
showSearch = false,
}: Props) {
const navigation = useNavigation();
const navigation = useNavigation<DrawerNavigationHelpers>();
const explorerStore = useExplorerStore();
const routeParams = route?.route.params as any;
const headerHeight = useSafeAreaInsets().top;
@ -54,7 +52,7 @@ export default function Header({
paddingTop: headerHeight + (isAndroid ? 15 : 0)
})}
>
<View style={tw`mx-auto h-auto w-full justify-center px-5 pb-4`}>
<View style={tw`mx-auto h-auto w-full justify-center px-5 pb-3`}>
<View style={tw`w-full flex-row items-center justify-between`}>
<View style={tw`flex-row items-center gap-3`}>
{navBack && (
@ -69,6 +67,11 @@ export default function Header({
)}
<View style={tw`flex-row items-center gap-2`}>
<HeaderIconKind headerKind={headerKind} routeParams={routeParams} />
{showDrawer && (
<Pressable onPress={() => navigation.openDrawer()}>
<List size={24} color={tw.color('text-zinc-300')} />
</Pressable>
)}
<Text
numberOfLines={1}
style={tw`max-w-[200px] text-xl font-bold text-white`}
@ -83,7 +86,7 @@ export default function Header({
<Pressable
hitSlop={24}
onPress={() => {
navigation.navigate('ExplorerSearch', {
navigation.navigate('SearchStack', {
screen: 'Search'
});
}}
@ -113,8 +116,6 @@ export default function Header({
)}
</View>
</View>
{showLibrary && <BrowseLibraryManager style="mt-4" />}
{searchType && <HeaderSearchType searchType={searchType} />}
</View>
</View>
@ -131,6 +132,8 @@ const HeaderSearchType = ({ searchType }: HeaderSearchTypeProps) => {
return 'Explorer'; //TODO
case 'location':
return <Search placeholder="Location name..." />;
case 'tags':
return <Search placeholder="Tag name..." />;
case 'categories':
return <Search placeholder="Category name..." />;
default:
@ -150,7 +153,7 @@ const HeaderIconKind = ({ headerKind, routeParams }: HeaderIconKindProps) => {
case 'tag':
return (
<View
style={twStyle('h-[30px] w-[30px] rounded-full', {
style={twStyle('h-[24px] w-[24px] rounded-full', {
backgroundColor: routeParams.color
})}
/>

View file

@ -1,5 +1,5 @@
import { Folder, Folder_Light } from '@sd/assets/icons';
import { Image } from 'react-native';
import { Image } from 'expo-image';
type FolderProps = {
/**

View file

@ -1,5 +1,5 @@
import { getIcon, iconNames } from '@sd/assets/util';
import { Image, ImageProps } from 'react-native';
import { Image, ImageProps } from 'expo-image';
import { ClassInput } from 'twrnc';
import { isDarkTheme } from '@sd/client';
import { twStyle } from '~/lib/tailwind';

View file

@ -1,6 +1,7 @@
import { Image } from 'expo-image';
import { Icon } from 'phosphor-react-native';
import { Fragment } from 'react';
import { Image, Text, View, ViewStyle } from 'react-native';
import { Text, View, ViewStyle } from 'react-native';
import { TextItems } from '@sd/client';
import { styled, tw, twStyle } from '~/lib/tailwind';

View file

@ -150,19 +150,10 @@ const toastErrorSuccess = (
) => {
return {
onError: () => {
errorMessage &&
toast({
type: 'error',
text: errorMessage
});
errorMessage && toast.error(errorMessage);
},
onSuccess: () => {
successMessage &&
toast({
type: 'success',
text: successMessage
}),
successCallBack?.();
successMessage && toast.success(successMessage), successCallBack?.();
}
};
};

View file

@ -1,9 +1,7 @@
import { useRoute } from '@react-navigation/native';
import { DimensionValue, Platform } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { ClassInput } from 'twrnc';
import { tw, twStyle } from '~/lib/tailwind';
import { useExplorerStore } from '~/stores/explorerStore';
interface Props {
children: React.ReactNode; // children of fade
@ -13,7 +11,6 @@ interface Props {
orientation?: 'horizontal' | 'vertical'; // orientation of fade
fadeSides?: 'left-right' | 'top-bottom'; // which sides to fade
screenFade?: boolean; // if true, the fade will consider the bottom tab bar height
noConditions?: boolean; // if true, the fade will be rendered as is
bottomFadeStyle?: ClassInput; // tailwind style for bottom fade
topFadeStyle?: ClassInput; // tailwind style for top fade
}
@ -25,20 +22,15 @@ const Fade = ({
height,
bottomFadeStyle,
topFadeStyle,
noConditions = false,
screenFade = false,
fadeSides = 'left-right',
orientation = 'horizontal'
}: Props) => {
const route = useRoute();
const { toggleMenu } = useExplorerStore();
const bottomTabBarHeight = Platform.OS === 'ios' ? 80 : 60;
const gradientStartEndMap = {
'left-right': { start: { x: 0, y: 0 }, end: { x: 1, y: 0 } },
'top-bottom': { start: { x: 0, y: 1 }, end: { x: 0, y: 0 } }
};
const menuHeight = 57; // height of the explorer menu
const routesWithMenu = ['Location', 'Search', 'Tag']; // routes that are associated with the explorer
return (
<>
<LinearGradient
@ -46,10 +38,7 @@ const Fade = ({
width: orientation === 'vertical' ? height : width,
height: orientation === 'vertical' ? width : height,
position: 'absolute',
top:
!noConditions && toggleMenu && routesWithMenu.includes(route.name)
? menuHeight
: 0,
top: 0,
alignSelf: 'center',
left: fadeSides === 'left-right' ? 0 : undefined,
transform: fadeSides === 'left-right' ? undefined : [{ rotate: '180deg' }],

View file

@ -3,8 +3,6 @@ import { Platform, ScrollView, View } from 'react-native';
import { ClassInput } from 'twrnc/dist/esm/types';
import { tw, twStyle } from '~/lib/tailwind';
import Fade from './Fade';
interface Props {
children: ReactNode;
/** If true, the container will be a ScrollView */
@ -13,16 +11,11 @@ interface Props {
/** If true, the bottom tab bar height will be added to the bottom of the container */
tabHeight?: boolean;
scrollToBottomOnChange?: boolean;
/** Styling of both side fades */
topFadeStyle?: string;
bottomFadeStyle?: string;
}
const ScreenContainer = ({
children,
style,
topFadeStyle,
bottomFadeStyle,
scrollview = true,
tabHeight = true,
scrollToBottomOnChange = false
@ -31,16 +24,6 @@ const ScreenContainer = ({
const bottomTabBarHeight = Platform.OS === 'ios' ? 80 : 60;
return scrollview ? (
<View style={tw`relative flex-1`}>
<Fade
topFadeStyle={topFadeStyle}
bottomFadeStyle={bottomFadeStyle}
screenFade
fadeSides="top-bottom"
orientation="vertical"
color="black"
width={30}
height="100%"
>
<ScrollView
ref={ref}
onContentSizeChange={() => {
@ -55,20 +38,9 @@ const ScreenContainer = ({
>
{children}
</ScrollView>
</Fade>
</View>
) : (
<View style={tw`relative flex-1`}>
<Fade
topFadeStyle={topFadeStyle}
bottomFadeStyle={bottomFadeStyle}
screenFade
fadeSides="top-bottom"
orientation="vertical"
color="black"
width={30}
height="100%"
>
<View
style={twStyle(
'flex-1 justify-between gap-10 bg-black py-6',
@ -78,7 +50,6 @@ const ScreenContainer = ({
>
{children}
</View>
</Fade>
</View>
);
};

View file

@ -28,7 +28,7 @@ const GridLocation: React.FC<GridLocationProps> = ({ location, modalRef }: GridL
)}
/>
</View>
<Pressable hitSlop={24} onPress={() => modalRef.current?.present()}>
<Pressable onPress={() => modalRef.current?.present()}>
<DotsThreeOutlineVertical
weight="fill"
size={20}

View file

@ -1,5 +1,6 @@
import { useNavigation } from '@react-navigation/native';
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
import { useRef } from 'react';
import { Pressable, Text, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { arraysEqual, byteSize, Location, useOnlineLocations } from '@sd/client';
@ -8,20 +9,22 @@ import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
import FolderIcon from '../icons/FolderIcon';
import Card from '../layout/Card';
import { ModalRef } from '../layout/Modal';
import RightActions from './RightActions';
interface ListLocationProps {
location: Location;
modalRef: React.RefObject<ModalRef>;
}
const ListLocation = ({ location, modalRef }: ListLocationProps) => {
const ListLocation = ({ location }: ListLocationProps) => {
const swipeRef = useRef<Swipeable>(null);
const navigation = useNavigation<SettingsStackScreenProps<'LocationSettings'>['navigation']>();
const onlineLocations = useOnlineLocations();
const online = onlineLocations.some((l) => arraysEqual(location.pub_id, l));
return (
<Swipeable
ref={swipeRef}
containerStyle={tw`rounded-md border border-app-cardborder bg-app-card`}
enableTrackpadTwoFingerGesture
renderRightActions={(progress, _, swipeable) => (
@ -36,7 +39,7 @@ const ListLocation = ({ location, modalRef }: ListLocationProps) => {
)}
>
<Card style={tw`h-auto flex-row justify-between gap-3 border-0 p-3`}>
<View style={tw`w-[50%] flex-row items-center gap-2`}>
<View style={tw`w-1/2 flex-row items-center gap-2`}>
<View style={tw`relative`}>
<FolderIcon size={38} />
<View
@ -63,13 +66,13 @@ const ListLocation = ({ location, modalRef }: ListLocationProps) => {
style={tw`rounded-md border border-app-lightborder bg-app-highlight p-1.5`}
>
<Text
style={tw`text-left text-xs font-bold text-ink-dull`}
style={tw`text-left text-xs font-medium text-ink-dull`}
numberOfLines={1}
>
{`${byteSize(location.size_in_bytes)}`}
</Text>
</View>
<Pressable hitSlop={24} onPress={() => modalRef.current?.present()}>
<Pressable hitSlop={24} onPress={() => swipeRef.current?.openRight()}>
<DotsThreeOutlineVertical
weight="fill"
size={20}

View file

@ -29,19 +29,21 @@ export const LocationItem = ({
onPress={onPress}
>
{viewStyle === 'grid' ? (
<GridLocation location={location} modalRef={modalRef} />
<>
<GridLocation location={location} modalRef={modalRef} />
<LocationModal
editLocation={() => {
editLocation();
modalRef.current?.close();
}}
locationId={location.id}
ref={modalRef}
/>
</>
) : (
<ListLocation location={location} modalRef={modalRef} />
<ListLocation location={location} />
)}
</Pressable>
<LocationModal
editLocation={() => {
editLocation();
modalRef.current?.close();
}}
locationId={location.id}
ref={modalRef}
/>
</>
);
};

View file

@ -102,7 +102,10 @@ export const ActionsModal = () => {
<View style={tw`flex-1 px-4`}>
<View style={tw`flex flex-row items-center`}>
{/* Thumbnail/Icon */}
<Pressable onPress={() => fileInfoRef.current?.present()}>
<Pressable
onPress={handleOpen}
onLongPress={() => fileInfoRef.current?.present()}
>
<FileThumb data={data} size={1} />
</Pressable>
<View style={tw`ml-2 flex-1`}>

View file

@ -28,9 +28,6 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
const submitPlausibleEvent = usePlausibleEvent();
const { mutate: createTag } = useLibraryMutation('tags.create', {
onMutate: () => {
console.log('Creating tag');
},
onSuccess: () => {
// Reset form
setTagName('');

View file

@ -4,7 +4,7 @@ import { Pressable, Text, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack';
import Fade from '../layout/Fade';
import { ModalRef } from '../layout/Modal';
@ -15,7 +15,7 @@ import OverviewSection from './OverviewSection';
import StatCard from './StatCard';
const Locations = () => {
const navigation = useNavigation<BrowseStackScreenProps<'Browse'>['navigation']>();
const navigation = useNavigation<OverviewStackScreenProps<'Overview'>['navigation']>();
const modalRef = useRef<ModalRef>(null);
const locationsQuery = useLibraryQuery(['locations.list']);
@ -26,7 +26,6 @@ const Locations = () => {
<>
<OverviewSection title="Locations" count={locations?.length}>
<View style={tw`flex-row items-center`}>
<Fade height={'100%'} width={30} color="black">
<FlatList
horizontal
data={locations}
@ -60,7 +59,8 @@ const Locations = () => {
renderItem={({ item }) => (
<Pressable
onPress={() =>
navigation.navigate('BrowseStack', {
navigation.jumpTo('BrowseStack', {
initial: false,
screen: 'Location',
params: { id: item.id }
})
@ -76,7 +76,6 @@ const Locations = () => {
</Pressable>
)}
/>
</Fade>
</View>
</OverviewSection>
<ImportModal ref={modalRef} />

View file

@ -7,6 +7,7 @@ import { ClassInput } from 'twrnc/dist/esm/types';
import { byteSize, Statistics, StatisticsResponse, useLibraryContext } from '@sd/client';
import useCounter from '~/hooks/useCounter';
import { tw, twStyle } from '~/lib/tailwind';
import Card from '../layout/Card';
const StatItemNames: Partial<Record<keyof Statistics, string>> = {
@ -31,13 +32,9 @@ const StatItem = ({ title, bytes, isLoading, style }: StatItemProps) => {
return (
<Card
style={twStyle(
'flex flex-col items-center justify-center p-2',
style,
{
hidden: isLoading
}
)}
style={twStyle('flex flex-col items-center justify-center p-2', style, {
hidden: isLoading
})}
>
<Text style={tw`text-sm font-bold text-zinc-400`}>{title}</Text>
<View style={tw`mt-1 flex-row items-baseline`}>

View file

@ -84,7 +84,7 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
style={tw`flex h-10 flex-row items-center gap-1.5 border-t border-app-cardborder px-2`}
>
<View
style={tw`rounded border border-app-lightborder bg-app-highlight px-1.5 py-[1px]`}
style={tw`rounded border border-app-lightborder bg-app-highlight px-1.5 py-px`}
>
<Text style={tw`text-xs font-medium uppercase text-ink-dull`}>
{connectionType || 'Local'}

View file

@ -0,0 +1,28 @@
import { View } from 'react-native';
import { ClassInput } from 'twrnc';
import { tw, twStyle } from '~/lib/tailwind';
//border-style is not supported - so this is a way to do it
interface Props {
color?: string;
dotCount?: number;
style?: ClassInput;
}
const DottedDivider = ({ dotCount = 100, color = 'bg-app-lightborder', style }: Props) => {
return (
<View style={tw`flex-1 flex-row items-center gap-0.5 overflow-hidden`}>
{Array.from({ length: dotCount }).map((_, index) => (
<View
key={index}
style={twStyle(`h-0.5 w-0.5 rounded-full`, style, {
backgroundColor: tw.color(color)
})}
/>
))}
</View>
);
};
export default DottedDivider;

View file

@ -12,7 +12,7 @@ export const InfoPill = (props: Props) => {
return (
<View
style={twStyle(
'rounded-md border border-transparent bg-app-highlight px-[6px] py-[1px] shadow shadow-app-shade/5',
'rounded-md border border-transparent bg-app-highlight px-[6px] py-px shadow shadow-app-shade/5',
props.containerStyle
)}
>
@ -27,7 +27,7 @@ export function PlaceholderPill(props: Props) {
return (
<View
style={twStyle(
'rounded-md border border-dashed border-app-highlight bg-transparent px-[6px] py-[1px] shadow shadow-app-shade/10',
'rounded-md border border-dashed border-app-highlight bg-transparent px-[6px] py-px shadow shadow-app-shade/10',
props.containerStyle
)}
>

View file

@ -16,7 +16,7 @@ type SwitchContainerProps = { title: string; description?: string } & SwitchProp
export const SwitchContainer: FC<SwitchContainerProps> = ({ title, description, ...props }) => {
return (
<View style={tw`flex flex-row items-center justify-between pb-6`}>
<View style={tw`w-[80%]`}>
<View style={tw`w-4/5`}>
<Text style={tw`text-sm font-medium text-ink`}>{title}</Text>
{description && <Text style={tw`mt-2 text-sm text-ink-dull`}>{description}</Text>}
</View>

View file

@ -3,25 +3,25 @@ import { Text, View } from 'react-native';
import Toast, { ToastConfig } from 'react-native-toast-message';
import { tw } from '~/lib/tailwind';
// TODO:
// - Expand toast on press to show full message if it's too long
// - Add a onPress option
// - Add leading icon & trailing icon
const baseStyles = 'w-[340px] flex-row overflow-hidden rounded-md border p-3 shadow-lg';
const toastConfig: ToastConfig = {
success: ({ text1, ...rest }) => (
<View
style={tw`w-[340px] flex-row overflow-hidden rounded-md border border-app-line bg-app-darkBox/90 p-3 shadow-lg`}
>
<View style={tw.style(baseStyles, 'border-app-line bg-app-darkBox/90 ')}>
<Text style={tw`text-sm font-medium text-ink`} numberOfLines={3}>
{text1}
</Text>
</View>
),
error: ({ text1, ...rest }) => (
<View
style={tw`border-app-red bg-app-red/90 w-[340px] flex-row overflow-hidden rounded-md border p-3 shadow-lg`}
>
<View style={tw.style(baseStyles, 'border-red-500 bg-red-500/90')}>
<Text style={tw`text-sm font-medium text-ink`} numberOfLines={3}>
{text1}
</Text>
</View>
),
info: ({ text1, ...rest }) => (
<View style={tw.style(baseStyles, 'border-app-line bg-app-darkBox/90')}>
<Text style={tw`text-sm font-medium text-ink`} numberOfLines={3}>
{text1}
</Text>
@ -29,8 +29,26 @@ const toastConfig: ToastConfig = {
)
};
function toast({ text, type }: { type: 'success' | 'error' | 'info'; text: string }) {
Toast.show({ type, text1: text, visibilityTime: 3000, topOffset: 60 });
function showToast({ text, type }: { type: 'success' | 'error' | 'info'; text: string }) {
const visibilityTime = 3000;
const topOffset = 60;
Toast.show({ type, text1: text, visibilityTime, topOffset });
}
const toast: {
success: (text: string) => void;
error: (text: string) => void;
info: (text: string) => void;
} = {
success: function (text: string): void {
showToast({ text, type: 'success' });
},
error: function (text: string): void {
showToast({ text, type: 'error' });
},
info: function (text: string): void {
showToast({ text, type: 'info' });
}
};
export { Toast, toast, toastConfig };

View file

@ -18,7 +18,7 @@ export default function Search({ placeholder }: Props) {
}, [searchStore]);
return (
<View
style={tw`mt-4 flex h-11 w-full flex-row items-center justify-between rounded-md border border-app-inputborder bg-app-input px-3 shadow-sm`}
style={tw`mt-3 h-10 w-full flex-row items-center justify-between rounded-md border border-app-inputborder bg-app-input px-3 shadow-sm`}
>
<TextInput
onChangeText={(text) => searchStore.setSearch(text)}

View file

@ -47,7 +47,7 @@ const FiltersBar = () => {
<Plus weight="bold" size={20} color={tw.color('text-ink-dull')} />
</Button>
<View style={tw`relative flex-1`}>
<Fade noConditions height={'100%'} width={30} color="app-header">
<Fade height={'100%'} width={30} color="app-header">
<FlatList
ref={flatListRef}
showsHorizontalScrollIndicator={false}

View file

@ -19,6 +19,7 @@ import Extension from './Extension';
import Kind from './Kind';
import Locations from './Locations';
import Name from './Name';
import SavedSearches from './SavedSearches';
import Tags from './Tags';
const options = [
@ -84,6 +85,7 @@ const FiltersList = () => {
return (
<View style={tw`gap-10`}>
<SavedSearches />
<View>
<SectionTitle
style={tw`px-6 pb-3`}

View file

@ -38,9 +38,7 @@ const Locations = () => {
data={locations}
renderItem={({ item }) => <LocationFilter data={item} />}
numColumns={
(locations && locations.length < 3
? 2
: Math.ceil(locations.length / 2)) ?? 1
locations ? Math.max(Math.ceil(locations.length / 2), 2) : 1
}
contentContainerStyle={tw`w-full`}
ListEmptyComponent={

View file

@ -0,0 +1,59 @@
import { MotiView } from 'moti';
import { MotiPressable } from 'moti/interactions';
import { FlatList, Text, View } from 'react-native';
import { Icon } from '~/components/icons/Icon';
import Card from '~/components/layout/Card';
import Fade from '~/components/layout/Fade';
import SectionTitle from '~/components/layout/SectionTitle';
import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
import DottedDivider from '~/components/primitive/DottedDivider';
import { tw } from '~/lib/tailwind';
const SavedSearches = () => {
return (
<Fade color="black" width={30} height="100%">
<MotiView
from={{ opacity: 0, translateY: 20 }}
animate={{ opacity: 1, translateY: 0 }}
transition={{ type: 'timing', duration: 300 }}
>
<SectionTitle
style={tw`px-6 pb-3`}
title="Saved searches"
sub="Tap a saved search for searching quickly"
/>
<VirtualizedListWrapper contentContainerStyle={tw`px-6`} horizontal>
<FlatList
data={Array.from({ length: 6 })}
renderItem={() => <SavedSearch />}
keyExtractor={(_, index) => index.toString()}
numColumns={Math.ceil(6 / 2)}
scrollEnabled={false}
contentContainerStyle={tw`w-full`}
showsHorizontalScrollIndicator={false}
style={tw`flex-row`}
ItemSeparatorComponent={() => <View style={tw`h-2 w-2`} />}
/>
</VirtualizedListWrapper>
<DottedDivider style={'mt-6'} />
</MotiView>
</Fade>
);
};
const SavedSearch = () => {
return (
<MotiPressable
from={{ opacity: 0, translateY: 20 }}
animate={{ opacity: 1, translateY: 0 }}
transition={{ type: 'timing', duration: 300 }}
>
<Card style={tw`mr-2 w-auto flex-row gap-2 p-2.5`}>
<Icon name="Folder" size={20} />
<Text style={tw`text-sm font-medium text-ink`}>Saved search</Text>
</Card>
</MotiPressable>
);
};
export default SavedSearches;

View file

@ -38,11 +38,7 @@ const Tags = () => {
data={tagsData}
renderItem={({ item }) => <TagFilter tag={item} />}
extraData={searchStore.filters.tags}
numColumns={
tagsData && tagsData.length < 3
? 2
: Math.ceil(tagsData.length / 2) ?? 1
}
numColumns={tagsData ? Math.max(Math.ceil(tagsData.length / 2), 2) : 1}
key={tagsData ? 'tagsSearch' : '_'}
contentContainerStyle={tw`w-full`}
ListEmptyComponent={

View file

@ -24,7 +24,7 @@ const SettingsToggle = ({ title, description, onEnabledChange, control, name }:
return (
<View style={tw`flex-row items-center justify-between`}>
<View style={tw`w-[75%]`}>
<View style={tw`w-3/4`}>
<Text style={tw`text-sm font-medium text-ink`}>{title}</Text>
{description && <Text style={tw`mt-1 text-xs text-ink-dull`}>{description}</Text>}
</View>

View file

@ -20,7 +20,7 @@ const GridTag = ({ tag, modalRef }: GridTagProps) => {
backgroundColor: tag.color!
})}
/>
<Pressable hitSlop={24} onPress={() => modalRef.current?.present()}>
<Pressable onPress={() => modalRef.current?.present()}>
<DotsThreeOutlineVertical
weight="fill"
size={20}

View file

@ -1,28 +1,28 @@
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
import { useRef } from 'react';
import { Pressable, Text, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { ClassInput } from 'twrnc';
import { Tag } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { ModalRef } from '../layout/Modal';
import RightActions from './RightActions';
interface ListTagProps {
tag: Tag;
tagStyle?: ClassInput;
modalRef: React.RefObject<ModalRef>;
}
const ListTag = ({ tag, tagStyle, modalRef }: ListTagProps) => {
const ListTag = ({ tag, tagStyle }: ListTagProps) => {
const swipeRef = useRef<Swipeable>(null);
return (
<Swipeable
ref={swipeRef}
containerStyle={tw`rounded-md border border-app-cardborder bg-app-card p-3`}
enableTrackpadTwoFingerGesture
renderRightActions={(progress, _, swipeable) => (
<>
<RightActions progress={progress} swipeable={swipeable} tag={tag} />
</>
<RightActions progress={progress} swipeable={swipeable} tag={tag} />
)}
>
<View style={twStyle('h-auto flex-row items-center justify-between', tagStyle)}>
@ -39,7 +39,7 @@ const ListTag = ({ tag, tagStyle, modalRef }: ListTagProps) => {
{tag.name}
</Text>
</View>
<Pressable hitSlop={24} onPress={() => modalRef.current?.present()}>
<Pressable onPress={() => swipeRef.current?.openRight()}>
<DotsThreeOutlineVertical
weight="fill"
size={20}

View file

@ -24,12 +24,14 @@ export const TagItem = ({ tag, onPress, viewStyle = 'grid' }: TagItemProps) => {
testID="browse-tag"
>
{viewStyle === 'grid' ? (
<GridTag tag={tag} modalRef={modalRef} />
<>
<GridTag tag={tag} modalRef={modalRef} />
<TagModal ref={modalRef} tag={tag} />
</>
) : (
<ListTag tag={tag} modalRef={modalRef} />
<ListTag tag={tag} />
)}
</Pressable>
<TagModal ref={modalRef} tag={tag} />
</>
);
};

View file

@ -70,7 +70,7 @@ module.exports = {
iconborder: `hsla(${DARK_HUE}, 10%, 100%, ${ALPHA})`,
// background (dark)
box: `hsla(${DARK_HUE}, 15%, 18%, ${ALPHA})`,
darkBox: `hsla(${DARK_HUE}, 15%, 7%, ${ALPHA})`,
darkBox: `hsla(${DARK_HUE}, 10%, 7%, ${ALPHA})`,
// foreground (light)
overlay: `hsla(${DARK_HUE}, 15%, 17%, ${ALPHA})`,
// border

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