mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-26 03:28:31 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
954ee010c1 | ||
|
|
c219afa54e | ||
|
|
06e54d1b01 | ||
|
|
386147d5ff | ||
|
|
2eeba5f428 | ||
|
|
dc1cb41e61 | ||
|
|
99c426c15c | ||
|
|
77f9f81dea | ||
|
|
5cc2c1640c | ||
|
|
24b45446cc | ||
|
|
f3cc4a021b | ||
|
|
b6cfda7648 | ||
|
|
3b9910da0a | ||
|
|
a88b17c92f | ||
|
|
20aeb6a796 | ||
|
|
13efd8a629 |
31
README.md
31
README.md
@@ -244,7 +244,8 @@ winget install OpenListTeam.OpenListDesktop
|
||||
"port": 5244,
|
||||
"data_dir": "",
|
||||
"auto_launch": true,
|
||||
"ssl_enabled": false
|
||||
"ssl_enabled": false,
|
||||
"admin_password": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -290,7 +291,7 @@ winget install OpenListTeam.OpenListDesktop
|
||||
#### 先决条件
|
||||
|
||||
- **Node.js**:v22+ 和 yarn
|
||||
- **Rust**:最新稳定版本
|
||||
- **Rust**:最新nightly版本
|
||||
- **Git**:版本控制
|
||||
|
||||
#### 设置步骤
|
||||
@@ -303,35 +304,11 @@ cd openlist-desktop
|
||||
# 安装 Node.js 依赖
|
||||
yarn install
|
||||
|
||||
# 安装 Rust 依赖
|
||||
cd src-tauri
|
||||
cargo fetch
|
||||
|
||||
# 准备开发环境
|
||||
cd ..
|
||||
yarn run prebuild:dev
|
||||
|
||||
# 启动开发服务器
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
#### 开发命令
|
||||
|
||||
```bash
|
||||
# 启动带热重载的开发服务器
|
||||
yarn run dev
|
||||
|
||||
# 启动不带文件监视的开发
|
||||
yarn run nowatch
|
||||
|
||||
# 运行代码检查
|
||||
yarn run lint
|
||||
|
||||
# 修复代码检查问题
|
||||
yarn run lint:fix
|
||||
|
||||
# 类型检查
|
||||
yarn run build --dry-run
|
||||
yarn tauri dev
|
||||
```
|
||||
|
||||
#### 提交PR
|
||||
|
||||
31
README_en.md
31
README_en.md
@@ -244,7 +244,8 @@ Add custom Rclone flags for optimal performance:
|
||||
"port": 5244,
|
||||
"data_dir": "",
|
||||
"auto_launch": true,
|
||||
"ssl_enabled": false
|
||||
"ssl_enabled": false,
|
||||
"admin_password": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -290,7 +291,7 @@ Add custom Rclone flags for optimal performance:
|
||||
#### Prerequisites
|
||||
|
||||
- **Node.js**: v22+ with yarn
|
||||
- **Rust**: Latest stable version
|
||||
- **Rust**: Latest nightly version
|
||||
- **Git**: Version control
|
||||
|
||||
#### Setup Steps
|
||||
@@ -303,35 +304,11 @@ cd openlist-desktop
|
||||
# Install Node.js dependencies
|
||||
yarn install
|
||||
|
||||
# Install Rust dependencies
|
||||
cd src-tauri
|
||||
cargo fetch
|
||||
|
||||
# Prepare development environment
|
||||
cd ..
|
||||
yarn run prebuild:dev
|
||||
|
||||
# Start development server
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
#### Development Commands
|
||||
|
||||
```bash
|
||||
# Start development server with hot reload
|
||||
yarn run dev
|
||||
|
||||
# Start development without file watching
|
||||
yarn run nowatch
|
||||
|
||||
# Run linting
|
||||
yarn run lint
|
||||
|
||||
# Fix linting issues
|
||||
yarn run lint:fix
|
||||
|
||||
# Type checking
|
||||
yarn run build --dry-run
|
||||
yarn tauri dev
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"tauri"
|
||||
],
|
||||
"private": true,
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"author": {
|
||||
"name": "OpenList Team",
|
||||
"email": "96409857+Kuingsmile@users.noreply.github.com"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { execSync } from 'node:child_process'
|
||||
import crypto from 'node:crypto'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
@@ -112,6 +113,16 @@ const resolveSimpleServicePlugin = async pluginDir => {
|
||||
}
|
||||
}
|
||||
|
||||
const calculateSha256 = async filePath => {
|
||||
const hash = crypto.createHash('sha256')
|
||||
const fileStream = fs.createReadStream(filePath)
|
||||
fileStream.on('data', chunk => hash.update(chunk))
|
||||
fileStream.on('end', () => {
|
||||
const digest = hash.digest('hex')
|
||||
console.log(`SHA-256 hash of ${filePath}: ${digest}`)
|
||||
})
|
||||
}
|
||||
|
||||
const resolveAccessControlPlugin = async pluginDir => {
|
||||
const url = 'https://nsis.sourceforge.io/mediawiki/images/4/4a/AccessControl.zip'
|
||||
const TEMP_DIR = path.join(cwd, 'temp')
|
||||
@@ -184,6 +195,7 @@ async function resolveSidecar(binInfo) {
|
||||
}
|
||||
await fs.remove(zipPath)
|
||||
await fs.chmod(binaryPath, 0o755)
|
||||
await calculateSha256(binaryPath)
|
||||
} catch (err) {
|
||||
console.error(`Error preparing "${name}":`, err.message)
|
||||
await fs.rm(binaryPath, { recursive: true, force: true })
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -2898,7 +2898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openlist-desktop"
|
||||
version = "0.4.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openlist-desktop"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["Kuingsmile"]
|
||||
edition = "2024"
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::cmd::http_api::{delete_process, get_process_list, start_process, stop
|
||||
use crate::cmd::openlist_core::create_openlist_core_process;
|
||||
use crate::conf::config::MergedSettings;
|
||||
use crate::object::structs::AppState;
|
||||
use crate::utils::path::app_config_file_path;
|
||||
use crate::utils::path::{app_config_file_path, get_default_openlist_data_dir};
|
||||
|
||||
fn write_json_to_file<T: serde::Serialize>(path: PathBuf, value: &T) -> Result<(), String> {
|
||||
let json = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
|
||||
@@ -21,16 +21,10 @@ fn persist_app_settings(settings: &MergedSettings) -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn update_data_config(port: u16, data_dir: Option<&str>) -> Result<(), String> {
|
||||
let exe_dir = std::env::current_exe()
|
||||
.map_err(|e| e.to_string())?
|
||||
.parent()
|
||||
.ok_or("Failed to get exe parent dir")?
|
||||
.to_path_buf();
|
||||
|
||||
let data_config_path = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||
PathBuf::from(dir).join("config.json")
|
||||
} else {
|
||||
exe_dir.join("data").join("config.json")
|
||||
get_default_openlist_data_dir()?.join("config.json")
|
||||
};
|
||||
|
||||
if let Some(parent) = data_config_path.parent() {
|
||||
|
||||
@@ -66,6 +66,7 @@ fn rule_stdout() -> Result<Option<String>, String> {
|
||||
"rule",
|
||||
&format!("name={RULE}"),
|
||||
])
|
||||
.creation_flags(0x08000000)
|
||||
.output()
|
||||
.map_err(|e| format!("netsh: {e}"))?;
|
||||
if out.status.success() {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::process::Command;
|
||||
use tauri::State;
|
||||
|
||||
use crate::object::structs::AppState;
|
||||
use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir, get_service_log_path};
|
||||
|
||||
fn generate_random_password() -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -73,14 +74,22 @@ async fn execute_openlist_admin_set(
|
||||
|
||||
let mut cmd = Command::new(&openlist_exe);
|
||||
cmd.args(["admin", "set", password]);
|
||||
cmd.current_dir(app_dir);
|
||||
|
||||
if let Some(settings) = state.get_settings()
|
||||
let effective_data_dir = if let Some(settings) = state.get_settings()
|
||||
&& !settings.openlist.data_dir.is_empty()
|
||||
{
|
||||
cmd.arg("--data");
|
||||
cmd.arg(&settings.openlist.data_dir);
|
||||
log::info!("Using data directory: {}", settings.openlist.data_dir);
|
||||
}
|
||||
settings.openlist.data_dir
|
||||
} else {
|
||||
get_default_openlist_data_dir()
|
||||
.map_err(|e| format!("Failed to get default data directory: {e}"))?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
cmd.arg("--data");
|
||||
cmd.arg(&effective_data_dir);
|
||||
log::info!("Using data directory: {effective_data_dir}");
|
||||
log::info!("Executing command: {cmd:?}");
|
||||
let output = cmd
|
||||
.output()
|
||||
@@ -100,30 +109,29 @@ async fn execute_openlist_admin_set(
|
||||
}
|
||||
|
||||
fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
|
||||
let exe_path =
|
||||
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?;
|
||||
let app_dir = exe_path
|
||||
.parent()
|
||||
.ok_or("Executable has no parent directory")?
|
||||
.to_path_buf();
|
||||
let logs_dir = get_app_logs_dir()?;
|
||||
let service_path = get_service_log_path()?;
|
||||
|
||||
let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||
PathBuf::from(dir)
|
||||
} else {
|
||||
app_dir.join("data")
|
||||
get_default_openlist_data_dir()
|
||||
.map_err(|e| format!("Failed to get default data directory: {e}"))?
|
||||
};
|
||||
|
||||
let mut paths = Vec::new();
|
||||
match source {
|
||||
Some("openlist") => paths.push(openlist_log_base.join("log/log.log")),
|
||||
Some("app") => paths.push(app_dir.join("logs/app.log")),
|
||||
Some("rclone") => paths.push(app_dir.join("logs/process_rclone.log")),
|
||||
Some("openlist_core") => paths.push(app_dir.join("logs/process_openlist_core.log")),
|
||||
None => {
|
||||
Some("app") => paths.push(logs_dir.join("app.log")),
|
||||
Some("rclone") => paths.push(logs_dir.join("process_rclone.log")),
|
||||
Some("openlist_core") => paths.push(logs_dir.join("process_openlist_core.log")),
|
||||
Some("service") => paths.push(service_path),
|
||||
Some("all") => {
|
||||
paths.push(openlist_log_base.join("log/log.log"));
|
||||
paths.push(app_dir.join("logs/app.log"));
|
||||
paths.push(app_dir.join("logs/process_rclone.log"));
|
||||
paths.push(app_dir.join("logs/process_openlist_core.log"));
|
||||
paths.push(logs_dir.join("app.log"));
|
||||
paths.push(logs_dir.join("process_rclone.log"));
|
||||
paths.push(logs_dir.join("process_openlist_core.log"));
|
||||
paths.push(service_path);
|
||||
}
|
||||
_ => return Err("Invalid log source".into()),
|
||||
}
|
||||
@@ -220,9 +228,13 @@ pub async fn get_logs(
|
||||
let mut logs = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
let content =
|
||||
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {path:?}: {e}"))?;
|
||||
logs.extend(content.lines().map(str::to_string));
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.map_err(|e| format!("Failed to read {path:?}: {e}"))?;
|
||||
logs.extend(content.lines().map(str::to_string));
|
||||
} else {
|
||||
log::info!("Log file does not exist: {path:?}");
|
||||
}
|
||||
}
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ use url::Url;
|
||||
|
||||
use crate::object::structs::{AppState, ServiceStatus};
|
||||
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
||||
use crate::utils::path::{get_app_logs_dir, get_openlist_binary_path};
|
||||
use crate::utils::path::{
|
||||
get_app_logs_dir, get_default_openlist_data_dir, get_openlist_binary_path,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_openlist_core_process(
|
||||
@@ -27,10 +29,19 @@ pub async fn create_openlist_core_process(
|
||||
let api_key = get_api_key();
|
||||
let port = get_server_port();
|
||||
let mut args = vec!["server".into()];
|
||||
if !data_dir.is_empty() {
|
||||
args.push("--data".into());
|
||||
args.push(data_dir);
|
||||
}
|
||||
|
||||
// Use custom data dir if set, otherwise use platform-specific default
|
||||
let effective_data_dir = if !data_dir.is_empty() {
|
||||
data_dir
|
||||
} else {
|
||||
get_default_openlist_data_dir()
|
||||
.map_err(|e| format!("Failed to get default data directory: {e}"))?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
args.push("--data".into());
|
||||
args.push(effective_data_dir);
|
||||
let config = ProcessConfig {
|
||||
id: "openlist_core".into(),
|
||||
name: "single_openlist_core_process".into(),
|
||||
|
||||
@@ -6,7 +6,10 @@ use tauri::{AppHandle, State};
|
||||
use crate::cmd::http_api::{get_process_list, start_process, stop_process};
|
||||
use crate::object::structs::{AppState, FileItem};
|
||||
use crate::utils::github_proxy::apply_github_proxy;
|
||||
use crate::utils::path::{get_openlist_binary_path, get_rclone_binary_path};
|
||||
use crate::utils::path::{
|
||||
app_config_file_path, get_app_logs_dir, get_default_openlist_data_dir,
|
||||
get_openlist_binary_path, get_rclone_binary_path, get_rclone_config_path,
|
||||
};
|
||||
|
||||
fn normalize_path(path: &str) -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -615,3 +618,45 @@ fn extract_tar_gz(
|
||||
executable_path
|
||||
.ok_or_else(|| format!("Executable '{executable_name}' not found in tar.gz archive"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_logs_directory() -> Result<bool, String> {
|
||||
let logs_dir = get_app_logs_dir()?;
|
||||
if !logs_dir.exists() {
|
||||
fs::create_dir_all(&logs_dir)
|
||||
.map_err(|e| format!("Failed to create logs directory: {e}"))?;
|
||||
}
|
||||
open::that(logs_dir.as_os_str()).map_err(|e| e.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_openlist_data_dir() -> Result<bool, String> {
|
||||
let config_path = get_default_openlist_data_dir()?;
|
||||
if !config_path.exists() {
|
||||
fs::create_dir_all(&config_path)
|
||||
.map_err(|e| format!("Failed to create config directory: {e}"))?;
|
||||
}
|
||||
open::that(config_path.as_os_str()).map_err(|e| e.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_rclone_config_file() -> Result<bool, String> {
|
||||
let config_path = get_rclone_config_path()?;
|
||||
if !config_path.exists() {
|
||||
fs::File::create(&config_path).map_err(|e| format!("Failed to create config file: {e}"))?;
|
||||
}
|
||||
open::that_detached(config_path.as_os_str()).map_err(|e| e.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_settings_file() -> Result<bool, String> {
|
||||
let settings_path = app_config_file_path()?;
|
||||
if !settings_path.exists() {
|
||||
return Err("Settings file does not exist".to_string());
|
||||
}
|
||||
open::that_detached(settings_path.as_os_str()).map_err(|e| e.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use tauri::State;
|
||||
use crate::cmd::http_api::{get_process_list, start_process};
|
||||
use crate::object::structs::AppState;
|
||||
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
||||
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path};
|
||||
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path, get_rclone_config_path};
|
||||
|
||||
// use 45572 due to the reserved port on Windows
|
||||
pub const RCLONE_API_BASE: &str = "http://127.0.0.1:45572";
|
||||
@@ -40,10 +40,8 @@ pub async fn create_rclone_backend_process(
|
||||
get_rclone_binary_path().map_err(|e| format!("Failed to get rclone binary path: {e}"))?;
|
||||
let log_file_path =
|
||||
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?;
|
||||
let rclone_conf_path = binary_path
|
||||
.parent()
|
||||
.map(|p| p.join("rclone.conf"))
|
||||
.ok_or_else(|| "Failed to determine rclone.conf path".to_string())?;
|
||||
let rclone_conf_path =
|
||||
get_rclone_config_path().map_err(|e| format!("Failed to get rclone config path: {e}"))?;
|
||||
let log_file_path = log_file_path.join("process_rclone.log");
|
||||
let api_key = get_api_key();
|
||||
let port = get_server_port();
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::object::structs::{
|
||||
};
|
||||
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
||||
use crate::utils::args::split_args_vec;
|
||||
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path};
|
||||
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path, get_rclone_config_path};
|
||||
|
||||
struct RcloneApi {
|
||||
client: Client,
|
||||
@@ -189,10 +189,8 @@ pub async fn create_rclone_mount_remote_process(
|
||||
let log_file_path =
|
||||
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?;
|
||||
let log_file_path = log_file_path.join("process_rclone.log");
|
||||
let rclone_conf_path = binary_path
|
||||
.parent()
|
||||
.map(|p| p.join("rclone.conf"))
|
||||
.ok_or_else(|| "Failed to determine rclone.conf path".to_string())?;
|
||||
let rclone_conf_path =
|
||||
get_rclone_config_path().map_err(|e| format!("Failed to get rclone config path: {e}"))?;
|
||||
|
||||
let api_key = get_api_key();
|
||||
let port = get_server_port();
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use super::app::AppConfig;
|
||||
use crate::conf::core::OpenListCoreConfig;
|
||||
use crate::conf::rclone::RcloneConfig;
|
||||
use crate::utils::path::app_config_file_path;
|
||||
use crate::utils::path::{app_config_file_path, get_default_openlist_data_dir};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MergedSettings {
|
||||
@@ -33,12 +33,7 @@ impl MergedSettings {
|
||||
if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||
Ok(PathBuf::from(dir).join("config.json"))
|
||||
} else {
|
||||
let exe = std::env::current_exe()
|
||||
.map_err(|e| format!("Failed to get current exe path: {e}"))?;
|
||||
let dir = exe
|
||||
.parent()
|
||||
.ok_or_else(|| "Failed to get executable parent directory".to_string())?;
|
||||
Ok(dir.join("data").join("config.json"))
|
||||
Ok(get_default_openlist_data_dir()?.join("config.json"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ use cmd::logs::{
|
||||
};
|
||||
use cmd::openlist_core::{create_openlist_core_process, get_openlist_core_status};
|
||||
use cmd::os_operate::{
|
||||
get_available_versions, list_files, open_file, open_folder, open_url, open_url_in_browser,
|
||||
select_directory, update_tool_version,
|
||||
get_available_versions, list_files, open_file, open_folder, open_logs_directory,
|
||||
open_openlist_data_dir, open_rclone_config_file, open_settings_file, open_url,
|
||||
open_url_in_browser, select_directory, update_tool_version,
|
||||
};
|
||||
use cmd::rclone_core::{
|
||||
create_and_start_rclone_backend, create_rclone_backend_process, get_rclone_backend_status,
|
||||
@@ -141,6 +142,10 @@ pub fn run() {
|
||||
list_files,
|
||||
open_file,
|
||||
open_folder,
|
||||
open_logs_directory,
|
||||
open_openlist_data_dir,
|
||||
open_rclone_config_file,
|
||||
open_settings_file,
|
||||
open_url,
|
||||
open_url_in_browser,
|
||||
save_settings,
|
||||
|
||||
@@ -16,6 +16,46 @@ fn get_app_dir() -> Result<PathBuf, String> {
|
||||
Ok(app_dir)
|
||||
}
|
||||
|
||||
fn get_user_data_dir() -> Result<PathBuf, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||
let data_dir = PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("OpenList Desktop");
|
||||
fs::create_dir_all(&data_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {e}"))?;
|
||||
Ok(data_dir)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
get_app_dir()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_user_logs_dir() -> Result<PathBuf, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||
let logs_dir = PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Logs")
|
||||
.join("OpenList Desktop");
|
||||
fs::create_dir_all(&logs_dir)
|
||||
.map_err(|e| format!("Failed to create logs directory: {e}"))?;
|
||||
Ok(logs_dir)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let logs = get_app_dir()?.join("logs");
|
||||
fs::create_dir_all(&logs).map_err(|e| e.to_string())?;
|
||||
Ok(logs)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_binary_path(binary: &str, service_name: &str) -> Result<PathBuf, String> {
|
||||
let mut name = binary.to_string();
|
||||
if cfg!(target_os = "windows") {
|
||||
@@ -40,7 +80,7 @@ pub fn get_rclone_binary_path() -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
pub fn get_app_config_dir() -> Result<PathBuf, String> {
|
||||
get_app_dir()
|
||||
get_user_data_dir()
|
||||
}
|
||||
|
||||
pub fn app_config_file_path() -> Result<PathBuf, String> {
|
||||
@@ -48,7 +88,41 @@ pub fn app_config_file_path() -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
pub fn get_app_logs_dir() -> Result<PathBuf, String> {
|
||||
let logs = get_app_dir()?.join("logs");
|
||||
fs::create_dir_all(&logs).map_err(|e| e.to_string())?;
|
||||
Ok(logs)
|
||||
get_user_logs_dir()
|
||||
}
|
||||
|
||||
pub fn get_rclone_config_path() -> Result<PathBuf, String> {
|
||||
Ok(get_user_data_dir()?.join("rclone.conf"))
|
||||
}
|
||||
|
||||
pub fn get_default_openlist_data_dir() -> Result<PathBuf, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(get_user_data_dir()?.join("data"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Ok(get_app_dir()?.join("data"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_service_log_path() -> Result<PathBuf, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||
let logs = PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("io.github.openlistteam.openlist.service.bundle")
|
||||
.join("Contents")
|
||||
.join("MacOS")
|
||||
.join("openlist-desktop-service.log");
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Ok(get_app_dir()?.join("openlist-desktop-service.log"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenList Desktop",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"identifier": "io.github.openlistteam.openlist.desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn run dev",
|
||||
|
||||
@@ -61,7 +61,11 @@ export class TauriAPI {
|
||||
open: (path: string): Promise<boolean> => call('open_file', { path }),
|
||||
folder: (path: string): Promise<boolean> => call('open_folder', { path }),
|
||||
url: (url: string): Promise<boolean> => call('open_url', { url }),
|
||||
urlInBrowser: (url: string): Promise<boolean> => call('open_url_in_browser', { url })
|
||||
urlInBrowser: (url: string): Promise<boolean> => call('open_url_in_browser', { url }),
|
||||
openOpenListDataDir: (): Promise<boolean> => call('open_openlist_data_dir'),
|
||||
openLogsDirectory: (): Promise<boolean> => call('open_logs_directory'),
|
||||
openRcloneConfigFile: (): Promise<boolean> => call('open_rclone_config_file'),
|
||||
openSettingsFile: (): Promise<boolean> => call('open_settings_file')
|
||||
}
|
||||
|
||||
// --- Settings management ---
|
||||
@@ -75,9 +79,9 @@ export class TauriAPI {
|
||||
|
||||
// --- Logs management ---
|
||||
static logs = {
|
||||
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<string[]> =>
|
||||
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<string[]> =>
|
||||
call('get_logs', { source: src }),
|
||||
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<boolean> =>
|
||||
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<boolean> =>
|
||||
call('clear_logs', { source: src }),
|
||||
adminPassword: (): Promise<string> => call('get_admin_password'),
|
||||
resetAdminPassword: (): Promise<string> => call('reset_admin_password'),
|
||||
|
||||
@@ -34,7 +34,9 @@ const openLink = async (url: string) => {
|
||||
} catch (error) {
|
||||
console.error('Failed to open link:', error)
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
setTimeout(() => {
|
||||
window.open(url, '_blank')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -109,7 +109,9 @@ const openLink = async (url: string) => {
|
||||
} catch (error) {
|
||||
console.error('Failed to open link:', error)
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
setTimeout(() => {
|
||||
window.open(url, '_blank')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,21 +4,34 @@
|
||||
<div class="action-section">
|
||||
<div class="section-header">
|
||||
<h4>{{ t('dashboard.quickActions.openlistService') }}</h4>
|
||||
<div v-if="isCoreLoading" class="section-loading-indicator">
|
||||
<Loader :size="12" class="loading-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button @click="toggleCore" :class="['action-btn', 'service-btn', { running: isCoreRunning }]">
|
||||
<component :is="serviceButtonIcon" :size="20" />
|
||||
<span>{{ serviceButtonText }}</span>
|
||||
<button
|
||||
@click="toggleCore"
|
||||
:disabled="isCoreLoading"
|
||||
:class="['action-btn', 'service-btn', { running: isCoreRunning, loading: isCoreLoading }]"
|
||||
>
|
||||
<component v-if="!isCoreLoading" :is="serviceButtonIcon" :size="20" />
|
||||
<Loader v-else :size="20" class="loading-icon" />
|
||||
<span>{{ isCoreLoading ? t('dashboard.quickActions.processing') : serviceButtonText }}</span>
|
||||
</button>
|
||||
|
||||
<button @click="restartCore" :disabled="!isCoreRunning" class="action-btn restart-btn">
|
||||
<RotateCcw :size="18" />
|
||||
<button
|
||||
@click="restartCore"
|
||||
:disabled="!isCoreRunning || isCoreLoading"
|
||||
:class="['action-btn', 'restart-btn', { loading: isCoreLoading }]"
|
||||
>
|
||||
<RotateCcw v-if="!isCoreLoading" :size="18" />
|
||||
<Loader v-else :size="18" class="loading-icon" />
|
||||
<span>{{ t('dashboard.quickActions.restart') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openWebUI"
|
||||
:disabled="!isCoreRunning"
|
||||
:disabled="!isCoreRunning || isCoreLoading"
|
||||
class="action-btn web-btn"
|
||||
:title="appStore.openListCoreUrl"
|
||||
>
|
||||
@@ -47,15 +60,26 @@
|
||||
<div class="action-section">
|
||||
<div class="section-header">
|
||||
<h4>{{ t('dashboard.quickActions.rclone') }}</h4>
|
||||
<div v-if="isRcloneLoading" class="section-loading-indicator">
|
||||
<Loader :size="12" class="loading-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
|
||||
:class="['action-btn', 'service-indicator-btn', { active: rcloneStore.serviceRunning }]"
|
||||
:disabled="isRcloneLoading"
|
||||
:class="[
|
||||
'action-btn',
|
||||
'service-indicator-btn',
|
||||
{ active: rcloneStore.serviceRunning, loading: isRcloneLoading }
|
||||
]"
|
||||
>
|
||||
<component :is="rcloneStore.serviceRunning ? Square : Play" :size="18" />
|
||||
<component v-if="!isRcloneLoading" :is="rcloneStore.serviceRunning ? Square : Play" :size="18" />
|
||||
<Loader v-else :size="18" class="loading-icon" />
|
||||
<span>{{
|
||||
rcloneStore.serviceRunning
|
||||
isRcloneLoading
|
||||
? t('dashboard.quickActions.processing')
|
||||
: rcloneStore.serviceRunning
|
||||
? t('dashboard.quickActions.stopRclone')
|
||||
: t('dashboard.quickActions.startRclone')
|
||||
}}</span>
|
||||
@@ -118,7 +142,7 @@ import { useAppStore } from '../../stores/app'
|
||||
import { useRcloneStore } from '../../stores/rclone'
|
||||
import { useTranslation } from '../../composables/useI18n'
|
||||
import Card from '../ui/Card.vue'
|
||||
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key, Shield } from 'lucide-vue-next'
|
||||
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key, Shield, Loader } from 'lucide-vue-next'
|
||||
import { TauriAPI } from '@/api/tauri'
|
||||
|
||||
const { t } = useTranslation()
|
||||
@@ -127,6 +151,8 @@ const appStore = useAppStore()
|
||||
const rcloneStore = useRcloneStore()
|
||||
|
||||
const isCoreRunning = computed(() => appStore.isCoreRunning)
|
||||
const isCoreLoading = computed(() => appStore.loading)
|
||||
const isRcloneLoading = computed(() => rcloneStore.loading)
|
||||
const settings = computed(() => appStore.settings)
|
||||
let statusCheckInterval: number | null = null
|
||||
|
||||
@@ -409,7 +435,9 @@ const openLink = async (url: string) => {
|
||||
} catch (error) {
|
||||
console.error('Failed to open link:', error)
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
setTimeout(() => {
|
||||
window.open(url, '_blank')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -452,6 +480,16 @@ onUnmounted(() => {
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.section-loading-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
opacity: 0.7;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.icon-only-btn {
|
||||
flex: 0 0 auto;
|
||||
min-width: auto;
|
||||
@@ -509,6 +547,15 @@ onUnmounted(() => {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.action-btn.loading {
|
||||
opacity: 0.8;
|
||||
cursor: wait !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.action-btn.loading .loading-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.service-btn.running {
|
||||
background: rgb(239, 68, 68);
|
||||
|
||||
@@ -225,7 +225,17 @@ const stopService = async () => {
|
||||
if (!result) {
|
||||
throw new Error('Service stop failed')
|
||||
}
|
||||
await checkServiceStatus()
|
||||
let attempts = 0
|
||||
const maxAttempts = 5
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const status = await checkServiceStatus()
|
||||
if (status === 'stopped' || status === 'not-installed' || status === 'error') {
|
||||
serviceStatus.value = status
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to stop service:', error)
|
||||
serviceStatus.value = 'error'
|
||||
@@ -265,7 +275,6 @@ const cancelUninstall = () => {
|
||||
|
||||
onMounted(async () => {
|
||||
await checkServiceStatus()
|
||||
statusCheckInterval = window.setInterval(checkServiceStatus, 30 * 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
<span class="current-version">{{ currentVersions.openlist }}</span>
|
||||
</div>
|
||||
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn">
|
||||
<component :is="refreshing ? LoaderIcon : RefreshCw" :size="16" />
|
||||
<component
|
||||
:is="refreshing ? Loader : RefreshCw"
|
||||
:size="16"
|
||||
:class="{ 'rotate-animation': refreshing && !loading.openlist }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="version-controls">
|
||||
@@ -26,8 +30,10 @@
|
||||
"
|
||||
class="update-btn"
|
||||
>
|
||||
<component :is="loading.openlist ? LoaderIcon : Download" :size="14" />
|
||||
<span>{{ loading.openlist ? t('common.loading') : t('dashboard.versionManager.update') }}</span>
|
||||
<component :is="loading.openlist ? Loader : Download" :size="14" />
|
||||
<span>{{
|
||||
loading.openlist ? t('dashboard.versionManager.updating') : t('dashboard.versionManager.update')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,7 +44,11 @@
|
||||
<span class="current-version">{{ currentVersions.rclone }}</span>
|
||||
</div>
|
||||
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn">
|
||||
<component :is="refreshing ? LoaderIcon : RefreshCw" :size="16" />
|
||||
<component
|
||||
:is="refreshing ? Loader : RefreshCw"
|
||||
:size="16"
|
||||
:class="{ 'rotate-animation': refreshing && !loading.rclone }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="version-controls">
|
||||
@@ -55,8 +65,10 @@
|
||||
"
|
||||
class="update-btn"
|
||||
>
|
||||
<component :is="loading.rclone ? LoaderIcon : Download" :size="14" />
|
||||
<span>{{ loading.rclone ? t('common.loading') : t('dashboard.versionManager.update') }}</span>
|
||||
<component :is="loading.rclone ? Loader : Download" :size="14" />
|
||||
<span>{{
|
||||
loading.rclone ? t('dashboard.versionManager.updating') : t('dashboard.versionManager.update')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,7 +80,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useTranslation } from '../../composables/useI18n'
|
||||
import { Download, RefreshCw, Loader2 as LoaderIcon } from 'lucide-vue-next'
|
||||
import { Download, RefreshCw, Loader } from 'lucide-vue-next'
|
||||
import Card from '../ui/Card.vue'
|
||||
import { TauriAPI } from '../../api/tauri'
|
||||
|
||||
@@ -144,20 +156,95 @@ const refreshVersions = async () => {
|
||||
|
||||
const updateVersion = async (type: 'openlist' | 'rclone') => {
|
||||
loading.value[type] = true
|
||||
|
||||
try {
|
||||
const result = await TauriAPI.bin.updateVersion(type, selectedVersions.value[type])
|
||||
|
||||
currentVersions.value[type] = selectedVersions.value[type]
|
||||
selectedVersions.value[type] = ''
|
||||
|
||||
showNotification(
|
||||
'success',
|
||||
t('dashboard.versionManager.updateSuccess', { type: type.charAt(0).toUpperCase() + type.slice(1) })
|
||||
)
|
||||
|
||||
console.log(`Updated ${type}:`, result)
|
||||
} catch (error) {
|
||||
console.error(`Failed to update ${type}:`, error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
showNotification(
|
||||
'error',
|
||||
t('dashboard.versionManager.updateError', {
|
||||
type: type.charAt(0).toUpperCase() + type.slice(1),
|
||||
error: errorMessage
|
||||
})
|
||||
)
|
||||
} finally {
|
||||
loading.value[type] = false
|
||||
}
|
||||
}
|
||||
|
||||
const showNotification = (type: 'success' | 'error', message: string) => {
|
||||
const notification = document.createElement('div')
|
||||
const bgColor =
|
||||
type === 'success'
|
||||
? 'linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105))'
|
||||
: 'linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38))'
|
||||
const icon = type === 'success' ? '✓' : '⚠'
|
||||
|
||||
notification.innerHTML = `
|
||||
<div style="
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${bgColor};
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10000;
|
||||
font-weight: 500;
|
||||
max-width: 350px;
|
||||
word-break: break-word;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="font-size: 18px;">${icon}</div>
|
||||
<div style="font-size: 14px;">${message}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
if (!document.querySelector('#notification-styles')) {
|
||||
const style = document.createElement('style')
|
||||
style.id = 'notification-styles'
|
||||
style.innerHTML = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
document.body.appendChild(notification)
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.style.animation = 'slideInRight 0.3s ease-in reverse'
|
||||
setTimeout(() => {
|
||||
notification.parentNode?.removeChild(notification)
|
||||
}, 300)
|
||||
}
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshVersions()
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"reset": "Reset",
|
||||
"close": "Close",
|
||||
"minimize": "Minimize",
|
||||
@@ -23,10 +24,11 @@
|
||||
"openlistService": "OpenList Core",
|
||||
"rclone": "RClone",
|
||||
"quickSettings": "Quick Settings",
|
||||
"startOpenListCore": "Start Core",
|
||||
"stopOpenListCore": "Stop Core",
|
||||
"startOpenListCore": "Start",
|
||||
"stopOpenListCore": "Stop",
|
||||
"processing": "Processing...",
|
||||
"restart": "Restart",
|
||||
"openWeb": "Web UI",
|
||||
"openWeb": "Web",
|
||||
"configRclone": "Configure RClone",
|
||||
"startRclone": "Start RClone",
|
||||
"stopRclone": "Stop RClone",
|
||||
@@ -56,7 +58,10 @@
|
||||
"openlist": "OpenList",
|
||||
"rclone": "Rclone",
|
||||
"selectVersion": "Select Version",
|
||||
"update": "Update"
|
||||
"update": "Update",
|
||||
"updating": "Updating...",
|
||||
"updateSuccess": "{type} updated successfully!",
|
||||
"updateError": "Failed to update {type}: {error}"
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Documentation",
|
||||
@@ -98,7 +103,10 @@
|
||||
"subtitle": "Configure your OpenList Desktop application",
|
||||
"saveChanges": "Save Changes",
|
||||
"resetToDefaults": "Reset to defaults",
|
||||
"confirmReset": "Are you sure you want to reset all settings to defaults? This action cannot be undone.",
|
||||
"confirmReset": {
|
||||
"title": "Reset Settings",
|
||||
"message": "Are you sure you want to reset all settings to defaults? This action cannot be undone."
|
||||
},
|
||||
"saved": "Settings saved successfully!",
|
||||
"saveFailed": "Failed to save settings. Please try again.",
|
||||
"resetSuccess": "Settings reset to defaults successfully!",
|
||||
@@ -139,7 +147,10 @@
|
||||
"placeholder": "Optional. Custom data directory path",
|
||||
"help": "Optional. Specify a custom directory for OpenList data storage",
|
||||
"selectTitle": "Select Data Directory",
|
||||
"selectError": "Failed to select directory. Please try again or enter path manually."
|
||||
"selectError": "Failed to select directory. Please try again or enter path manually.",
|
||||
"openTitle": "Open Data Directory",
|
||||
"openSuccess": "Data directory opened successfully",
|
||||
"openError": "Failed to open data directory"
|
||||
},
|
||||
"ssl": {
|
||||
"title": "Enable SSL/HTTPS",
|
||||
@@ -172,7 +183,10 @@
|
||||
"subtitle": "Configure rclone for remote storage access",
|
||||
"label": "Rclone Configuration (JSON)",
|
||||
"tips": "View your rclone configuration as JSON. This will be used to configure rclone remotes.",
|
||||
"invalidJson": "Invalid JSON configuration. Please check your syntax."
|
||||
"invalidJson": "Invalid JSON configuration. Please check your syntax.",
|
||||
"openFile": "Open rclone.conf",
|
||||
"openSuccess": "Rclone config file opened successfully",
|
||||
"openError": "Failed to open rclone config file"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
@@ -185,6 +199,13 @@
|
||||
"auto": "Auto",
|
||||
"autoDesc": "Follow system"
|
||||
},
|
||||
"config": {
|
||||
"title": "Configuration Files",
|
||||
"subtitle": "Access application configuration files",
|
||||
"openFile": "Open settings.json",
|
||||
"openSuccess": "Settings file opened successfully",
|
||||
"openError": "Failed to open settings file"
|
||||
},
|
||||
"ghProxy": {
|
||||
"title": "GitHub Proxy",
|
||||
"subtitle": "Accelerate GitHub with proxy service",
|
||||
@@ -228,7 +249,9 @@
|
||||
"copyFailed": "Failed to copy logs to clipboard",
|
||||
"exportSuccess": "Successfully exported {count} logs entries to file",
|
||||
"clearSuccess": "Logs cleared successfully",
|
||||
"clearFailed": "Failed to clear logs"
|
||||
"clearFailed": "Failed to clear logs",
|
||||
"openDirectorySuccess": "Logs directory opened successfully",
|
||||
"openDirectoryFailed": "Failed to open logs directory"
|
||||
},
|
||||
"toolbar": {
|
||||
"pause": "Pause (Space)",
|
||||
@@ -239,6 +262,7 @@
|
||||
"copyToClipboard": "Copy to Clipboard (Ctrl+C)",
|
||||
"exportLogs": "Export Logs",
|
||||
"clearLogs": "Clear Logs (Ctrl+Delete)",
|
||||
"openLogsDirectory": "Open Logs Directory",
|
||||
"toggleFullscreen": "Toggle Fullscreen (F11)",
|
||||
"scrollToTop": "Scroll to Top (Home)",
|
||||
"scrollToBottom": "Scroll to Bottom (End)"
|
||||
@@ -263,7 +287,8 @@
|
||||
"sources": {
|
||||
"all": "All Sources",
|
||||
"rclone": "Rclone",
|
||||
"openlist": "OpenList"
|
||||
"openlist": "OpenList",
|
||||
"service": "Service"
|
||||
},
|
||||
"actions": {
|
||||
"selectAll": "Select All (Ctrl+A)",
|
||||
@@ -283,7 +308,8 @@
|
||||
"stripAnsiColors": "Strip ANSI Colors"
|
||||
},
|
||||
"messages": {
|
||||
"confirmClear": "Are you sure you want to clear all logs?"
|
||||
"confirmClear": "Are you sure you want to clear all logs?",
|
||||
"confirmTitle": "Clear Logs"
|
||||
},
|
||||
"headers": {
|
||||
"timestamp": "Timestamp",
|
||||
@@ -348,7 +374,7 @@
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Password",
|
||||
"mountPoint": "Mount Path",
|
||||
"mountPointPlaceholder": "e.g., T: (Windows) or /mnt/remote (Linux)",
|
||||
"mountPointPlaceholder": "e.g., T: or /mnt/remote",
|
||||
"volumeName": "Remote Path",
|
||||
"volumeNamePlaceholder": "e.g., /",
|
||||
"autoMount": "Auto-mount on startup",
|
||||
@@ -381,7 +407,7 @@
|
||||
"checkers": "Number of checkers to run in parallel (default 8)",
|
||||
"vfs-cache-max-age": "Max age of objects in cache (default 24h)",
|
||||
"vfs-cache-max-size": "Max total size of cache (default 10G)",
|
||||
"vfs-dir-cache-time": "How long to cache directory listings (default 5m)",
|
||||
"dir-cache-time": "How long to cache directory listings (default 5m)",
|
||||
"bwlimit-10M": "Bandwidth limit (e.g. 10M)",
|
||||
"bwlimit-10M:100M": "Set separate upload and download bandwidth limits",
|
||||
"bwlimit-schedule": "Time-based bandwidth limits",
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"reset": "重置",
|
||||
"close": "关闭",
|
||||
"minimize": "最小化",
|
||||
"maximize": "最大化",
|
||||
"loading": "加载中...",
|
||||
"loading": "处理中...",
|
||||
"saving": "保存中...",
|
||||
"add": "添加"
|
||||
},
|
||||
@@ -23,10 +24,11 @@
|
||||
"openlistService": "OpenList 核心",
|
||||
"rclone": "RClone",
|
||||
"quickSettings": "快速设置",
|
||||
"startOpenListCore": "启动核心",
|
||||
"stopOpenListCore": "停止核心",
|
||||
"startOpenListCore": "启动",
|
||||
"stopOpenListCore": "停止",
|
||||
"processing": "处理中...",
|
||||
"restart": "重启",
|
||||
"openWeb": "网页界面",
|
||||
"openWeb": "网页",
|
||||
"configRclone": "配置 RClone",
|
||||
"startRclone": "启动 RClone",
|
||||
"stopRclone": "停止 RClone",
|
||||
@@ -56,7 +58,10 @@
|
||||
"openlist": "OpenList",
|
||||
"rclone": "Rclone",
|
||||
"selectVersion": "选择版本",
|
||||
"update": "更新"
|
||||
"update": "更新",
|
||||
"updating": "更新中...",
|
||||
"updateSuccess": "{type} 更新成功!",
|
||||
"updateError": "更新 {type} 失败:{error}"
|
||||
},
|
||||
"documentation": {
|
||||
"title": "文档",
|
||||
@@ -98,7 +103,10 @@
|
||||
"subtitle": "配置您的 OpenList 桌面应用程序",
|
||||
"saveChanges": "保存更改",
|
||||
"resetToDefaults": "重置为默认值",
|
||||
"confirmReset": "您确定要将所有设置重置为默认值吗?此操作无法撤消。",
|
||||
"confirmReset": {
|
||||
"title": "重置设置",
|
||||
"message": "您确定要将所有设置重置为默认值吗?此操作无法撤消。"
|
||||
},
|
||||
"saved": "设置保存成功!",
|
||||
"saveFailed": "保存设置失败,请重试。",
|
||||
"resetSuccess": "设置已重置为默认值!",
|
||||
@@ -139,7 +147,10 @@
|
||||
"placeholder": "可选。自定义数据目录路径",
|
||||
"help": "可选。为 OpenList 数据存储指定自定义目录",
|
||||
"selectTitle": "选择数据目录",
|
||||
"selectError": "选择目录失败。请重试或手动输入路径。"
|
||||
"selectError": "选择目录失败。请重试或手动输入路径。",
|
||||
"openTitle": "打开数据目录",
|
||||
"openSuccess": "数据目录打开成功",
|
||||
"openError": "打开数据目录失败"
|
||||
},
|
||||
"ssl": {
|
||||
"title": "启用 SSL/HTTPS",
|
||||
@@ -172,7 +183,10 @@
|
||||
"subtitle": "配置 rclone 远程存储访问",
|
||||
"label": "Rclone 配置 (JSON)",
|
||||
"invalidJson": "无效的 JSON 配置。请检查您的语法。",
|
||||
"tips": "查看你的JSON配置"
|
||||
"tips": "查看你的JSON配置",
|
||||
"openFile": "打开 rclone.conf",
|
||||
"openSuccess": "Rclone 配置文件打开成功",
|
||||
"openError": "打开 Rclone 配置文件失败"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
@@ -185,6 +199,13 @@
|
||||
"auto": "自动",
|
||||
"autoDesc": "跟随系统"
|
||||
},
|
||||
"config": {
|
||||
"title": "配置文件",
|
||||
"subtitle": "访问应用程序配置文件",
|
||||
"openFile": "打开 settings.json",
|
||||
"openSuccess": "设置文件打开成功",
|
||||
"openError": "打开设置文件失败"
|
||||
},
|
||||
"ghProxy": {
|
||||
"title": "GitHub 代理",
|
||||
"subtitle": "使用代理服务加速 GitHub",
|
||||
@@ -228,7 +249,9 @@
|
||||
"copyFailed": "复制日志到剪贴板失败",
|
||||
"exportSuccess": "成功导出 {count} 条日志到文件",
|
||||
"clearSuccess": "日志清理成功",
|
||||
"clearFailed": "清理日志失败"
|
||||
"clearFailed": "清理日志失败",
|
||||
"openDirectorySuccess": "日志目录打开成功",
|
||||
"openDirectoryFailed": "打开日志目录失败"
|
||||
},
|
||||
"toolbar": {
|
||||
"pause": "暂停 (Space)",
|
||||
@@ -239,6 +262,7 @@
|
||||
"copyToClipboard": "复制到剪贴板 (Ctrl+C)",
|
||||
"exportLogs": "导出日志",
|
||||
"clearLogs": "清除日志 (Ctrl+Delete)",
|
||||
"openLogsDirectory": "打开日志目录",
|
||||
"toggleFullscreen": "切换全屏 (F11)",
|
||||
"scrollToTop": "滚动到顶部 (Home)",
|
||||
"scrollToBottom": "滚动到底部 (End)"
|
||||
@@ -263,7 +287,8 @@
|
||||
"sources": {
|
||||
"all": "所有来源",
|
||||
"rclone": "Rclone",
|
||||
"openlist": "OpenList"
|
||||
"openlist": "OpenList",
|
||||
"service": "服务"
|
||||
},
|
||||
"actions": {
|
||||
"selectAll": "全选 (Ctrl+A)",
|
||||
@@ -283,7 +308,8 @@
|
||||
"stripAnsiColors": "去除 ANSI 颜色"
|
||||
},
|
||||
"messages": {
|
||||
"confirmClear": "您确定要清除所有日志吗?"
|
||||
"confirmClear": "您确定要清除所有日志吗?",
|
||||
"confirmTitle": "清除日志"
|
||||
},
|
||||
"headers": {
|
||||
"timestamp": "时间",
|
||||
@@ -348,7 +374,7 @@
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "密码",
|
||||
"mountPoint": "挂载点",
|
||||
"mountPointPlaceholder": "例如:T: (Windows) 或 /mnt/remote (Linux)",
|
||||
"mountPointPlaceholder": "例如:T: 或 /mnt/remote",
|
||||
"volumeName": "远程路径",
|
||||
"volumeNamePlaceholder": "例如:/",
|
||||
"autoMount": "开机自动挂载",
|
||||
@@ -381,7 +407,7 @@
|
||||
"checkers": "并行运行的检查器数量(默认 8)",
|
||||
"vfs-cache-max-age": "缓存的最大生命周期(默认 24h)",
|
||||
"vfs-cache-max-size": "缓存文件的最大大小(默认 10G)",
|
||||
"vfs-dir-cache-time": "缓存目录列表的时间(默认 5m)",
|
||||
"dir-cache-time": "缓存目录列表的时间(默认 5m)",
|
||||
"bwlimit-10M": "带宽限制(例如 10M)",
|
||||
"bwlimit-10M:100M": "分别设置上传和下载宽带限制",
|
||||
"bwlimit-schedule": "基于时间的带宽限制",
|
||||
|
||||
@@ -553,7 +553,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs(source?: 'openlist' | 'rclone' | 'app') {
|
||||
async function loadLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
|
||||
try {
|
||||
source = source || 'openlist'
|
||||
const logEntries = await TauriAPI.logs.get(source)
|
||||
@@ -563,9 +563,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function clearLogs(source?: 'openlist' | 'rclone' | 'app') {
|
||||
async function clearLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
|
||||
try {
|
||||
loading.value = true
|
||||
source = source || 'openlist'
|
||||
const result = await TauriAPI.logs.clear(source)
|
||||
if (result) {
|
||||
logs.value = []
|
||||
@@ -604,6 +605,46 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function openLogsDirectory() {
|
||||
try {
|
||||
await TauriAPI.files.openLogsDirectory()
|
||||
} catch (err) {
|
||||
error.value = 'Failed to open logs directory'
|
||||
console.error('Failed to open logs directory:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function openOpenListDataDir() {
|
||||
try {
|
||||
await TauriAPI.files.openOpenListDataDir()
|
||||
} catch (err) {
|
||||
error.value = 'Failed to open openlist data directory'
|
||||
console.error('Failed to open openlist data directory:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function openRcloneConfigFile() {
|
||||
try {
|
||||
await TauriAPI.files.openRcloneConfigFile()
|
||||
} catch (err) {
|
||||
error.value = 'Failed to open rclone config file'
|
||||
console.error('Failed to open rclone config file:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function openSettingsFile() {
|
||||
try {
|
||||
await TauriAPI.files.openSettingsFile()
|
||||
} catch (err) {
|
||||
error.value = 'Failed to open settings file'
|
||||
console.error('Failed to open settings file:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function selectDirectory(title: string): Promise<string | null> {
|
||||
try {
|
||||
return await TauriAPI.util.selectDirectory(title)
|
||||
@@ -789,6 +830,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
listFiles,
|
||||
openFile,
|
||||
openFolder,
|
||||
openLogsDirectory,
|
||||
openOpenListDataDir,
|
||||
openRcloneConfigFile,
|
||||
openSettingsFile,
|
||||
selectDirectory,
|
||||
clearError,
|
||||
init,
|
||||
|
||||
@@ -18,9 +18,13 @@ import {
|
||||
Minimize2,
|
||||
AlertCircle,
|
||||
Info,
|
||||
AlertTriangle
|
||||
AlertTriangle,
|
||||
FolderOpen
|
||||
} from 'lucide-vue-next'
|
||||
import * as chrono from 'chrono-node'
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
|
||||
|
||||
type filterSourceType = 'openlist' | 'rclone' | 'app' | 'service' | 'all'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { t } = useTranslation()
|
||||
@@ -29,10 +33,10 @@ const searchInputRef = ref<HTMLInputElement>()
|
||||
const autoScroll = ref(true)
|
||||
const isPaused = ref(false)
|
||||
const filterLevel = ref<string>('all')
|
||||
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'all')
|
||||
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'openlist')
|
||||
const searchQuery = ref('')
|
||||
const selectedEntries = ref<Set<number>>(new Set())
|
||||
const showFilters = ref(false)
|
||||
const showFilters = ref(true)
|
||||
const showSettings = ref(false)
|
||||
const fontSize = ref(13)
|
||||
const maxLines = ref(1000)
|
||||
@@ -42,14 +46,19 @@ const stripAnsiColors = ref(true)
|
||||
const showNotification = ref(false)
|
||||
const notificationMessage = ref('')
|
||||
const notificationType = ref<'success' | 'info' | 'warning' | 'error'>('success')
|
||||
const showConfirmDialog = ref(false)
|
||||
const confirmDialogConfig = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {}
|
||||
})
|
||||
|
||||
watch(
|
||||
filterSource,
|
||||
newValue => {
|
||||
localStorage.setItem('logFilterSource', newValue)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(filterSource, async newValue => {
|
||||
localStorage.setItem('logFilterSource', newValue)
|
||||
await appStore.loadLogs((newValue !== 'gin' ? newValue : 'openlist') as filterSourceType)
|
||||
await scrollToBottom()
|
||||
})
|
||||
|
||||
let logRefreshInterval: NodeJS.Timeout | null = null
|
||||
|
||||
@@ -63,6 +72,16 @@ const showNotificationMessage = (message: string, type: 'success' | 'info' | 'wa
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const openLogsDirectory = async () => {
|
||||
try {
|
||||
await appStore.openLogsDirectory()
|
||||
showNotificationMessage(t('logs.notifications.openDirectorySuccess'), 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to open logs directory:', error)
|
||||
showNotificationMessage(t('logs.notifications.openDirectoryFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const stripAnsiCodes = (text: string): string => {
|
||||
return text.replace(/\u001b\[[0-9;]*[mGKHF]/g, '')
|
||||
}
|
||||
@@ -104,11 +123,8 @@ const parseLogEntry = (logText: string) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cleanText.includes('openlist_desktop') || cleanText.includes('tao::')) {
|
||||
source = 'app'
|
||||
level = 'info'
|
||||
} else if (cleanText.toLowerCase().includes('rclone')) {
|
||||
source = 'rclone'
|
||||
} else {
|
||||
source = filterSource.value
|
||||
}
|
||||
|
||||
message = message
|
||||
@@ -187,21 +203,30 @@ const scrollToTop = () => {
|
||||
}
|
||||
|
||||
const clearLogs = async () => {
|
||||
if (confirm(t('logs.messages.confirmClear'))) {
|
||||
try {
|
||||
await appStore.clearLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
||||
| 'openlist'
|
||||
| 'rclone'
|
||||
| 'app'
|
||||
)
|
||||
selectedEntries.value.clear()
|
||||
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear logs:', error)
|
||||
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
|
||||
confirmDialogConfig.value = {
|
||||
title: t('logs.messages.confirmTitle') || t('common.confirm'),
|
||||
message: t('logs.messages.confirmClear'),
|
||||
onConfirm: async () => {
|
||||
showConfirmDialog.value = false
|
||||
try {
|
||||
await appStore.clearLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin'
|
||||
? filterSource.value
|
||||
: 'openlist') as filterSourceType
|
||||
)
|
||||
selectedEntries.value.clear()
|
||||
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear logs:', error)
|
||||
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
showConfirmDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
showConfirmDialog.value = true
|
||||
}
|
||||
|
||||
const copyLogsToClipboard = async () => {
|
||||
@@ -273,7 +298,9 @@ const togglePause = () => {
|
||||
}
|
||||
|
||||
const refreshLogs = async () => {
|
||||
await appStore.loadLogs()
|
||||
await appStore.loadLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType
|
||||
)
|
||||
await scrollToBottom()
|
||||
if (isPaused.value) {
|
||||
isPaused.value = false
|
||||
@@ -347,28 +374,16 @@ const handleKeydown = (event: KeyboardEvent) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
appStore
|
||||
.loadLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
||||
| 'openlist'
|
||||
| 'rclone'
|
||||
| 'app'
|
||||
)
|
||||
.then(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType).then(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
|
||||
logRefreshInterval = setInterval(async () => {
|
||||
if (!isPaused.value) {
|
||||
const oldLength = appStore.logs.length
|
||||
await appStore.loadLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
||||
| 'openlist'
|
||||
| 'rclone'
|
||||
| 'app'
|
||||
)
|
||||
await appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType)
|
||||
|
||||
if (appStore.logs.length > oldLength) {
|
||||
await scrollToBottom()
|
||||
@@ -485,6 +500,10 @@ onUnmounted(() => {
|
||||
<Trash2 :size="16" />
|
||||
</button>
|
||||
|
||||
<button class="toolbar-btn" @click="openLogsDirectory" :title="t('logs.toolbar.openLogsDirectory')">
|
||||
<FolderOpen :size="16" />
|
||||
</button>
|
||||
|
||||
<div class="toolbar-separator"></div>
|
||||
|
||||
<button class="toolbar-btn" @click="toggleFullscreen" :title="t('logs.toolbar.toggleFullscreen')">
|
||||
@@ -512,6 +531,7 @@ onUnmounted(() => {
|
||||
<option value="openlist">{{ t('logs.filters.sources.openlist') }}</option>
|
||||
<option value="gin">GIN Server</option>
|
||||
<option value="rclone">{{ t('logs.filters.sources.rclone') }}</option>
|
||||
<option value="service">{{ t('logs.filters.sources.service') }}</option>
|
||||
<option value="app">{{ t('logs.filters.app') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -639,6 +659,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<ConfirmDialog
|
||||
:is-open="showConfirmDialog"
|
||||
:title="confirmDialogConfig.title"
|
||||
:message="confirmDialogConfig.message"
|
||||
:confirm-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
variant="danger"
|
||||
@confirm="confirmDialogConfig.onConfirm"
|
||||
@cancel="confirmDialogConfig.onCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const commonFlags = ref([
|
||||
{ flag: '--vfs-cache-mode', value: 'minimal', descriptionKey: 'vfs-cache-mode-minimal' },
|
||||
{ flag: '--vfs-cache-max-age', value: '24h', descriptionKey: 'vfs-cache-max-age' },
|
||||
{ flag: '--vfs-cache-max-size', value: '10G', descriptionKey: 'vfs-cache-max-size' },
|
||||
{ flag: '--vfs-dir-cache-time', value: '5m', descriptionKey: 'vfs-dir-cache-time' }
|
||||
{ flag: 'dir-cache-time', value: '5m', descriptionKey: 'dir-cache-time' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,9 +3,20 @@ import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useTranslation } from '../composables/useI18n'
|
||||
import { Settings, Server, HardDrive, Save, RotateCcw, AlertCircle, CheckCircle, FolderOpen } from 'lucide-vue-next'
|
||||
import {
|
||||
Settings,
|
||||
Server,
|
||||
HardDrive,
|
||||
Save,
|
||||
RotateCcw,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
FolderOpen,
|
||||
ExternalLink
|
||||
} from 'lucide-vue-next'
|
||||
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
@@ -17,12 +28,20 @@ const activeTab = ref('openlist')
|
||||
const rcloneConfigJson = ref('')
|
||||
const autoStartApp = ref(false)
|
||||
const isResettingPassword = ref(false)
|
||||
const showConfirmDialog = ref(false)
|
||||
const confirmDialogConfig = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {}
|
||||
})
|
||||
|
||||
const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
|
||||
const rcloneSettings = reactive({ ...appStore.settings.rclone })
|
||||
const appSettings = reactive({ ...appStore.settings.app })
|
||||
let originalOpenlistPort = openlistCoreSettings.port || 5244
|
||||
let originalDataDir = openlistCoreSettings.data_dir
|
||||
let originalAdminPassword = appStore.settings.app.admin_password || ''
|
||||
|
||||
watch(autoStartApp, async newValue => {
|
||||
if (newValue) {
|
||||
@@ -117,7 +136,6 @@ const handleSave = async () => {
|
||||
appStore.settings.rclone = { ...rcloneSettings }
|
||||
appStore.settings.app = { ...appSettings }
|
||||
|
||||
const originalAdminPassword = appStore.settings.app.admin_password
|
||||
const needsPasswordUpdate = originalAdminPassword !== appSettings.admin_password && appSettings.admin_password
|
||||
|
||||
if (originalOpenlistPort !== openlistCoreSettings.port || originalDataDir !== openlistCoreSettings.data_dir) {
|
||||
@@ -157,24 +175,33 @@ const handleSave = async () => {
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
if (!confirm(t('settings.confirmReset'))) {
|
||||
return
|
||||
confirmDialogConfig.value = {
|
||||
title: t('settings.confirmReset.title'),
|
||||
message: t('settings.confirmReset.message'),
|
||||
onConfirm: async () => {
|
||||
showConfirmDialog.value = false
|
||||
|
||||
try {
|
||||
await appStore.resetSettings()
|
||||
Object.assign(openlistCoreSettings, appStore.settings.openlist)
|
||||
Object.assign(rcloneSettings, appStore.settings.rclone)
|
||||
Object.assign(appSettings, appStore.settings.app)
|
||||
|
||||
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
|
||||
|
||||
message.value = t('settings.resetSuccess')
|
||||
messageType.value = 'info'
|
||||
} catch (error) {
|
||||
message.value = t('settings.resetFailed')
|
||||
messageType.value = 'error'
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
showConfirmDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await appStore.resetSettings()
|
||||
Object.assign(openlistCoreSettings, appStore.settings.openlist)
|
||||
Object.assign(rcloneSettings, appStore.settings.rclone)
|
||||
Object.assign(appSettings, appStore.settings.app)
|
||||
|
||||
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
|
||||
|
||||
message.value = t('settings.resetSuccess')
|
||||
messageType.value = 'info'
|
||||
} catch (error) {
|
||||
message.value = t('settings.resetFailed')
|
||||
messageType.value = 'error'
|
||||
}
|
||||
showConfirmDialog.value = true
|
||||
}
|
||||
|
||||
const handleSelectDataDir = async () => {
|
||||
@@ -199,6 +226,26 @@ const handleSelectDataDir = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenDataDir = async () => {
|
||||
try {
|
||||
if (openlistCoreSettings.data_dir) {
|
||||
await appStore.openFolder(openlistCoreSettings.data_dir)
|
||||
} else {
|
||||
await appStore.openOpenListDataDir()
|
||||
}
|
||||
message.value = t('settings.service.network.dataDir.openSuccess')
|
||||
messageType.value = 'success'
|
||||
} catch (error) {
|
||||
console.error('Failed to open data directory:', error)
|
||||
message.value = t('settings.service.network.dataDir.openError')
|
||||
messageType.value = 'error'
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
message.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetAdminPassword = async () => {
|
||||
isResettingPassword.value = true
|
||||
try {
|
||||
@@ -224,11 +271,44 @@ const handleResetAdminPassword = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenRcloneConfig = async () => {
|
||||
try {
|
||||
await appStore.openRcloneConfigFile()
|
||||
message.value = t('settings.rclone.config.openSuccess')
|
||||
messageType.value = 'success'
|
||||
} catch (error) {
|
||||
console.error('Failed to open rclone config file:', error)
|
||||
message.value = t('settings.rclone.config.openError')
|
||||
messageType.value = 'error'
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
message.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenSettingsFile = async () => {
|
||||
try {
|
||||
await appStore.openSettingsFile()
|
||||
message.value = t('settings.app.config.openSuccess')
|
||||
messageType.value = 'success'
|
||||
} catch (error) {
|
||||
console.error('Failed to open settings file:', error)
|
||||
message.value = t('settings.app.config.openError')
|
||||
messageType.value = 'error'
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
message.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const loadCurrentAdminPassword = async () => {
|
||||
try {
|
||||
const password = await appStore.getAdminPassword()
|
||||
if (password) {
|
||||
appSettings.admin_password = password
|
||||
originalAdminPassword = password
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load admin password:', error)
|
||||
@@ -313,6 +393,14 @@ const loadCurrentAdminPassword = async () => {
|
||||
>
|
||||
<FolderOpen :size="16" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleOpenDataDir"
|
||||
class="input-addon-btn"
|
||||
:title="t('settings.service.network.dataDir.openTitle')"
|
||||
>
|
||||
<ExternalLink :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<small>{{ t('settings.service.network.dataDir.help') }}</small>
|
||||
</div>
|
||||
@@ -381,6 +469,17 @@ const loadCurrentAdminPassword = async () => {
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ t('settings.rclone.config.label') }}</label>
|
||||
<div class="settings-section-actions">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleOpenRcloneConfig"
|
||||
class="btn btn-secondary"
|
||||
:title="t('settings.rclone.config.openFile')"
|
||||
>
|
||||
<ExternalLink :size="16" />
|
||||
{{ t('settings.rclone.config.openFile') }}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="rcloneConfigJson"
|
||||
class="form-textarea"
|
||||
@@ -415,6 +514,25 @@ const loadCurrentAdminPassword = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>{{ t('settings.app.config.title') }}</h2>
|
||||
<p>{{ t('settings.app.config.subtitle') }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="settings-section-actions">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleOpenSettingsFile"
|
||||
class="btn btn-secondary"
|
||||
:title="t('settings.app.config.openFile')"
|
||||
>
|
||||
<ExternalLink :size="16" />
|
||||
{{ t('settings.app.config.openFile') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>{{ t('settings.app.ghProxy.title') }}</h2>
|
||||
<p>{{ t('settings.app.ghProxy.subtitle') }}</p>
|
||||
@@ -493,6 +611,17 @@ const loadCurrentAdminPassword = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
:is-open="showConfirmDialog"
|
||||
:title="confirmDialogConfig.title"
|
||||
:message="confirmDialogConfig.message"
|
||||
:confirm-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
variant="danger"
|
||||
@confirm="confirmDialogConfig.onConfirm"
|
||||
@cancel="confirmDialogConfig.onCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -527,3 +527,33 @@
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings Section Actions */
|
||||
.settings-section-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-section-actions .btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-group .input-addon-btn:last-child:not(:only-child) {
|
||||
border-radius: 0 8px 8px 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.input-group .input-addon-btn:not(:first-child):not(:last-child) {
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.input-group .input-addon-btn:first-child:not(:only-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user