欢迎页面支持多语言
This commit is contained in:
83
src-tauri/src/app_error.rs
Normal file
83
src-tauri/src/app_error.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::db::error::DbError;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AppErrorCode {
|
||||
Unknown,
|
||||
InvalidInput,
|
||||
NotFound,
|
||||
AlreadyExists,
|
||||
PermissionDenied,
|
||||
DependencyMissing,
|
||||
NetworkError,
|
||||
AuthenticationFailed,
|
||||
DatabaseError,
|
||||
IoError,
|
||||
ExternalCommandFailed,
|
||||
WindowOperationFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, thiserror::Error)]
|
||||
#[error("{message}")]
|
||||
pub struct AppCommandError {
|
||||
pub code: AppErrorCode,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
impl AppCommandError {
|
||||
pub fn new(code: AppErrorCode, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
detail: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
|
||||
self.detail = Some(detail.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn db(err: DbError) -> Self {
|
||||
Self::new(AppErrorCode::DatabaseError, "Database operation failed")
|
||||
.with_detail(err.to_string())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn io(err: std::io::Error) -> Self {
|
||||
let code = match err.kind() {
|
||||
std::io::ErrorKind::NotFound => AppErrorCode::NotFound,
|
||||
std::io::ErrorKind::PermissionDenied => AppErrorCode::PermissionDenied,
|
||||
std::io::ErrorKind::AlreadyExists => AppErrorCode::AlreadyExists,
|
||||
_ => AppErrorCode::IoError,
|
||||
};
|
||||
|
||||
let message = match code {
|
||||
AppErrorCode::NotFound => "Resource not found",
|
||||
AppErrorCode::PermissionDenied => "Permission denied",
|
||||
AppErrorCode::AlreadyExists => "Resource already exists",
|
||||
_ => "I/O operation failed",
|
||||
};
|
||||
|
||||
Self::new(code, message).with_detail(err.to_string())
|
||||
}
|
||||
|
||||
pub fn window(message: impl Into<String>, detail: impl Into<String>) -> Self {
|
||||
Self::new(AppErrorCode::WindowOperationFailed, message).with_detail(detail)
|
||||
}
|
||||
|
||||
pub fn external_command(message: impl Into<String>, detail: impl Into<String>) -> Self {
|
||||
Self::new(AppErrorCode::ExternalCommandFailed, message).with_detail(detail)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DbError> for AppCommandError {
|
||||
fn from(value: DbError) -> Self {
|
||||
Self::db(value)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use tauri::Emitter;
|
||||
use tokio::sync::Semaphore;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::app_error::{AppCommandError, AppErrorCode};
|
||||
use crate::db::error::DbError;
|
||||
use crate::db::service::folder_service;
|
||||
use crate::db::AppDatabase;
|
||||
@@ -278,8 +279,10 @@ pub async fn get_folder(
|
||||
#[tauri::command]
|
||||
pub async fn load_folder_history(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
) -> Result<Vec<FolderHistoryEntry>, DbError> {
|
||||
folder_service::list_folders(&db.conn).await
|
||||
) -> Result<Vec<FolderHistoryEntry>, AppCommandError> {
|
||||
folder_service::list_folders(&db.conn)
|
||||
.await
|
||||
.map_err(AppCommandError::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -318,8 +321,10 @@ pub async fn set_folder_parent_branch(
|
||||
pub async fn remove_folder_from_history(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
path: String,
|
||||
) -> Result<(), DbError> {
|
||||
folder_service::remove_folder(&db.conn, &path).await
|
||||
) -> Result<(), AppCommandError> {
|
||||
folder_service::remove_folder(&db.conn, &path)
|
||||
.await
|
||||
.map_err(AppCommandError::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -337,26 +342,90 @@ pub async fn create_folder_directory(path: String) -> Result<(), String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clone_repository(url: String, target_dir: String) -> Result<(), String> {
|
||||
pub async fn clone_repository(url: String, target_dir: String) -> Result<(), AppCommandError> {
|
||||
if url.trim().is_empty() || target_dir.trim().is_empty() {
|
||||
return Err(AppCommandError::new(
|
||||
AppErrorCode::InvalidInput,
|
||||
"Repository URL and target directory are required",
|
||||
));
|
||||
}
|
||||
|
||||
let output = crate::process::tokio_command("git")
|
||||
.args(["clone", &url, &target_dir])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
"Git is not installed. Please install Git first: https://git-scm.com".to_string()
|
||||
AppCommandError::new(
|
||||
AppErrorCode::DependencyMissing,
|
||||
"Git is not installed. Please install Git first.",
|
||||
)
|
||||
.with_detail("https://git-scm.com")
|
||||
} else {
|
||||
format!("Failed to run git: {}", e)
|
||||
AppCommandError::external_command("Failed to run git clone", e.to_string())
|
||||
}
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("Clone failed: {}", stderr.trim()));
|
||||
return Err(classify_git_clone_error(stderr.trim()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn classify_git_clone_error(stderr: &str) -> AppCommandError {
|
||||
let normalized = stderr.to_lowercase();
|
||||
|
||||
if normalized.contains("already exists and is not an empty directory") {
|
||||
return AppCommandError::new(
|
||||
AppErrorCode::AlreadyExists,
|
||||
"Target directory already exists and is not empty",
|
||||
)
|
||||
.with_detail(stderr.to_string());
|
||||
}
|
||||
|
||||
if normalized.contains("repository not found") {
|
||||
return AppCommandError::new(
|
||||
AppErrorCode::NotFound,
|
||||
"Repository not found. Check URL and access permissions.",
|
||||
)
|
||||
.with_detail(stderr.to_string());
|
||||
}
|
||||
|
||||
if normalized.contains("could not resolve host")
|
||||
|| normalized.contains("network is unreachable")
|
||||
|| normalized.contains("connection timed out")
|
||||
|| normalized.contains("failed to connect")
|
||||
{
|
||||
return AppCommandError::new(
|
||||
AppErrorCode::NetworkError,
|
||||
"Network is unavailable while cloning repository",
|
||||
)
|
||||
.with_detail(stderr.to_string());
|
||||
}
|
||||
|
||||
if normalized.contains("authentication failed")
|
||||
|| normalized.contains("could not read username")
|
||||
|| normalized.contains("permission denied (publickey)")
|
||||
{
|
||||
return AppCommandError::new(
|
||||
AppErrorCode::AuthenticationFailed,
|
||||
"Authentication failed while cloning repository",
|
||||
)
|
||||
.with_detail(stderr.to_string());
|
||||
}
|
||||
|
||||
if normalized.contains("permission denied") {
|
||||
return AppCommandError::new(
|
||||
AppErrorCode::PermissionDenied,
|
||||
"Permission denied while cloning repository",
|
||||
)
|
||||
.with_detail(stderr.to_string());
|
||||
}
|
||||
|
||||
AppCommandError::external_command("Git clone failed", stderr.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_git_branch(path: String) -> Result<Option<String>, String> {
|
||||
let output = crate::process::tokio_command("git")
|
||||
@@ -1094,9 +1163,7 @@ pub async fn git_delete_branch(
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
const WATCH_IGNORED_DIRS: &[&str] = &[
|
||||
"__pycache__",
|
||||
];
|
||||
const WATCH_IGNORED_DIRS: &[&str] = &["__pycache__"];
|
||||
const FILE_TREE_IGNORED_DIRS: &[&str] = &[".git", "__pycache__"];
|
||||
|
||||
const FILE_PREVIEW_DEFAULT_MAX_BYTES: usize = 200_000;
|
||||
@@ -1239,19 +1306,16 @@ fn is_allowed_git_watch_path(relative: &Path) -> bool {
|
||||
|
||||
let second_name = second.to_string_lossy();
|
||||
match second_name.as_ref() {
|
||||
"HEAD"
|
||||
| "index"
|
||||
| "packed-refs"
|
||||
| "FETCH_HEAD"
|
||||
| "ORIG_HEAD"
|
||||
| "MERGE_HEAD"
|
||||
| "CHERRY_PICK_HEAD"
|
||||
| "REVERT_HEAD" => true,
|
||||
"HEAD" | "index" | "packed-refs" | "FETCH_HEAD" | "ORIG_HEAD" | "MERGE_HEAD"
|
||||
| "CHERRY_PICK_HEAD" | "REVERT_HEAD" => true,
|
||||
"refs" => {
|
||||
let Some(Component::Normal(scope)) = components.next() else {
|
||||
return true;
|
||||
};
|
||||
matches!(scope.to_string_lossy().as_ref(), "heads" | "remotes" | "stash")
|
||||
matches!(
|
||||
scope.to_string_lossy().as_ref(),
|
||||
"heads" | "remotes" | "stash"
|
||||
)
|
||||
}
|
||||
"rebase-merge" | "rebase-apply" => true,
|
||||
_ => false,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Mutex;
|
||||
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
use crate::app_error::AppCommandError;
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::FolderHistoryEntry;
|
||||
|
||||
@@ -196,11 +197,11 @@ pub async fn open_folder_window(
|
||||
app: AppHandle,
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
path: String,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), AppCommandError> {
|
||||
// Add to history via DB
|
||||
let entry = crate::db::service::folder_service::add_folder(&db.conn, &path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
// Create folder window with unique label
|
||||
let label = format!("folder-{}", uuid::Uuid::new_v4());
|
||||
@@ -211,12 +212,14 @@ pub async fn open_folder_window(
|
||||
.min_inner_size(900.0, 600.0);
|
||||
let folder_window = apply_platform_window_style(builder)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
.map_err(|e| AppCommandError::window("Failed to open folder window", e.to_string()))?;
|
||||
ensure_windows_undecorated(&folder_window);
|
||||
|
||||
// Close welcome window
|
||||
if let Some(w) = app.get_webview_window("welcome") {
|
||||
w.close().map_err(|e| e.to_string())?;
|
||||
w.close().map_err(|e| {
|
||||
AppCommandError::window("Failed to close welcome window", e.to_string())
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -277,18 +280,24 @@ pub async fn open_settings_window(
|
||||
section: Option<String>,
|
||||
agent_type: Option<String>,
|
||||
state: tauri::State<'_, SettingsWindowState>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), AppCommandError> {
|
||||
let target_route = resolve_settings_target(section.as_deref(), agent_type.as_deref());
|
||||
if let Some(existing) = app.get_webview_window("settings") {
|
||||
ensure_windows_undecorated(&existing);
|
||||
if section.is_some() || agent_type.is_some() {
|
||||
let target_path = format!("/{target_route}");
|
||||
let target_json = serde_json::to_string(&target_path).map_err(|e| e.to_string())?;
|
||||
let target_json = serde_json::to_string(&target_path).map_err(|e| {
|
||||
AppCommandError::window("Failed to build settings navigation target", e.to_string())
|
||||
})?;
|
||||
let nav_script = format!("window.location.replace({target_json});");
|
||||
existing.eval(&nav_script).map_err(|e| e.to_string())?;
|
||||
existing.eval(&nav_script).map_err(|e| {
|
||||
AppCommandError::window("Failed to navigate settings window", e.to_string())
|
||||
})?;
|
||||
}
|
||||
let _ = existing.unminimize();
|
||||
existing.set_focus().map_err(|e| e.to_string())?;
|
||||
existing.set_focus().map_err(|e| {
|
||||
AppCommandError::window("Failed to focus settings window", e.to_string())
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -302,20 +311,24 @@ pub async fn open_settings_window(
|
||||
.center();
|
||||
let settings_window = apply_platform_window_style(builder)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
.map_err(|e| AppCommandError::window("Failed to open settings window", e.to_string()))?;
|
||||
ensure_windows_undecorated(&settings_window);
|
||||
|
||||
let mut disabled = HashSet::new();
|
||||
for (label, webview) in app.webview_windows() {
|
||||
if label != "settings" {
|
||||
webview.set_enabled(false).map_err(|e| e.to_string())?;
|
||||
webview.set_enabled(false).map_err(|e| {
|
||||
AppCommandError::window("Failed to update window enabled state", e.to_string())
|
||||
})?;
|
||||
disabled.insert(label);
|
||||
}
|
||||
}
|
||||
|
||||
state.set_owner(owner_label);
|
||||
state.set_disabled_windows(disabled);
|
||||
settings_window.set_focus().map_err(|e| e.to_string())?;
|
||||
settings_window
|
||||
.set_focus()
|
||||
.map_err(|e| AppCommandError::window("Failed to focus settings window", e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod acp;
|
||||
mod app_error;
|
||||
mod commands;
|
||||
mod db;
|
||||
mod models;
|
||||
|
||||
Reference in New Issue
Block a user