mirror of
https://github.com/armbian/imager.git
synced 2026-01-06 12:31:28 -08:00
feat: add settings panel with theme, language, and developer options
Implement comprehensive settings modal with: - Theme switching (light/dark/auto) with system preference detection - Language selection for 17 languages with native name sorting - Developer mode with detailed logging and log viewer - About section with app info and external links - Update notification improvements with reduced log spam Technical improvements: - Added ThemeContext with persistent state management - Implemented memory-safe log file reading (5MB limit) - Fixed all ESLint, TypeScript, and Clippy warnings - Added JSDoc documentation for public APIs - Updated README.md and DEVELOPMENT.md with new features
This commit is contained in:
@@ -150,6 +150,13 @@ armbian-imager/
|
||||
│ │ │ ├── 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
|
||||
│ │ ├── AppVersion.tsx # Version display
|
||||
│ │ ├── ErrorDisplay.tsx # Error presentation
|
||||
@@ -158,12 +165,16 @@ armbian-imager/
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ │ ├── useTauri.ts # Tauri IPC wrappers
|
||||
│ │ ├── useVendorLogos.ts # Logo validation
|
||||
│ │ └── useAsyncData.ts # Async data fetching pattern
|
||||
│ │ ├── 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
|
||||
│ ├── locales/ # i18n translations (15 languages)
|
||||
│ │ ├── 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
|
||||
@@ -181,6 +192,7 @@ armbian-imager/
|
||||
│ │ │ ├── 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
|
||||
@@ -245,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
|
||||
@@ -254,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 |
|
||||
|
||||
@@ -51,6 +51,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",
|
||||
|
||||
@@ -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 {}",
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,23 @@
|
||||
//!
|
||||
//! Platform-specific system operations like opening URLs and locale detection.
|
||||
|
||||
use crate::log_info;
|
||||
use crate::{log_debug, log_info};
|
||||
use sys_locale::get_locale;
|
||||
|
||||
const MODULE: &str = "commands::system";
|
||||
|
||||
/// Log a message from the frontend
|
||||
/// Log a message from the frontend (INFO level)
|
||||
#[tauri::command]
|
||||
pub fn log_from_frontend(module: String, message: String) {
|
||||
log_info!(&format!("frontend::{}", module), "{}", message);
|
||||
}
|
||||
|
||||
/// Log a debug message from the frontend (DEBUG level - only shown in developer mode)
|
||||
#[tauri::command]
|
||||
pub fn log_debug_from_frontend(module: String, message: String) {
|
||||
log_debug!(&format!("frontend::{}", module), "{}", message);
|
||||
}
|
||||
|
||||
/// Get the system locale (e.g., "en-US", "it-IT", "de-DE")
|
||||
/// Returns the language code for i18n initialization
|
||||
#[tauri::command]
|
||||
|
||||
@@ -14,7 +14,7 @@ use tokio::sync::Mutex;
|
||||
|
||||
use crate::config;
|
||||
use crate::decompress::decompress_with_rust_xz;
|
||||
use crate::{log_error, log_info, log_warn};
|
||||
use crate::{log_debug, log_error, log_info, log_warn};
|
||||
|
||||
const MODULE: &str = "download";
|
||||
|
||||
@@ -59,12 +59,15 @@ impl Default for DownloadState {
|
||||
|
||||
/// Extract filename from URL
|
||||
fn extract_filename(url: &str) -> Result<&str, String> {
|
||||
log_debug!(MODULE, "Extracting filename from URL: {}", url);
|
||||
let url_path = url.split('?').next().unwrap_or(url);
|
||||
url_path
|
||||
let filename = url_path
|
||||
.split('/')
|
||||
.next_back()
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| "Invalid URL: no filename".to_string())
|
||||
.ok_or_else(|| "Invalid URL: no filename".to_string())?;
|
||||
log_debug!(MODULE, "Extracted filename: {}", filename);
|
||||
Ok(filename)
|
||||
}
|
||||
|
||||
/// Fetch expected SHA256 from URL
|
||||
@@ -108,10 +111,16 @@ async fn fetch_expected_sha(client: &Client, sha_url: &str) -> Result<String, St
|
||||
/// Calculate SHA256 of a file
|
||||
fn calculate_file_sha256(path: &Path, state: &Arc<DownloadState>) -> Result<String, String> {
|
||||
log_info!(MODULE, "Calculating SHA256 of: {}", path.display());
|
||||
log_debug!(
|
||||
MODULE,
|
||||
"File size: {:?} bytes",
|
||||
path.metadata().ok().map(|m| m.len())
|
||||
);
|
||||
|
||||
let mut file = File::open(path).map_err(|e| format!("Failed to open file for SHA: {}", e))?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0u8; 8192];
|
||||
let mut bytes_processed = 0u64;
|
||||
|
||||
loop {
|
||||
// Check for cancellation
|
||||
@@ -127,6 +136,16 @@ fn calculate_file_sha256(path: &Path, state: &Arc<DownloadState>) -> Result<Stri
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
bytes_processed += bytes_read as u64;
|
||||
|
||||
// Log progress every 10MB in debug mode
|
||||
if bytes_processed % (10 * 1024 * 1024) == 0 {
|
||||
log_debug!(
|
||||
MODULE,
|
||||
"SHA256 calculation progress: {} MB",
|
||||
bytes_processed / (1024 * 1024)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let result = hasher.finalize();
|
||||
|
||||
@@ -78,6 +78,11 @@ impl Logger {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the minimum log level at runtime
|
||||
fn set_min_level(&mut self, level: LogLevel) {
|
||||
self.config.min_level = level;
|
||||
}
|
||||
|
||||
fn create_log_file() -> (Option<File>, Option<PathBuf>) {
|
||||
let log_dir = get_log_dir();
|
||||
|
||||
@@ -217,6 +222,30 @@ pub fn error(module: &str, message: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the minimum log level at runtime
|
||||
///
|
||||
/// This function allows dynamically changing the log level, for example
|
||||
/// when developer mode is toggled. When enabled, debug messages are shown.
|
||||
/// When disabled, only info and above are shown.
|
||||
pub fn set_log_level(debug_enabled: bool) {
|
||||
if let Ok(mut logger) = LOGGER.lock() {
|
||||
let new_level = if debug_enabled {
|
||||
LogLevel::Debug
|
||||
} else {
|
||||
LogLevel::Info
|
||||
};
|
||||
logger.set_min_level(new_level);
|
||||
|
||||
// Log the change after setting it (using the new level)
|
||||
let level_str = if debug_enabled { "DEBUG" } else { "INFO" };
|
||||
logger.log(
|
||||
LogLevel::Info,
|
||||
"logging",
|
||||
&format!("Log level changed to {}", level_str),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a message with format arguments (debug level)
|
||||
#[macro_export]
|
||||
macro_rules! log_debug {
|
||||
|
||||
@@ -20,6 +20,7 @@ mod utils;
|
||||
use commands::AppState;
|
||||
#[allow(unused_imports)] // Used by get_webview_window in debug builds
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
use crate::utils::get_cache_dir;
|
||||
|
||||
@@ -97,7 +98,8 @@ fn main() {
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_process::init());
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build());
|
||||
|
||||
// Enable updater only for AppImage on Linux (other formats like .deb don't support it)
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -135,8 +137,22 @@ fn main() {
|
||||
commands::system::open_url,
|
||||
commands::system::get_system_locale,
|
||||
commands::system::log_from_frontend,
|
||||
commands::system::log_debug_from_frontend,
|
||||
commands::update::get_github_release,
|
||||
paste::upload::upload_logs,
|
||||
commands::settings::get_theme,
|
||||
commands::settings::set_theme,
|
||||
commands::settings::get_language,
|
||||
commands::settings::set_language,
|
||||
commands::settings::get_show_motd,
|
||||
commands::settings::set_show_motd,
|
||||
commands::settings::get_show_updater_modal,
|
||||
commands::settings::set_show_updater_modal,
|
||||
commands::settings::get_developer_mode,
|
||||
commands::settings::set_developer_mode,
|
||||
commands::settings::get_logs,
|
||||
commands::settings::get_system_info,
|
||||
commands::settings::get_tauri_version,
|
||||
])
|
||||
.setup(|app| {
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -145,6 +161,31 @@ fn main() {
|
||||
window.open_devtools();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize log level based on developer mode setting
|
||||
match app.store("settings.json") {
|
||||
Ok(store) => {
|
||||
let developer_mode = store
|
||||
.get("developer_mode")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if developer_mode {
|
||||
log_info!("main", "Developer mode enabled, setting log level to DEBUG");
|
||||
logging::set_log_level(true);
|
||||
} else {
|
||||
log_info!("main", "Developer mode disabled, using default log level");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log_warn!(
|
||||
"main",
|
||||
"Failed to access settings store: {}. Using default log level (INFO).",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app; // Suppress unused warning in release
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Header, HomePage } from './components/layout';
|
||||
import { ManufacturerModal, BoardModal, ImageModal, DeviceModal } from './components/modals';
|
||||
import { FlashProgress } from './components/flash';
|
||||
import { AppVersion } from './components/shared';
|
||||
import { SettingsButton } from './components/settings';
|
||||
import { selectCustomImage, detectBoardFromFilename, logInfo } from './hooks/useTauri';
|
||||
import { useDeviceMonitor } from './hooks/useDeviceMonitor';
|
||||
import type { BoardInfo, ImageInfo, BlockDevice, ModalType, SelectionStep, Manufacturer } from './types';
|
||||
@@ -208,7 +208,7 @@ function App() {
|
||||
onSelect={handleDeviceSelect}
|
||||
/>
|
||||
|
||||
<AppVersion />
|
||||
{!isFlashing && <SettingsButton />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
// Layout
|
||||
export * from './layout';
|
||||
|
||||
// Settings
|
||||
export * from './settings';
|
||||
|
||||
// Modals
|
||||
export * from './modals';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ReactNode, useEffect, useCallback, useState, useRef } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { X, ChevronLeft } from 'lucide-react';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -7,9 +7,11 @@ interface ModalProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
searchBar?: ReactNode;
|
||||
showBack?: boolean;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export function Modal({ isOpen, onClose, title, children, searchBar }: ModalProps) {
|
||||
export function Modal({ isOpen, onClose, title, children, searchBar, showBack, onBack }: ModalProps) {
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
const isExitingRef = useRef(false);
|
||||
|
||||
@@ -26,9 +28,13 @@ export function Modal({ isOpen, onClose, title, children, searchBar }: ModalProp
|
||||
|
||||
const handleEscape = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
if (showBack && onBack) {
|
||||
onBack();
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
}, [handleClose]);
|
||||
}, [handleClose, showBack, onBack]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -52,7 +58,14 @@ export function Modal({ isOpen, onClose, title, children, searchBar }: ModalProp
|
||||
<div className={`modal-overlay ${animationClass}`} onClick={handleClose}>
|
||||
<div className={`modal ${animationClass}`} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">{title}</h2>
|
||||
<div className="modal-header-left">
|
||||
{showBack && onBack && (
|
||||
<button className="modal-back" onClick={onBack}>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
)}
|
||||
<h2 className="modal-title">{title}</h2>
|
||||
</div>
|
||||
<button className="modal-close" onClick={handleClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
164
src/components/settings/AboutSection.tsx
Normal file
164
src/components/settings/AboutSection.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import {
|
||||
Cpu,
|
||||
Monitor,
|
||||
Tag,
|
||||
Box,
|
||||
Github,
|
||||
BookOpen,
|
||||
AlertCircle,
|
||||
MessageSquare,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { getTauriVersion, getSystemInfo } from '../../hooks/useTauri';
|
||||
import { LINKS } from '../../config/constants';
|
||||
import armbianLogo from '../../../src-tauri/icons/icon.png';
|
||||
|
||||
/**
|
||||
* About section
|
||||
*
|
||||
* Displays app information including:
|
||||
* - Hero with logo and description
|
||||
* - Technical details (version, platform, arch, Tauri version)
|
||||
* - External links (GitHub, Docs, Issues, Forum)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format platform name for display
|
||||
* Maps platform identifiers to user-friendly names
|
||||
*/
|
||||
function formatPlatformName(platform: string): string {
|
||||
const platformNames: Record<string, string> = {
|
||||
macos: 'macOS',
|
||||
windows: 'Windows',
|
||||
linux: 'Linux'
|
||||
};
|
||||
return platformNames[platform] || platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Info card component
|
||||
*
|
||||
* Displays a piece of information with an icon, label, and value.
|
||||
* Used in the About section to show app details.
|
||||
*/
|
||||
interface InfoCardProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function InfoCard({ icon: Icon, label, value }: InfoCardProps) {
|
||||
return (
|
||||
<div className="info-card">
|
||||
<Icon size={20} className="info-card-icon" />
|
||||
<div className="info-card-content">
|
||||
<div className="info-card-label">{label}</div>
|
||||
<div className="info-card-value">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link button component
|
||||
*/
|
||||
interface LinkButtonProps {
|
||||
icon: React.ComponentType<{ className?: string; size?: number }>;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function LinkButton({ icon: Icon, text, onClick }: LinkButtonProps) {
|
||||
return (
|
||||
<button className="link-button" onClick={onClick}>
|
||||
<Icon className="link-button-icon" size={20} />
|
||||
<span className="link-button-text">{text}</span>
|
||||
<svg className="link-button-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function AboutSection() {
|
||||
const { t } = useTranslation();
|
||||
const [appVersion, setAppVersion] = useState<string>('');
|
||||
const [platform, setPlatform] = useState<string>('');
|
||||
const [arch, setArch] = useState<string>('');
|
||||
const [tauriVersion, setTauriVersion] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadAppInfo = async () => {
|
||||
try {
|
||||
const [version, tauriVer, systemInfo] = await Promise.all([
|
||||
getVersion(),
|
||||
getTauriVersion(),
|
||||
getSystemInfo()
|
||||
]);
|
||||
setAppVersion(version);
|
||||
setTauriVersion(tauriVer);
|
||||
|
||||
// Format platform name for display
|
||||
setPlatform(formatPlatformName(systemInfo.platform));
|
||||
setArch(systemInfo.arch);
|
||||
} catch (error) {
|
||||
console.error('Failed to load app info:', error);
|
||||
}
|
||||
};
|
||||
loadAppInfo();
|
||||
}, []);
|
||||
|
||||
const openLink = (url: string) => {
|
||||
open(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="about-section">
|
||||
{/* Hero Section */}
|
||||
<div className="about-hero">
|
||||
<img src={armbianLogo} alt="Armbian" className="about-logo" />
|
||||
<h2 className="about-title">Armbian Imager</h2>
|
||||
<p className="about-description">{t('settings.appDescription')}</p>
|
||||
</div>
|
||||
|
||||
{/* Technical Info Cards */}
|
||||
<div className="about-info-cards">
|
||||
<InfoCard icon={Tag} label={t('settings.version')} value={`v${appVersion}`} />
|
||||
<InfoCard icon={Monitor} label={t('settings.platform')} value={platform} />
|
||||
<InfoCard icon={Cpu} label={t('settings.arch')} value={arch} />
|
||||
<InfoCard icon={Box} label={t('settings.tauriVersion')} value={`v${tauriVersion}`} />
|
||||
</div>
|
||||
|
||||
{/* Links Section */}
|
||||
<div className="about-links">
|
||||
<h4>{t('settings.links')}</h4>
|
||||
<div className="about-links-grid">
|
||||
<LinkButton
|
||||
icon={Github}
|
||||
text={t('settings.githubRepo')}
|
||||
onClick={() => openLink(LINKS.GITHUB_REPO)}
|
||||
/>
|
||||
<LinkButton
|
||||
icon={BookOpen}
|
||||
text={t('settings.documentation')}
|
||||
onClick={() => openLink(LINKS.DOCS)}
|
||||
/>
|
||||
<LinkButton
|
||||
icon={AlertCircle}
|
||||
text={t('settings.reportIssue')}
|
||||
onClick={() => openLink(`${LINKS.GITHUB_REPO}/issues`)}
|
||||
/>
|
||||
<LinkButton
|
||||
icon={MessageSquare}
|
||||
text={t('settings.community')}
|
||||
onClick={() => openLink(LINKS.FORUM)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
src/components/settings/AdvancedSection.tsx
Normal file
106
src/components/settings/AdvancedSection.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Code, FileText } from 'lucide-react';
|
||||
import { getDeveloperMode, setDeveloperMode } from '../../hooks/useSettings';
|
||||
import { LogsModal } from './LogsModal';
|
||||
|
||||
/**
|
||||
* Advanced settings section for power users
|
||||
*
|
||||
* Contains developer mode toggle and view logs button.
|
||||
*/
|
||||
export function AdvancedSection() {
|
||||
const { t } = useTranslation();
|
||||
const [developerMode, setDeveloperModeState] = useState<boolean>(false);
|
||||
const [logsModalOpen, setLogsModalOpen] = useState<boolean>(false);
|
||||
const [isToggling, setIsToggling] = useState<boolean>(false);
|
||||
|
||||
// Load developer mode preference on mount
|
||||
useEffect(() => {
|
||||
const loadDeveloperModePreference = async () => {
|
||||
try {
|
||||
const value = await getDeveloperMode();
|
||||
setDeveloperModeState(value);
|
||||
} catch (error) {
|
||||
console.error('Failed to load developer mode preference:', error);
|
||||
}
|
||||
};
|
||||
loadDeveloperModePreference();
|
||||
}, []);
|
||||
|
||||
const handleToggleDeveloperMode = async () => {
|
||||
// Prevent concurrent toggles
|
||||
if (isToggling) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousValue = developerMode;
|
||||
const newValue = !developerMode;
|
||||
|
||||
// Optimistic update
|
||||
setDeveloperModeState(newValue);
|
||||
setIsToggling(true);
|
||||
|
||||
try {
|
||||
await setDeveloperMode(newValue);
|
||||
|
||||
// Notify other components that settings changed
|
||||
window.dispatchEvent(new Event('armbian-settings-changed'));
|
||||
} catch (error) {
|
||||
// Rollback on error
|
||||
console.error('Failed to set developer mode preference:', error);
|
||||
setDeveloperModeState(previousValue);
|
||||
} finally {
|
||||
setIsToggling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h3 className="settings-section-title">{t('settings.advancedCategory')}</h3>
|
||||
|
||||
<div className="settings-list">
|
||||
{/* Developer Mode Toggle */}
|
||||
<div className="settings-item">
|
||||
<div className="settings-item-left">
|
||||
<div className="settings-item-icon">
|
||||
<Code />
|
||||
</div>
|
||||
<div className="settings-item-content">
|
||||
<div className="settings-item-label">{t('settings.developerMode')}</div>
|
||||
<div className="settings-item-description">{t('settings.developerModeDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={developerMode}
|
||||
onChange={handleToggleDeveloperMode}
|
||||
disabled={isToggling}
|
||||
/>
|
||||
<span className="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* View Logs Button */}
|
||||
<div className="settings-item settings-item-clickable" onClick={() => setLogsModalOpen(true)}>
|
||||
<div className="settings-item-left">
|
||||
<div className="settings-item-icon">
|
||||
<FileText />
|
||||
</div>
|
||||
<div className="settings-item-content">
|
||||
<div className="settings-item-label">{t('settings.viewLogs')}</div>
|
||||
<div className="settings-item-description">{t('settings.viewLogsDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logs Modal */}
|
||||
<LogsModal isOpen={logsModalOpen} onClose={() => setLogsModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user