19 Commits

Author SHA1 Message Date
Kuingsmile
15d789ef3a chore: bump version to 0.6.1 2025-07-23 13:46:31 +08:00
Kuingsmile
d6ff164b0c feat: add GitHub Actions workflow for cross-platform build testing 2025-07-23 13:43:38 +08:00
Kuingsmile
7f7cc8c8ce refactor: remove unnecessary logging in Rclone status check 2025-07-23 11:44:06 +08:00
Kuingsmile
9277c9380c feat: optimize rclone backend status check method 2025-07-23 11:38:06 +08:00
Kuingsmile
a6c83fe289 fix: fix reset admin passwd error on windows close #69 2025-07-23 10:35:12 +08:00
Kuingsmile
c47fc1443b feat: update macOS link opening behavior to allow opening in browser #71 2025-07-23 09:59:16 +08:00
GitHub Action
954ee010c1 chore: bump version to 0.6.0 [skip ci] 2025-07-22 09:49:13 +00:00
Kuingsmile
c219afa54e fix: update service log path for macOS 2025-07-22 17:30:29 +08:00
Kuingsmile
06e54d1b01 feat: add service log display in log page 2025-07-22 17:07:44 +08:00
Kuingsmile
386147d5ff feat: add functionality to open various directories and configuration files, change default path on macos close #63 2025-07-22 15:50:28 +08:00
Kuingsmile
2eeba5f428 fix: fix an issue that url can't be opened on macos #63 2025-07-22 13:26:43 +08:00
Kuingsmile
dc1cb41e61 feat: enhance version update notifications 2025-07-22 11:41:20 +08:00
Kuingsmile
99c426c15c fix: fix a bug makes log filter not working #63 2025-07-21 17:51:27 +08:00
Kuingsmile
77f9f81dea feat: add loading indicators and processing state for core and rclone actions close #66 2025-07-21 17:40:01 +08:00
Kuingsmile
5cc2c1640c feat: improve service stop status check #66 2025-07-21 17:19:42 +08:00
Kuingsmile
24b45446cc fix: set current directory for command execution and manage admin password state #66 2025-07-21 16:44:30 +08:00
Kuingsmile
f3cc4a021b fix: fix an issue processes can't be started when accessing from RDP, #68 2025-07-21 15:41:33 +08:00
Kuingsmile
b6cfda7648 feat: add SHA-256 hash calculation for binary files for better debug trace #63 2025-07-20 18:38:18 +08:00
Kuingsmile
3b9910da0a fix: fix vfs-dir-cache-time to dir-cache-time #63 2025-07-20 17:51:34 +08:00
28 changed files with 1260 additions and 192 deletions

424
.github/workflows/build-test.yml vendored Normal file
View File

