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
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
- 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
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -241,6 +269,39 @@ jobs:
|
||||
ditto -c -k --sequesterRsrc --keepParent "$app" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.app.zip"
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
245
.github/workflows/build.yml
vendored
245
.github/workflows/build.yml
vendored
@@ -6,12 +6,8 @@ on:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_linux_x64:
|
||||
description: 'Build Linux x64'
|
||||
type: boolean
|
||||
default: true
|
||||
build_linux_arm64:
|
||||
description: 'Build Linux ARM64'
|
||||
build_linux:
|
||||
description: 'Build Linux (x64 + ARM64)'
|
||||
type: boolean
|
||||
default: true
|
||||
build_macos:
|
||||
@@ -34,7 +30,7 @@ concurrency:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
NODE_VERSION: '20'
|
||||
TAURI_CLI_VERSION: '^2'
|
||||
TAURI_CLI_VERSION: '2.9.6'
|
||||
# Tauri updater signing key (set in GitHub Secrets)
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
@@ -93,11 +89,35 @@ jobs:
|
||||
omitNameDuringUpdate: true
|
||||
replacesArtifacts: false
|
||||
|
||||
build-linux-x64:
|
||||
name: build-linux (x86_64-unknown-linux-gnu)
|
||||
needs: [create-release]
|
||||
if: ${{ github.event_name == 'push' || inputs.build_linux_x64 }}
|
||||
runs-on: ubuntu-24.04
|
||||
build-linux:
|
||||
name: build-linux ${{ matrix.type.name }} (${{ matrix.arch.name }}/${{ matrix.type.distro_id }})
|
||||
needs: [ create-release ]
|
||||
if: ${{ github.event_name == 'push' || inputs.build_linux }} # can't use matrix here; if is evaluated before matrix expansion
|
||||
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:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -125,10 +145,13 @@ jobs:
|
||||
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
|
||||
src-tauri/tauri.conf.json
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install dependencies (apt)
|
||||
if: ${{ matrix.type.deps == 'apt' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
${{ matrix.type.deps_gain_root || '' }} apt-get update
|
||||
${{ matrix.type.deps_gain_root || '' }} apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
@@ -139,10 +162,17 @@ jobs:
|
||||
xdg-utils
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
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
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -151,12 +181,16 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
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
|
||||
with:
|
||||
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
|
||||
run: npm ci
|
||||
@@ -172,7 +206,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- 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
|
||||
uses: ncipollo/release-action@v1
|
||||
@@ -186,106 +220,7 @@ jobs:
|
||||
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-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
|
||||
${{ matrix.type.artifacts }}
|
||||
|
||||
build-macos:
|
||||
name: build-macos (${{ matrix.target }})
|
||||
@@ -306,12 +241,39 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
# Ad-hoc signing: allows app to run after "xattr -cr" on macOS
|
||||
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:
|
||||
- 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
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -397,6 +359,39 @@ jobs:
|
||||
mv "$sig" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.tar.gz.sig"
|
||||
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
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
@@ -593,8 +588,7 @@ jobs:
|
||||
name: Generate latest.json for updater
|
||||
needs:
|
||||
- create-release
|
||||
- build-linux-x64
|
||||
- build-linux-arm64
|
||||
- build-linux
|
||||
- build-macos
|
||||
- build-windows-x64
|
||||
- build-windows-arm64
|
||||
@@ -710,8 +704,7 @@ jobs:
|
||||
name: Publish release (draft -> false) + cleanup
|
||||
needs:
|
||||
- create-release
|
||||
- build-linux-x64
|
||||
- build-linux-arm64
|
||||
- build-linux
|
||||
- build-macos
|
||||
- build-windows-x64
|
||||
- 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' }}
|
||||
RETRY_FAILED: ${{ vars.RETRY_FAILED || 'false' }}
|
||||
run: |
|
||||
node scripts/sync-locales.js
|
||||
node scripts/locales/sync-locales.js
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check for changes
|
||||
|
||||
140
DEVELOPMENT.md
140
DEVELOPMENT.md
@@ -104,23 +104,23 @@ npm run tauri:dev
|
||||
### Single Platform
|
||||
|
||||
```bash
|
||||
./scripts/build-macos.sh # macOS (Intel + ARM)
|
||||
./scripts/build-linux.sh # Linux (x64 + ARM)
|
||||
npm run tauri:build # Windows
|
||||
./scripts/build/build-macos.sh # macOS (Intel + ARM)
|
||||
./scripts/build/build-linux.sh # Linux (x64 + ARM)
|
||||
npm run tauri:build # Windows
|
||||
```
|
||||
|
||||
### All Platforms
|
||||
|
||||
```bash
|
||||
./scripts/build-all.sh
|
||||
./scripts/build/build-all.sh
|
||||
```
|
||||
|
||||
### Build Options
|
||||
|
||||
```bash
|
||||
./scripts/build-macos.sh --clean # Clean build
|
||||
./scripts/build-macos.sh --dev # Debug symbols
|
||||
./scripts/build-macos.sh --clean --dev # Both
|
||||
./scripts/build/build-macos.sh --clean # Clean build
|
||||
./scripts/build/build-macos.sh --dev # Debug symbols
|
||||
./scripts/build/build-macos.sh --clean --dev # Both
|
||||
```
|
||||
|
||||
### Output
|
||||
@@ -132,46 +132,118 @@ npm run tauri:build # Windows
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Directory Overview
|
||||
|
||||
```
|
||||
armbian-imager/
|
||||
├── src/ # React Frontend
|
||||
│ ├── components/ # UI Components
|
||||
│ │ ├── flash/ # Flash progress
|
||||
│ │ ├── layout/ # Header, HomePage
|
||||
│ │ ├── modals/ # Board, Image, Device, Manufacturer
|
||||
│ │ ├── flash/ # Flash progress components
|
||||
│ │ │ ├── FlashActions.tsx # Action buttons (cancel, retry)
|
||||
│ │ │ ├── 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
|
||||
│ ├── hooks/ # React Hooks
|
||||
│ ├── config/ # Badges, manufacturers, OS info
|
||||
│ ├── locales/ # i18n (15 languages)
|
||||
│ │ ├── AppVersion.tsx # Version display
|
||||
│ │ ├── ErrorDisplay.tsx # Error presentation
|
||||
│ │ ├── 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
|
||||
│ │ ├── theme.css # Theme variables (light/dark)
|
||||
│ │ ├── components.css # Component styles
|
||||
│ │ └── responsive.css # Responsive design
|
||||
│ ├── types/ # TypeScript interfaces
|
||||
│ ├── utils/ # Utilities
|
||||
│ └── assets/ # Images, logos, icons
|
||||
│ ├── utils/ # Utility functions
|
||||
│ ├── assets/ # Static assets
|
||||
│ ├── App.tsx # Main app component
|
||||
│ └── main.tsx # React entry point
|
||||
│
|
||||
├── src-tauri/ # Rust Backend
|
||||
│ ├── src/
|
||||
│ │ ├── commands/ # Tauri IPC handlers
|
||||
│ │ ├── config/ # Configuration
|
||||
│ │ ├── devices/ # Device detection
|
||||
│ │ ├── flash/ # Platform flash implementations
|
||||
│ │ ├── images/ # Image management
|
||||
│ │ ├── commands/ # Tauri IPC command handlers
|
||||
│ │ │ ├── board_queries.rs # Board/image API queries
|
||||
│ │ │ ├── operations.rs # Download & flash operations
|
||||
│ │ │ ├── custom_image.rs # Custom image handling
|
||||
│ │ │ ├── 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
|
||||
│ │ ├── paste/ # Log upload
|
||||
│ │ ├── utils/ # Utilities
|
||||
│ │ ├── download.rs # HTTP downloads
|
||||
│ │ └── decompress.rs # XZ, GZ, ZSTD
|
||||
│ ├── icons/ # App icons
|
||||
│ │ ├── paste/ # Log upload service
|
||||
│ │ ├── utils/ # Rust utilities
|
||||
│ │ ├── download.rs # HTTP streaming downloads
|
||||
│ │ ├── decompress.rs # Archive extraction
|
||||
│ │ └── main.rs # Rust entry point
|
||||
│ ├── icons/ # App icons (all platforms)
|
||||
│ ├── Cargo.toml # Rust dependencies
|
||||
│ ├── tauri.conf.json # Tauri configuration
|
||||
│ └── target/ # Compiled binaries (gitignored)
|
||||
│
|
||||
├── scripts/ # Build and setup
|
||||
│ ├── build-*.sh # Platform build scripts
|
||||
├── scripts/ # Build and utility 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
|
||||
│ ├── install.sh
|
||||
│ ├── install-linux.sh
|
||||
│ ├── install-macos.sh
|
||||
│ └── install-windows.ps1
|
||||
│ ├── install.sh # Universal installer
|
||||
│ ├── install-linux.sh # Linux dependencies
|
||||
│ ├── install-macos.sh # macOS dependencies
|
||||
│ └── 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 |
|
||||
| TypeScript | Type Safety |
|
||||
| Vite | Build Tool & Dev Server |
|
||||
| i18next | i18n (15 languages) |
|
||||
| React Context API | State Management (Theme) |
|
||||
| i18next | i18n (17 languages) |
|
||||
| Lucide | Icons |
|
||||
|
||||
### Backend
|
||||
@@ -194,6 +267,7 @@ armbian-imager/
|
||||
|------------|---------|
|
||||
| Rust | Systems Programming |
|
||||
| Tauri 2 | Desktop Framework |
|
||||
| Tauri Store Plugin | Persistent Settings |
|
||||
| Tokio | Async Runtime |
|
||||
| Serde | Serialization |
|
||||
| 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 |
|
||||
| <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
|
||||
|
||||
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
|
||||
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 | 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-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",
|
||||
"ansi-to-html": "^0.7.0",
|
||||
"i18next": "^25.7.2",
|
||||
"lucide-react": "^0.560.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.2",
|
||||
"react-dom": "^19.2.2",
|
||||
"react-i18next": "^16.5.0"
|
||||
"react-i18next": "^16.5.0",
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -1602,6 +1605,15 @@
|
||||
"@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": {
|
||||
"version": "2.9.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -2310,6 +2337,15 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"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": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
@@ -2650,6 +2686,29 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -2710,6 +2769,12 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -2921,6 +2986,18 @@
|
||||
"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": {
|
||||
"version": "4.5.4",
|
||||
"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",
|
||||
"integrity": "sha512-fhyD2BLrew6qYf4NNtHff1rLXvzR25rq49p+FeqByOazc6TcSi2n8EYulo5C1PbH+1uBW++5S1SG7FcUU6mlDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -3490,6 +3568,24 @@
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -3548,6 +3644,15 @@
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||
|
||||
@@ -31,16 +31,19 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@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-shell": "^2.3.3",
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@tauri-apps/plugin-updater": "^2",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"ansi-to-html": "^0.7.0",
|
||||
"i18next": "^25.7.2",
|
||||
"lucide-react": "^0.560.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.2",
|
||||
"react-dom": "^19.2.2",
|
||||
"react-i18next": "^16.5.0"
|
||||
"react-i18next": "^16.5.0",
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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-updater = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-store = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@@ -11,3 +14,26 @@ fn main() {
|
||||
#[cfg(not(windows))]
|
||||
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",
|
||||
"dialog: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,
|
||||
ImageInfo,
|
||||
};
|
||||
use crate::{log_error, log_info};
|
||||
use crate::{log_debug, log_error, log_info};
|
||||
|
||||
use super::state::AppState;
|
||||
|
||||
@@ -59,6 +59,13 @@ pub async fn get_images_for_board(
|
||||
board_slug,
|
||||
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 = json_guard.as_ref().ok_or_else(|| {
|
||||
@@ -71,6 +78,7 @@ pub async fn get_images_for_board(
|
||||
})?;
|
||||
|
||||
let images = extract_images(json);
|
||||
log_debug!("board_queries", "Total images available: {}", images.len());
|
||||
let filtered = filter_images_for_board(
|
||||
&images,
|
||||
&board_slug,
|
||||
@@ -79,6 +87,12 @@ pub async fn get_images_for_board(
|
||||
variant_filter.as_deref(),
|
||||
stable_only,
|
||||
);
|
||||
log_debug!(
|
||||
"board_queries",
|
||||
"Filtered down to {} images for board {}",
|
||||
filtered.len(),
|
||||
board_slug
|
||||
);
|
||||
log_info!(
|
||||
"board_queries",
|
||||
"Found {} images for board {}",
|
||||
|
||||
@@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tauri::State;
|
||||
|
||||
use crate::config;
|
||||
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 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 progress;
|
||||
pub mod scraping;
|
||||
pub mod settings;
|
||||
mod state;
|
||||
pub mod system;
|
||||
pub mod update;
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
//! Handles download and flash operations.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tauri::State;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
use crate::config;
|
||||
use crate::download::download_image as do_download;
|
||||
use crate::flash::{flash_image as do_flash, request_authorization};
|
||||
use crate::utils::get_cache_dir;
|
||||
use crate::{log_error, log_info};
|
||||
use crate::{log_debug, log_error, log_info};
|
||||
|
||||
use super::state::AppState;
|
||||
|
||||
@@ -57,10 +57,16 @@ pub async fn download_image(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
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 {
|
||||
log_info!("operations", "SHA URL: {}", sha);
|
||||
} else {
|
||||
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");
|
||||
|
||||
@@ -92,6 +98,7 @@ pub async fn flash_image(
|
||||
device_path: String,
|
||||
verify: bool,
|
||||
state: State<'_, AppState>,
|
||||
_app: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
log_info!(
|
||||
"operations",
|
||||
@@ -100,15 +107,32 @@ pub async fn flash_image(
|
||||
device_path,
|
||||
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 flash_state = state.flash_state.clone();
|
||||
|
||||
let result = do_flash(&path, &device_path, flash_state, verify).await;
|
||||
if let Err(ref e) = result {
|
||||
log_error!("operations", "Flash failed: {}", e);
|
||||
} else {
|
||||
log_info!("operations", "Flash completed successfully");
|
||||
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
log_info!("operations", "Flash completed successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log_error!("operations", "Flash failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
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