mirror of
https://github.com/armbian/imager.git
synced 2026-01-06 12:31:28 -08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009a36fac3 | ||
|
|
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
|
||||
|
||||
44
.github/workflows/pages-test.yml
vendored
Normal file
44
.github/workflows/pages-test.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Deploy test branch to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ test ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: test
|
||||
|
||||
- name: Fetch Latest Release
|
||||
id: fetch_release
|
||||
run: |
|
||||
# Fetch the release JSON from the Armbian repo
|
||||
curl -s https://api.github.com/repos/armbian/imager/releases/latest > latest.json
|
||||
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: .
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
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 |
|
||||
|
||||
436
index.html
436
index.html
@@ -1,13 +1,427 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/armbian-icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Armbian Imager</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Armbian Imager - Download</title>
|
||||
<style>
|
||||
/* --- CSS Reset & Base Variables --- */
|
||||
:root {
|
||||
--primary: #f98d05; /* Armbian Orange */
|
||||
--primary-hover: #d67d04;
|
||||
--dark-bg: #1a1a1a;
|
||||
--card-bg: #ffffff;
|
||||
--text-main: #333333;
|
||||
--text-light: #666666;
|
||||
--border-radius: 8px;
|
||||
--font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* --- Header --- */
|
||||
header {
|
||||
padding: 20px 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.logo { font-weight: 700; font-size: 24px; color: white; text-decoration: none; display: flex; align-items: center; gap: 10px; }
|
||||
.logo span { color: var(--primary); }
|
||||
.nav-links a { color: #ccc; text-decoration: none; margin-left: 20px; font-size: 14px; transition: 0.2s; }
|
||||
.nav-links a:hover { color: white; }
|
||||
|
||||
/* --- Hero Section --- */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #2b2b2b 0%, #1a1a1a 100%);
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
.hero h1 { font-size: 3rem; color: white; margin-bottom: 15px; font-weight: 700; }
|
||||
.hero p { font-size: 1.2rem; color: #aaa; max-width: 600px; margin: 0 auto; }
|
||||
|
||||
/* --- Main Download Card --- */
|
||||
.download-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 1. Auto Download Area */
|
||||
.auto-download {
|
||||
padding: 40px;
|
||||
background: #f9f9f9;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.auto-label { text-transform: uppercase; letter-spacing: 1px; font-size: 12px; color: var(--text-light); margin-bottom: 15px; display: block; }
|
||||
|
||||
.main-btn {
|
||||
display: inline-block;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 18px 40px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
border-radius: 50px; /* Pill shape */
|
||||
box-shadow: 0 4px 15px rgba(249, 141, 5, 0.4);
|
||||
transition: transform 0.2s, background-color 0.2s;
|
||||
cursor: pointer;
|
||||
pointer-events: none; /* Disabled until loaded */
|
||||
opacity: 0.8;
|
||||
min-width: 300px;
|
||||
}
|
||||
.main-btn:hover { background-color: var(--primary-hover); transform: translateY(-2px); }
|
||||
.main-btn.ready { pointer-events: auto; opacity: 1; }
|
||||
.main-btn .sub { display: block; font-size: 14px; font-weight: 400; opacity: 0.9; margin-top: 4px; }
|
||||
|
||||
.detected-info { margin-top: 15px; font-size: 14px; color: var(--text-light); }
|
||||
|
||||
/* 2. Manual Tabs Area */
|
||||
.manual-download { padding: 30px; min-height: 200px; }
|
||||
|
||||
.tabs-header { display: flex; justify-content: center; border-bottom: 2px solid #eee; margin-bottom: 20px; }
|
||||
.tab-btn {
|
||||
background: none; border: none; padding: 10px 20px;
|
||||
font-size: 16px; font-weight: 600; color: var(--text-light);
|
||||
cursor: pointer; border-bottom: 3px solid transparent; transition: 0.2s;
|
||||
}
|
||||
.tab-btn:hover { color: var(--primary); }
|
||||
.tab-btn.active { border-bottom-color: var(--primary); color: var(--text-main); }
|
||||
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; animation: fadeIn 0.3s; }
|
||||
|
||||
.file-list { list-style: none; }
|
||||
.file-item {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 12px; border-bottom: 1px solid #f0f0f0; transition: 0.2s;
|
||||
}
|
||||
.file-item:hover { background-color: #fafafa; }
|
||||
.file-name { font-weight: 500; color: #333; display: flex; align-items: center; gap: 10px; }
|
||||
.file-meta { font-size: 13px; color: #888; text-align: right; }
|
||||
.download-link {
|
||||
color: var(--primary); text-decoration: none; font-weight: 600; margin-left: 15px;
|
||||
border: 1px solid var(--primary); padding: 5px 12px; border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.download-link:hover { background-color: var(--primary); color: white; }
|
||||
|
||||
.empty-msg { text-align: center; color: #999; font-style: italic; padding: 20px; }
|
||||
|
||||
/* Loading Spinner */
|
||||
.spinner { border: 3px solid #f3f3f3; border-top: 3px solid var(--primary); border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-bottom: 5px; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
/* Footer */
|
||||
footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a href="#" class="logo">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
|
||||
<span>Armbian</span> Imager
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="#">Docs</a>
|
||||
<a href="#">Source</a>
|
||||
<a href="#">Community</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="hero">
|
||||
<h1>Flash OS images easily</h1>
|
||||
<p>The ultimate tool for creating bootable USB drives and SD cards for Armbian.</p>
|
||||
</div>
|
||||
|
||||
<div class="download-section">
|
||||
<div class="card">
|
||||
|
||||
<!-- Automatic Section -->
|
||||
<div class="auto-download">
|
||||
<span class="auto-label">Recommended for your system</span>
|
||||
<br>
|
||||
<a id="main-btn" href="#" class="main-btn">
|
||||
<span class="spinner"></span> Detecting...
|
||||
</a>
|
||||
<div id="detected-info" class="detected-info"></div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Section -->
|
||||
<div class="manual-download">
|
||||
<div class="tabs-header">
|
||||
<button class="tab-btn active" onclick="switchTab('windows')">Windows</button>
|
||||
<button class="tab-btn" onclick="switchTab('mac')">macOS</button>
|
||||
<button class="tab-btn" onclick="switchTab('linux')">Linux</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-windows" class="tab-content active">
|
||||
<ul class="file-list" id="list-windows"></ul>
|
||||
</div>
|
||||
<div id="tab-mac" class="tab-content">
|
||||
<ul class="file-list" id="list-mac"></ul>
|
||||
</div>
|
||||
<div id="tab-linux" class="tab-content">
|
||||
<ul class="file-list" id="list-linux"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
© 2024 Armbian. Open Source software.
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
//const REPO_OWNER = "armbian";
|
||||
//const REPO_NAME = "imager";
|
||||
//const API_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
|
||||
// This points to the file generated by the GitHub Action
|
||||
const API_URL = `./latest.json`;
|
||||
|
||||
const mainBtn = document.getElementById('main-btn');
|
||||
const infoDiv = document.getElementById('detected-info');
|
||||
|
||||
// --- 1. INIT ---
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
// Fetch Data
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) throw new Error("API Limit Reached");
|
||||
const release = await response.json();
|
||||
|
||||
// Detect Client
|
||||
const { os, arch } = await getClientInfo();
|
||||
|
||||
// Render Auto Button
|
||||
renderAutoButton(release, os, arch);
|
||||
|
||||
// Render Manual Lists (Updated Logic)
|
||||
renderManualLists(release);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
mainBtn.innerHTML = "Manual Download";
|
||||
mainBtn.href = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
|
||||
mainBtn.classList.add('ready');
|
||||
infoDiv.innerText = "Could not auto-detect. Please select manually below.";
|
||||
}
|
||||
});
|
||||
|
||||
// --- 2. RENDER FUNCTIONS ---
|
||||
|
||||
function renderAutoButton(release, os, arch) {
|
||||
const asset = findBestAsset(release.assets, os, arch);
|
||||
const tagName = release.tag_name;
|
||||
|
||||
if (asset) {
|
||||
mainBtn.href = asset.browser_download_url;
|
||||
mainBtn.setAttribute('download', asset.name);
|
||||
|
||||
const osDisplay = os.charAt(0).toUpperCase() + os.slice(1);
|
||||
|
||||
mainBtn.innerHTML = `Download for ${osDisplay}`;
|
||||
const sub = document.createElement('span');
|
||||
sub.className = 'sub';
|
||||
sub.innerText = `${tagName} • ${formatBytes(asset.size)}`;
|
||||
mainBtn.appendChild(sub);
|
||||
|
||||
mainBtn.classList.add('ready');
|
||||
infoDiv.innerText = `Detected Architecture: ${arch.toUpperCase()}`;
|
||||
} else {
|
||||
mainBtn.innerHTML = "Download Latest";
|
||||
mainBtn.href = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
|
||||
mainBtn.classList.add('ready');
|
||||
infoDiv.innerText = `We couldn't find a specific file for ${os} (${arch}). Check the tabs below.`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderManualLists(release) {
|
||||
const assets = release.assets;
|
||||
|
||||
// Helper to add "No files found" message if empty
|
||||
const checkEmpty = (id) => {
|
||||
const el = document.getElementById(id);
|
||||
if(el.children.length === 0) {
|
||||
el.innerHTML = `<li class="empty-msg">No files found for this OS in this release.</li>`;
|
||||
}
|
||||
};
|
||||
|
||||
// Clear lists
|
||||
document.getElementById('list-windows').innerHTML = '';
|
||||
document.getElementById('list-mac').innerHTML = '';
|
||||
document.getElementById('list-linux').innerHTML = '';
|
||||
|
||||
let processedCount = 0;
|
||||
|
||||
assets.forEach(asset => {
|
||||
const name = asset.name;
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
// Filter out non-binary files
|
||||
if (name.endsWith('.txt') || name.endsWith('.json') || name.endsWith('.sha256') || name.endsWith('.md5')) return;
|
||||
|
||||
let targetListId = null;
|
||||
|
||||
// --- IMPROVED FILTERING LOGIC ---
|
||||
|
||||
// 1. Windows Check
|
||||
if (lowerName.includes('win') || name.endsWith('.exe')) {
|
||||
targetListId = 'list-windows';
|
||||
}
|
||||
|
||||
// 2. macOS Check
|
||||
else if (lowerName.includes('mac') || lowerName.includes('darwin') || name.endsWith('.dmg')) {
|
||||
targetListId = 'list-mac';
|
||||
}
|
||||
|
||||
// 3. Linux Check (Specific formats)
|
||||
else if (name.endsWith('.AppImage') || name.endsWith('.deb') || name.endsWith('.rpm')) {
|
||||
targetListId = 'list-linux';
|
||||
}
|
||||
|
||||
// 4. Ambiguous Files (.zip, .tar, etc.)
|
||||
else if (name.endsWith('.zip')) {
|
||||
// For zips, we rely heavily on naming
|
||||
if (lowerName.includes('win')) targetListId = 'list-windows';
|
||||
else if (lowerName.includes('mac')) targetListId = 'list-mac';
|
||||
else if (lowerName.includes('linux') || lowerName.includes('arm64') || lowerName.includes('amd64')) {
|
||||
// Assuming zips with arch keywords are Linux unless proven otherwise
|
||||
targetListId = 'list-linux';
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a target, render it
|
||||
if (targetListId) {
|
||||
processedCount++;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'file-item';
|
||||
li.innerHTML = `
|
||||
<div class="file-name">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
${name}
|
||||
</div>
|
||||
<div class="file-meta">
|
||||
${formatBytes(asset.size)}
|
||||
<a href="${asset.browser_download_url}" class="download-link">Download</a>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById(targetListId).appendChild(li);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Processed ${processedCount} assets out of ${assets.length} total.`);
|
||||
|
||||
// Final check for empty tabs
|
||||
checkEmpty('list-windows');
|
||||
checkEmpty('list-mac');
|
||||
checkEmpty('list-linux');
|
||||
}
|
||||
|
||||
// --- 3. LOGIC HELPERS ---
|
||||
|
||||
function switchTab(os) {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
document.getElementById(`tab-${os}`).classList.add('active');
|
||||
}
|
||||
|
||||
async function getClientInfo() {
|
||||
if (navigator.userAgentData && navigator.userAgentData.getHighEntropyValues) {
|
||||
const uaData = await navigator.userAgentData.getHighEntropyValues(['architecture', 'platform']);
|
||||
return {
|
||||
os: normalizeOS(uaData.platform),
|
||||
arch: normalizeArch(uaData.architecture)
|
||||
};
|
||||
}
|
||||
const ua = navigator.userAgent;
|
||||
let os = "unknown", arch = "unknown";
|
||||
if (ua.indexOf("Win") !== -1) os = "windows";
|
||||
else if (ua.indexOf("Mac") !== -1) os = "mac";
|
||||
else if (ua.indexOf("Linux") !== -1) os = "linux";
|
||||
|
||||
if (ua.indexOf("arm") !== -1 || ua.indexOf("aarch64") !== -1) arch = "arm64";
|
||||
else if (ua.indexOf("x86_64") !== -1 || ua.indexOf("x64") !== -1) arch = "x64";
|
||||
else if (os === "mac") arch = "arm64";
|
||||
else arch = "x64";
|
||||
return { os, arch };
|
||||
}
|
||||
|
||||
function normalizeOS(platform) {
|
||||
if (platform === "Windows") return "windows";
|
||||
if (platform === "macOS") return "mac";
|
||||
if (platform === "Linux") return "linux";
|
||||
return platform.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeArch(arch) {
|
||||
if (arch === "arm" || arch === "arm64" || arch === "aarch64") return "arm64";
|
||||
if (arch === "x86" || arch === "x86_64" || arch === "x64") return "x64";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function findBestAsset(assets, os, arch) {
|
||||
const compatible = assets.filter(a => {
|
||||
const name = a.name.toLowerCase();
|
||||
if (os === 'windows') {
|
||||
if (arch === 'x64') return name.includes('win') && name.includes('x64');
|
||||
}
|
||||
if (os === 'mac') {
|
||||
if (arch === 'arm64') return name.includes('macos') && (name.includes('arm64') || name.includes('aarch64'));
|
||||
if (arch === 'x64') return name.includes('macos') && name.includes('x64');
|
||||
}
|
||||
if (os === 'linux') {
|
||||
if (name.includes('win') || name.includes('macos') || name.includes('darwin')) return false;
|
||||
if (arch === 'x64') return name.includes('amd64') || name.includes('x86_64') || name.includes('x64');
|
||||
if (arch === 'arm64') return name.includes('arm64') || name.includes('aarch64');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// Prefer AppImage
|
||||
compatible.sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase();
|
||||
const nameB = b.name.toLowerCase();
|
||||
if (nameA.includes('appimage') && !nameB.includes('appimage')) return -1;
|
||||
return 0;
|
||||
});
|
||||
return compatible[0] || null;
|
||||
}
|
||||
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user