@@ -0,0 +1,424 @@
name: 'Build Test - All Platforms'
on:
workflow_dispatch:
inputs:
test_version:
description: 'Test version (e.g., 1.0.0-test). Leave empty to use package.json version with -test suffix'
required: false
type: string
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# macOS signing and notarization (optional for testing)
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: true
jobs:
prepare:
name: Prepare Build Information
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
build-date: ${{ steps.version.outputs.build-date }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Calculate test version
id: version
run: |
# If manual test version is provided, use it
if [ -n "${{ inputs.test_version }}" ]; then
TEST_VERSION="${{ inputs.test_version }}"
echo "Using manual test version: $TEST_VERSION"
echo "version=$TEST_VERSION" >> $GITHUB_OUTPUT
else
# Use package.json version with test suffix
CURRENT_VERSION=$(node -p "require('./package.json').version")
TEST_VERSION="$CURRENT_VERSION-test"
echo "Using auto test version: $TEST_VERSION"
echo "version=$TEST_VERSION" >> $GITHUB_OUTPUT
fi
# Add build date for reference
BUILD_DATE=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
echo "build-date=$BUILD_DATE" >> $GITHUB_OUTPUT
build:
name: Build
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
platform: windows
arch: x64
- os: windows-latest
target: aarch64-pc-windows-msvc
platform: windows
arch: arm64
- os: macos-latest
target: aarch64-apple-darwin
platform: macos
arch: arm64
- os: macos-latest
target: x86_64-apple-darwin
platform: macos
arch: x64
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
platform: linux
arch: x64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
uses: borales/actions-yarn@v5
with:
cmd: install
- name: Prebuild and check
run: |
yarn install
yarn run prebuild:dev --target=${{ matrix.target }}
- name: Update version for test build (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
# Update package.json
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
# Update Cargo.toml
(Get-Content src-tauri/Cargo.toml) -replace '^version = ".*"', 'version = "${{ needs.prepare.outputs.version }}"' | Set-Content src-tauri/Cargo.toml
# Update tauri.conf.json
(Get-Content src-tauri/tauri.conf.json) -replace '"version": ".*"', '"version": "${{ needs.prepare.outputs.version }}"' | Set-Content src-tauri/tauri.conf.json
- name: Update version for test build (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
# Update package.json
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
# Update Cargo.toml
sed -i '' 's/^version = "[^"]*"/version = "${{ needs.prepare.outputs.version }}"/' src-tauri/Cargo.toml
# Update tauri.conf.json
sed -i '' 's/"version": "[^"]*"/"version": "${{ needs.prepare.outputs.version }}"/' src-tauri/tauri.conf.json
- name: Update version for test build (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
# Update package.json
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
# Update Cargo.toml
sed -i 's/^version = "[^"]*"/version = "${{ needs.prepare.outputs.version }}"/' src-tauri/Cargo.toml
# Update tauri.conf.json
sed -i 's/"version": "[^"]*"/"version": "${{ needs.prepare.outputs.version }}"/' src-tauri/tauri.conf.json
- name: Import Apple Developer Certificate (macOS only)
if: matrix.os == 'macos-latest' && env.APPLE_CERTIFICATE != ''
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Import Windows certificate
if: matrix.os == 'windows-latest' && env.WINDOWS_CERTIFICATE != ''
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
if ($env:WINDOWS_CERTIFICATE) {
New-Item -ItemType directory -Path certificate
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
Remove-Item -path certificate -include tempCert.txt
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
}
- name: Build the app
if: matrix.platform == 'windows' || matrix.platform == 'linux'
run: |
yarn build --target ${{ matrix.target }}
env:
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# macOS signing and notarization environment variables
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
- name: Build the app (macOS)
if: matrix.platform == 'macos'
run: |
export TAURI_SKIP_SIDECAR_SIGNATURE_CHECK=true
yarn build --target ${{ matrix.target }}
env:
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# macOS signing and notarization environment variables
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}-${{ matrix.arch }}-build
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/**/*
retention-days: 30
build-linux-arm:
name: Build Linux ARM
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
target: aarch64-unknown-linux-gnu
arch: arm64
- os: ubuntu-22.04
target: armv7-unknown-linux-gnueabihf
arch: armhf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
uses: borales/actions-yarn@v5
with:
cmd: install
- name: Prebuild and check
run: |
yarn install
yarn run prebuild:dev --target=${{ matrix.target }}
- name: Update version for test build
run: |
# Update package.json
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
# Update Cargo.toml
sed -i 's/^version = "[^"]*"/version = "${{ needs.prepare.outputs.version }}"/' src-tauri/Cargo.toml
# Update tauri.conf.json
sed -i 's/"version": "[^"]*"/"version": "${{ needs.prepare.outputs.version }}"/' src-tauri/tauri.conf.json
- name: Setup for Linux ARM cross-compilation
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
EOF
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
sudo mv /tmp/sources.list /etc/apt/sources.list
sudo dpkg --add-architecture ${{ matrix.arch }}
sudo apt update
sudo apt install -y \
libxslt1.1:${{ matrix.arch }} \
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
libayatana-appindicator3-dev:${{ matrix.arch }} \
libssl-dev:${{ matrix.arch }} \
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: Install aarch64 tools
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: Install armv7 tools
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux ARM
run: |
export PKG_CONFIG_ALLOW_CROSS=1
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
fi
yarn build --target ${{ matrix.target }}
env:
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Upload Linux ARM artifacts
uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.target }}-build
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
retention-days: 30
summary:
name: Build Summary
needs: [prepare, build, build-linux-arm]
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create build summary
run: |
echo "# Build Test Summary" > build-summary.md
echo "" >> build-summary.md
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> build-summary.md
echo "**Build Date:** ${{ needs.prepare.outputs.build-date }}" >> build-summary.md
echo "**Commit:** ${{ github.sha }}" >> build-summary.md
echo "**Branch:** ${{ github.ref_name }}" >> build-summary.md
echo "" >> build-summary.md
echo "## Build Status" >> build-summary.md
echo "" >> build-summary.md
# Check build status
if [ "${{ needs.build.result }}" == "success" ]; then
echo "✅ **Main platforms build:** Success" >> build-summary.md
else
echo "❌ **Main platforms build:** Failed" >> build-summary.md
fi
if [ "${{ needs.build-linux-arm.result }}" == "success" ]; then
echo "✅ **Linux ARM build:** Success" >> build-summary.md
else
echo "❌ **Linux ARM build:** Failed" >> build-summary.md
fi
echo "" >> build-summary.md
echo "## Available Artifacts" >> build-summary.md
echo "" >> build-summary.md
echo "The following build artifacts are available for download from the Actions tab:" >> build-summary.md
echo "" >> build-summary.md
# List artifacts
if [ -d "artifacts" ]; then
find artifacts -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.pkg" -o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | while read file; do
echo "- $(basename "$file")" >> build-summary.md
done
fi
echo "" >> build-summary.md
echo "## Testing Instructions" >> build-summary.md
echo "" >> build-summary.md
echo "1. Download the appropriate artifact for your platform from the GitHub Actions page" >> build-summary.md
echo "2. Extract and install the application" >> build-summary.md
echo "3. Test the functionality" >> build-summary.md
echo "4. Report any issues found" >> build-summary.md
- name: Upload build summary
uses: actions/upload-artifact@v4
with:
name: build-summary
path: build-summary.md
retention-days: 30

View File

@@ -9,7 +9,7 @@
"tauri"
],
"private": true,
"version": "0.5.1",
"version": "0.6.1",
"author": {
"name": "OpenList Team",
"email": "96409857+Kuingsmile@users.noreply.github.com"

View File

@@ -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 })

