mirror of
https://github.com/armbian/imager.git
synced 2026-01-06 12:31:28 -08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4645cdece2 | ||
|
|
9cf8ca299b | ||
|
|
0431b243bb | ||
|
|
4a3aa68051 | ||
|
|
d63d047a0f | ||
|
|
4b9f5289f7 | ||
|
|
840509ec0c | ||
|
|
8e99d25c8d | ||
|
|
41c0e90f54 | ||
|
|
054c41fcec |
63
.github/workflows/build-artifacts.yml
vendored
63
.github/workflows/build-artifacts.yml
vendored
@@ -184,10 +184,38 @@ jobs:
|
|||||||
os: macos-latest
|
os: macos-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
APPLE_SIGNING_IDENTITY: "-"
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Import Apple Developer Certificate
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
run: |
|
||||||
|
# Create a temporary keychain
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||||
|
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Import certificate
|
||||||
|
echo -n "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
|
||||||
|
security import certificate.p12 -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Verify certificate
|
||||||
|
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Set as default keychain
|
||||||
|
security default-keychain -s "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -241,6 +269,39 @@ jobs:
|
|||||||
ditto -c -k --sequesterRsrc --keepParent "$app" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.app.zip"
|
ditto -c -k --sequesterRsrc --keepParent "$app" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.app.zip"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Notarize and staple DMG
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: src-tauri/target/${{ matrix.target }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Find the DMG file
|
||||||
|
DMG_FILE=$(find "$CARGO_TARGET_DIR/release/bundle/dmg/" -name "*.dmg" -type f | head -n 1)
|
||||||
|
|
||||||
|
if [[ -z "$DMG_FILE" ]]; then
|
||||||
|
echo "No DMG file found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Notarizing: $DMG_FILE"
|
||||||
|
|
||||||
|
# Submit DMG for notarization
|
||||||
|
xcrun notarytool submit "$DMG_FILE" \
|
||||||
|
--apple-id "$APPLE_ID" \
|
||||||
|
--password "$APPLE_ID_PASSWORD" \
|
||||||
|
--team-id "$APPLE_TEAM_ID" \
|
||||||
|
--wait \
|
||||||
|
--output-format json
|
||||||
|
|
||||||
|
# Staple the notarization ticket to DMG
|
||||||
|
xcrun stapler staple "$DMG_FILE"
|
||||||
|
|
||||||
|
# Verify stapling
|
||||||
|
xcrun stapler validate -v "$DMG_FILE"
|
||||||
|
|
||||||
|
echo "Notarization completed successfully"
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
245
.github/workflows/build.yml
vendored
245
.github/workflows/build.yml
vendored
@@ -6,12 +6,8 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
build_linux_x64:
|
build_linux:
|
||||||
description: 'Build Linux x64'
|
description: 'Build Linux (x64 + ARM64)'
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
build_linux_arm64:
|
|
||||||
description: 'Build Linux ARM64'
|
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
build_macos:
|
build_macos:
|
||||||
@@ -34,7 +30,7 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
NODE_VERSION: '20'
|
NODE_VERSION: '20'
|
||||||
TAURI_CLI_VERSION: '^2'
|
TAURI_CLI_VERSION: '2.9.6'
|
||||||
# Tauri updater signing key (set in GitHub Secrets)
|
# Tauri updater signing key (set in GitHub Secrets)
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
@@ -93,11 +89,35 @@ jobs:
|
|||||||
omitNameDuringUpdate: true
|
omitNameDuringUpdate: true
|
||||||
replacesArtifacts: false
|
replacesArtifacts: false
|
||||||
|
|
||||||
build-linux-x64:
|
build-linux:
|
||||||
name: build-linux (x86_64-unknown-linux-gnu)
|
name: build-linux ${{ matrix.type.name }} (${{ matrix.arch.name }}/${{ matrix.type.distro_id }})
|
||||||
needs: [create-release]
|
needs: [ create-release ]
|
||||||
if: ${{ github.event_name == 'push' || inputs.build_linux_x64 }}
|
if: ${{ github.event_name == 'push' || inputs.build_linux }} # can't use matrix here; if is evaluated before matrix expansion
|
||||||
runs-on: ubuntu-24.04
|
strategy:
|
||||||
|
fail-fast: false # let other jobs try to complete if one fails
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- { name: 'amd64', runner: 'ubuntu-24.04' }
|
||||||
|
- { name: 'arm64', runner: "ubuntu-24.04-arm" }
|
||||||
|
type:
|
||||||
|
# deb: build in the oldest still-supported matching container: oldstable (bookworm)
|
||||||
|
- name: 'deb'
|
||||||
|
distro_id: "bookworm"
|
||||||
|
container: { image: 'debian:bookworm' }
|
||||||
|
bundles: 'deb'
|
||||||
|
deps: "apt"
|
||||||
|
artifacts: "src-tauri/target/release/bundle/deb/*.deb"
|
||||||
|
# appimage: doesn't use a container (instead, runs directly on the runner); requires sudo to install deps
|
||||||
|
- name: 'appimage'
|
||||||
|
distro_id: "gharunner"
|
||||||
|
bundles: 'appimage'
|
||||||
|
deps: "apt"
|
||||||
|
deps_gain_root: "sudo"
|
||||||
|
artifacts: |
|
||||||
|
src-tauri/target/release/bundle/appimage/*.AppImage
|
||||||
|
src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||||||
|
runs-on: ${{ matrix.arch.runner }}
|
||||||
|
container: ${{ matrix.type.container }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
@@ -125,10 +145,13 @@ jobs:
|
|||||||
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
|
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
|
||||||
src-tauri/tauri.conf.json
|
src-tauri/tauri.conf.json
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies (apt)
|
||||||
|
if: ${{ matrix.type.deps == 'apt' }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
${{ matrix.type.deps_gain_root || '' }} apt-get update
|
||||||
sudo apt-get install -y \
|
${{ matrix.type.deps_gain_root || '' }} apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
libwebkit2gtk-4.1-dev \
|
libwebkit2gtk-4.1-dev \
|
||||||
libayatana-appindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
librsvg2-dev \
|
librsvg2-dev \
|
||||||
@@ -139,10 +162,17 @@ jobs:
|
|||||||
xdg-utils
|
xdg-utils
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
package-manager-cache: false # setup-node can't add key to cache, so we do it ourselves in below step
|
||||||
|
|
||||||
|
# Cache npm, just like setup-node would do it with "cache: npm", but with our own key that includes arch and distro
|
||||||
|
- name: Cache npm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: npm-${{ matrix.arch.runner }}-${{ matrix.arch.name }}-${{ matrix.type.distro_id }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -151,12 +181,16 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
|
# ensure cache separation per host distro; this also ends up caching tauri-cli, so add that to key too.
|
||||||
|
key: "rust-cache-${{ matrix.arch.runner }}-${{ matrix.arch.name }}-${{ matrix.type.distro_id }}-${{env.TAURI_CLI_VERSION}}"
|
||||||
|
# @TODO: Cargo.lock is in gitignore, so it's not included here - it would via rust-cache Action magic, no need to specify it.
|
||||||
|
|
||||||
- name: Cache cargo bin (tauri-cli)
|
- name: Cache cargo bin (tauri-cli) # @TODO: Swatinem/rust-cache already caches this. maybe just drop this.
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin
|
path: ~/.cargo/bin
|
||||||
key: cargo-bin-${{ runner.os }}-${{ runner.arch }}-stable-${{ hashFiles('**/Cargo.lock') }}
|
key: "cargo-bin-${{ matrix.arch.runner }}-${{ matrix.arch.name }}-${{ matrix.type.distro_id }}-stable-${{env.TAURI_CLI_VERSION}}-${{ hashFiles('**/Cargo.lock') }}"
|
||||||
|
# @TODO: Cargo.lock is in gitignore, so it's not included here, albeit being specified.
|
||||||
|
|
||||||
- name: Install npm dependencies
|
- name: Install npm dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -172,7 +206,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
run: cargo tauri build --bundles deb,appimage
|
run: cargo tauri build --bundles "${{ matrix.type.bundles }}"
|
||||||
|
|
||||||
- name: Upload artifacts to GitHub Release
|
- name: Upload artifacts to GitHub Release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
@@ -186,106 +220,7 @@ jobs:
|
|||||||
omitNameDuringUpdate: true
|
omitNameDuringUpdate: true
|
||||||
replacesArtifacts: false
|
replacesArtifacts: false
|
||||||
artifacts: |
|
artifacts: |
|
||||||
src-tauri/target/release/bundle/deb/*.deb
|
${{ matrix.type.artifacts }}
|
||||||
src-tauri/target/release/bundle/appimage/*.AppImage
|
|
||||||
src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
|
||||||
|
|
||||||
build-linux-arm64:
|
|
||||||
name: build-linux (aarch64-unknown-linux-gnu)
|
|
||||||
needs: [create-release]
|
|
||||||
if: ${{ github.event_name == 'push' || inputs.build_linux_arm64 }}
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set version from release tag
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
TAG='${{ needs.create-release.outputs.release_tag }}'
|
|
||||||
VERSION="${TAG#v}"
|
|
||||||
|
|
||||||
echo "Setting version to $VERSION"
|
|
||||||
|
|
||||||
sed -i \
|
|
||||||
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
|
|
||||||
package.json
|
|
||||||
|
|
||||||
sed -i \
|
|
||||||
-e "s/^version = \".*\"/version = \"$VERSION\"/" \
|
|
||||||
src-tauri/Cargo.toml
|
|
||||||
|
|
||||||
sed -i \
|
|
||||||
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
|
|
||||||
src-tauri/tauri.conf.json
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
libwebkit2gtk-4.1-dev \
|
|
||||||
libayatana-appindicator3-dev \
|
|
||||||
librsvg2-dev \
|
|
||||||
patchelf \
|
|
||||||
libssl-dev \
|
|
||||||
libgtk-3-dev \
|
|
||||||
squashfs-tools \
|
|
||||||
xdg-utils
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
|
|
||||||
- name: Cache cargo bin (tauri-cli)
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/bin
|
|
||||||
key: cargo-bin-${{ runner.os }}-${{ runner.arch }}-stable-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Install npm dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build frontend
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Install Tauri CLI (if missing)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if ! command -v cargo-tauri >/dev/null 2>&1; then
|
|
||||||
cargo install tauri-cli --version "${TAURI_CLI_VERSION}" --locked
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build Tauri app
|
|
||||||
run: cargo tauri build --bundles deb,appimage
|
|
||||||
|
|
||||||
- name: Upload artifacts to GitHub Release
|
|
||||||
uses: ncipollo/release-action@v1
|
|
||||||
with:
|
|
||||||
tag: ${{ needs.create-release.outputs.release_tag }}
|
|
||||||
name: "Armbian Imager ${{ needs.create-release.outputs.release_tag }}"
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
allowUpdates: true
|
|
||||||
omitBodyDuringUpdate: true
|
|
||||||
omitNameDuringUpdate: true
|
|
||||||
replacesArtifacts: false
|
|
||||||
artifacts: |
|
|
||||||
src-tauri/target/release/bundle/deb/*.deb
|
|
||||||
src-tauri/target/release/bundle/appimage/*.AppImage
|
|
||||||
src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
name: build-macos (${{ matrix.target }})
|
name: build-macos (${{ matrix.target }})
|
||||||
@@ -306,12 +241,39 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
env:
|
env:
|
||||||
# Ad-hoc signing: allows app to run after "xattr -cr" on macOS
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
APPLE_SIGNING_IDENTITY: "-"
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Import Apple Developer Certificate
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
run: |
|
||||||
|
# Create a temporary keychain
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||||
|
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Import certificate
|
||||||
|
echo -n "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
|
||||||
|
security import certificate.p12 -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Verify certificate
|
||||||
|
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Set as default keychain
|
||||||
|
security default-keychain -s "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
- name: Set version from release tag
|
- name: Set version from release tag
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -397,6 +359,39 @@ jobs:
|
|||||||
mv "$sig" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.tar.gz.sig"
|
mv "$sig" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.tar.gz.sig"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Notarize and staple DMG
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: src-tauri/target/${{ matrix.target }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Find the DMG file
|
||||||
|
DMG_FILE=$(find "$CARGO_TARGET_DIR/release/bundle/dmg/" -name "*.dmg" -type f | head -n 1)
|
||||||
|
|
||||||
|
if [[ -z "$DMG_FILE" ]]; then
|
||||||
|
echo "No DMG file found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Notarizing: $DMG_FILE"
|
||||||
|
|
||||||
|
# Submit DMG for notarization
|
||||||
|
xcrun notarytool submit "$DMG_FILE" \
|
||||||
|
--apple-id "$APPLE_ID" \
|
||||||
|
--password "$APPLE_ID_PASSWORD" \
|
||||||
|
--team-id "$APPLE_TEAM_ID" \
|
||||||
|
--wait \
|
||||||
|
--output-format json
|
||||||
|
|
||||||
|
# Staple the notarization ticket to DMG
|
||||||
|
xcrun stapler staple "$DMG_FILE"
|
||||||
|
|
||||||
|
# Verify stapling
|
||||||
|
xcrun stapler validate -v "$DMG_FILE"
|
||||||
|
|
||||||
|
echo "Notarization completed successfully"
|
||||||
|
|
||||||
- name: Upload macOS artifacts to GitHub Release
|
- name: Upload macOS artifacts to GitHub Release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -593,8 +588,7 @@ jobs:
|
|||||||
name: Generate latest.json for updater
|
name: Generate latest.json for updater
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- build-linux-x64
|
- build-linux
|
||||||
- build-linux-arm64
|
|
||||||
- build-macos
|
- build-macos
|
||||||
- build-windows-x64
|
- build-windows-x64
|
||||||
- build-windows-arm64
|
- build-windows-arm64
|
||||||
@@ -710,8 +704,7 @@ jobs:
|
|||||||
name: Publish release (draft -> false) + cleanup
|
name: Publish release (draft -> false) + cleanup
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- build-linux-x64
|
- build-linux
|
||||||
- build-linux-arm64
|
|
||||||
- build-macos
|
- build-macos
|
||||||
- build-windows-x64
|
- build-windows-x64
|
||||||
- build-windows-arm64
|
- build-windows-arm64
|
||||||
|
|||||||
2
.github/workflows/sync-locales.yml
vendored
2
.github/workflows/sync-locales.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
OPENAI_TIER: ${{ vars.OPENAI_TIER || 'free' }}
|
OPENAI_TIER: ${{ vars.OPENAI_TIER || 'free' }}
|
||||||
RETRY_FAILED: ${{ vars.RETRY_FAILED || 'false' }}
|
RETRY_FAILED: ${{ vars.RETRY_FAILED || 'false' }}
|
||||||
run: |
|
run: |
|
||||||
node scripts/sync-locales.js
|
node scripts/locales/sync-locales.js
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
|
|||||||
140
DEVELOPMENT.md
140
DEVELOPMENT.md
@@ -104,23 +104,23 @@ npm run tauri:dev
|
|||||||
### Single Platform
|
### Single Platform
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build-macos.sh # macOS (Intel + ARM)
|
./scripts/build/build-macos.sh # macOS (Intel + ARM)
|
||||||
./scripts/build-linux.sh # Linux (x64 + ARM)
|
./scripts/build/build-linux.sh # Linux (x64 + ARM)
|
||||||
npm run tauri:build # Windows
|
npm run tauri:build # Windows
|
||||||
```
|
```
|
||||||
|
|
||||||
### All Platforms
|
### All Platforms
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build-all.sh
|
./scripts/build/build-all.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build Options
|
### Build Options
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build-macos.sh --clean # Clean build
|
./scripts/build/build-macos.sh --clean # Clean build
|
||||||
./scripts/build-macos.sh --dev # Debug symbols
|
./scripts/build/build-macos.sh --dev # Debug symbols
|
||||||
./scripts/build-macos.sh --clean --dev # Both
|
./scripts/build/build-macos.sh --clean --dev # Both
|
||||||
```
|
```
|
||||||
|
|
||||||
### Output
|
### Output
|
||||||
@@ -132,46 +132,118 @@ npm run tauri:build # Windows
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
### Directory Overview
|
||||||
|
|
||||||
```
|
```
|
||||||
armbian-imager/
|
armbian-imager/
|
||||||
├── src/ # React Frontend
|
├── src/ # React Frontend
|
||||||
│ ├── components/ # UI Components
|
│ ├── components/ # UI Components
|
||||||
│ │ ├── flash/ # Flash progress
|
│ │ ├── flash/ # Flash progress components
|
||||||
│ │ ├── layout/ # Header, HomePage
|
│ │ │ ├── FlashActions.tsx # Action buttons (cancel, retry)
|
||||||
│ │ ├── modals/ # Board, Image, Device, Manufacturer
|
│ │ │ ├── FlashProgress.tsx # Progress display
|
||||||
|
│ │ │ └── FlashStageIcon.tsx # Stage indicators
|
||||||
|
│ │ ├── layout/ # Main layout
|
||||||
|
│ │ │ ├── Header.tsx # Top navigation bar
|
||||||
|
│ │ │ └── HomePage.tsx # Main page
|
||||||
|
│ │ ├── modals/ # Selection flow modals
|
||||||
|
│ │ │ ├── ManufacturerModal.tsx
|
||||||
|
│ │ │ ├── BoardModal.tsx
|
||||||
|
│ │ │ ├── ImageModal.tsx
|
||||||
|
│ │ │ └── DeviceModal.tsx
|
||||||
|
│ │ ├── settings/ # Settings modal components
|
||||||
|
│ │ │ ├── SettingsModal.tsx # Main settings modal
|
||||||
|
│ │ │ ├── GeneralSection.tsx# General settings (MOTD, updates)
|
||||||
|
│ │ │ ├── ThemeSection.tsx # Theme selection (light/dark/auto)
|
||||||
|
│ │ │ ├── LanguageSection.tsx# Language selection (17 languages)
|
||||||
|
│ │ │ ├── AdvancedSection.tsx# Developer mode & logs
|
||||||
|
│ │ │ └── AboutSection.tsx # App info & links
|
||||||
│ │ └── shared/ # Reusable components
|
│ │ └── shared/ # Reusable components
|
||||||
│ ├── hooks/ # React Hooks
|
│ │ ├── AppVersion.tsx # Version display
|
||||||
│ ├── config/ # Badges, manufacturers, OS info
|
│ │ ├── ErrorDisplay.tsx # Error presentation
|
||||||
│ ├── locales/ # i18n (15 languages)
|
│ │ ├── LoadingState.tsx # Loading indicators
|
||||||
|
│ │ └── SearchBox.tsx # Search functionality
|
||||||
|
│ ├── hooks/ # Custom React Hooks
|
||||||
|
│ │ ├── useTauri.ts # Tauri IPC wrappers
|
||||||
|
│ │ ├── useVendorLogos.ts # Logo validation
|
||||||
|
│ │ ├── useAsyncData.ts # Async data fetching pattern
|
||||||
|
│ │ └── useSettings.ts # Settings persistence hook
|
||||||
|
│ ├── contexts/ # React Context providers
|
||||||
|
│ │ └── ThemeContext.tsx # Theme state management (light/dark/auto)
|
||||||
|
│ ├── config/ # Static configuration
|
||||||
|
│ │ ├── constants.ts # App constants
|
||||||
|
│ │ ├── deviceColors.ts # Device color mapping
|
||||||
|
│ │ ├── os-info.ts # OS information
|
||||||
|
│ │ └── i18n.ts # i18n config & language metadata
|
||||||
|
│ ├── locales/ # i18n translations (17 languages)
|
||||||
│ ├── styles/ # Modular CSS
|
│ ├── styles/ # Modular CSS
|
||||||
|
│ │ ├── theme.css # Theme variables (light/dark)
|
||||||
|
│ │ ├── components.css # Component styles
|
||||||
|
│ │ └── responsive.css # Responsive design
|
||||||
│ ├── types/ # TypeScript interfaces
|
│ ├── types/ # TypeScript interfaces
|
||||||
│ ├── utils/ # Utilities
|
│ ├── utils/ # Utility functions
|
||||||
│ └── assets/ # Images, logos, icons
|
│ ├── assets/ # Static assets
|
||||||
|
│ ├── App.tsx # Main app component
|
||||||
|
│ └── main.tsx # React entry point
|
||||||
│
|
│
|
||||||
├── src-tauri/ # Rust Backend
|
├── src-tauri/ # Rust Backend
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── commands/ # Tauri IPC handlers
|
│ │ ├── commands/ # Tauri IPC command handlers
|
||||||
│ │ ├── config/ # Configuration
|
│ │ │ ├── board_queries.rs # Board/image API queries
|
||||||
│ │ ├── devices/ # Device detection
|
│ │ │ ├── operations.rs # Download & flash operations
|
||||||
│ │ ├── flash/ # Platform flash implementations
|
│ │ │ ├── custom_image.rs # Custom image handling
|
||||||
│ │ ├── images/ # Image management
|
│ │ │ ├── progress.rs # Progress event emission
|
||||||
|
│ │ │ ├── settings.rs # Settings commands (get/set dev mode, logs)
|
||||||
|
│ │ │ ├── system.rs # System utilities
|
||||||
|
│ │ │ └── state.rs # Shared application state
|
||||||
|
│ │ ├── devices/ # Platform-specific device detection
|
||||||
|
│ │ │ ├── linux.rs # Linux (UDisks2)
|
||||||
|
│ │ │ ├── macos.rs # macOS (diskutil)
|
||||||
|
│ │ │ └── windows.rs # Windows (WMI)
|
||||||
|
│ │ ├── flash/ # Platform-specific flash operations
|
||||||
|
│ │ │ ├── linux/ # Linux implementation
|
||||||
|
│ │ │ ├── macos/ # macOS implementation
|
||||||
|
│ │ │ └── windows/ # Windows implementation
|
||||||
|
│ │ ├── images/ # Image file management
|
||||||
│ │ ├── logging/ # Session logging
|
│ │ ├── logging/ # Session logging
|
||||||
│ │ ├── paste/ # Log upload
|
│ │ ├── paste/ # Log upload service
|
||||||
│ │ ├── utils/ # Utilities
|
│ │ ├── utils/ # Rust utilities
|
||||||
│ │ ├── download.rs # HTTP downloads
|
│ │ ├── download.rs # HTTP streaming downloads
|
||||||
│ │ └── decompress.rs # XZ, GZ, ZSTD
|
│ │ ├── decompress.rs # Archive extraction
|
||||||
│ ├── icons/ # App icons
|
│ │ └── main.rs # Rust entry point
|
||||||
|
│ ├── icons/ # App icons (all platforms)
|
||||||
|
│ ├── Cargo.toml # Rust dependencies
|
||||||
|
│ ├── tauri.conf.json # Tauri configuration
|
||||||
│ └── target/ # Compiled binaries (gitignored)
|
│ └── target/ # Compiled binaries (gitignored)
|
||||||
│
|
│
|
||||||
├── scripts/ # Build and setup
|
├── scripts/ # Build and utility scripts
|
||||||
│ ├── build-*.sh # Platform build scripts
|
│ ├── build/ # Platform build scripts
|
||||||
|
│ │ ├── build-all.sh # All platforms
|
||||||
|
│ │ ├── build-linux.sh # Linux builds
|
||||||
|
│ │ └── build-macos.sh # macOS universal binaries
|
||||||
|
│ ├── locales/ # Locale management
|
||||||
|
│ │ └── sync-locales.js # Translation sync script
|
||||||
│ └── setup/ # Dependency installation
|
│ └── setup/ # Dependency installation
|
||||||
│ ├── install.sh
|
│ ├── install.sh # Universal installer
|
||||||
│ ├── install-linux.sh
|
│ ├── install-linux.sh # Linux dependencies
|
||||||
│ ├── install-macos.sh
|
│ ├── install-macos.sh # macOS dependencies
|
||||||
│ └── install-windows.ps1
|
│ └── install-windows.ps1 # Windows dependencies
|
||||||
│
|
│
|
||||||
└── .github/workflows/ # CI/CD
|
├── .github/workflows/ # CI/CD pipelines
|
||||||
|
│ ├── build.yml # CI builds
|
||||||
|
│ ├── build-artifacts.yml # Release builds
|
||||||
|
│ ├── pr-check.yml # PR validation
|
||||||
|
│ └── sync-locales.yml # Auto translation sync
|
||||||
|
│
|
||||||
|
├── public/ # Static assets
|
||||||
|
│ └── locales/ # i18n fallback data
|
||||||
|
│
|
||||||
|
├── docs/ # Additional documentation
|
||||||
|
├── images/ # Project images/screenshots
|
||||||
|
├── package.json # Node dependencies & scripts
|
||||||
|
│
|
||||||
|
├── CONTRIBUTING.md # Contribution guidelines
|
||||||
|
├── DEVELOPMENT.md # This file
|
||||||
|
└── README.md # Project overview
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -185,7 +257,8 @@ armbian-imager/
|
|||||||
| React 19 | UI Framework |
|
| React 19 | UI Framework |
|
||||||
| TypeScript | Type Safety |
|
| TypeScript | Type Safety |
|
||||||
| Vite | Build Tool & Dev Server |
|
| Vite | Build Tool & Dev Server |
|
||||||
| i18next | i18n (15 languages) |
|
| React Context API | State Management (Theme) |
|
||||||
|
| i18next | i18n (17 languages) |
|
||||||
| Lucide | Icons |
|
| Lucide | Icons |
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
@@ -194,6 +267,7 @@ armbian-imager/
|
|||||||
|------------|---------|
|
|------------|---------|
|
||||||
| Rust | Systems Programming |
|
| Rust | Systems Programming |
|
||||||
| Tauri 2 | Desktop Framework |
|
| Tauri 2 | Desktop Framework |
|
||||||
|
| Tauri Store Plugin | Persistent Settings |
|
||||||
| Tokio | Async Runtime |
|
| Tokio | Async Runtime |
|
||||||
| Serde | Serialization |
|
| Serde | Serialization |
|
||||||
| Reqwest | HTTP Client |
|
| Reqwest | HTTP Client |
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -40,10 +40,6 @@ Prebuilt binaries are available for all supported platforms.
|
|||||||
| Intel & Apple Silicon | x64 & ARM64 | x64 & ARM64 |
|
| Intel & Apple Silicon | x64 & ARM64 | x64 & ARM64 |
|
||||||
| <code>.dmg</code> / <code>.app.zip</code> | <code>.exe</code> / <code>.msi</code> | <code>.deb</code> / <code>.AppImage</code> |
|
| <code>.dmg</code> / <code>.app.zip</code> | <code>.exe</code> / <code>.msi</code> | <code>.deb</code> / <code>.AppImage</code> |
|
||||||
|
|
||||||
**macOS: First Launch**
|
|
||||||
|
|
||||||
On first launch, macOS may block the application because it is not signed. If this happens, open **System Settings → Privacy & Security** and click **Open Anyway** next to *Armbian Imager was blocked*. This only needs to be done once.
|
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. **Select Manufacturer** — Choose from 70+ supported SBC manufacturers or load a custom image
|
1. **Select Manufacturer** — Choose from 70+ supported SBC manufacturers or load a custom image
|
||||||
@@ -51,6 +47,12 @@ On first launch, macOS may block the application because it is not signed. If th
|
|||||||
3. **Select Image** — Choose desktop or server, kernel variant, and stable or nightly builds
|
3. **Select Image** — Choose desktop or server, kernel variant, and stable or nightly builds
|
||||||
4. **Flash** — Download, decompress, write, and verify automatically
|
4. **Flash** — Download, decompress, write, and verify automatically
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
- **Theme Selection**: Light, dark, or automatic based on system preferences
|
||||||
|
- **Developer Mode**: Enable detailed logging and view application logs
|
||||||
|
- **Language Selection**: 17 languages with automatic system detection
|
||||||
|
|
||||||
## Platform Support
|
## Platform Support
|
||||||
|
|
||||||
| Platform | Architecture | Notes |
|
| Platform | Architecture | Notes |
|
||||||
|
|||||||
107
package-lock.json
generated
107
package-lock.json
generated
@@ -12,14 +12,17 @@
|
|||||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-process": "^2",
|
"@tauri-apps/plugin-process": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||||
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
"@tauri-apps/plugin-updater": "^2",
|
"@tauri-apps/plugin-updater": "^2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"ansi-to-html": "^0.7.0",
|
||||||
"i18next": "^25.7.2",
|
"i18next": "^25.7.2",
|
||||||
"lucide-react": "^0.560.0",
|
"lucide-react": "^0.560.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.2",
|
"react": "^19.2.2",
|
||||||
"react-dom": "^19.2.2",
|
"react-dom": "^19.2.2",
|
||||||
"react-i18next": "^16.5.0"
|
"react-i18next": "^16.5.0",
|
||||||
|
"twemoji": "^14.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -1602,6 +1605,15 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-store": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-updater": {
|
"node_modules/@tauri-apps/plugin-updater": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.9.0.tgz",
|
||||||
@@ -2065,6 +2077,21 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-to-html": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^2.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ansi-to-html": "bin/ansi-to-html"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@@ -2310,6 +2337,15 @@
|
|||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -2650,6 +2686,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6 <7 || >=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra/node_modules/jsonfile": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -2710,6 +2769,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -2921,6 +2986,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^0.1.2"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -3242,6 +3319,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.2.tgz",
|
||||||
"integrity": "sha512-fhyD2BLrew6qYf4NNtHff1rLXvzR25rq49p+FeqByOazc6TcSi2n8EYulo5C1PbH+1uBW++5S1SG7FcUU6mlDg==",
|
"integrity": "sha512-fhyD2BLrew6qYf4NNtHff1rLXvzR25rq49p+FeqByOazc6TcSi2n8EYulo5C1PbH+1uBW++5S1SG7FcUU6mlDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -3490,6 +3568,24 @@
|
|||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/twemoji": {
|
||||||
|
"version": "14.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz",
|
||||||
|
"integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": "^8.0.1",
|
||||||
|
"jsonfile": "^5.0.0",
|
||||||
|
"twemoji-parser": "14.0.0",
|
||||||
|
"universalify": "^0.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/twemoji-parser": {
|
||||||
|
"version": "14.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
|
||||||
|
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -3548,6 +3644,15 @@
|
|||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||||
|
|||||||
@@ -31,16 +31,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.9.1",
|
"@tauri-apps/api": "^2.9.1",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
|
||||||
"@tauri-apps/plugin-updater": "^2",
|
|
||||||
"@tauri-apps/plugin-process": "^2",
|
"@tauri-apps/plugin-process": "^2",
|
||||||
|
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||||
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"ansi-to-html": "^0.7.0",
|
||||||
"i18next": "^25.7.2",
|
"i18next": "^25.7.2",
|
||||||
"lucide-react": "^0.560.0",
|
"lucide-react": "^0.560.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.2",
|
"react": "^19.2.2",
|
||||||
"react-dom": "^19.2.2",
|
"react-dom": "^19.2.2",
|
||||||
"react-i18next": "^16.5.0"
|
"react-i18next": "^16.5.0",
|
||||||
|
"twemoji": "^14.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
# Translation Sync Scripts
|
|
||||||
|
|
||||||
This directory contains automation scripts for managing i18n translations.
|
|
||||||
|
|
||||||
## sync-locales.js
|
|
||||||
|
|
||||||
Automatically syncs all translation files with `src/locales/en.json` (the source of truth) and translates missing keys using AI.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- **Automatic Detection**: Finds missing keys in all locale files
|
|
||||||
- **AI Translation**: Uses OpenAI API to automatically translate new keys with high quality
|
|
||||||
- **Context-Aware**: Provides section/key context to the AI for better translations
|
|
||||||
- **Placeholder Preservation**: Maintains i18next placeholders like `{{count}}` and `{{boardName}}`
|
|
||||||
- **Adaptive Rate Limiting**: Automatically adjusts based on model and payment tier
|
|
||||||
- **Error Handling**: Falls back to `TODO:` prefix if translation fails
|
|
||||||
- **Smart Prompts**: Uses specialized prompts for technical UI translation
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
#### Local Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic usage (requires OpenAI API key)
|
|
||||||
export OPENAI_API_KEY=sk-...
|
|
||||||
node scripts/sync-locales.js
|
|
||||||
|
|
||||||
# With custom model (default: gpt-4o-mini)
|
|
||||||
OPENAI_MODEL=gpt-4o node scripts/sync-locales.js
|
|
||||||
|
|
||||||
# With custom OpenAI-compatible API endpoint
|
|
||||||
OPENAI_API=https://api.openai.com/v1 node scripts/sync-locales.js
|
|
||||||
|
|
||||||
# For paid tier (much faster - 50-100x speedup)
|
|
||||||
OPENAI_TIER=paid node scripts/sync-locales.js
|
|
||||||
|
|
||||||
# Retry failed translations (keys marked with TODO:)
|
|
||||||
RETRY_FAILED=true node scripts/sync-locales.js
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GitHub Actions
|
|
||||||
|
|
||||||
The workflow runs automatically:
|
|
||||||
- **Daily** at 00:00 UTC
|
|
||||||
- **On push** to the branch
|
|
||||||
- **On manual trigger** via workflow_dispatch
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description | Default | Required |
|
|
||||||
|----------|-------------|---------|----------|
|
|
||||||
| `OPENAI_API_KEY` | OpenAI API key | - | Yes |
|
|
||||||
| `OPENAI_MODEL` | Model to use for translation | `gpt-4o-mini` | No |
|
|
||||||
| `OPENAI_API` | API endpoint URL | `https://api.openai.com/v1` | No |
|
|
||||||
| `OPENAI_TIER` | Account tier for rate limits | `free` | No |
|
|
||||||
| `RETRY_FAILED` | Retry keys marked with `TODO:` | `false` | No |
|
|
||||||
|
|
||||||
#### GitHub Secrets/Variables
|
|
||||||
|
|
||||||
To configure the GitHub Action:
|
|
||||||
|
|
||||||
1. **Required - Add API key**:
|
|
||||||
```bash
|
|
||||||
gh secret set OPENAI_API_KEY
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Optional - Custom model** (for cost/quality tuning):
|
|
||||||
```bash
|
|
||||||
gh variable set OPENAI_MODEL --value "gpt-4o-mini"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Optional - Custom endpoint** (for OpenAI-compatible APIs):
|
|
||||||
```bash
|
|
||||||
gh variable set OPENAI_API --value "https://api.openai.com/v1"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Optional - Set account tier** (for faster translations with paid account):
|
|
||||||
```bash
|
|
||||||
gh variable set OPENAI_TIER --value "paid"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Optional - Retry failed translations** (re-attempt keys marked with `TODO:`):
|
|
||||||
```bash
|
|
||||||
gh variable set RETRY_FAILED --value "true"
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenAI Setup
|
|
||||||
|
|
||||||
#### Getting an API Key
|
|
||||||
|
|
||||||
1. Visit [platform.openai.com](https://platform.openai.com/)
|
|
||||||
2. Sign up or log in
|
|
||||||
3. Navigate to API Keys section
|
|
||||||
4. Create a new API key
|
|
||||||
5. Add it to your environment or GitHub secrets
|
|
||||||
|
|
||||||
#### Account Tier & Rate Limits
|
|
||||||
|
|
||||||
The script automatically adjusts speed based on your account tier:
|
|
||||||
|
|
||||||
| Tier | RPM Limit | Batch Size | Delay | 100 Keys Time |
|
|
||||||
|------|-----------|------------|-------|---------------|
|
|
||||||
| **Free** | 3/min | 1 | 21s | ~35 min |
|
|
||||||
| **Paid (Tier 1-2)** | 200/min | 50 | 300ms | ~1 min |
|
|
||||||
| **Paid (Tier 3-5)** | 500/min | 50 | 120ms | ~30 sec |
|
|
||||||
|
|
||||||
To use paid tier rates:
|
|
||||||
1. Add a payment method to your OpenAI account (even $5 works)
|
|
||||||
2. Set `OPENAI_TIER=paid` environment variable
|
|
||||||
|
|
||||||
**Recommendation**: With just a $5 balance, you get Tier 1-2 rates which are **65x faster** than free tier.
|
|
||||||
|
|
||||||
#### Choosing a Model
|
|
||||||
|
|
||||||
| Model | Cost | Quality | Speed | Best For |
|
|
||||||
|-------|------|---------|-------|----------|
|
|
||||||
| `gpt-4o-mini` | Low | High | Fast | Most translations (default) |
|
|
||||||
| `gpt-4o` | Medium | Very High | Fast | Complex UI text |
|
|
||||||
| `gpt-3.5-turbo` | Very Low | Medium | Very Fast | Simple translations |
|
|
||||||
|
|
||||||
**Recommendation**: Start with `gpt-4o-mini` for the best balance of cost, quality, and speed.
|
|
||||||
|
|
||||||
### Supported Languages
|
|
||||||
|
|
||||||
| Code | Language |
|
|
||||||
|------|----------|
|
|
||||||
| `de` | German |
|
|
||||||
| `es` | Spanish |
|
|
||||||
| `fr` | French |
|
|
||||||
| `it` | Italian |
|
|
||||||
| `ja` | Japanese |
|
|
||||||
| `ko` | Korean |
|
|
||||||
| `nl` | Dutch |
|
|
||||||
| `pl` | Polish |
|
|
||||||
| `pt` | Portuguese |
|
|
||||||
| `ru` | Russian |
|
|
||||||
| `sl` | Slovenian |
|
|
||||||
| `tr` | Turkish |
|
|
||||||
| `uk` | Ukrainian |
|
|
||||||
| `zh` | Chinese (Simplified) |
|
|
||||||
|
|
||||||
### Output
|
|
||||||
|
|
||||||
The script will:
|
|
||||||
1. âś… Show which keys are missing for each language
|
|
||||||
2. 🤖 Translate missing keys with OpenAI
|
|
||||||
3. 📊 Show translation statistics (success/failure)
|
|
||||||
4. ⚠️ Warn about any translation failures
|
|
||||||
5. đź’ľ Update locale files with translated content
|
|
||||||
6. 🔍 Exit with code 1 if changes were made (useful for CI/CD)
|
|
||||||
|
|
||||||
### Example Output
|
|
||||||
|
|
||||||
```
|
|
||||||
🔍 Syncing translation files with en.json (source of truth)
|
|
||||||
|
|
||||||
🤖 Using OpenAI API: https://api.openai.com/v1
|
|
||||||
📦 Model: gpt-4o-mini
|
|
||||||
âś… API key is configured
|
|
||||||
|
|
||||||
âś… Source file has 93 keys
|
|
||||||
|
|
||||||
📝 Processing de (German)...
|
|
||||||
âś… de is up to date (93 keys)
|
|
||||||
|
|
||||||
📝 Processing hr (Croatian)...
|
|
||||||
⚠️ Found 64 missing keys
|
|
||||||
🤖 Translating 64 strings with OpenAI...
|
|
||||||
âś… Updated hr with 64 new keys
|
|
||||||
|
|
||||||
✨ Translation files updated successfully!
|
|
||||||
|
|
||||||
📊 Summary:
|
|
||||||
- Total translated: 64 keys
|
|
||||||
- Please review translations for accuracy and context
|
|
||||||
```
|
|
||||||
|
|
||||||
### AI Translation Features
|
|
||||||
|
|
||||||
The script uses specialized prompts to ensure:
|
|
||||||
|
|
||||||
1. **Context Awareness**: Provides section/key context for each translation
|
|
||||||
2. **Technical Terminology**: Knows when to keep terms like "Flash", "SD card", "USB" in English
|
|
||||||
3. **Placeholder Preservation**: Maintains `{{variables}}` exactly as they appear
|
|
||||||
4. **UI Appropriateness**: Uses concise, natural text for buttons and labels
|
|
||||||
5. **Plural Forms**: Handles i18next plural suffixes (_one, _other) correctly
|
|
||||||
6. **Consistent Tone**: Maintains formal but friendly tone throughout
|
|
||||||
|
|
||||||
### Best Practices
|
|
||||||
|
|
||||||
1. **Review Translations**: AI translations are excellent but may need context-specific adjustments
|
|
||||||
2. **Test in App**: Always test translations in the actual application
|
|
||||||
3. **Handle Plurals**: The script preserves `_one` and `_other` suffixes for plural forms
|
|
||||||
4. **Check Placeholders**: Verify that `{{variables}}` are correctly preserved
|
|
||||||
5. **Cultural Nuances**: Review translations for cultural appropriateness
|
|
||||||
6. **Cost Management**: Use `gpt-4o-mini` for best cost/quality balance
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
#### API Key Not Found
|
|
||||||
|
|
||||||
```
|
|
||||||
❌ OPENAI_API_KEY is not set!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Set the environment variable or GitHub secret:
|
|
||||||
```bash
|
|
||||||
export OPENAI_API_KEY=sk-...
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Translation Failures
|
|
||||||
|
|
||||||
If some translations fail with `TODO:`:
|
|
||||||
- Check your API key has sufficient credits
|
|
||||||
- Verify API endpoint is accessible
|
|
||||||
- Check rate limits (especially for larger translation batches
|
|
||||||
|
|
||||||
#### Poor Quality Translations
|
|
||||||
|
|
||||||
If translations seem off:
|
|
||||||
- The AI might lack specific UI context
|
|
||||||
- Try a more capable model like `gpt-4o`
|
|
||||||
- Manually edit the JSON files to fix issues
|
|
||||||
- Consider the translation context provided in the prompt
|
|
||||||
|
|
||||||
#### Cost Concerns
|
|
||||||
|
|
||||||
For cost optimization:
|
|
||||||
- Use `gpt-4o-mini` (very cost-effective)
|
|
||||||
- Run the script less frequently
|
|
||||||
- Review and merge translations in batches
|
|
||||||
- Consider caching previous translations
|
|
||||||
|
|
||||||
### Cost Estimation
|
|
||||||
|
|
||||||
Approximate costs for translating missing keys (using `gpt-4o-mini`):
|
|
||||||
|
|
||||||
- **10 keys**: ~$0.00001
|
|
||||||
- **50 keys**: ~$0.00005
|
|
||||||
- **100 keys**: ~$0.0001
|
|
||||||
|
|
||||||
*Costs vary based on text length and model used.*
|
|
||||||
@@ -15,6 +15,7 @@ tauri-plugin-shell = "2"
|
|||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
|
tauri-plugin-store = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
// Extract Tauri version from Cargo.toml and expose it as a compile-time env var
|
||||||
|
println!("cargo:rustc-env=TAURI_VERSION={}", tauri_version());
|
||||||
|
|
||||||
// On Windows, embed the manifest to request admin privileges at startup
|
// On Windows, embed the manifest to request admin privileges at startup
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
@@ -11,3 +14,26 @@ fn main() {
|
|||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract Tauri version from Cargo.toml dependencies
|
||||||
|
fn tauri_version() -> String {
|
||||||
|
let cargo_toml = std::path::PathBuf::from("Cargo.toml");
|
||||||
|
|
||||||
|
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
|
||||||
|
// Look for tauri dependency version
|
||||||
|
for line in content.lines() {
|
||||||
|
if line.contains("tauri =") || line.contains("tauri =") {
|
||||||
|
// Extract version from line like: tauri = { version = "2.x", ... }
|
||||||
|
if let Some(start) = line.find("version = \"") {
|
||||||
|
let after_version = &line[start + 11..];
|
||||||
|
if let Some(end) = after_version.find('"') {
|
||||||
|
return after_version[..end].to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to unknown if parsing fails
|
||||||
|
"unknown".to_string()
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"process:allow-restart"
|
"process:allow-restart",
|
||||||
|
"store:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::images::{
|
|||||||
extract_images, fetch_all_images, filter_images_for_board, get_unique_boards, BoardInfo,
|
extract_images, fetch_all_images, filter_images_for_board, get_unique_boards, BoardInfo,
|
||||||
ImageInfo,
|
ImageInfo,
|
||||||
};
|
};
|
||||||
use crate::{log_error, log_info};
|
use crate::{log_debug, log_error, log_info};
|
||||||
|
|
||||||
use super::state::AppState;
|
use super::state::AppState;
|
||||||
|
|
||||||
@@ -59,6 +59,13 @@ pub async fn get_images_for_board(
|
|||||||
board_slug,
|
board_slug,
|
||||||
stable_only
|
stable_only
|
||||||
);
|
);
|
||||||
|
log_debug!(
|
||||||
|
"board_queries",
|
||||||
|
"Filters - preapp: {:?}, kernel: {:?}, variant: {:?}",
|
||||||
|
preapp_filter,
|
||||||
|
kernel_filter,
|
||||||
|
variant_filter
|
||||||
|
);
|
||||||
|
|
||||||
let json_guard = state.images_json.lock().await;
|
let json_guard = state.images_json.lock().await;
|
||||||
let json = json_guard.as_ref().ok_or_else(|| {
|
let json = json_guard.as_ref().ok_or_else(|| {
|
||||||
@@ -71,6 +78,7 @@ pub async fn get_images_for_board(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let images = extract_images(json);
|
let images = extract_images(json);
|
||||||
|
log_debug!("board_queries", "Total images available: {}", images.len());
|
||||||
let filtered = filter_images_for_board(
|
let filtered = filter_images_for_board(
|
||||||
&images,
|
&images,
|
||||||
&board_slug,
|
&board_slug,
|
||||||
@@ -79,6 +87,12 @@ pub async fn get_images_for_board(
|
|||||||
variant_filter.as_deref(),
|
variant_filter.as_deref(),
|
||||||
stable_only,
|
stable_only,
|
||||||
);
|
);
|
||||||
|
log_debug!(
|
||||||
|
"board_queries",
|
||||||
|
"Filtered down to {} images for board {}",
|
||||||
|
filtered.len(),
|
||||||
|
board_slug
|
||||||
|
);
|
||||||
log_info!(
|
log_info!(
|
||||||
"board_queries",
|
"board_queries",
|
||||||
"Found {} images for board {}",
|
"Found {} images for board {}",
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
use crate::decompress::{decompress_local_file, needs_decompression};
|
use crate::decompress::{decompress_local_file, needs_decompression};
|
||||||
|
use crate::images::{extract_images, fetch_all_images, get_unique_boards, BoardInfo};
|
||||||
|
use crate::utils::{get_cache_dir, normalize_slug};
|
||||||
use crate::{log_error, log_info};
|
use crate::{log_error, log_info};
|
||||||
|
|
||||||
use super::state::AppState;
|
use super::state::AppState;
|
||||||
@@ -130,3 +133,176 @@ pub async fn select_custom_image(window: tauri::Window) -> Result<Option<CustomI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a decompressed custom image file
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_decompressed_custom_image(image_path: String) -> Result<(), String> {
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Deleting decompressed custom image: {}",
|
||||||
|
image_path
|
||||||
|
);
|
||||||
|
let path = PathBuf::from(&image_path);
|
||||||
|
|
||||||
|
// Safety check: only delete files in our custom-decompress directory
|
||||||
|
let custom_decompress_dir = get_cache_dir(config::app::NAME).join("custom-decompress");
|
||||||
|
|
||||||
|
if !path.starts_with(&custom_decompress_dir) {
|
||||||
|
log_error!(
|
||||||
|
"custom_image",
|
||||||
|
"Attempted to delete file outside custom-decompress cache: {}",
|
||||||
|
image_path
|
||||||
|
);
|
||||||
|
return Err("Cannot delete files outside custom-decompress directory".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(&path).map_err(|e| {
|
||||||
|
log_error!(
|
||||||
|
"custom_image",
|
||||||
|
"Failed to delete decompressed image {}: {}",
|
||||||
|
image_path,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
format!("Failed to delete decompressed image: {}", e)
|
||||||
|
})?;
|
||||||
|
log_info!("custom_image", "Deleted decompressed image: {}", image_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to remove empty parent directory (ignore errors)
|
||||||
|
let _ = std::fs::remove_dir(&custom_decompress_dir);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect board information from custom image filename
|
||||||
|
/// Parses Armbian naming convention: Armbian_VERSION_BOARD_DISTRO_VENDOR_KERNEL_FLAVOR.img.xz
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn detect_board_from_filename(
|
||||||
|
filename: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<Option<BoardInfo>, String> {
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"=== Starting board detection from filename: {} ===",
|
||||||
|
filename
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Extract filename from path (remove directory)
|
||||||
|
let path = PathBuf::from(&filename);
|
||||||
|
let filename_only = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.ok_or("Invalid filename")?;
|
||||||
|
|
||||||
|
// 2. Remove extension(s) - handle .img.xz, .img.gz, .img.zst, .img.bz2, .img
|
||||||
|
let stem = filename_only
|
||||||
|
.strip_suffix(".xz")
|
||||||
|
.or_else(|| filename_only.strip_suffix(".gz"))
|
||||||
|
.or_else(|| filename_only.strip_suffix(".zst"))
|
||||||
|
.or_else(|| filename_only.strip_suffix(".bz2"))
|
||||||
|
.or_else(|| filename_only.strip_suffix(".img"))
|
||||||
|
.unwrap_or(filename_only);
|
||||||
|
|
||||||
|
// 3. Parse Armbian naming pattern: Armbian_VERSION_BOARD_DISTRO_VENDOR_KERNEL_FLAVOR
|
||||||
|
let parts: Vec<&str> = stem.split('_').collect();
|
||||||
|
|
||||||
|
// 4. Validate Armbian format (at least 4 parts, starts with "Armbian")
|
||||||
|
if parts.len() < 4 || !parts[0].eq_ignore_ascii_case("Armbian") {
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Not an Armbian image or invalid format: {}",
|
||||||
|
filename_only
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Extract board name (index 2)
|
||||||
|
let board_name = parts[2];
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Extracted board name from filename: {}",
|
||||||
|
board_name
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Normalize board name to slug format
|
||||||
|
let normalized_slug = normalize_slug(board_name);
|
||||||
|
log_info!("custom_image", "Normalized board slug: {}", normalized_slug);
|
||||||
|
|
||||||
|
// 7. Ensure board data is loaded (auto-load if not cached)
|
||||||
|
// Use compare-and-swap pattern to prevent race conditions
|
||||||
|
log_info!("custom_image", "Checking if board data is cached...");
|
||||||
|
{
|
||||||
|
let needs_loading = {
|
||||||
|
let json_guard = state.images_json.lock().await;
|
||||||
|
json_guard.is_none()
|
||||||
|
};
|
||||||
|
|
||||||
|
if needs_loading {
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Board data not cached, fetching from API..."
|
||||||
|
);
|
||||||
|
let json = fetch_all_images().await.map_err(|e| {
|
||||||
|
log_error!("custom_image", "Failed to fetch board data: {}", e);
|
||||||
|
format!("Failed to fetch board data: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Cache the fetched data
|
||||||
|
let mut json_guard = state.images_json.lock().await;
|
||||||
|
// Double-check: another thread might have loaded it while we were fetching
|
||||||
|
if json_guard.is_none() {
|
||||||
|
*json_guard = Some(json);
|
||||||
|
log_info!("custom_image", "Board data cached successfully");
|
||||||
|
} else {
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Board data was already cached by another thread"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Get cached boards data (now guaranteed to be loaded)
|
||||||
|
// Extract boards in a scoped block to release lock early
|
||||||
|
let matching_board = {
|
||||||
|
log_info!("custom_image", "Accessing cached board data...");
|
||||||
|
let json_guard = state.images_json.lock().await;
|
||||||
|
let json = json_guard.as_ref().ok_or("Images not loaded")?;
|
||||||
|
|
||||||
|
log_info!("custom_image", "Loaded images JSON, extracting boards...");
|
||||||
|
let images = extract_images(json);
|
||||||
|
log_info!("custom_image", "Extracted {} images", images.len());
|
||||||
|
let boards = get_unique_boards(&images);
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Found {} unique boards in database",
|
||||||
|
boards.len()
|
||||||
|
);
|
||||||
|
// Lock released here
|
||||||
|
|
||||||
|
// 9. Find matching board by slug
|
||||||
|
boards
|
||||||
|
.iter()
|
||||||
|
.find(|board| board.slug == normalized_slug)
|
||||||
|
.cloned()
|
||||||
|
}; // matching_board is now owned, lock is released
|
||||||
|
|
||||||
|
if let Some(ref board) = matching_board {
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Detected board: {} (slug: {})",
|
||||||
|
board.name,
|
||||||
|
board.slug
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log_info!(
|
||||||
|
"custom_image",
|
||||||
|
"Board not found in database: {}",
|
||||||
|
normalized_slug
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info!("custom_image", "Board detection completed successfully");
|
||||||
|
Ok(matching_board)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod custom_image;
|
|||||||
pub mod operations;
|
pub mod operations;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub mod scraping;
|
pub mod scraping;
|
||||||
|
pub mod settings;
|
||||||
mod state;
|
mod state;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
//! Handles download and flash operations.
|
//! Handles download and flash operations.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::State;
|
use tauri::{AppHandle, State};
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::download::download_image as do_download;
|
use crate::download::download_image as do_download;
|
||||||
use crate::flash::{flash_image as do_flash, request_authorization};
|
use crate::flash::{flash_image as do_flash, request_authorization};
|
||||||
use crate::utils::get_cache_dir;
|
use crate::utils::get_cache_dir;
|
||||||
use crate::{log_error, log_info};
|
use crate::{log_debug, log_error, log_info};
|
||||||
|
|
||||||
use super::state::AppState;
|
use super::state::AppState;
|
||||||
|
|
||||||
@@ -57,10 +57,16 @@ pub async fn download_image(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
log_info!("operations", "Starting download: {}", file_url);
|
log_info!("operations", "Starting download: {}", file_url);
|
||||||
|
log_debug!(
|
||||||
|
"operations",
|
||||||
|
"Download directory: {:?}",
|
||||||
|
get_cache_dir(config::app::NAME).join("images")
|
||||||
|
);
|
||||||
if let Some(ref sha) = file_url_sha {
|
if let Some(ref sha) = file_url_sha {
|
||||||
log_info!("operations", "SHA URL: {}", sha);
|
log_info!("operations", "SHA URL: {}", sha);
|
||||||
} else {
|
} else {
|
||||||
log_info!("operations", "No SHA URL provided");
|
log_info!("operations", "No SHA URL provided");
|
||||||
|
log_debug!("operations", "SHA verification will be skipped");
|
||||||
}
|
}
|
||||||
let download_dir = get_cache_dir(config::app::NAME).join("images");
|
let download_dir = get_cache_dir(config::app::NAME).join("images");
|
||||||
|
|
||||||
@@ -92,6 +98,7 @@ pub async fn flash_image(
|
|||||||
device_path: String,
|
device_path: String,
|
||||||
verify: bool,
|
verify: bool,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
_app: AppHandle,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
log_info!(
|
log_info!(
|
||||||
"operations",
|
"operations",
|
||||||
@@ -100,15 +107,32 @@ pub async fn flash_image(
|
|||||||
device_path,
|
device_path,
|
||||||
verify
|
verify
|
||||||
);
|
);
|
||||||
|
log_debug!(
|
||||||
|
"operations",
|
||||||
|
"Image path exists: {}",
|
||||||
|
std::path::Path::new(&image_path).exists()
|
||||||
|
);
|
||||||
|
log_debug!(
|
||||||
|
"operations",
|
||||||
|
"Device path exists: {}",
|
||||||
|
std::path::Path::new(&device_path).exists()
|
||||||
|
);
|
||||||
|
log_debug!("operations", "Verification enabled: {}", verify);
|
||||||
|
|
||||||
let path = PathBuf::from(&image_path);
|
let path = PathBuf::from(&image_path);
|
||||||
let flash_state = state.flash_state.clone();
|
let flash_state = state.flash_state.clone();
|
||||||
|
|
||||||
let result = do_flash(&path, &device_path, flash_state, verify).await;
|
let result = do_flash(&path, &device_path, flash_state, verify).await;
|
||||||
if let Err(ref e) = result {
|
|
||||||
log_error!("operations", "Flash failed: {}", e);
|
match &result {
|
||||||
} else {
|
Ok(_) => {
|
||||||
log_info!("operations", "Flash completed successfully");
|
log_info!("operations", "Flash completed successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_error!("operations", "Flash failed: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
302
src-tauri/src/commands/settings.rs
Normal file
302
src-tauri/src/commands/settings.rs
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
//! Settings persistence commands using Tauri Store plugin
|
||||||
|
//!
|
||||||
|
//! Manages user preferences like theme and language using the Tauri Store plugin.
|
||||||
|
|
||||||
|
use crate::log_info;
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
|
const MODULE: &str = "commands::settings";
|
||||||
|
const SETTINGS_STORE: &str = "settings.json";
|
||||||
|
const MAX_LOG_SIZE: u64 = 5 * 1024 * 1024; // 5MB
|
||||||
|
const MAX_LOG_LINES: usize = 10_000;
|
||||||
|
|
||||||
|
/// Default values for settings
|
||||||
|
fn default_theme() -> String {
|
||||||
|
"auto".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_language() -> String {
|
||||||
|
"auto".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_show_motd() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_show_updater_modal() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_developer_mode() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current theme preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_theme(app: tauri::AppHandle) -> String {
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => match store.get("theme") {
|
||||||
|
Some(value) => value.as_str().unwrap_or("auto").to_string(),
|
||||||
|
None => {
|
||||||
|
log_info!(MODULE, "Theme not found in store, using default");
|
||||||
|
default_theme()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log_info!(MODULE, "Error loading store, using default theme: {}", e);
|
||||||
|
default_theme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the theme preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_theme(theme: String, app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
log_info!(MODULE, "Setting theme to: {}", theme);
|
||||||
|
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => {
|
||||||
|
store.set("theme", theme);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Failed to access store: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current language preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_language(app: tauri::AppHandle) -> String {
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => match store.get("language") {
|
||||||
|
Some(value) => value.as_str().unwrap_or("auto").to_string(),
|
||||||
|
None => {
|
||||||
|
log_info!(MODULE, "Language not found in store, using default");
|
||||||
|
default_language()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log_info!(MODULE, "Error loading store, using default language: {}", e);
|
||||||
|
default_language()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the language preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_language(language: String, app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
log_info!(MODULE, "Setting language to: {}", language);
|
||||||
|
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => {
|
||||||
|
store.set("language", language);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Failed to access store: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the MOTD visibility preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_show_motd(app: tauri::AppHandle) -> bool {
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => match store.get("show_motd") {
|
||||||
|
Some(value) => value.as_bool().unwrap_or(true),
|
||||||
|
None => {
|
||||||
|
log_info!(MODULE, "show_motd not found in store, using default");
|
||||||
|
default_show_motd()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log_info!(
|
||||||
|
MODULE,
|
||||||
|
"Error loading store, using default show_motd: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
default_show_motd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the MOTD visibility preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_show_motd(show: bool, app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
log_info!(MODULE, "Setting show_motd to: {}", show);
|
||||||
|
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => {
|
||||||
|
store.set("show_motd", show);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Failed to access store: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System information structure
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct SystemInfo {
|
||||||
|
pub platform: String,
|
||||||
|
pub arch: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the real system platform and architecture
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_system_info() -> SystemInfo {
|
||||||
|
let platform = std::env::consts::OS.to_string();
|
||||||
|
let arch = match std::env::consts::ARCH {
|
||||||
|
"x86_64" => "x64",
|
||||||
|
"aarch64" => "ARM64",
|
||||||
|
"x86" => "x86",
|
||||||
|
"arm" => "ARM",
|
||||||
|
_ => std::env::consts::ARCH,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
SystemInfo { platform, arch }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Tauri version
|
||||||
|
///
|
||||||
|
/// Returns the Tauri framework version as a compile-time constant.
|
||||||
|
/// The version is extracted from Cargo.toml during build time via build.rs.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_tauri_version() -> String {
|
||||||
|
env!("TAURI_VERSION").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the updater modal visibility preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_show_updater_modal(app: tauri::AppHandle) -> bool {
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => match store.get("show_updater_modal") {
|
||||||
|
Some(value) => value.as_bool().unwrap_or(true),
|
||||||
|
None => {
|
||||||
|
log_info!(
|
||||||
|
MODULE,
|
||||||
|
"show_updater_modal not found in store, using default"
|
||||||
|
);
|
||||||
|
default_show_updater_modal()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log_info!(
|
||||||
|
MODULE,
|
||||||
|
"Error loading store, using default show_updater_modal: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
default_show_updater_modal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the updater modal visibility preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_show_updater_modal(show: bool, app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
log_info!(MODULE, "Setting show_updater_modal to: {}", show);
|
||||||
|
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => {
|
||||||
|
store.set("show_updater_modal", show);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Failed to access store: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the developer mode preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_developer_mode(app: tauri::AppHandle) -> bool {
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => match store.get("developer_mode") {
|
||||||
|
Some(value) => value.as_bool().unwrap_or_else(default_developer_mode),
|
||||||
|
None => {
|
||||||
|
log_info!(MODULE, "developer_mode not found in store, using default");
|
||||||
|
default_developer_mode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log_info!(
|
||||||
|
MODULE,
|
||||||
|
"Error loading store, using default developer_mode: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
default_developer_mode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the developer mode preference
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_developer_mode(enabled: bool, app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
log_info!(MODULE, "Setting developer_mode to: {}", enabled);
|
||||||
|
|
||||||
|
// Update the log level based on developer mode
|
||||||
|
crate::logging::set_log_level(enabled);
|
||||||
|
|
||||||
|
match app.store(SETTINGS_STORE) {
|
||||||
|
Ok(store) => {
|
||||||
|
store.set("developer_mode", enabled);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Failed to access store: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read only the last N lines from a file to avoid loading large files into memory
|
||||||
|
///
|
||||||
|
/// This function is optimized for large log files by reading line-by-line
|
||||||
|
/// and only keeping the last N lines in memory.
|
||||||
|
fn read_last_lines(path: &std::path::PathBuf, lines: usize) -> Result<String, String> {
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
|
||||||
|
let file = std::fs::File::open(path).map_err(|e| format!("Failed to open log file: {}", e))?;
|
||||||
|
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let all_lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
|
||||||
|
|
||||||
|
let start = if all_lines.len() > lines {
|
||||||
|
all_lines.len() - lines
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(all_lines[start..].join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the latest log file contents
|
||||||
|
///
|
||||||
|
/// For large log files (>5MB), only the last 10,000 lines are returned
|
||||||
|
/// to avoid memory issues. This prevents the application from consuming
|
||||||
|
/// excessive memory when viewing logs.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_logs() -> Result<String, String> {
|
||||||
|
use crate::logging;
|
||||||
|
use std::fs::Metadata;
|
||||||
|
|
||||||
|
match logging::get_current_log_path() {
|
||||||
|
Some(log_path) => {
|
||||||
|
if !log_path.exists() {
|
||||||
|
return Ok("No log file found".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file metadata to check size
|
||||||
|
let metadata: Metadata = std::fs::metadata(&log_path)
|
||||||
|
.map_err(|e| format!("Failed to read log file metadata: {}", e))?;
|
||||||
|
|
||||||
|
// For large files, use optimized line reader
|
||||||
|
if metadata.len() > MAX_LOG_SIZE {
|
||||||
|
log_info!(
|
||||||
|
MODULE,
|
||||||
|
"Log file is large ({} bytes), reading last {} lines",
|
||||||
|
metadata.len(),
|
||||||
|
MAX_LOG_LINES
|
||||||
|
);
|
||||||
|
return read_last_lines(&log_path, MAX_LOG_LINES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For small files, read entire contents
|
||||||
|
std::fs::read_to_string(&log_path)
|
||||||
|
.map_err(|e| format!("Failed to read log file: {}", e))
|
||||||
|
}
|
||||||
|
None => Ok("No log file available".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user