36
src-tauri/Cargo.lock generated
View File

@@ -2601,6 +2601,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -2790,6 +2799,16 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.1"
@@ -2898,7 +2917,7 @@ dependencies = [
[[package]]
name = "openlist-desktop"
version = "0.5.1"
version = "0.6.1"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -2917,6 +2936,7 @@ dependencies = [
"runas",
"serde",
"serde_json",
"sysinfo",
"tar",
"tauri",
"tauri-build",
@@ -4492,6 +4512,20 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "sysinfo"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]]
name = "system-configuration"
version = "0.6.1"

View File

@@ -1,6 +1,6 @@
[package]
name = "openlist-desktop"
version = "0.5.1"
version = "0.6.1"
description = "A Tauri App"
authors = ["Kuingsmile"]
edition = "2024"
@@ -45,6 +45,7 @@ zip = "4.2.0"
tar = "0.4.44"
flate2 = "1.1.2"
regex = "1.11.1"
sysinfo = "0.36.1"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"

View File

@@ -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() {

View File

@@ -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,15 +74,28 @@ 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:?}");
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
let output = cmd
.output()
.map_err(|e| format!("Failed to execute openlist command: {e}"))?;
@@ -100,30 +114,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 +233,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)
}

View File

@@ -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(),

View File

@@ -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)
}

View File

@@ -1,12 +1,11 @@
use std::time::Duration;
use reqwest::{self, Client};
use reqwest;
use sysinfo::System;
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 +39,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();
@@ -110,13 +107,19 @@ pub async fn get_rclone_backend_status(_state: State<'_, AppState>) -> Result<bo
}
async fn is_rclone_running() -> bool {
let client = Client::new();
let mut system = System::new_all();
system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let response = client
.get(format!("{RCLONE_API_BASE}/"))
.timeout(Duration::from_secs(3))
.send()
.await;
for (_pid, process) in system.processes() {
let process_name = process.name().to_string_lossy().to_lowercase();
response.is_ok()
if process_name.contains("rclone") {
let cmd_args = process.cmd();
if cmd_args.iter().any(|arg| arg == "rcd") {
return true;
}
}
}
false
}

View File

@@ -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();

View File

@@ -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"))
}
}

View File

@@ -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,

View File

@@ -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"))
}
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenList Desktop",
"version": "0.5.1",
"version": "0.6.1",
"identifier": "io.github.openlistteam.openlist.desktop",
"build": {
"beforeDevCommand": "yarn run dev",

View File

@@ -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'),

View File

@@ -25,16 +25,22 @@ const navigationItems = computed(() => [
}
])
const isMacOs = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'darwin'
})
const openLink = async (url: string) => {
try {
if (appStore.settings.app.open_links_in_browser) {
if (appStore.settings.app.open_links_in_browser || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
}
window.open(url, '_blank')
setTimeout(() => {
window.open(url, '_blank')
})
}
</script>

View File

@@ -80,6 +80,7 @@ import { ExternalLink, Github, BookOpen, Cloud, Code, Terminal, HelpCircle, Mess
import Card from '../ui/Card.vue'
import { TauriAPI } from '../../api/tauri'
import { useAppStore } from '../../stores/app'
import { computed } from 'vue'
const { t } = useTranslation()
const appStore = useAppStore()
@@ -100,16 +101,22 @@ const openRcloneGitHub = () => {
openLink('https://github.com/rclone/rclone')
}
const isMacOs = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'darwin'
})
const openLink = async (url: string) => {
try {
if (appStore.settings.app.open_links_in_browser) {
if (appStore.settings.app.open_links_in_browser || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
}
window.open(url, '_blank')
setTimeout(() => {
window.open(url, '_blank')
})
}
</script>

View File

@@ -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
@@ -400,16 +426,22 @@ const showNotification = (type: 'success' | 'error', message: string) => {
}, 4000)
}
const isMacOs = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'darwin'
})
const openLink = async (url: string) => {
try {
if (appStore.settings.app.open_links_in_browser) {
if (appStore.settings.app.open_links_in_browser || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
}
window.open(url, '_blank')
setTimeout(() => {
window.open(url, '_blank')
})
}
onMounted(async () => {
@@ -452,6 +484,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 +551,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);

View File

@@ -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(() => {

View File

@@ -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()
})

View File

@@ -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",
@@ -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",

View File

@@ -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": "时间",
@@ -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": "基于时间的带宽限制",

View File

@@ -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,

View File

@@ -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()
@@ -481,10 +496,19 @@ onUnmounted(() => {
<Download :size="16" />
</button>
<button class="toolbar-btn danger" @click="clearLogs" :title="t('logs.toolbar.clearLogs')">
<button
class="toolbar-btn danger"
@click="clearLogs"
:disabled="filteredLogs.length === 0 || filterSource === 'gin' || filterSource === 'all'"
:title="t('logs.toolbar.clearLogs')"
>
<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 +536,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 +664,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>

View File

@@ -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' }
]
},
{

View File

@@ -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 {
@@ -207,7 +254,6 @@ const handleResetAdminPassword = async () => {
appSettings.admin_password = newPassword
message.value = t('settings.service.admin.resetSuccess')
messageType.value = 'success'
await navigator.clipboard.writeText(newPassword)
} else {
message.value = t('settings.service.admin.resetFailed')
messageType.value = 'error'
@@ -224,11 +270,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 +392,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 +468,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 +513,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 +610,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>

View File

@@ -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;
}