37 Commits

Author SHA1 Message Date
renovate[bot]
9495db4f7f Update dependency typescript to v5.9.3 2025-11-10 16:08:09 +00:00
GitHub Action
f3a5e2556f chore: bump version to 0.8.0 [skip ci] 2025-11-08 04:29:05 +00:00
Suyunmeng
a3a6ef03a5 feat(ui): persist log filter preferences in settings file
This commit improves the log viewer by persisting user filter preferences
across page navigation and application restarts.

Changes:
- Add log_filter_level and log_filter_source fields to AppConfig
- Store filter preferences in settings.json instead of localStorage
- Ensure filter preferences survive app restarts and page switches
- Fix clippy warning: use &Path instead of &PathBuf in normalize_path

The filter level and source selections are now saved to the persistent
settings file, providing a more reliable storage mechanism for Tauri
applications.
2025-11-08 11:19:18 +08:00
Suyunmeng
e4ab2184eb fix(data): fix data directory errors and log display issues 2025-11-08 09:53:57 +08:00
Suyunjing
4c76f31885 chore(ci): update ci configurations 2025-10-30 15:48:58 +08:00
Yinan Qin
68411aaaf3 fix!: update log and data dirs for linux and windows (#108)
* fix: update log dir for linux and windows

Signed-off-by: Yinan Qin <39023210+elysia-best@users.noreply.github.com>

* fix: update log dirs for linux and windows

Signed-off-by: Yinan Qin <39023210+elysia-best@users.noreply.github.com>

* chore: refactor APPDATA variable retrieval for clarity

Signed-off-by: Yinan Qin <39023210+elysia-best@users.noreply.github.com>

* fix: update version to 0.7.0 and improve data directory handling

* fix: fix clippy error

---------

Signed-off-by: Yinan Qin <39023210+elysia-best@users.noreply.github.com>
2025-10-27 11:06:31 +08:00
GitHub Action
e00c0d5ffd chore: bump version to 0.7.0 [skip ci] 2025-08-28 07:18:22 +00:00
Kuingsmile
ca0b9251d3 Feature(custom): change default value of show window on startup to true 2025-08-28 15:11:34 +08:00
Kuingsmile
94027733fc Feature(custom): add visible false in macos configuration 2025-08-28 13:23:49 +08:00
Kuingsmile
674c9b0041 Feature(custom): add option to show/hide main window on startup 2025-08-27 17:55:29 +08:00
Kuingsmile
a81f0ed9eb 📦 Chore(custom): add prettier rules 2025-08-27 14:13:30 +08:00
Kuingsmile
d4a42de814 Feature(custom): optimize some setting description 2025-08-26 10:08:53 +08:00
Kuingsmile
bf0f481086 🐛 Fix(custom): update padding for dashboard container and grid for improved layout 2025-08-26 09:53:39 +08:00
Kuingsmile
6d6c38eab9 feat: use task scheduler to start service 2025-08-25 17:56:04 +08:00
Kuingsmile
d2c834d936 Feature(custom): add rclone api port setting
ISSUES CLOSED: #73
2025-08-25 15:28:00 +08:00
Kuingsmile
31980949a3 Feature(custom): add show_window_on_startup option
ISSUES CLOSED: #77
2025-08-25 14:43:56 +08:00
Kuingsmile
58b7128e4e Feature(custom): add show_window_on_startup option
ISSUES CLOSED: #77
2025-08-25 14:43:10 +08:00
Kuingsmile
62f2710a7c 🐛 Fix(custom): fix dir cache time flag
ISSUES CLOSED: #80
2025-08-20 11:54:54 +08:00
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
57 changed files with 4285 additions and 2136 deletions

View File

@@ -0,0 +1,230 @@
# Connect-SimplySign-Enhanced.ps1
# Registry-Enhanced TOTP Authentication for SimplySign Desktop
# Uses registry pre-configuration + TOTP credential injection approach
param(
[string]$OtpUri = $env:CERTUM_OTP_URI,
[string]$UserId = $env:CERTUM_USERNAME,
[string]$ExePath = $env:CERTUM_EXE_PATH
)
# Validate required parameters
if (-not $OtpUri) {
Write-Host "ERROR: CERTUM_OTP_URI environment variable not provided"
exit 1
}
if (-not $UserId) {
Write-Host "ERROR: CERTUM_USERNAME environment variable not provided"
exit 1
}
if (-not $ExePath) {
$ExePath = "C:\Program Files\Certum\SimplySign Desktop\SimplySignDesktop.exe"
}
Write-Host "=== REGISTRY-ENHANCED TOTP AUTHENTICATION ==="
Write-Host "Using registry pre-configuration + credential injection"
Write-Host "OTP URI provided (length: $($OtpUri.Length))"
Write-Host "User ID: $UserId"
Write-Host "Executable: $ExePath"
Write-Host ""
# Verify SimplySign Desktop exists
if (-not (Test-Path $ExePath)) {
Write-Host "ERROR: SimplySign Desktop not found at: $ExePath"
exit 1
}
# Parse the otpauth:// URI
$uri = [Uri]$OtpUri
# Parse query parameters (compatible with both PowerShell 5.1 and 7+)
try {
$q = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
} catch {
$q = @{}
foreach ($part in $uri.Query.TrimStart('?') -split '&') {
$kv = $part -split '=', 2
if ($kv.Count -eq 2) {
$q[$kv[0]] = [Uri]::UnescapeDataString($kv[1])
}
}
}
$Base32 = $q['secret']
$Digits = if ($q['digits']) { [int]$q['digits'] } else { 6 }
$Period = if ($q['period']) { [int]$q['period'] } else { 30 }
$Algorithm = if ($q['algorithm']) { $q['algorithm'].ToUpper() } else { 'SHA256' }
# Validate supported algorithms
$SupportedAlgorithms = @('SHA1', 'SHA256', 'SHA512')
if ($Algorithm -notin $SupportedAlgorithms) {
Write-Host "ERROR: Unsupported algorithm: $Algorithm. Supported: $($SupportedAlgorithms -join ', ')"
exit 1
}
# TOTP Generator (inline C# implementation)
Add-Type -Language CSharp @"
using System;
using System.Security.Cryptography;
public static class Totp
{
private const string B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static byte[] Base32Decode(string s)
{
s = s.TrimEnd('=').ToUpperInvariant();
int byteCount = s.Length * 5 / 8;
byte[] bytes = new byte[byteCount];
int bitBuffer = 0, bitsLeft = 0, idx = 0;
foreach (char c in s)
{
int val = B32.IndexOf(c);
if (val < 0) throw new ArgumentException("Invalid Base32 char: " + c);
bitBuffer = (bitBuffer << 5) | val;
bitsLeft += 5;
if (bitsLeft >= 8)
{
bytes[idx++] = (byte)(bitBuffer >> (bitsLeft - 8));
bitsLeft -= 8;
}
}
return bytes;
}
private static HMAC GetHmacAlgorithm(string algorithm, byte[] key)
{
switch (algorithm.ToUpper())
{
case "SHA1":
return new HMACSHA1(key);
case "SHA256":
return new HMACSHA256(key);
case "SHA512":
return new HMACSHA512(key);
default:
throw new ArgumentException("Unsupported algorithm: " + algorithm);
}
}
public static string Now(string secret, int digits, int period, string algorithm = "SHA256")
{
byte[] key = Base32Decode(secret);
long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / period;
byte[] cnt = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian) Array.Reverse(cnt);
byte[] hash;
using (var hmac = GetHmacAlgorithm(algorithm, key))
{
hash = hmac.ComputeHash(cnt);
}
int offset = hash[hash.Length - 1] & 0x0F;
int binary =
((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
int otp = binary % (int)Math.Pow(10, digits);
return otp.ToString(new string('0', digits));
}
}
"@
function Get-TotpCode {
param([string]$Secret, [int]$Digits = 6, [int]$Period = 30, [string]$Algorithm = 'SHA256')
[Totp]::Now($Secret, $Digits, $Period, $Algorithm)
}
# Generate current TOTP code
$otp = Get-TotpCode -Secret $Base32 -Digits $Digits -Period $Period -Algorithm $Algorithm
Write-Host "Generated TOTP: $otp (using $Algorithm algorithm)"
Write-Host ""
# Launch SimplySign Desktop (registry should auto-open login dialog)
Write-Host "Launching SimplySign Desktop..."
Write-Host "Registry pre-configuration should auto-open login dialog"
$proc = Start-Process -FilePath $ExePath -PassThru
Write-Host "Process started with ID: $($proc.Id)"
Write-Host ""
# Wait for the application to initialize
Write-Host "Waiting for SimplySign Desktop to initialize..."
Start-Sleep -Seconds 3
# Create WScript.Shell for window interaction
$wshell = New-Object -ComObject WScript.Shell
# Try to focus the SimplySign Desktop window
Write-Host "Attempting to focus SimplySign Desktop window..."
$focused = $false
# Method 1: Focus by process ID (most reliable)
$focused = $wshell.AppActivate($proc.Id)
# Method 2: Focus by window title (fallback)
if (-not $focused) {
$focused = $wshell.AppActivate('SimplySign Desktop')
}
# Method 3: Multiple attempts with slight delays
for ($i = 0; (-not $focused) -and ($i -lt 10); $i++) {
Start-Sleep -Milliseconds 500
$focused = $wshell.AppActivate($proc.Id) -or $wshell.AppActivate('SimplySign Desktop')
Write-Host "Focus attempt $($i + 1): $focused"
}
if (-not $focused) {
Write-Host "ERROR: Could not bring SimplySign Desktop to foreground"
Write-Host "Login dialog may not be visible for credential injection"
exit 1
}
Write-Host "Successfully focused SimplySign Desktop window"
Write-Host ""
# Small delay to ensure window is ready for input
Start-Sleep -Milliseconds 400
# Inject credentials: Username + TAB + TOTP + ENTER
Write-Host "Injecting credentials into login dialog..."
Write-Host "Sending: Username -> TAB -> TOTP -> ENTER"
# Send the credential sequence
$wshell.SendKeys($UserId)
Start-Sleep -Milliseconds 200
$wshell.SendKeys("{TAB}")
Start-Sleep -Milliseconds 200
$wshell.SendKeys($otp)
Start-Sleep -Milliseconds 200
$wshell.SendKeys("{ENTER}")
Write-Host "Credentials injected successfully"
Write-Host ""
# Wait for authentication to process
Write-Host "Waiting for authentication to complete..."
Start-Sleep -Seconds 5
# Verify SimplySign Desktop is still running
$stillRunning = Get-Process -Id $proc.Id -ErrorAction SilentlyContinue
if ($stillRunning) {
Write-Host "SUCCESS: SimplySign Desktop is running"
Write-Host "Authentication should be complete"
Write-Host "Cloud certificate should now be available"
} else {
Write-Host "WARNING: SimplySign Desktop process has exited"
Write-Host "This may indicate authentication failure"
}
Write-Host ""
Write-Host "=== TOTP AUTHENTICATION COMPLETE ==="
Write-Host "Registry pre-configuration + credential injection finished"

View File

@@ -0,0 +1,252 @@
param(
[switch]$DebugMode = $false,
[switch]$VerifyOnly = $false
)
# SimplySign Desktop Registry Configuration Script
# Pre-configures optimal registry settings for automated login dialog display
Write-Host "=== SimplySign Desktop Registry Configuration ==="
if ($DebugMode) {
Write-Host "Debug mode enabled - verbose logging active"
}
# Registry path for SimplySign Desktop settings
$RegistryPath = "HKCU:\Software\Certum\SimplySign"
# Optimal configuration values for automation
$OptimalSettings = @{
"ShowLoginDialogOnStart" = 1
"ShowLoginDialogOnAppRequest" = 1
"RememberLastUserName" = 1
"Autostart" = 0
"UnregisterCertificatesOnDisconnect" = 0
"RememberPINinCSP" = 1
"ForgetPINinCSPonDisconnect" = 1
"LangID" = 9
}
# Function to check if registry path exists
function Test-RegistryPath {
param([string]$Path)
try {
$null = Get-Item -Path $Path -ErrorAction Stop
return $true
} catch {
return $false
}
}
# Function to get current registry value
function Get-RegistryValue {
param(
[string]$Path,
[string]$Name
)
try {
$value = Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop
return $value.$Name
} catch {
return $null
}
}
# Function to set registry value safely
function Set-RegistryValue {
param(
[string]$Path,
[string]$Name,
[int]$Value
)
try {
Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type DWord -ErrorAction Stop
if ($DebugMode) {
Write-Host " Set $Name = $Value"
}
return $true
} catch {
Write-Host " ERROR: Failed to set $Name = $Value - $($_.Exception.Message)"
return $false
}
}
# Function to display current settings
function Show-CurrentSettings {
Write-Host "Current SimplySign Desktop registry settings:"
Write-Host "============================================="
if (-not (Test-RegistryPath $RegistryPath)) {
Write-Host "Registry path does not exist: $RegistryPath"
return
}
foreach ($setting in $OptimalSettings.Keys) {
$currentValue = Get-RegistryValue -Path $RegistryPath -Name $setting
if ($null -eq $currentValue) {
Write-Host " $setting : NOT SET"
} else {
Write-Host " $setting : $currentValue"
}
}
Write-Host ""
}
# Function to create registry structure
function Initialize-RegistryStructure {
Write-Host "Initializing registry structure..."
# Create parent keys if they don't exist
$ParentPaths = @(
"HKCU:\Software\Certum",
$RegistryPath
)
$allCreated = $true
foreach ($path in $ParentPaths) {
if (-not (Test-RegistryPath $path)) {
try {
New-Item -Path $path -Force -ErrorAction Stop | Out-Null
if ($DebugMode) {
Write-Host " Created registry path: $path"
}
} catch {
Write-Host " ERROR: Failed to create registry path: $path - $($_.Exception.Message)"
$allCreated = $false
}
} else {
if ($DebugMode) {
Write-Host " Registry path exists: $path"
}
}
}
return $allCreated
}
# Function to apply optimal configuration
function Set-OptimalConfiguration {
Write-Host "Applying optimal configuration for automation..."
$successCount = 0
$totalSettings = $OptimalSettings.Count
foreach ($setting in $OptimalSettings.Keys) {
$value = $OptimalSettings[$setting]
if (Set-RegistryValue -Path $RegistryPath -Name $setting -Value $value) {
$successCount++
}
}
Write-Host "Applied $successCount of $totalSettings settings successfully"
return ($successCount -eq $totalSettings)
}
# Function to verify configuration
function Test-Configuration {
Write-Host "Verifying configuration..."
$verificationResults = @{}
$allCorrect = $true
foreach ($setting in $OptimalSettings.Keys) {
$expectedValue = $OptimalSettings[$setting]
$actualValue = Get-RegistryValue -Path $RegistryPath -Name $setting
$isCorrect = ($actualValue -eq $expectedValue)
$verificationResults[$setting] = @{
Expected = $expectedValue
Actual = $actualValue
Correct = $isCorrect
}
if (-not $isCorrect) {
$allCorrect = $false
}
if ($DebugMode -or -not $isCorrect) {
$status = if ($isCorrect) { "OK" } else { "MISMATCH" }
Write-Host " $setting : Expected=$expectedValue, Actual=$actualValue [$status]"
}
}
return $verificationResults, $allCorrect
}
# Main execution
try {
Write-Host "Starting registry configuration process..."
Write-Host ""
# Show current state
Write-Host "BEFORE CONFIGURATION:"
Show-CurrentSettings
if ($VerifyOnly) {
Write-Host "Verification-only mode - no changes will be made"
$verificationResults, $allCorrect = Test-Configuration
if ($allCorrect) {
Write-Host "SUCCESS: All settings are correctly configured"
exit 0
} else {
Write-Host "CONFIGURATION NEEDED: Some settings require adjustment"
exit 1
}
}
# Initialize registry structure
if (-not (Initialize-RegistryStructure)) {
Write-Host "FATAL ERROR: Failed to initialize registry structure"
exit 1
}
# Apply optimal configuration
if (-not (Set-OptimalConfiguration)) {
Write-Host "ERROR: Failed to apply complete configuration"
exit 1
}
Write-Host ""
Write-Host "AFTER CONFIGURATION:"
Show-CurrentSettings
# Verify the configuration was applied correctly
$verificationResults, $allCorrect = Test-Configuration
if ($allCorrect) {
Write-Host "SUCCESS: Registry configuration completed successfully"
Write-Host ""
Write-Host "Key automation settings enabled:"
Write-Host " ShowLoginDialogOnStart = 1 (Login dialog will appear automatically)"
Write-Host " ShowLoginDialogOnAppRequest = 1 (Dialog appears when apps request access)"
Write-Host " RememberLastUserName = 1 (Username persistence for efficiency)"
Write-Host ""
Write-Host "Next steps:"
Write-Host "1. Launch SimplySign Desktop"
Write-Host "2. Login dialog should appear automatically"
Write-Host "3. Complete authentication process"
# Create a status file for the workflow to check
"REGISTRY_CONFIGURATION_SUCCESS" | Out-File -FilePath "registry_config_status.log" -Encoding UTF8
exit 0
} else {
Write-Host "ERROR: Configuration verification failed"
Write-Host "Some settings were not applied correctly"
"REGISTRY_CONFIGURATION_PARTIAL" | Out-File -FilePath "registry_config_status.log" -Encoding UTF8
exit 1
}
} catch {
Write-Host "FATAL ERROR: Registry configuration failed - $($_.Exception.Message)"
"REGISTRY_CONFIGURATION_FAILED" | Out-File -FilePath "registry_config_status.log" -Encoding UTF8
exit 1
}

112
.github/scripts/install-simplysign.sh vendored Normal file
View File

@@ -0,0 +1,112 @@
#!/bin/bash
# Install SimplySign Desktop - Clean MSI Installation
set -euo pipefail
echo "=== INSTALLING SIMPLYSIGN DESKTOP ==="
echo "Using proven installation method from successful testing..."
# Download SimplySign Desktop MSI
CERTUM_INSTALLER="SimplySignDesktop.msi"
echo "Downloading SimplySign Desktop MSI..."
if curl -L "https://files.certum.eu/software/SimplySignDesktop/Windows/9.3.2.67/SimplySignDesktop-9.3.2.67-64-bit-en.msi" -o "$CERTUM_INSTALLER" --fail --max-time 60; then
echo "✅ Downloaded SimplySign Desktop MSI ($(ls -lh "$CERTUM_INSTALLER" | awk '{print $5}'))"
else
echo "❌ Failed to download SimplySign Desktop"
exit 1
fi
# Install with proven method (matching successful test)
echo "Installing SimplySign Desktop..."
echo "Full command: msiexec /i \"$CERTUM_INSTALLER\" /quiet /norestart /l*v install.log ALLUSERS=1 REBOOT=ReallySuppress"
# Check for administrative privileges (like the successful test)
ADMIN_RIGHTS=false
if powershell -Command "([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)" 2>/dev/null; then
echo "✅ Running with administrative privileges"
ADMIN_RIGHTS=true
else
echo "⚠️ No explicit administrative privileges detected"
fi
# Use the exact method that worked: PowerShell with admin privileges
if [ "$ADMIN_RIGHTS" = true ]; then
echo "Running MSI installation with administrator privileges..."
powershell -Command "Start-Process -FilePath 'msiexec.exe' -ArgumentList '/i', '\"$CERTUM_INSTALLER\"', '/quiet', '/norestart', '/l*v', 'install.log', 'ALLUSERS=1', 'REBOOT=ReallySuppress' -Wait -NoNewWindow -PassThru" &
INSTALL_PID=$!
else
echo "Running MSI installation without explicit admin elevation..."
timeout 300 msiexec /i "$CERTUM_INSTALLER" /quiet /norestart /l*v install.log ALLUSERS=1 REBOOT=ReallySuppress &
INSTALL_PID=$!
fi
# Monitor with the same logic as successful test
echo "Monitoring installation progress..."
INSTALL_START_TIME=$(date +%s)
sleep 10
# Check if msiexec process is actually running (like successful test)
if kill -0 $INSTALL_PID 2>/dev/null; then
echo "MSI installation process is running (PID: $INSTALL_PID)"
# Monitor for up to 3 minutes with status updates
for i in {1..18}; do
sleep 10
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - INSTALL_START_TIME))
if kill -0 $INSTALL_PID 2>/dev/null; then
echo "Installation still running after ${ELAPSED} seconds..."
# Check log file growth
if [ -f "install.log" ]; then
LOG_SIZE=$(stat -c%s "install.log" 2>/dev/null || stat -f%z "install.log" 2>/dev/null || echo 0)
echo " Log file size: $LOG_SIZE bytes"
fi
else
echo "MSI installation completed after ${ELAPSED} seconds"
break
fi
done
# Final wait if still running
if kill -0 $INSTALL_PID 2>/dev/null; then
echo "Installation taking longer, waiting for completion..."
wait $INSTALL_PID 2>/dev/null || echo "Installation process ended"
fi
else
echo "MSI installation process ended quickly"
fi
# Quick success check using proven patterns
INSTALLATION_SUCCESSFUL=false
if [ -f "install.log" ]; then
if grep -qi "Installation.*operation.*completed.*successfully\|Installation.*success.*or.*error.*status.*0\|MainEngineThread.*is.*returning.*0\|Windows.*Installer.*installed.*the.*product" install.log 2>/dev/null; then
echo "✅ Installation successful (confirmed by log patterns)"
INSTALLATION_SUCCESSFUL=true
fi
fi
# Verify installation directory
INSTALL_PATH="/c/Program Files/Certum/SimplySign Desktop"
if [ -d "$INSTALL_PATH" ]; then
echo "✅ SimplySign Desktop installed successfully"
echo "✅ Virtual card emulation now active for code signing"
INSTALLATION_SUCCESSFUL=true
# Set output for GitHub Actions
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "SIMPLYSIGN_PATH=$INSTALL_PATH" >> "$GITHUB_OUTPUT"
fi
fi
if [ "$INSTALLATION_SUCCESSFUL" = false ]; then
echo "❌ Installation verification failed"
echo "Last 10 lines of install log:"
tail -10 install.log 2>/dev/null || echo "No install log available"
exit 1
fi
echo "🎉 SimplySign Desktop installation completed successfully!"

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

@@ -0,0 +1,450 @@
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 }}
# Certum cloud code signing for Windows
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
CERTUM_USERNAME: ${{ secrets.CERTUM_USERNAME }}
CERTUM_CERTIFICATE_SHA1: ${{ secrets.CERTUM_CERTIFICATE_SHA1 }}
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: Setup Certum Code Signing (Windows)
if: matrix.platform == 'windows'
run: |
echo "=== SETTING UP CERTUM CODE SIGNING FOR WINDOWS ==="
echo "Installing SimplySign Desktop and configuring for automatic authentication"
# Install SimplySign Desktop
chmod +x ./.github/scripts/install-simplysign.sh
./.github/scripts/install-simplysign.sh
# Configure registry for auto-login dialog
echo "Configuring registry for automatic login dialog..."
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign-registry.ps1"
echo "Certum signing environment ready"
shell: bash
- name: Authenticate Certum (Windows)
if: matrix.platform == 'windows'
env:
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
CERTUM_USERNAME: ${{ secrets.CERTUM_USERNAME }}
run: |
echo "=== CERTUM AUTHENTICATION ==="
echo "Authenticating with Certum cloud certificate using TOTP"
# Authenticate with Certum using our enhanced script
powershell -ExecutionPolicy Bypass -File "./.github/scripts/Connect-SimplySign-Enhanced.ps1"
echo "Authentication completed"
shell: bash
- name: Configure Certum Certificate Thumbprint (Windows)
if: matrix.platform == 'windows'
shell: bash
run: |
echo "=== CONFIGURING CERTUM CERTIFICATE THUMBPRINT ==="
CONFIG_PATH="src-tauri/tauri.windows.conf.json"
THUMBPRINT="${{ secrets.CERTUM_CERTIFICATE_SHA1 }}"
# Update the certificateThumbprint field using jq
jq --arg thumbprint "$THUMBPRINT" '.bundle.windows.certificateThumbprint = $thumbprint' "$CONFIG_PATH" > tmp.$$ && mv tmp.$$ "$CONFIG_PATH"
echo "Certificate thumbprint configured: $THUMBPRINT"
- name: Build the app
if: matrix.platform == 'windows' || matrix.platform == 'linux'
uses: tauri-apps/tauri-action@v0
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 }}
with:
args: --target ${{ matrix.target }}
- name: Build the app (macOS)
uses: tauri-apps/tauri-action@v0
if: matrix.platform == 'macos'
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 }}
TAURI_SKIP_SIDECAR_SIGNATURE_CHECK: "true"
with:
args: --target ${{ matrix.target }}
- 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

@@ -26,6 +26,10 @@ env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Certum cloud code signing for Windows
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
CERTUM_USERNAME: ${{ secrets.CERTUM_USERNAME }}
CERTUM_CERTIFICATE_SHA1: ${{ secrets.CERTUM_CERTIFICATE_SHA1 }}
PERSONAL_GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} PERSONAL_GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
concurrency: concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}" group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
@@ -396,17 +400,50 @@ jobs:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: import windows certificate - name: Setup Certum Code Signing (Windows)
if: matrix.os == 'windows-latest'
run: |
echo "=== SETTING UP CERTUM CODE SIGNING FOR WINDOWS ==="
echo "Installing SimplySign Desktop and configuring for automatic authentication"
# Install SimplySign Desktop
chmod +x ./.github/scripts/install-simplysign.sh
./.github/scripts/install-simplysign.sh
# Configure registry for auto-login dialog
echo "Configuring registry for automatic login dialog..."
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign-registry.ps1"
echo "Certum signing environment ready"
shell: bash
- name: Authenticate Certum (Windows)
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
env: env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} CERTUM_USERNAME: ${{ secrets.CERTUM_USERNAME }}
run: | run: |
New-Item -ItemType directory -Path certificate echo "=== CERTUM AUTHENTICATION ==="
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE echo "Authenticating with Certum cloud certificate using TOTP"
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
Remove-Item -path certificate -include tempCert.txt # Authenticate with Certum using our enhanced script
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText) powershell -ExecutionPolicy Bypass -File "./.github/scripts/Connect-SimplySign-Enhanced.ps1"
echo "Authentication completed"
shell: bash
- name: Configure Certum Certificate Thumbprint (Windows)
if: matrix.os == 'windows-latest'
shell: bash
run: |
echo "=== CONFIGURING CERTUM CERTIFICATE THUMBPRINT ==="
CONFIG_PATH="src-tauri/tauri.windows.conf.json"
THUMBPRINT="${{ secrets.CERTUM_CERTIFICATE_SHA1 }}"
# Update the certificateThumbprint field using jq
jq --arg thumbprint "$THUMBPRINT" '.bundle.windows.certificateThumbprint = $thumbprint' "$CONFIG_PATH" > tmp.$$ && mv tmp.$$ "$CONFIG_PATH"
echo "Certificate thumbprint configured: $THUMBPRINT"
- name: Build the app - name: Build the app
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -423,8 +460,6 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Enable signing and notarization for macOS # Enable signing and notarization for macOS
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
with: with:
tagName: ${{ needs.changelog.outputs.tag }} tagName: ${{ needs.changelog.outputs.tag }}
releaseName: 'OpenList Desktop ${{ needs.changelog.outputs.tag }}' releaseName: 'OpenList Desktop ${{ needs.changelog.outputs.tag }}'

View File

@@ -31,6 +31,11 @@
"source.fixAll.eslint": "explicit" "source.fixAll.eslint": "explicit"
}, },
"typescript.tsdk": "node_modules\\typescript\\lib", "typescript.tsdk": "node_modules\\typescript\\lib",
"i18n-ally.localesPaths": [
"src/i18n",
"src/i18n/locales"
],
"i18n-ally.keystyle": "nested",
// "rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu" // "rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu"
//"rust-analyzer.cargo.target": "x86_64-pc-windows-msvc" //"rust-analyzer.cargo.target": "x86_64-pc-windows-msvc"
// "rust-analyzer.cargo.target": "x86_64-apple-darwin", // "rust-analyzer.cargo.target": "x86_64-apple-darwin",

View File

@@ -245,7 +245,6 @@ winget install OpenListTeam.OpenListDesktop
"data_dir": "", "data_dir": "",
"auto_launch": true, "auto_launch": true,
"ssl_enabled": false, "ssl_enabled": false,
"admin_password": ""
} }
} }
``` ```
@@ -266,6 +265,7 @@ winget install OpenListTeam.OpenListDesktop
"extraFlags": ["--vfs-cache-mode", "full"] "extraFlags": ["--vfs-cache-mode", "full"]
} }
}, },
"api_port": 45572
} }
} }
``` ```
@@ -280,6 +280,8 @@ winget install OpenListTeam.OpenListDesktop
"gh_proxy": "https://ghproxy.com/", "gh_proxy": "https://ghproxy.com/",
"gh_proxy_api": false, "gh_proxy_api": false,
"open_links_in_browser": true, "open_links_in_browser": true,
"admin_password": "",
"show_window_on_startup": true
} }
} }
``` ```

View File

@@ -244,8 +244,7 @@ Add custom Rclone flags for optimal performance:
"port": 5244, "port": 5244,
"data_dir": "", "data_dir": "",
"auto_launch": true, "auto_launch": true,
"ssl_enabled": false, "ssl_enabled": false
"admin_password": ""
} }
} }
``` ```
@@ -266,6 +265,7 @@ Add custom Rclone flags for optimal performance:
"extraFlags": ["--vfs-cache-mode", "full"] "extraFlags": ["--vfs-cache-mode", "full"]
} }
}, },
"api_port": 45572
} }
} }
``` ```
@@ -280,6 +280,8 @@ Add custom Rclone flags for optimal performance:
"gh_proxy": "https://ghproxy.com/", "gh_proxy": "https://ghproxy.com/",
"gh_proxy_api": false, "gh_proxy_api": false,
"open_links_in_browser": true, "open_links_in_browser": true,
"admin_password": "",
"show_window_on_startup": true
} }
} }
``` ```

View File

@@ -1,7 +1,8 @@
// @ts-check
import eslint from '@eslint/js' import eslint from '@eslint/js'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import simpleImportSort from 'eslint-plugin-simple-import-sort' import simpleImportSort from 'eslint-plugin-simple-import-sort'
import eslintPluginUnicorn from 'eslint-plugin-unicorn' import eslintPluginUnicorn from 'eslint-plugin-unicorn'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals' import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
@@ -24,6 +25,8 @@ export default tseslint.config(
eslint.configs.recommended, eslint.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
...tseslint.configs.stylistic, ...tseslint.configs.stylistic,
...pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
{ {
plugins: { plugins: {
'simple-import-sort': simpleImportSort, 'simple-import-sort': simpleImportSort,
@@ -39,7 +42,10 @@ export default tseslint.config(
parserOptions: { parserOptions: {
warnOnUnsupportedTypeScriptVersion: false warnOnUnsupportedTypeScriptVersion: false
}, },
globals: globals.node globals: {
...globals.node,
...globals.browser
}
} }
}, },
{ {
@@ -102,5 +108,19 @@ export default tseslint.config(
{ name: 'exports' } { name: 'exports' }
] ]
} }
},
{
files: ['*.vue', '**/*.vue'],
rules: {
'no-undef': 'off'
},
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.browser,
parserOptions: {
parser: tseslint.parser
}
}
} }
) )

View File

@@ -9,7 +9,7 @@
"tauri" "tauri"
], ],
"private": true, "private": true,
"version": "0.5.1", "version": "0.8.0",
"author": { "author": {
"name": "OpenList Team", "name": "OpenList Team",
"email": "96409857+Kuingsmile@users.noreply.github.com" "email": "96409857+Kuingsmile@users.noreply.github.com"
@@ -26,8 +26,9 @@
"tauri:dev": "cross-env RUST_BACKTRACE=1 tauri dev", "tauri:dev": "cross-env RUST_BACKTRACE=1 tauri dev",
"tauri": "tauri", "tauri": "tauri",
"nowatch": "tauri dev --no-watch", "nowatch": "tauri dev --no-watch",
"lint": "eslint src/**/*.ts", "lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src/ scripts/",
"lint:fix": "eslint src/**/*.ts --fix", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx,.vue src/ scripts/ --fix",
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/main.ts",
"i18n:check": "node scripts/find-unused-i18n.js", "i18n:check": "node scripts/find-unused-i18n.js",
"i18n:check:verbose": "node scripts/find-unused-i18n.js --verbose", "i18n:check:verbose": "node scripts/find-unused-i18n.js --verbose",
"cz": "git-cz", "cz": "git-cz",
@@ -57,43 +58,51 @@
] ]
}, },
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.7.23", "@tauri-apps/api": "^2.8.0",
"@tauri-apps/api": "^2.6.0",
"@tauri-apps/plugin-autostart": "^2.5.0", "@tauri-apps/plugin-autostart": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-fs": "^2.4.0", "@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-opener": "^2.4.0", "@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-process": "^2.3.0", "@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-shell": "^2.3.1",
"@tauri-apps/plugin-store": "^2.3.0", "@tauri-apps/plugin-store": "^2.4.0",
"chrono-node": "^2.8.3", "chrono-node": "^2.8.4",
"lucide-vue-next": "^0.525.0", "lucide-vue-next": "^0.542.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.17", "vue": "^3.5.19",
"vue-i18n": "11.1.10", "vue-i18n": "11.1.11",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.6.2", "@tauri-apps/cli": "^2.8.3",
"@types/node": "^22.9.3", "@types/node": "^24.3.0",
"@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/eslint-plugin": "^8.41.0",
"@typescript-eslint/parser": "^8.36.0", "@typescript-eslint/parser": "^8.41.0",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.1",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^9.30.1", "dpdm": "^3.14.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unicorn": "^59.0.1", "eslint-plugin-unicorn": "^60.0.0",
"fs-extra": "^11.3.0", "eslint-plugin-vue": "^10.4.0",
"fs-extra": "^11.3.1",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.1.2", "lint-staged": "^16.1.5",
"node-bump-version": "^2.0.0", "node-bump-version": "^2.0.0",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^3.6.2",
"tar": "^7.4.3", "tar": "^7.4.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.36.0", "typescript-eslint": "^8.41.0",
"vite": "^7.0.3", "vite": "^7.1.11",
"vue-tsc": "^3.0.1" "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.6"
},
"overrides": {
"tmp": "^0.2.4"
} }
} }

View File

@@ -1,404 +1,404 @@
#!/usr/bin/env node #!/usr/bin/env node
import { readdirSync, readFileSync } from 'node:fs' import { readdirSync, readFileSync } from 'node:fs'
import { basename, dirname, extname, join, relative } from 'node:path' import { basename, dirname, extname, join, relative } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
const LOCALE_DIR = join(__dirname, '../src/i18n/locales') const LOCALE_DIR = join(__dirname, '../src/i18n/locales')
const SRC_DIR = join(__dirname, '../src') const SRC_DIR = join(__dirname, '../src')
console.log(`\n🔍 Analyzing i18n keys in ${LOCALE_DIR} and source files in ${SRC_DIR}\n`) console.log(`\n🔍 Analyzing i18n keys in ${LOCALE_DIR} and source files in ${SRC_DIR}\n`)
const colors = { const colors = {
reset: '\x1b[0m', reset: '\x1b[0m',
bright: '\x1b[1m', bright: '\x1b[1m',
red: '\x1b[31m', red: '\x1b[31m',
green: '\x1b[32m', green: '\x1b[32m',
yellow: '\x1b[33m', yellow: '\x1b[33m',
blue: '\x1b[34m', blue: '\x1b[34m',
magenta: '\x1b[35m', magenta: '\x1b[35m',
cyan: '\x1b[36m' cyan: '\x1b[36m'
} }
function colorize(text, color) { function colorize(text, color) {
return `${colors[color]}${text}${colors.reset}` return `${colors[color]}${text}${colors.reset}`
} }
function flattenKeys(obj, prefix = '') { function flattenKeys(obj, prefix = '') {
const keys = [] const keys = []
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key const fullKey = prefix ? `${prefix}.${key}` : key
if (typeof value === 'object' && value !== null && !Array.isArray(value)) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
keys.push(...flattenKeys(value, fullKey)) keys.push(...flattenKeys(value, fullKey))
} else { } else {
keys.push(fullKey) keys.push(fullKey)
} }
} }
return keys return keys
} }
function readLocaleFile(filePath) { function readLocaleFile(filePath) {
try { try {
const content = readFileSync(filePath, 'utf8') const content = readFileSync(filePath, 'utf8')
return JSON.parse(content) return JSON.parse(content)
} catch (error) { } catch (error) {
console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red'))
return {} return {}
} }
} }
function getAllI18nKeys() { function getAllI18nKeys() {
const localeFiles = readdirSync(LOCALE_DIR).filter(file => file.endsWith('.json')) const localeFiles = readdirSync(LOCALE_DIR).filter(file => file.endsWith('.json'))
const allKeys = new Set() const allKeys = new Set()
const localeData = {} const localeData = {}
console.log(colorize('\n📁 Found locale files:', 'blue')) console.log(colorize('\n📁 Found locale files:', 'blue'))
for (const file of localeFiles) { for (const file of localeFiles) {
const filePath = join(LOCALE_DIR, file) const filePath = join(LOCALE_DIR, file)
const locale = basename(file, '.json') const locale = basename(file, '.json')
const data = readLocaleFile(filePath) const data = readLocaleFile(filePath)
const keys = flattenKeys(data) const keys = flattenKeys(data)
localeData[locale] = { localeData[locale] = {
file: filePath, file: filePath,
keys, keys,
data data
} }
keys.forEach(key => allKeys.add(key)) keys.forEach(key => allKeys.add(key))
console.log(` ${colorize('✓', 'green')} ${file} (${keys.length} keys)`) console.log(` ${colorize('✓', 'green')} ${file} (${keys.length} keys)`)
} }
return { return {
allKeys: Array.from(allKeys).sort(), allKeys: Array.from(allKeys).sort(),
localeData localeData
} }
} }
function findFiles(dir, extensions = ['.vue', '.ts', '.js']) { function findFiles(dir, extensions = ['.vue', '.ts', '.js']) {
const files = [] const files = []
function walk(currentDir) { function walk(currentDir) {
const entries = readdirSync(currentDir, { withFileTypes: true }) const entries = readdirSync(currentDir, { withFileTypes: true })
for (const entry of entries) { for (const entry of entries) {
const fullPath = join(currentDir, entry.name) const fullPath = join(currentDir, entry.name)
if (entry.isDirectory()) { if (entry.isDirectory()) {
if (!['node_modules', '.git', 'dist', 'build', 'target'].includes(entry.name)) { if (!['node_modules', '.git', 'dist', 'build', 'target'].includes(entry.name)) {
walk(fullPath) walk(fullPath)
} }
} else if (entry.isFile()) { } else if (entry.isFile()) {
const ext = extname(entry.name) const ext = extname(entry.name)
if (extensions.includes(ext)) { if (extensions.includes(ext)) {
files.push(fullPath) files.push(fullPath)
} }
} }
} }
} }
walk(dir) walk(dir)
return files return files
} }
function findKeyUsage(keys) { function findKeyUsage(keys) {
const usage = {} const usage = {}
const dynamicPatterns = [] const dynamicPatterns = []
keys.forEach(key => { keys.forEach(key => {
usage[key] = { usage[key] = {
used: false, used: false,
files: [], files: [],
patterns: [], patterns: [],
dynamicMatch: false dynamicMatch: false
} }
}) })
console.log(colorize('\n🔍 Searching for key usage in source files...', 'blue')) console.log(colorize('\n🔍 Searching for key usage in source files...', 'blue'))
const sourceFiles = findFiles(SRC_DIR) const sourceFiles = findFiles(SRC_DIR)
console.log(` Found ${sourceFiles.length} source files to analyze`) console.log(` Found ${sourceFiles.length} source files to analyze`)
const searchPatterns = [ const searchPatterns = [
/\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g, /\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g,
/(?:^|[^a-zA-Z])t\s*\(\s*['"`]([^'"`]+)['"`]/g, /(?:^|[^a-zA-Z])t\s*\(\s*['"`]([^'"`]+)['"`]/g,
/\{\{\s*\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g /\{\{\s*\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g
] ]
const dynamicPattern = /\$?t\s*\(\s*`([^`]*\$\{[^}]+\}[^`]*)`/g const dynamicPattern = /\$?t\s*\(\s*`([^`]*\$\{[^}]+\}[^`]*)`/g
sourceFiles.forEach(filePath => { sourceFiles.forEach(filePath => {
try { try {
const content = readFileSync(filePath, 'utf8') const content = readFileSync(filePath, 'utf8')
const relativePath = relative(join(__dirname, '..'), filePath) const relativePath = relative(join(__dirname, '..'), filePath)
searchPatterns.forEach((pattern, patternIndex) => { searchPatterns.forEach((pattern, patternIndex) => {
let match let match
while ((match = pattern.exec(content)) !== null) { while ((match = pattern.exec(content)) !== null) {
const key = match[1] const key = match[1]
if (usage[key]) { if (usage[key]) {
usage[key].used = true usage[key].used = true
if (!usage[key].files.includes(relativePath)) { if (!usage[key].files.includes(relativePath)) {
usage[key].files.push(relativePath) usage[key].files.push(relativePath)
} }
if (!usage[key].patterns.includes(patternIndex)) { if (!usage[key].patterns.includes(patternIndex)) {
usage[key].patterns.push(patternIndex) usage[key].patterns.push(patternIndex)
} }
} }
} }
}) })
let dynamicMatch let dynamicMatch
while ((dynamicMatch = dynamicPattern.exec(content)) !== null) { while ((dynamicMatch = dynamicPattern.exec(content)) !== null) {
const templateString = dynamicMatch[1] const templateString = dynamicMatch[1]
const staticParts = templateString.split(/\$\{[^}]+\}/) const staticParts = templateString.split(/\$\{[^}]+\}/)
const patternInfo = { const patternInfo = {
template: templateString, template: templateString,
file: relativePath, file: relativePath,
staticParts staticParts
} }
if (!dynamicPatterns.some(p => p.template === templateString && p.file === relativePath)) { if (!dynamicPatterns.some(p => p.template === templateString && p.file === relativePath)) {
dynamicPatterns.push(patternInfo) dynamicPatterns.push(patternInfo)
} }
keys.forEach(key => { keys.forEach(key => {
if (matchesDynamicPattern(key, staticParts)) { if (matchesDynamicPattern(key, staticParts)) {
if (usage[key]) { if (usage[key]) {
usage[key].used = true usage[key].used = true
usage[key].dynamicMatch = true usage[key].dynamicMatch = true
if (!usage[key].files.includes(relativePath)) { if (!usage[key].files.includes(relativePath)) {
usage[key].files.push(relativePath) usage[key].files.push(relativePath)
} }
if (!usage[key].patterns.includes('dynamic')) { if (!usage[key].patterns.includes('dynamic')) {
usage[key].patterns.push('dynamic') usage[key].patterns.push('dynamic')
} }
} }
} }
}) })
} }
} catch (error) { } catch (error) {
console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red'))
} }
}) })
usage._dynamicPatterns = dynamicPatterns usage._dynamicPatterns = dynamicPatterns
return usage return usage
} }
function matchesDynamicPattern(key, staticParts) { function matchesDynamicPattern(key, staticParts) {
if (staticParts.length === 0) return false if (staticParts.length === 0) return false
let keyIndex = 0 let keyIndex = 0
for (let i = 0; i < staticParts.length; i++) { for (let i = 0; i < staticParts.length; i++) {
const part = staticParts[i] const part = staticParts[i]
if (part === '') { if (part === '') {
if (i < staticParts.length - 1) { if (i < staticParts.length - 1) {
const nextPart = staticParts[i + 1] const nextPart = staticParts[i + 1]
if (nextPart) { if (nextPart) {
const nextIndex = key.indexOf(nextPart, keyIndex) const nextIndex = key.indexOf(nextPart, keyIndex)
if (nextIndex === -1) return false if (nextIndex === -1) return false
keyIndex = nextIndex keyIndex = nextIndex
} }
} }
continue continue
} }
if (i === 0) { if (i === 0) {
if (!key.startsWith(part)) return false if (!key.startsWith(part)) return false
keyIndex = part.length keyIndex = part.length
} else if (i === staticParts.length - 1) { } else if (i === staticParts.length - 1) {
if (part && !key.endsWith(part)) return false if (part && !key.endsWith(part)) return false
} else { } else {
const index = key.indexOf(part, keyIndex) const index = key.indexOf(part, keyIndex)
if (index === -1) return false if (index === -1) return false
keyIndex = index + part.length keyIndex = index + part.length
} }
} }
return true return true
} }
function findLocaleInconsistencies(localeData) { function findLocaleInconsistencies(localeData) {
const locales = Object.keys(localeData) const locales = Object.keys(localeData)
const inconsistencies = {} const inconsistencies = {}
if (locales.length < 2) { if (locales.length < 2) {
return inconsistencies return inconsistencies
} }
locales.forEach(locale => { locales.forEach(locale => {
const currentKeys = new Set(localeData[locale].keys) const currentKeys = new Set(localeData[locale].keys)
inconsistencies[locale] = { inconsistencies[locale] = {
missing: [], missing: [],
extra: [] extra: []
} }
locales.forEach(otherLocale => { locales.forEach(otherLocale => {
if (locale !== otherLocale) { if (locale !== otherLocale) {
localeData[otherLocale].keys.forEach(key => { localeData[otherLocale].keys.forEach(key => {
if (!currentKeys.has(key) && !inconsistencies[locale].missing.includes(key)) { if (!currentKeys.has(key) && !inconsistencies[locale].missing.includes(key)) {
inconsistencies[locale].missing.push(key) inconsistencies[locale].missing.push(key)
} }
}) })
} }
}) })
localeData[locale].keys.forEach(key => { localeData[locale].keys.forEach(key => {
const existsInOthers = locales.some( const existsInOthers = locales.some(
otherLocale => locale !== otherLocale && localeData[otherLocale].keys.includes(key) otherLocale => locale !== otherLocale && localeData[otherLocale].keys.includes(key)
) )
if (!existsInOthers) { if (!existsInOthers) {
inconsistencies[locale].extra.push(key) inconsistencies[locale].extra.push(key)
} }
}) })
}) })
return inconsistencies return inconsistencies
} }
function main() { function main() {
console.log(colorize('🌐 OpenList Desktop - I18n Usage Analyzer', 'cyan')) console.log(colorize('🌐 OpenList Desktop - I18n Usage Analyzer', 'cyan'))
console.log(colorize('==========================================', 'cyan')) console.log(colorize('==========================================', 'cyan'))
const { allKeys, localeData } = getAllI18nKeys() const { allKeys, localeData } = getAllI18nKeys()
console.log(colorize(`\n📊 Total unique keys found: ${allKeys.length}`, 'yellow')) console.log(colorize(`\n📊 Total unique keys found: ${allKeys.length}`, 'yellow'))
const usage = findKeyUsage(allKeys) const usage = findKeyUsage(allKeys)
const dynamicPatterns = usage._dynamicPatterns || [] const dynamicPatterns = usage._dynamicPatterns || []
delete usage._dynamicPatterns delete usage._dynamicPatterns
const usedKeys = allKeys.filter(key => usage[key].used) const usedKeys = allKeys.filter(key => usage[key].used)
const unusedKeys = allKeys.filter(key => !usage[key].used) const unusedKeys = allKeys.filter(key => !usage[key].used)
const dynamicallyUsedKeys = usedKeys.filter(key => usage[key].dynamicMatch) const dynamicallyUsedKeys = usedKeys.filter(key => usage[key].dynamicMatch)
const staticUsedKeys = usedKeys.filter(key => !usage[key].dynamicMatch) const staticUsedKeys = usedKeys.filter(key => !usage[key].dynamicMatch)
const inconsistencies = findLocaleInconsistencies(localeData) const inconsistencies = findLocaleInconsistencies(localeData)
console.log(colorize('\n📈 Usage Summary:', 'blue')) console.log(colorize('\n📈 Usage Summary:', 'blue'))
console.log(` ${colorize('✓', 'green')} Used keys: ${usedKeys.length}`) console.log(` ${colorize('✓', 'green')} Used keys: ${usedKeys.length}`)
console.log(` ${colorize('→', 'cyan')} Static usage: ${staticUsedKeys.length}`) console.log(` ${colorize('→', 'cyan')} Static usage: ${staticUsedKeys.length}`)
console.log(` ${colorize('→', 'magenta')} Dynamic usage: ${dynamicallyUsedKeys.length}`) console.log(` ${colorize('→', 'magenta')} Dynamic usage: ${dynamicallyUsedKeys.length}`)
console.log(` ${colorize('✗', 'red')} Unused keys: ${unusedKeys.length}`) console.log(` ${colorize('✗', 'red')} Unused keys: ${unusedKeys.length}`)
console.log(` ${colorize('📊', 'yellow')} Usage rate: ${((usedKeys.length / allKeys.length) * 100).toFixed(1)}%`) console.log(` ${colorize('📊', 'yellow')} Usage rate: ${((usedKeys.length / allKeys.length) * 100).toFixed(1)}%`)
if (dynamicPatterns.length > 0) { if (dynamicPatterns.length > 0) {
console.log(colorize('\n🔮 Dynamic I18n Patterns Detected:', 'magenta')) console.log(colorize('\n🔮 Dynamic I18n Patterns Detected:', 'magenta'))
console.log(colorize('===================================', 'magenta')) console.log(colorize('===================================', 'magenta'))
dynamicPatterns.forEach((pattern, index) => { dynamicPatterns.forEach((pattern, index) => {
console.log(colorize(`\n${index + 1}. Template: \`${pattern.template}\``, 'cyan')) console.log(colorize(`\n${index + 1}. Template: \`${pattern.template}\``, 'cyan'))
console.log(` File: ${pattern.file}`) console.log(` File: ${pattern.file}`)
console.log(` Static parts: [${pattern.staticParts.map(p => `"${p}"`).join(', ')}]`) console.log(` Static parts: [${pattern.staticParts.map(p => `"${p}"`).join(', ')}]`)
const matchingKeys = allKeys.filter(key => matchesDynamicPattern(key, pattern.staticParts)) const matchingKeys = allKeys.filter(key => matchesDynamicPattern(key, pattern.staticParts))
if (matchingKeys.length > 0) { if (matchingKeys.length > 0) {
console.log( console.log(
` ${colorize('Matches', 'green')} (${matchingKeys.length}): ${matchingKeys.slice(0, 5).join(', ')}${ ` ${colorize('Matches', 'green')} (${matchingKeys.length}): ${matchingKeys.slice(0, 5).join(', ')}${
matchingKeys.length > 5 ? '...' : '' matchingKeys.length > 5 ? '...' : ''
}` }`
) )
} }
}) })
} }
if (unusedKeys.length > 0) { if (unusedKeys.length > 0) {
console.log(colorize('\n🗑 Unused I18n Keys:', 'red')) console.log(colorize('\n🗑 Unused I18n Keys:', 'red'))
console.log(colorize('====================', 'red')) console.log(colorize('====================', 'red'))
const groupedUnused = {} const groupedUnused = {}
unusedKeys.forEach(key => { unusedKeys.forEach(key => {
const namespace = key.split('.')[0] const namespace = key.split('.')[0]
if (!groupedUnused[namespace]) { if (!groupedUnused[namespace]) {
groupedUnused[namespace] = [] groupedUnused[namespace] = []
} }
groupedUnused[namespace].push(key) groupedUnused[namespace].push(key)
}) })
Object.entries(groupedUnused).forEach(([namespace, keys]) => { Object.entries(groupedUnused).forEach(([namespace, keys]) => {
console.log(colorize(`\n[${namespace}] - ${keys.length} unused keys:`, 'yellow')) console.log(colorize(`\n[${namespace}] - ${keys.length} unused keys:`, 'yellow'))
keys.forEach(key => { keys.forEach(key => {
console.log(` ${colorize('✗', 'red')} ${key}`) console.log(` ${colorize('✗', 'red')} ${key}`)
}) })
}) })
} else { } else {
console.log(colorize('\n🎉 No unused keys found! All i18n keys are being used.', 'green')) console.log(colorize('\n🎉 No unused keys found! All i18n keys are being used.', 'green'))
} }
const hasInconsistencies = Object.values(inconsistencies).some(inc => inc.missing.length > 0 || inc.extra.length > 0) const hasInconsistencies = Object.values(inconsistencies).some(inc => inc.missing.length > 0 || inc.extra.length > 0)
if (hasInconsistencies) { if (hasInconsistencies) {
console.log(colorize('\n⚠ Locale Inconsistencies:', 'yellow')) console.log(colorize('\n⚠ Locale Inconsistencies:', 'yellow'))
console.log(colorize('=========================', 'yellow')) console.log(colorize('=========================', 'yellow'))
Object.entries(inconsistencies).forEach(([locale, data]) => { Object.entries(inconsistencies).forEach(([locale, data]) => {
if (data.missing.length > 0 || data.extra.length > 0) { if (data.missing.length > 0 || data.extra.length > 0) {
console.log(colorize(`\n[${locale}.json]:`, 'cyan')) console.log(colorize(`\n[${locale}.json]:`, 'cyan'))
if (data.missing.length > 0) { if (data.missing.length > 0) {
console.log(colorize(` Missing ${data.missing.length} keys:`, 'red')) console.log(colorize(` Missing ${data.missing.length} keys:`, 'red'))
data.missing.forEach(key => { data.missing.forEach(key => {
console.log(` ${colorize('✗', 'red')} ${key}`) console.log(` ${colorize('✗', 'red')} ${key}`)
}) })
} }
if (data.extra.length > 0) { if (data.extra.length > 0) {
console.log(colorize(` Extra ${data.extra.length} keys:`, 'blue')) console.log(colorize(` Extra ${data.extra.length} keys:`, 'blue'))
data.extra.forEach(key => { data.extra.forEach(key => {
console.log(` ${colorize('!', 'blue')} ${key}`) console.log(` ${colorize('!', 'blue')} ${key}`)
}) })
} }
} }
}) })
} }
if (process.argv.includes('--verbose') || process.argv.includes('-v')) { if (process.argv.includes('--verbose') || process.argv.includes('-v')) {
console.log(colorize('\n📋 Sample Used Keys (first 10):', 'blue')) console.log(colorize('\n📋 Sample Used Keys (first 10):', 'blue'))
console.log(colorize('=================================', 'blue')) console.log(colorize('=================================', 'blue'))
usedKeys.slice(0, 10).forEach(key => { usedKeys.slice(0, 10).forEach(key => {
const files = usage[key].files.slice(0, 3) // Show first 3 files const files = usage[key].files.slice(0, 3) // Show first 3 files
const moreFiles = usage[key].files.length > 3 ? ` (+${usage[key].files.length - 3} more)` : '' const moreFiles = usage[key].files.length > 3 ? ` (+${usage[key].files.length - 3} more)` : ''
const usageType = usage[key].dynamicMatch ? colorize('(dynamic)', 'magenta') : colorize('(static)', 'cyan') const usageType = usage[key].dynamicMatch ? colorize('(dynamic)', 'magenta') : colorize('(static)', 'cyan')
console.log(` ${colorize('✓', 'green')} ${key} ${usageType}`) console.log(` ${colorize('✓', 'green')} ${key} ${usageType}`)
console.log(` Used in: ${files.join(', ')}${moreFiles}`) console.log(` Used in: ${files.join(', ')}${moreFiles}`)
}) })
if (dynamicallyUsedKeys.length > 0) { if (dynamicallyUsedKeys.length > 0) {
console.log(colorize('\n🔮 Dynamic Key Usage Details:', 'magenta')) console.log(colorize('\n🔮 Dynamic Key Usage Details:', 'magenta'))
console.log(colorize('=============================', 'magenta')) console.log(colorize('=============================', 'magenta'))
dynamicallyUsedKeys.slice(0, 5).forEach(key => { dynamicallyUsedKeys.slice(0, 5).forEach(key => {
const files = usage[key].files.slice(0, 2) const files = usage[key].files.slice(0, 2)
console.log(` ${colorize('✨', 'magenta')} ${key}`) console.log(` ${colorize('✨', 'magenta')} ${key}`)
console.log(` Files: ${files.join(', ')}`) console.log(` Files: ${files.join(', ')}`)
}) })
if (dynamicallyUsedKeys.length > 5) { if (dynamicallyUsedKeys.length > 5) {
console.log(` ... and ${dynamicallyUsedKeys.length - 5} more dynamic keys`) console.log(` ... and ${dynamicallyUsedKeys.length - 5} more dynamic keys`)
} }
} }
} }
console.log(colorize('\n✨ Analysis complete!', 'cyan')) console.log(colorize('\n✨ Analysis complete!', 'cyan'))
if (unusedKeys.length > 0) { if (unusedKeys.length > 0) {
console.log(colorize('\n💡 Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue')) console.log(colorize('\n💡 Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue'))
} }
} }
main() main()

View File

@@ -1,4 +1,5 @@
import { execSync } from 'node:child_process' import { execSync } from 'node:child_process'
import crypto from 'node:crypto'
import fsp from 'node:fs/promises' import fsp from 'node:fs/promises'
import path from 'node:path' 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 resolveAccessControlPlugin = async pluginDir => {
const url = 'https://nsis.sourceforge.io/mediawiki/images/4/4a/AccessControl.zip' const url = 'https://nsis.sourceforge.io/mediawiki/images/4/4a/AccessControl.zip'
const TEMP_DIR = path.join(cwd, 'temp') const TEMP_DIR = path.join(cwd, 'temp')
@@ -184,6 +195,7 @@ async function resolveSidecar(binInfo) {
} }
await fs.remove(zipPath) await fs.remove(zipPath)
await fs.chmod(binaryPath, 0o755) await fs.chmod(binaryPath, 0o755)
await calculateSha256(binaryPath)
} catch (err) { } catch (err) {
console.error(`Error preparing "${name}":`, err.message) console.error(`Error preparing "${name}":`, err.message)
await fs.rm(binaryPath, { recursive: true, force: true }) await fs.rm(binaryPath, { recursive: true, force: true })

262
src-tauri/Cargo.lock generated
View File

@@ -69,9 +69,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.98" version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
@@ -513,7 +513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257"
dependencies = [ dependencies = [
"serde", "serde",
"toml", "toml 0.8.23",
] ]
[[package]] [[package]]
@@ -989,9 +989,9 @@ dependencies = [
[[package]] [[package]]
name = "dlopen2" name = "dlopen2"
version = "0.7.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff"
dependencies = [ dependencies = [
"dlopen2_derive", "dlopen2_derive",
"libc", "libc",
@@ -1070,7 +1070,7 @@ dependencies = [
"cc", "cc",
"memchr", "memchr",
"rustc_version", "rustc_version",
"toml", "toml 0.8.23",
"vswhom", "vswhom",
"winreg 0.55.0", "winreg 0.55.0",
] ]
@@ -1887,7 +1887,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.5.10",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@@ -2601,6 +2601,15 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -2790,6 +2799,16 @@ dependencies = [
"objc2-core-foundation", "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]] [[package]]
name = "objc2-io-surface" name = "objc2-io-surface"
version = "0.3.1" version = "0.3.1"
@@ -2801,6 +2820,16 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-javascript-core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a"
dependencies = [
"objc2 0.6.1",
"objc2-core-foundation",
]
[[package]] [[package]]
name = "objc2-metal" name = "objc2-metal"
version = "0.2.2" version = "0.2.2"
@@ -2837,6 +2866,17 @@ dependencies = [
"objc2-foundation 0.3.1", "objc2-foundation 0.3.1",
] ]
[[package]]
name = "objc2-security"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44"
dependencies = [
"bitflags 2.9.1",
"objc2 0.6.1",
"objc2-core-foundation",
]
[[package]] [[package]]
name = "objc2-ui-kit" name = "objc2-ui-kit"
version = "0.3.1" version = "0.3.1"
@@ -2861,6 +2901,8 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.1", "objc2-foundation 0.3.1",
"objc2-javascript-core",
"objc2-security",
] ]
[[package]] [[package]]
@@ -2898,7 +2940,7 @@ dependencies = [
[[package]] [[package]]
name = "openlist-desktop" name = "openlist-desktop"
version = "0.5.1" version = "0.7.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
@@ -2917,6 +2959,7 @@ dependencies = [
"runas", "runas",
"serde", "serde",
"serde_json", "serde_json",
"sysinfo",
"tar", "tar",
"tauri", "tauri",
"tauri-build", "tauri-build",
@@ -3450,7 +3493,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2 0.5.10",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tracing", "tracing",
@@ -3487,7 +3530,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2", "socket2 0.5.10",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -3676,9 +3719,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -3705,9 +3748,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.22" version = "0.12.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -4082,9 +4125,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.140" version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -4112,6 +4155,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@@ -4170,9 +4222,9 @@ dependencies = [
[[package]] [[package]]
name = "serialize-to-javascript" name = "serialize-to-javascript"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@@ -4181,13 +4233,13 @@ dependencies = [
[[package]] [[package]]
name = "serialize-to-javascript-impl" name = "serialize-to-javascript-impl"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 2.0.104",
] ]
[[package]] [[package]]
@@ -4342,6 +4394,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "softbuffer" name = "softbuffer"
version = "0.4.6" version = "0.4.6"
@@ -4492,6 +4554,20 @@ dependencies = [
"syn 2.0.104", "syn 2.0.104",
] ]
[[package]]
name = "sysinfo"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.6.1" version = "0.6.1"
@@ -4522,17 +4598,18 @@ dependencies = [
"cfg-expr", "cfg-expr",
"heck 0.5.0", "heck 0.5.0",
"pkg-config", "pkg-config",
"toml", "toml 0.8.23",
"version-compare", "version-compare",
] ]
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.34.0" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a" checksum = "4daa814018fecdfb977b59a094df4bd43b42e8e21f88fddfc05807e6f46efaaf"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"block2 0.6.1",
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-graphics", "core-graphics",
"crossbeam-channel", "crossbeam-channel",
@@ -4595,12 +4672,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.6.2" version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "124e129c9c0faa6bec792c5948c89e86c90094133b0b9044df0ce5f0a8efaa0d" checksum = "dcead52ec80df0e9e4be671c0f2596a1f3bd7b6b2c9418c1eb7dd737499ff4bd"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"cookie",
"dirs 6.0.0", "dirs 6.0.0",
"dunce", "dunce",
"embed_plist", "embed_plist",
@@ -4618,6 +4696,7 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.1", "objc2-foundation 0.3.1",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit",
"percent-encoding", "percent-encoding",
"plist", "plist",
"raw-window-handle", "raw-window-handle",
@@ -4645,9 +4724,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.3.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83" checksum = "67945dbaf8920dbe3a1e56721a419a0c3d085254ab24cff5b9ad55e2b0016e0b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -4661,15 +4740,15 @@ dependencies = [
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"tauri-winres", "tauri-winres",
"toml", "toml 0.9.5",
"walkdir", "walkdir",
] ]
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.3.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5df493a1075a241065bc865ed5ef8d0fbc1e76c7afdc0bf0eccfaa7d4f0e406" checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@@ -4694,9 +4773,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.3.1" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f237fbea5866fa5f2a60a21bea807a2d6e0379db070d89c3a10ac0f2d4649bbc" checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -4708,9 +4787,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin" name = "tauri-plugin"
version = "2.3.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9a0bd00bf1930ad1a604d08b0eb6b2a9c1822686d65d7f4731a7723b8901d3" checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@@ -4719,7 +4798,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"toml", "toml 0.9.5",
"walkdir", "walkdir",
] ]
@@ -4739,9 +4818,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.3.0" version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aefb14219b492afb30b12647b5b1247cadd2c0603467310c36e0f7ae1698c28" checksum = "0ee5a3c416dc59d7d9aa0de5490a82d6e201c67ffe97388979d77b69b08cda40"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -4757,9 +4836,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.4.0" version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c341290d31991dbca38b31d412c73dfbdb070bb11536784f19dd2211d13b778f" checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
@@ -4773,15 +4852,15 @@ dependencies = [
"tauri-plugin", "tauri-plugin",
"tauri-utils", "tauri-utils",
"thiserror 2.0.12", "thiserror 2.0.12",
"toml", "toml 0.9.5",
"url", "url",
] ]
[[package]] [[package]]
name = "tauri-plugin-opener" name = "tauri-plugin-opener"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321" checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5"
dependencies = [ dependencies = [
"dunce", "dunce",
"glob", "glob",
@@ -4832,9 +4911,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-single-instance" name = "tauri-plugin-single-instance"
version = "2.3.0" version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b441b6d5d1a194e9fee0b358fe0d602ded845d0f580e1f8c8ef78ebc3c8b225d" checksum = "236043404a4d1502ed7cce11a8ec88ea1e85597eec9887b4701bb10b66b13b6e"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@@ -4847,9 +4926,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.7.0" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4" checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846"
dependencies = [ dependencies = [
"cookie", "cookie",
"dpi", "dpi",
@@ -4858,20 +4937,23 @@ dependencies = [
"jni", "jni",
"objc2 0.6.1", "objc2 0.6.1",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit",
"raw-window-handle", "raw-window-handle",
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"thiserror 2.0.12", "thiserror 2.0.12",
"url", "url",
"webkit2gtk",
"webview2-com",
"windows", "windows",
] ]
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.7.1" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902b5aa9035e16f342eb64f8bf06ccdc2808e411a2525ed1d07672fa4e780bad" checksum = "5bb0f10f831f75832ac74d14d98f701868f9a8adccef2c249b466cf70b607db9"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@@ -4896,9 +4978,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.5.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41743bbbeb96c3a100d234e5a0b60a46d5aa068f266160862c7afdbf828ca02e" checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"brotli", "brotli",
@@ -4925,7 +5007,7 @@ dependencies = [
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror 2.0.12", "thiserror 2.0.12",
"toml", "toml 0.9.5",
"url", "url",
"urlpattern", "urlpattern",
"uuid", "uuid",
@@ -4940,7 +5022,7 @@ checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4"
dependencies = [ dependencies = [
"embed-resource", "embed-resource",
"indexmap 2.10.0", "indexmap 2.10.0",
"toml", "toml 0.8.23",
] ]
[[package]] [[package]]
@@ -5129,9 +5211,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.46.1" version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -5142,10 +5224,10 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"slab", "slab",
"socket2", "socket2 0.6.0",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -5199,11 +5281,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned 0.6.9",
"toml_datetime", "toml_datetime 0.6.11",
"toml_edit 0.22.27", "toml_edit 0.22.27",
] ]
[[package]]
name = "toml"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap 2.10.0",
"serde",
"serde_spanned 1.0.0",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow 0.7.11",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.11" version = "0.6.11"
@@ -5213,6 +5310,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.19.15" version = "0.19.15"
@@ -5220,7 +5326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [ dependencies = [
"indexmap 2.10.0", "indexmap 2.10.0",
"toml_datetime", "toml_datetime 0.6.11",
"winnow 0.5.40", "winnow 0.5.40",
] ]
@@ -5231,7 +5337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [ dependencies = [
"indexmap 2.10.0", "indexmap 2.10.0",
"toml_datetime", "toml_datetime 0.6.11",
"winnow 0.5.40", "winnow 0.5.40",
] ]
@@ -5243,18 +5349,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap 2.10.0", "indexmap 2.10.0",
"serde", "serde",
"serde_spanned", "serde_spanned 0.6.9",
"toml_datetime", "toml_datetime 0.6.11",
"toml_write", "toml_write",
"winnow 0.7.11", "winnow 0.7.11",
] ]
[[package]]
name = "toml_parser"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow 0.7.11",
]
[[package]] [[package]]
name = "toml_write" name = "toml_write"
version = "0.1.2" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@@ -6362,14 +6483,15 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.52.1" version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9" checksum = "5698e50a589268aec06d2219f48b143222f7b5ad9aa690118b8dce0a8dcac574"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2 0.6.1", "block2 0.6.1",
"cookie", "cookie",
"crossbeam-channel", "crossbeam-channel",
"dirs 6.0.0",
"dpi", "dpi",
"dunce", "dunce",
"gdkx11", "gdkx11",
@@ -6461,9 +6583,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "5.7.1" version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" checksum = "67a073be99ace1adc48af593701c8015cd9817df372e14a1a6b0ee8f8bf043be"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor", "async-executor",
@@ -6486,7 +6608,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"uds_windows", "uds_windows",
"windows-sys 0.59.0", "windows-sys 0.60.2",
"winnow 0.7.11", "winnow 0.7.11",
"zbus_macros", "zbus_macros",
"zbus_names", "zbus_names",
@@ -6495,9 +6617,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "5.7.1" version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" checksum = "0e80cd713a45a49859dcb648053f63265f4f2851b6420d47a958e5697c68b131"
dependencies = [ dependencies = [
"proc-macro-crate 3.3.0", "proc-macro-crate 3.3.0",
"proc-macro2", "proc-macro2",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "openlist-desktop" name = "openlist-desktop"
version = "0.5.1" version = "0.8.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["Kuingsmile"] authors = ["Kuingsmile"]
edition = "2024" edition = "2024"
@@ -15,27 +15,27 @@ name = "openlist_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.3.0", features = [] } tauri-build = { version = "2.4.0", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.6.2", features = ["tray-icon", "devtools"] } tauri = { version = "2.8.3", features = ["tray-icon", "devtools"] }
tauri-plugin-opener = "2.4.0" tauri-plugin-opener = "2.5.0"
tauri-plugin-process = "2.3.0" tauri-plugin-process = "2.3.0"
tauri-plugin-fs = "2.4.0" tauri-plugin-fs = "2.4.2"
tauri-plugin-dialog = "2.3.0" tauri-plugin-dialog = "2.3.3"
tauri-plugin-shell = "2.3.0" tauri-plugin-shell = "2.3.0"
tauri-plugin-autostart = "2.5.0" tauri-plugin-autostart = "2.5.0"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.143"
tokio = { version = "1.46.1", features = ["full"] } tokio = { version = "1.47.1", features = ["full"] }
anyhow = "1.0.98" anyhow = "1.0.99"
thiserror = "2.0.12" thiserror = "2.0.12"
chrono = { version = "0.4.41", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
log = "0.4.27" log = "0.4.27"
log4rs = "1.3.0" log4rs = "1.3.0"
dirs = "6.0.0" dirs = "6.0.0"
open = "5.3.2" open = "5.3.2"
reqwest = { version = "0.12.22", features = ["json", "rustls-tls", "cookies"] } reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "cookies"] }
once_cell = "1.21.3" once_cell = "1.21.3"
parking_lot = "0.12.4" parking_lot = "0.12.4"
url = "2.5.4" url = "2.5.4"
@@ -44,7 +44,8 @@ base64 = "0.22.1"
zip = "4.2.0" zip = "4.2.0"
tar = "0.4.44" tar = "0.4.44"
flate2 = "1.1.2" flate2 = "1.1.2"
regex = "1.11.1" regex = "1.11.2"
sysinfo = "0.37.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
@@ -55,7 +56,7 @@ windows-service = "0.8.0"
uzers = "0.12.1" uzers = "0.12.1"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.3.0" tauri-plugin-single-instance = "2.3.3"
[profile.release] [profile.release]
panic = "abort" panic = "abort"

View File

@@ -6,9 +6,10 @@ use tokio::time::{Duration, sleep};
use crate::cmd::http_api::{delete_process, get_process_list, start_process, stop_process}; use crate::cmd::http_api::{delete_process, get_process_list, start_process, stop_process};
use crate::cmd::openlist_core::create_openlist_core_process; use crate::cmd::openlist_core::create_openlist_core_process;
use crate::cmd::rclone_core::create_rclone_backend_process;
use crate::conf::config::MergedSettings; use crate::conf::config::MergedSettings;
use crate::object::structs::AppState; 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> { 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())?; let json = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
@@ -21,16 +22,10 @@ fn persist_app_settings(settings: &MergedSettings) -> Result<(), String> {
} }
fn update_data_config(port: u16, data_dir: Option<&str>) -> 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()) { let data_config_path = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
PathBuf::from(dir).join("config.json") PathBuf::from(dir).join("config.json")
} else { } else {
exe_dir.join("data").join("config.json") get_default_openlist_data_dir()?.join("config.json")
}; };
if let Some(parent) = data_config_path.parent() { if let Some(parent) = data_config_path.parent() {
@@ -94,6 +89,23 @@ async fn recreate_openlist_core_process(state: State<'_, AppState>) -> Result<()
Ok(()) Ok(())
} }
async fn recreate_rclone_backend_process(state: State<'_, AppState>) -> Result<(), String> {
let procs = get_process_list(state.clone()).await?;
if let Some(proc) = procs
.into_iter()
.find(|p| p.config.name == "single_rclone_backend_process")
{
let id = proc.config.id.clone();
let _ = stop_process(id.clone(), state.clone()).await;
sleep(Duration::from_millis(1000)).await;
let _ = delete_process(id, state.clone()).await;
sleep(Duration::from_millis(1000)).await;
create_rclone_backend_process(state.clone()).await?;
}
Ok(())
}
#[tauri::command] #[tauri::command]
pub async fn load_settings(state: State<'_, AppState>) -> Result<Option<MergedSettings>, String> { pub async fn load_settings(state: State<'_, AppState>) -> Result<Option<MergedSettings>, String> {
state.load_settings()?; state.load_settings()?;
@@ -117,12 +129,18 @@ pub async fn save_settings_with_update_port(
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let old_settings = state.get_settings(); let old_settings = state.get_settings();
let needs_process_recreation = if let Some(old) = old_settings { let needs_openlist_recreation = if let Some(old) = &old_settings {
old.openlist.data_dir != settings.openlist.data_dir old.openlist.data_dir != settings.openlist.data_dir
} else { } else {
false false
}; };
let needs_rclone_recreation = if let Some(old) = &old_settings {
old.rclone.api_port != settings.rclone.api_port
} else {
false
};
state.update_settings(settings.clone()); state.update_settings(settings.clone());
persist_app_settings(&settings)?; persist_app_settings(&settings)?;
let data_dir = if settings.openlist.data_dir.is_empty() { let data_dir = if settings.openlist.data_dir.is_empty() {
@@ -132,7 +150,7 @@ pub async fn save_settings_with_update_port(
}; };
update_data_config(settings.openlist.port, data_dir)?; update_data_config(settings.openlist.port, data_dir)?;
if needs_process_recreation { if needs_openlist_recreation {
if let Err(e) = recreate_openlist_core_process(state.clone()).await { if let Err(e) = recreate_openlist_core_process(state.clone()).await {
log::error!("{e}"); log::error!("{e}");
return Err(e); return Err(e);
@@ -148,6 +166,16 @@ pub async fn save_settings_with_update_port(
log::info!("Settings saved and OpenList core restarted with new port successfully"); log::info!("Settings saved and OpenList core restarted with new port successfully");
} }
if needs_rclone_recreation {
if let Err(e) = recreate_rclone_backend_process(state.clone()).await {
log::error!("Failed to recreate rclone backend process: {e}");
return Err(format!("Failed to recreate rclone backend process: {e}"));
}
log::info!("Rclone backend process recreated with new API port successfully");
} else {
log::info!("Settings saved successfully (no rclone port change detected)");
}
Ok(true) Ok(true)
} }

View File

@@ -5,6 +5,7 @@ use std::process::Command;
use tauri::State; use tauri::State;
use crate::object::structs::AppState; 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 { fn generate_random_password() -> String {
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
@@ -73,15 +74,28 @@ async fn execute_openlist_admin_set(
let mut cmd = Command::new(&openlist_exe); let mut cmd = Command::new(&openlist_exe);
cmd.args(["admin", "set", password]); 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() && !settings.openlist.data_dir.is_empty()
{ {
cmd.arg("--data"); settings.openlist.data_dir
cmd.arg(&settings.openlist.data_dir); } else {
log::info!("Using data directory: {}", settings.openlist.data_dir); 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:?}"); log::info!("Executing command: {cmd:?}");
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
let output = cmd let output = cmd
.output() .output()
.map_err(|e| format!("Failed to execute openlist command: {e}"))?; .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> { fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
let exe_path = let logs_dir = get_app_logs_dir()?;
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?; let service_path = get_service_log_path()?;
let app_dir = exe_path
.parent()
.ok_or("Executable has no parent directory")?
.to_path_buf();
let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) { let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
PathBuf::from(dir) PathBuf::from(dir)
} else { } 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(); let mut paths = Vec::new();
match source { match source {
Some("openlist") => paths.push(openlist_log_base.join("log/log.log")), Some("openlist") => paths.push(openlist_log_base.join("log/log.log")),
Some("app") => paths.push(app_dir.join("logs/app.log")), Some("app") => paths.push(logs_dir.join("app.log")),
Some("rclone") => paths.push(app_dir.join("logs/process_rclone.log")), Some("rclone") => paths.push(logs_dir.join("process_rclone.log")),
Some("openlist_core") => paths.push(app_dir.join("logs/process_openlist_core.log")), Some("openlist_core") => paths.push(logs_dir.join("process_openlist_core.log")),
None => { Some("service") => paths.push(service_path),
Some("all") => {
paths.push(openlist_log_base.join("log/log.log")); paths.push(openlist_log_base.join("log/log.log"));
paths.push(app_dir.join("logs/app.log")); paths.push(logs_dir.join("app.log"));
paths.push(app_dir.join("logs/process_rclone.log")); paths.push(logs_dir.join("process_rclone.log"));
paths.push(app_dir.join("logs/process_openlist_core.log")); paths.push(logs_dir.join("process_openlist_core.log"));
paths.push(service_path);
} }
_ => return Err("Invalid log source".into()), _ => return Err("Invalid log source".into()),
} }
@@ -220,9 +233,13 @@ pub async fn get_logs(
let mut logs = Vec::new(); let mut logs = Vec::new();
for path in paths { for path in paths {
let content = if path.exists() {
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {path:?}: {e}"))?; let content = std::fs::read_to_string(&path)
logs.extend(content.lines().map(str::to_string)); .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) Ok(logs)
} }

View File

@@ -4,7 +4,9 @@ use url::Url;
use crate::object::structs::{AppState, ServiceStatus}; use crate::object::structs::{AppState, ServiceStatus};
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port}; 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] #[tauri::command]
pub async fn create_openlist_core_process( 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 api_key = get_api_key();
let port = get_server_port(); let port = get_server_port();
let mut args = vec!["server".into()]; let mut args = vec!["server".into()];
if !data_dir.is_empty() {
args.push("--data".into()); // Use custom data dir if set, otherwise use platform-specific default
args.push(data_dir); 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 { let config = ProcessConfig {
id: "openlist_core".into(), id: "openlist_core".into(),
name: "single_openlist_core_process".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::cmd::http_api::{get_process_list, start_process, stop_process};
use crate::object::structs::{AppState, FileItem}; use crate::object::structs::{AppState, FileItem};
use crate::utils::github_proxy::apply_github_proxy; 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 { fn normalize_path(path: &str) -> String {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -615,3 +618,45 @@ fn extract_tar_gz(
executable_path executable_path
.ok_or_else(|| format!("Executable '{executable_name}' not found in tar.gz archive")) .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,15 +1,12 @@
use std::time::Duration; use reqwest;
use sysinfo::System;
use reqwest::{self, Client};
use tauri::State; use tauri::State;
use crate::cmd::http_api::{get_process_list, start_process}; use crate::cmd::http_api::{get_process_list, start_process};
use crate::object::structs::AppState; use crate::object::structs::AppState;
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port}; 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";
// admin:admin base64 encoded // admin:admin base64 encoded
pub const RCLONE_AUTH: &str = "Basic YWRtaW46YWRtaW4="; pub const RCLONE_AUTH: &str = "Basic YWRtaW46YWRtaW4=";
@@ -34,19 +31,23 @@ pub async fn create_and_start_rclone_backend(
#[tauri::command] #[tauri::command]
pub async fn create_rclone_backend_process( pub async fn create_rclone_backend_process(
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<ProcessConfig, String> { ) -> Result<ProcessConfig, String> {
let binary_path = let binary_path =
get_rclone_binary_path().map_err(|e| format!("Failed to get rclone binary path: {e}"))?; get_rclone_binary_path().map_err(|e| format!("Failed to get rclone binary path: {e}"))?;
let log_file_path = let log_file_path =
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?; get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?;
let rclone_conf_path = binary_path let rclone_conf_path =
.parent() get_rclone_config_path().map_err(|e| format!("Failed to get rclone config path: {e}"))?;
.map(|p| p.join("rclone.conf"))
.ok_or_else(|| "Failed to determine rclone.conf path".to_string())?;
let log_file_path = log_file_path.join("process_rclone.log"); let log_file_path = log_file_path.join("process_rclone.log");
let api_key = get_api_key(); let api_key = get_api_key();
let port = get_server_port(); let port = get_server_port();
let rclone_port = state
.get_settings()
.map(|settings| settings.rclone.api_port)
.unwrap_or(45572);
let config = ProcessConfig { let config = ProcessConfig {
id: "rclone_backend".into(), id: "rclone_backend".into(),
name: "single_rclone_backend_process".into(), name: "single_rclone_backend_process".into(),
@@ -60,7 +61,7 @@ pub async fn create_rclone_backend_process(
"--rc-pass".into(), "--rc-pass".into(),
"admin".into(), "admin".into(),
"--rc-addr".into(), "--rc-addr".into(),
format!("127.0.0.1:45572"), format!("127.0.0.1:{}", rclone_port),
"--rc-web-gui-no-open-browser".into(), "--rc-web-gui-no-open-browser".into(),
], ],
log_file: log_file_path.to_string_lossy().into_owned(), log_file: log_file_path.to_string_lossy().into_owned(),
@@ -110,13 +111,19 @@ pub async fn get_rclone_backend_status(_state: State<'_, AppState>) -> Result<bo
} }
async fn is_rclone_running() -> bool { 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 for process in system.processes().values() {
.get(format!("{RCLONE_API_BASE}/")) let process_name = process.name().to_string_lossy().to_lowercase();
.timeout(Duration::from_secs(3))
.send()
.await;
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

@@ -7,23 +7,33 @@ use serde_json::{Value, json};
use tauri::State; use tauri::State;
use super::http_api::get_process_list; use super::http_api::get_process_list;
use super::rclone_core::{RCLONE_API_BASE, RCLONE_AUTH}; use super::rclone_core::RCLONE_AUTH;
use crate::conf::rclone::{RcloneCreateRemoteRequest, RcloneMountRequest, RcloneWebdavConfig}; use crate::conf::rclone::{RcloneCreateRemoteRequest, RcloneMountRequest, RcloneWebdavConfig};
use crate::object::structs::{ use crate::object::structs::{
AppState, RcloneMountInfo, RcloneMountListResponse, RcloneRemoteListResponse, AppState, RcloneMountInfo, RcloneMountListResponse, RcloneRemoteListResponse,
}; };
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port}; use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
use crate::utils::args::split_args_vec; 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};
fn get_rclone_api_base_url(state: &State<AppState>) -> String {
let port = state
.get_settings()
.map(|settings| settings.rclone.api_port)
.unwrap_or(45572);
format!("http://127.0.0.1:{}", port)
}
struct RcloneApi { struct RcloneApi {
client: Client, client: Client,
api_base: String,
} }
impl RcloneApi { impl RcloneApi {
fn new() -> Self { fn new(api_base: String) -> Self {
Self { Self {
client: Client::new(), client: Client::new(),
api_base,
} }
} }
@@ -32,7 +42,7 @@ impl RcloneApi {
endpoint: &str, endpoint: &str,
body: Option<Value>, body: Option<Value>,
) -> Result<T, String> { ) -> Result<T, String> {
let url = format!("{RCLONE_API_BASE}/{endpoint}"); let url = format!("{}/{endpoint}", self.api_base);
let mut req = self.client.post(&url).header("Authorization", RCLONE_AUTH); let mut req = self.client.post(&url).header("Authorization", RCLONE_AUTH);
if let Some(b) = body { if let Some(b) = body {
req = req.json(&b).header("Content-Type", "application/json"); req = req.json(&b).header("Content-Type", "application/json");
@@ -53,7 +63,7 @@ impl RcloneApi {
} }
async fn post_text(&self, endpoint: &str) -> Result<String, String> { async fn post_text(&self, endpoint: &str) -> Result<String, String> {
let url = format!("{RCLONE_API_BASE}/{endpoint}"); let url = format!("{}/{endpoint}", self.api_base);
let resp = self let resp = self
.client .client
.post(&url) .post(&url)
@@ -76,9 +86,9 @@ impl RcloneApi {
#[tauri::command] #[tauri::command]
pub async fn rclone_list_config( pub async fn rclone_list_config(
remote_type: String, remote_type: String,
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Value, String> { ) -> Result<Value, String> {
let api = RcloneApi::new(); let api = RcloneApi::new(get_rclone_api_base_url(&state));
let text = api.post_text("config/dump").await?; let text = api.post_text("config/dump").await?;
let all: Value = serde_json::from_str(&text).map_err(|e| format!("Invalid JSON: {e}"))?; let all: Value = serde_json::from_str(&text).map_err(|e| format!("Invalid JSON: {e}"))?;
let remotes = match (remote_type.as_str(), all.as_object()) { let remotes = match (remote_type.as_str(), all.as_object()) {
@@ -101,15 +111,17 @@ pub async fn rclone_list_config(
} }
#[tauri::command] #[tauri::command]
pub async fn rclone_list_remotes() -> Result<Vec<String>, String> { pub async fn rclone_list_remotes(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let api = RcloneApi::new(); let api = RcloneApi::new(get_rclone_api_base_url(&state));
let resp: RcloneRemoteListResponse = api.post_json("config/listremotes", None).await?; let resp: RcloneRemoteListResponse = api.post_json("config/listremotes", None).await?;
Ok(resp.remotes) Ok(resp.remotes)
} }
#[tauri::command] #[tauri::command]
pub async fn rclone_list_mounts() -> Result<RcloneMountListResponse, String> { pub async fn rclone_list_mounts(
let api = RcloneApi::new(); state: State<'_, AppState>,
) -> Result<RcloneMountListResponse, String> {
let api = RcloneApi::new(get_rclone_api_base_url(&state));
api.post_json("mount/listmounts", None).await api.post_json("mount/listmounts", None).await
} }
@@ -118,9 +130,9 @@ pub async fn rclone_create_remote(
name: String, name: String,
r#type: String, r#type: String,
config: RcloneWebdavConfig, config: RcloneWebdavConfig,
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let api = RcloneApi::new(); let api = RcloneApi::new(get_rclone_api_base_url(&state));
let req = RcloneCreateRemoteRequest { let req = RcloneCreateRemoteRequest {
name, name,
r#type, r#type,
@@ -136,9 +148,9 @@ pub async fn rclone_update_remote(
name: String, name: String,
r#type: String, r#type: String,
config: RcloneWebdavConfig, config: RcloneWebdavConfig,
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let api = RcloneApi::new(); let api = RcloneApi::new(get_rclone_api_base_url(&state));
let body = json!({ "name": name, "type": r#type, "parameters": config }); let body = json!({ "name": name, "type": r#type, "parameters": config });
api.post_json::<Value>("config/update", Some(body)) api.post_json::<Value>("config/update", Some(body))
.await .await
@@ -148,9 +160,9 @@ pub async fn rclone_update_remote(
#[tauri::command] #[tauri::command]
pub async fn rclone_delete_remote( pub async fn rclone_delete_remote(
name: String, name: String,
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let api = RcloneApi::new(); let api = RcloneApi::new(get_rclone_api_base_url(&state));
let body = json!({ "name": name }); let body = json!({ "name": name });
api.post_json::<Value>("config/delete", Some(body)) api.post_json::<Value>("config/delete", Some(body))
.await .await
@@ -160,9 +172,9 @@ pub async fn rclone_delete_remote(
#[tauri::command] #[tauri::command]
pub async fn rclone_mount_remote( pub async fn rclone_mount_remote(
mount_request: RcloneMountRequest, mount_request: RcloneMountRequest,
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let api = RcloneApi::new(); let api = RcloneApi::new(get_rclone_api_base_url(&state));
api.post_json::<Value>("mount/mount", Some(json!(mount_request))) api.post_json::<Value>("mount/mount", Some(json!(mount_request)))
.await .await
.map(|_| true) .map(|_| true)
@@ -171,9 +183,9 @@ pub async fn rclone_mount_remote(
#[tauri::command] #[tauri::command]
pub async fn rclone_unmount_remote( pub async fn rclone_unmount_remote(
mount_point: String, mount_point: String,
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let api = RcloneApi::new(); let api = RcloneApi::new(get_rclone_api_base_url(&state));
api.post_json::<Value>("mount/unmount", Some(json!({ "mountPoint": mount_point }))) api.post_json::<Value>("mount/unmount", Some(json!({ "mountPoint": mount_point })))
.await .await
.map(|_| true) .map(|_| true)
@@ -189,10 +201,8 @@ pub async fn create_rclone_mount_remote_process(
let log_file_path = let log_file_path =
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?; 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 log_file_path = log_file_path.join("process_rclone.log");
let rclone_conf_path = binary_path let rclone_conf_path =
.parent() get_rclone_config_path().map_err(|e| format!("Failed to get rclone config path: {e}"))?;
.map(|p| p.join("rclone.conf"))
.ok_or_else(|| "Failed to determine rclone.conf path".to_string())?;
let api_key = get_api_key(); let api_key = get_api_key();
let port = get_server_port(); let port = get_server_port();

View File

@@ -8,6 +8,9 @@ pub struct AppConfig {
pub gh_proxy_api: Option<bool>, pub gh_proxy_api: Option<bool>,
pub open_links_in_browser: Option<bool>, pub open_links_in_browser: Option<bool>,
pub admin_password: Option<String>, pub admin_password: Option<String>,
pub show_window_on_startup: Option<bool>,
pub log_filter_level: Option<String>,
pub log_filter_source: Option<String>,
} }
impl AppConfig { impl AppConfig {
@@ -19,6 +22,9 @@ impl AppConfig {
gh_proxy_api: Some(false), gh_proxy_api: Some(false),
open_links_in_browser: Some(false), open_links_in_browser: Some(false),
admin_password: None, admin_password: None,
show_window_on_startup: Some(true),
log_filter_level: Some("all".to_string()),
log_filter_source: Some("openlist".to_string()),
} }
} }
} }

View File

@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use super::app::AppConfig; use super::app::AppConfig;
use crate::conf::core::OpenListCoreConfig; use crate::conf::core::OpenListCoreConfig;
use crate::conf::rclone::RcloneConfig; 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MergedSettings { pub struct MergedSettings {
@@ -33,12 +33,7 @@ impl MergedSettings {
if let Some(dir) = data_dir.filter(|d| !d.is_empty()) { if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
Ok(PathBuf::from(dir).join("config.json")) Ok(PathBuf::from(dir).join("config.json"))
} else { } else {
let exe = std::env::current_exe() Ok(get_default_openlist_data_dir()?.join("config.json"))
.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"))
} }
} }

View File

@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RcloneConfig { pub struct RcloneConfig {
pub config: serde_json::Value, pub config: serde_json::Value,
pub api_port: u16,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -45,6 +46,7 @@ impl RcloneConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
config: serde_json::Value::Object(Default::default()), config: serde_json::Value::Object(Default::default()),
api_port: 45572,
} }
} }
} }

View File

@@ -22,8 +22,9 @@ use cmd::logs::{
}; };
use cmd::openlist_core::{create_openlist_core_process, get_openlist_core_status}; use cmd::openlist_core::{create_openlist_core_process, get_openlist_core_status};
use cmd::os_operate::{ use cmd::os_operate::{
get_available_versions, list_files, open_file, open_folder, open_url, open_url_in_browser, get_available_versions, list_files, open_file, open_folder, open_logs_directory,
select_directory, update_tool_version, 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::{ use cmd::rclone_core::{
create_and_start_rclone_backend, create_rclone_backend_process, get_rclone_backend_status, create_and_start_rclone_backend, create_rclone_backend_process, get_rclone_backend_status,
@@ -141,6 +142,10 @@ pub fn run() {
list_files, list_files,
open_file, open_file,
open_folder, open_folder,
open_logs_directory,
open_openlist_data_dir,
open_rclone_config_file,
open_settings_file,
open_url, open_url,
open_url_in_browser, open_url_in_browser,
save_settings, save_settings,
@@ -180,6 +185,9 @@ pub fn run() {
utils::path::get_app_logs_dir()?; utils::path::get_app_logs_dir()?;
utils::init_log::init_log()?; utils::init_log::init_log()?;
utils::path::get_app_config_dir()?; utils::path::get_app_config_dir()?;
let settings = conf::config::MergedSettings::load().unwrap_or_default();
let show_window = settings.app.show_window_on_startup.unwrap_or(true);
let app_state = app.state::<AppState>(); let app_state = app.state::<AppState>();
if let Err(e) = app_state.init(app_handle) { if let Err(e) = app_state.init(app_handle) {
log::error!("Failed to initialize app state: {e}"); log::error!("Failed to initialize app state: {e}");
@@ -196,6 +204,13 @@ pub fn run() {
setup_background_update_checker(app_handle); setup_background_update_checker(app_handle);
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
if show_window {
let _ = window.show();
log::info!("Main window shown on startup based on user preference");
} else {
log::info!("Main window hidden on startup based on user preference");
}
let app_handle_clone = app_handle.clone(); let app_handle_clone = app_handle.clone();
window.on_window_event(move |event| { window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event { if let tauri::WindowEvent::CloseRequested { api, .. } = event {

View File

@@ -1,8 +1,33 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::{env, fs}; use std::{env, fs};
pub static APP_ID: &str = "io.github.openlistteam.openlist.desktop"; pub static APP_ID: &str = "io.github.openlistteam.openlist.desktop";
// Normalize path without Windows long path prefix (\\?\)
// The \\?\ prefix breaks compatibility with some applications like SQLite
fn normalize_path(path: &Path) -> Result<PathBuf, String> {
#[cfg(target_os = "windows")]
{
// On Windows, use canonicalize but strip the \\?\ prefix if present
let canonical = path
.canonicalize()
.map_err(|e| format!("Failed to canonicalize path: {e}"))?;
let path_str = canonical.to_string_lossy();
if let Some(stripped) = path_str.strip_prefix(r"\\?\") {
Ok(PathBuf::from(stripped))
} else {
Ok(canonical)
}
}
#[cfg(not(target_os = "windows"))]
{
path.canonicalize()
.map_err(|e| format!("Failed to canonicalize path: {e}"))
}
}
fn get_app_dir() -> Result<PathBuf, String> { fn get_app_dir() -> Result<PathBuf, String> {
let app_dir = env::current_exe() let app_dir = env::current_exe()
.map_err(|e| format!("Failed to get current exe path: {e}"))? .map_err(|e| format!("Failed to get current exe path: {e}"))?
@@ -16,6 +41,60 @@ fn get_app_dir() -> Result<PathBuf, String> {
Ok(app_dir) Ok(app_dir)
} }
fn get_user_data_dir() -> Result<PathBuf, String> {
let data_dir = {
#[cfg(target_os = "macos")]
{
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("OpenList Desktop")
}
#[cfg(target_os = "linux")]
{
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
PathBuf::from(home)
.join(".local")
.join("share")
.join("OpenList Desktop")
}
#[cfg(target_os = "windows")]
{
let appdata =
env::var("APPDATA").map_err(|_| "Failed to get APPDATA environment variable")?;
PathBuf::from(appdata).join("OpenList Desktop")
}
};
fs::create_dir_all(&data_dir).map_err(|e| format!("Failed to create data directory: {e}"))?;
normalize_path(&data_dir)
}
fn get_user_logs_dir() -> Result<PathBuf, String> {
let logs_dir = {
#[cfg(target_os = "macos")]
{
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
PathBuf::from(home)
.join("Library")
.join("Logs")
.join("OpenList Desktop")
}
#[cfg(not(target_os = "macos"))]
{
get_user_data_dir()?.join("logs")
}
};
fs::create_dir_all(&logs_dir).map_err(|e| format!("Failed to create logs directory: {e}"))?;
normalize_path(&logs_dir)
}
fn get_binary_path(binary: &str, service_name: &str) -> Result<PathBuf, String> { fn get_binary_path(binary: &str, service_name: &str) -> Result<PathBuf, String> {
let mut name = binary.to_string(); let mut name = binary.to_string();
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
@@ -40,7 +119,7 @@ pub fn get_rclone_binary_path() -> Result<PathBuf, String> {
} }
pub fn get_app_config_dir() -> 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> { pub fn app_config_file_path() -> Result<PathBuf, String> {
@@ -48,7 +127,31 @@ pub fn app_config_file_path() -> Result<PathBuf, String> {
} }
pub fn get_app_logs_dir() -> Result<PathBuf, String> { pub fn get_app_logs_dir() -> Result<PathBuf, String> {
let logs = get_app_dir()?.join("logs"); get_user_logs_dir()
fs::create_dir_all(&logs).map_err(|e| e.to_string())?; }
Ok(logs)
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> {
Ok(get_user_data_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("Logs")
.join("OpenList Desktop")
.join("openlist-desktop-service.log");
Ok(logs)
}
#[cfg(not(target_os = "macos"))]
{
Ok(get_app_logs_dir()?.join("openlist-desktop-service.log"))
}
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "OpenList Desktop", "productName": "OpenList Desktop",
"version": "0.5.1", "version": "0.8.0",
"identifier": "io.github.openlistteam.openlist.desktop", "identifier": "io.github.openlistteam.openlist.desktop",
"build": { "build": {
"beforeDevCommand": "yarn run dev", "beforeDevCommand": "yarn run dev",
@@ -19,7 +19,8 @@
"minHeight": 400, "minHeight": 400,
"resizable": true, "resizable": true,
"center": true, "center": true,
"decorations": false "decorations": false,
"visible": false
} }
], ],
"security": { "security": {

View File

@@ -13,7 +13,8 @@
"resizable": true, "resizable": true,
"center": true, "center": true,
"decorations": true, "decorations": true,
"titleBarStyle": "Transparent" "titleBarStyle": "Transparent",
"visible": false
} }
], ],
"security": { "security": {

View File

@@ -4,9 +4,9 @@
"bundle": { "bundle": {
"targets": ["nsis"], "targets": ["nsis"],
"windows": { "windows": {
"certificateThumbprint": "209114AD26E9B9B5788E4E9F6E522DFE8E4FABAD", "certificateThumbprint": "",
"digestAlgorithm": "sha256", "digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.comodoca.com", "timestampUrl": "http://time.certum.pl",
"webviewInstallMode": { "webviewInstallMode": {
"type": "embedBootstrapper", "type": "embedBootstrapper",
"silent": true "silent": true

View File

@@ -2,12 +2,12 @@
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAppStore } from './stores/app' import { TauriAPI } from './api/tauri'
import Navigation from './components/NavigationPage.vue'
import TitleBar from './components/ui/TitleBar.vue'
import { useTranslation } from './composables/useI18n' import { useTranslation } from './composables/useI18n'
import { useTray } from './composables/useTray' import { useTray } from './composables/useTray'
import { TauriAPI } from './api/tauri' import { useAppStore } from './stores/app'
import Navigation from './components/Navigation.vue'
import TitleBar from './components/ui/TitleBar.vue'
const appStore = useAppStore() const appStore = useAppStore()
const { t } = useTranslation() const { t } = useTranslation()
@@ -365,7 +365,8 @@ body {
.loading-backdrop { .loading-backdrop {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: radial-gradient(circle at 25% 25%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), background:
radial-gradient(circle at 25% 25%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.1) 0%, transparent 50%); radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
} }

View File

@@ -61,7 +61,11 @@ export class TauriAPI {
open: (path: string): Promise<boolean> => call('open_file', { path }), open: (path: string): Promise<boolean> => call('open_file', { path }),
folder: (path: string): Promise<boolean> => call('open_folder', { path }), folder: (path: string): Promise<boolean> => call('open_folder', { path }),
url: (url: string): Promise<boolean> => call('open_url', { url }), 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 --- // --- Settings management ---
@@ -75,9 +79,9 @@ export class TauriAPI {
// --- Logs management --- // --- Logs management ---
static logs = { 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 }), 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 }), call('clear_logs', { source: src }),
adminPassword: (): Promise<string> => call('get_admin_password'), adminPassword: (): Promise<string> => call('get_admin_password'),
resetAdminPassword: (): Promise<string> => call('reset_admin_password'), resetAdminPassword: (): Promise<string> => call('reset_admin_password'),

View File

@@ -1,43 +1,3 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useTranslation } from '../composables/useI18n'
import { useAppStore } from '../stores/app'
import LanguageSwitcher from './ui/LanguageSwitcher.vue'
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
import { Home, HardDrive, FileText, Settings, Download, DownloadCloud, Github } from 'lucide-vue-next'
import { TauriAPI } from '@/api/tauri'
const { t } = useTranslation()
const appStore = useAppStore()
const navigationItems = computed(() => [
{ name: t('navigation.dashboard'), path: '/', icon: Home, shortcut: 'Ctrl+H' },
{ name: t('navigation.mount'), path: '/mount', icon: HardDrive, shortcut: 'Ctrl+M' },
{ name: t('navigation.logs'), path: '/logs', icon: FileText, shortcut: 'Ctrl+L' },
{ name: t('navigation.settings'), path: '/settings', icon: Settings, shortcut: 'Ctrl+,' },
{
name: t('navigation.update'),
path: '/update',
icon: appStore.updateAvailable ? DownloadCloud : Download,
shortcut: 'Ctrl+U',
hasNotification: appStore.updateAvailable
}
])
const openLink = async (url: string) => {
try {
if (appStore.settings.app.open_links_in_browser) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
}
window.open(url, '_blank')
}
</script>
<template> <template>
<nav class="navigation"> <nav class="navigation">
<div class="title-bar"> <div class="title-bar">
@@ -76,9 +36,9 @@ const openLink = async (url: string) => {
<div class="github-section"> <div class="github-section">
<a <a
@click.prevent="openLink('https://github.com/OpenListTeam/openlist-desktop')"
class="github-link" class="github-link"
title="View on GitHub" title="View on GitHub"
@click.prevent="openLink('https://github.com/OpenListTeam/openlist-desktop')"
> >
<Github :size="20" /> <Github :size="20" />
</a> </a>
@@ -86,6 +46,53 @@ const openLink = async (url: string) => {
</nav> </nav>
</template> </template>
<script setup lang="ts">
import { Download, DownloadCloud, FileText, Github, HardDrive, Home, Settings } from 'lucide-vue-next'
import { computed } from 'vue'
import { TauriAPI } from '@/api/tauri'
import { useTranslation } from '../composables/useI18n'
import { useAppStore } from '../stores/app'
import LanguageSwitcher from './ui/LanguageSwitcher.vue'
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
const { t } = useTranslation()
const appStore = useAppStore()
const navigationItems = computed(() => [
{ name: t('navigation.dashboard'), path: '/', icon: Home, shortcut: 'Ctrl+H' },
{ name: t('navigation.mount'), path: '/mount', icon: HardDrive, shortcut: 'Ctrl+M' },
{ name: t('navigation.logs'), path: '/logs', icon: FileText, shortcut: 'Ctrl+L' },
{ name: t('navigation.settings'), path: '/settings', icon: Settings, shortcut: 'Ctrl+,' },
{
name: t('navigation.update'),
path: '/update',
icon: appStore.updateAvailable ? DownloadCloud : Download,
shortcut: 'Ctrl+U',
hasNotification: appStore.updateAvailable
}
])
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 || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
}
setTimeout(() => {
window.open(url, '_blank')
})
}
</script>
<style scoped> <style scoped>
.navigation { .navigation {
display: flex; display: flex;

View File

@@ -10,7 +10,7 @@
<div class="heartbeat-section"> <div class="heartbeat-section">
<div class="heartbeat-header"> <div class="heartbeat-header">
<h4></h4> <h4></h4>
<div class="metrics" v-if="isCoreRunning"> <div v-if="isCoreRunning" class="metrics">
<span class="metric info"> <span class="metric info">
<Globe :size="14" /> <Globe :size="14" />
Port: {{ openlistCoreStatus.port || 5244 }} Port: {{ openlistCoreStatus.port || 5244 }}
@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
<div class="heartbeat-chart" ref="chartContainer"> <div ref="chartContainer" class="heartbeat-chart">
<svg :width="chartWidth" :height="chartHeight" class="heartbeat-svg"> <svg :width="chartWidth" :height="chartHeight" class="heartbeat-svg">
<defs> <defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse"> <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
@@ -70,11 +70,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue' import { Activity, Globe } from 'lucide-vue-next'
import { useAppStore } from '../../stores/app' import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useTranslation } from '../../composables/useI18n' import { useTranslation } from '../../composables/useI18n'
import Card from '../ui/Card.vue' import { useAppStore } from '../../stores/app'
import { Globe, Activity } from 'lucide-vue-next' import Card from '../ui/CardPage.vue'
const { t } = useTranslation() const { t } = useTranslation()
const appStore = useAppStore() const appStore = useAppStore()
@@ -82,7 +83,7 @@ const appStore = useAppStore()
const chartContainer = ref<HTMLElement>() const chartContainer = ref<HTMLElement>()
const chartWidth = ref(400) const chartWidth = ref(400)
const chartHeight = ref(120) const chartHeight = ref(120)
const dataPoints = ref<Array<{ timestamp: number; responseTime: number; isHealthy: boolean }>>([]) const dataPoints = ref<{ timestamp: number; responseTime: number; isHealthy: boolean }[]>([])
const responseTime = ref(0) const responseTime = ref(0)
const startTime = ref(Date.now()) const startTime = ref(Date.now())
const monitoringInterval = ref<number>() const monitoringInterval = ref<number>()
@@ -258,7 +259,9 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
border-radius: 20px; border-radius: 20px;
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6); border: 1px solid rgba(226, 232, 240, 0.6);
transition: background-color 0.15s ease, border-color 0.15s ease; transition:
background-color 0.15s ease,
border-color 0.15s ease;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {

View File

@@ -13,11 +13,11 @@
</div> </div>
</div> </div>
<div class="doc-actions"> <div class="doc-actions">
<button @click="openOpenListDocs" class="doc-btn primary"> <button class="doc-btn primary" @click="openOpenListDocs">
<ExternalLink :size="14" /> <ExternalLink :size="14" />
<span>{{ t('dashboard.documentation.openDocs') }}</span> <span>{{ t('dashboard.documentation.openDocs') }}</span>
</button> </button>
<button @click="openOpenListGitHub" class="doc-btn secondary"> <button class="doc-btn secondary" @click="openOpenListGitHub">
<Github :size="14" /> <Github :size="14" />
<span>{{ t('dashboard.documentation.github') }}</span> <span>{{ t('dashboard.documentation.github') }}</span>
</button> </button>
@@ -35,11 +35,11 @@
</div> </div>
</div> </div>
<div class="doc-actions"> <div class="doc-actions">
<button @click="openRcloneDocs" class="doc-btn primary"> <button class="doc-btn primary" @click="openRcloneDocs">
<ExternalLink :size="14" /> <ExternalLink :size="14" />
<span>{{ t('dashboard.documentation.openDocs') }}</span> <span>{{ t('dashboard.documentation.openDocs') }}</span>
</button> </button>
<button @click="openRcloneGitHub" class="doc-btn secondary"> <button class="doc-btn secondary" @click="openRcloneGitHub">
<Github :size="14" /> <Github :size="14" />
<span>{{ t('dashboard.documentation.github') }}</span> <span>{{ t('dashboard.documentation.github') }}</span>
</button> </button>
@@ -52,19 +52,19 @@
<h4>{{ t('dashboard.documentation.quickLinks') }}</h4> <h4>{{ t('dashboard.documentation.quickLinks') }}</h4>
</div> </div>
<div class="links-grid"> <div class="links-grid">
<button @click="openLink('https://docs.oplist.org/guide/api')" class="link-btn"> <button class="link-btn" @click="openLink('https://docs.oplist.org/guide/api')">
<Code :size="16" /> <Code :size="16" />
<span>{{ t('dashboard.documentation.apiDocs') }}</span> <span>{{ t('dashboard.documentation.apiDocs') }}</span>
</button> </button>
<button @click="openLink('https://rclone.org/commands/')" class="link-btn"> <button class="link-btn" @click="openLink('https://rclone.org/commands/')">
<Terminal :size="16" /> <Terminal :size="16" />
<span>{{ t('dashboard.documentation.commands') }}</span> <span>{{ t('dashboard.documentation.commands') }}</span>
</button> </button>
<button @click="openLink('https://github.com/OpenListTeam/OpenList-desktop/issues')" class="link-btn"> <button class="link-btn" @click="openLink('https://github.com/OpenListTeam/OpenList-desktop/issues')">
<HelpCircle :size="16" /> <HelpCircle :size="16" />
<span>{{ t('dashboard.documentation.issues') }}</span> <span>{{ t('dashboard.documentation.issues') }}</span>
</button> </button>
<button @click="openLink('https://docs.oplist.org/faq/')" class="link-btn"> <button class="link-btn" @click="openLink('https://docs.oplist.org/faq/')">
<MessageCircle :size="16" /> <MessageCircle :size="16" />
<span>{{ t('dashboard.documentation.faq') }}</span> <span>{{ t('dashboard.documentation.faq') }}</span>
</button> </button>
@@ -75,11 +75,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useTranslation } from '../../composables/useI18n' import { BookOpen, Cloud, Code, ExternalLink, Github, HelpCircle, MessageCircle, Terminal } from 'lucide-vue-next'
import { ExternalLink, Github, BookOpen, Cloud, Code, Terminal, HelpCircle, MessageCircle } from 'lucide-vue-next' import { computed } from 'vue'
import Card from '../ui/Card.vue'
import { TauriAPI } from '../../api/tauri' import { TauriAPI } from '../../api/tauri'
import { useTranslation } from '../../composables/useI18n'
import { useAppStore } from '../../stores/app' import { useAppStore } from '../../stores/app'
import Card from '../ui/CardPage.vue'
const { t } = useTranslation() const { t } = useTranslation()
const appStore = useAppStore() const appStore = useAppStore()
@@ -100,16 +102,22 @@ const openRcloneGitHub = () => {
openLink('https://github.com/rclone/rclone') openLink('https://github.com/rclone/rclone')
} }
const isMacOs = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'darwin'
})
const openLink = async (url: string) => { const openLink = async (url: string) => {
try { try {
if (appStore.settings.app.open_links_in_browser) { if (appStore.settings.app.open_links_in_browser || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url) await TauriAPI.files.urlInBrowser(url)
return return
} }
} catch (error) { } catch (error) {
console.error('Failed to open link:', error) console.error('Failed to open link:', error)
} }
window.open(url, '_blank') setTimeout(() => {
window.open(url, '_blank')
})
} }
</script> </script>

View File

@@ -4,40 +4,53 @@
<div class="action-section"> <div class="action-section">
<div class="section-header"> <div class="section-header">
<h4>{{ t('dashboard.quickActions.openlistService') }}</h4> <h4>{{ t('dashboard.quickActions.openlistService') }}</h4>
<div v-if="isCoreLoading" class="section-loading-indicator">
<Loader :size="12" class="loading-icon" />
</div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button @click="toggleCore" :class="['action-btn', 'service-btn', { running: isCoreRunning }]"> <button
<component :is="serviceButtonIcon" :size="20" /> :disabled="isCoreLoading"
<span>{{ serviceButtonText }}</span> :class="['action-btn', 'service-btn', { running: isCoreRunning, loading: isCoreLoading }]"
@click="toggleCore"
>
<component :is="serviceButtonIcon" v-if="!isCoreLoading" :size="20" />
<Loader v-else :size="20" class="loading-icon" />
<span>{{ isCoreLoading ? t('dashboard.quickActions.processing') : serviceButtonText }}</span>
</button> </button>
<button @click="restartCore" :disabled="!isCoreRunning" class="action-btn restart-btn"> <button
<RotateCcw :size="18" /> :disabled="!isCoreRunning || isCoreLoading"
:class="['action-btn', 'restart-btn', { loading: isCoreLoading }]"
@click="restartCore"
>
<RotateCcw v-if="!isCoreLoading" :size="18" />
<Loader v-else :size="18" class="loading-icon" />
<span>{{ t('dashboard.quickActions.restart') }}</span> <span>{{ t('dashboard.quickActions.restart') }}</span>
</button> </button>
<button <button
@click="openWebUI" :disabled="!isCoreRunning || isCoreLoading"
:disabled="!isCoreRunning"
class="action-btn web-btn" class="action-btn web-btn"
:title="appStore.openListCoreUrl" :title="appStore.openListCoreUrl"
@click="openWebUI"
> >
<ExternalLink :size="18" /> <ExternalLink :size="18" />
<span>{{ t('dashboard.quickActions.openWeb') }}</span> <span>{{ t('dashboard.quickActions.openWeb') }}</span>
</button> </button>
<button <button
@click="copyAdminPassword"
class="action-btn password-btn icon-only-btn" class="action-btn password-btn icon-only-btn"
:title="t('dashboard.quickActions.copyAdminPassword')" :title="t('dashboard.quickActions.copyAdminPassword')"
@click="copyAdminPassword"
> >
<Key :size="16" /> <Key :size="16" />
</button> </button>
<button <button
@click="resetAdminPassword"
class="action-btn reset-password-btn icon-only-btn" class="action-btn reset-password-btn icon-only-btn"
:title="t('dashboard.quickActions.resetAdminPassword')" :title="t('dashboard.quickActions.resetAdminPassword')"
@click="resetAdminPassword"
> >
<RotateCcw :size="16" /> <RotateCcw :size="16" />
</button> </button>
@@ -47,26 +60,37 @@
<div class="action-section"> <div class="action-section">
<div class="section-header"> <div class="section-header">
<h4>{{ t('dashboard.quickActions.rclone') }}</h4> <h4>{{ t('dashboard.quickActions.rclone') }}</h4>
<div v-if="isRcloneLoading" class="section-loading-indicator">
<Loader :size="12" class="loading-icon" />
</div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button <button
:disabled="isRcloneLoading"
:class="[
'action-btn',
'service-indicator-btn',
{ active: rcloneStore.serviceRunning, loading: isRcloneLoading }
]"
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()" @click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
:class="['action-btn', 'service-indicator-btn', { active: rcloneStore.serviceRunning }]"
> >
<component :is="rcloneStore.serviceRunning ? Square : Play" :size="18" /> <component :is="rcloneStore.serviceRunning ? Square : Play" v-if="!isRcloneLoading" :size="18" />
<Loader v-else :size="18" class="loading-icon" />
<span>{{ <span>{{
rcloneStore.serviceRunning isRcloneLoading
? t('dashboard.quickActions.stopRclone') ? t('dashboard.quickActions.processing')
: t('dashboard.quickActions.startRclone') : rcloneStore.serviceRunning
? t('dashboard.quickActions.stopRclone')
: t('dashboard.quickActions.startRclone')
}}</span> }}</span>
</button> </button>
<button @click="openRcloneConfig" class="action-btn config-btn"> <button class="action-btn config-btn" @click="openRcloneConfig">
<Settings :size="18" /> <Settings :size="18" />
<span>{{ t('dashboard.quickActions.configRclone') }}</span> <span>{{ t('dashboard.quickActions.configRclone') }}</span>
</button> </button>
<button @click="viewMounts" class="action-btn mount-btn"> <button class="action-btn mount-btn" @click="viewMounts">
<HardDrive :size="18" /> <HardDrive :size="18" />
<span>{{ t('dashboard.quickActions.manageMounts') }}</span> <span>{{ t('dashboard.quickActions.manageMounts') }}</span>
</button> </button>
@@ -80,14 +104,13 @@
</div> </div>
<div class="settings-toggles"> <div class="settings-toggles">
<label class="toggle-item"> <label class="toggle-item">
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" /> <input v-model="settings.openlist.auto_launch" type="checkbox" @change="handleAutoLaunchToggle" />
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span> <span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
</label> </label>
<!-- Windows Firewall Management--> <!-- Windows Firewall Management-->
<button <button
v-if="isWindows" v-if="isWindows"
@click="toggleFirewallRule"
:class="['firewall-toggle-btn', { active: firewallEnabled }]" :class="['firewall-toggle-btn', { active: firewallEnabled }]"
:disabled="firewallLoading" :disabled="firewallLoading"
:title=" :title="
@@ -95,6 +118,7 @@
? t('dashboard.quickActions.firewall.disable') ? t('dashboard.quickActions.firewall.disable')
: t('dashboard.quickActions.firewall.enable') : t('dashboard.quickActions.firewall.enable')
" "
@click="toggleFirewallRule"
> >
<Shield :size="18" /> <Shield :size="18" />
<span> <span>
@@ -112,14 +136,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ExternalLink, HardDrive, Key, Loader, Play, RotateCcw, Settings, Shield, Square } from 'lucide-vue-next'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { TauriAPI } from '@/api/tauri'
import { useTranslation } from '../../composables/useI18n'
import { useAppStore } from '../../stores/app' import { useAppStore } from '../../stores/app'
import { useRcloneStore } from '../../stores/rclone' import { useRcloneStore } from '../../stores/rclone'
import { useTranslation } from '../../composables/useI18n' import Card from '../ui/CardPage.vue'
import Card from '../ui/Card.vue'
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key, Shield } from 'lucide-vue-next'
import { TauriAPI } from '@/api/tauri'
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
@@ -127,6 +153,8 @@ const appStore = useAppStore()
const rcloneStore = useRcloneStore() const rcloneStore = useRcloneStore()
const isCoreRunning = computed(() => appStore.isCoreRunning) const isCoreRunning = computed(() => appStore.isCoreRunning)
const isCoreLoading = computed(() => appStore.loading)
const isRcloneLoading = computed(() => rcloneStore.loading)
const settings = computed(() => appStore.settings) const settings = computed(() => appStore.settings)
let statusCheckInterval: number | null = null let statusCheckInterval: number | null = null
@@ -400,16 +428,22 @@ const showNotification = (type: 'success' | 'error', message: string) => {
}, 4000) }, 4000)
} }
const isMacOs = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'darwin'
})
const openLink = async (url: string) => { const openLink = async (url: string) => {
try { try {
if (appStore.settings.app.open_links_in_browser) { if (appStore.settings.app.open_links_in_browser || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url) await TauriAPI.files.urlInBrowser(url)
return return
} }
} catch (error) { } catch (error) {
console.error('Failed to open link:', error) console.error('Failed to open link:', error)
} }
window.open(url, '_blank') setTimeout(() => {
window.open(url, '_blank')
})
} }
onMounted(async () => { onMounted(async () => {
@@ -452,6 +486,16 @@ onUnmounted(() => {
letter-spacing: -0.025em; 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 { .icon-only-btn {
flex: 0 0 auto; flex: 0 0 auto;
min-width: auto; min-width: auto;
@@ -509,6 +553,15 @@ onUnmounted(() => {
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; 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 { .service-btn.running {
background: rgb(239, 68, 68); background: rgb(239, 68, 68);

View File

@@ -19,10 +19,10 @@
<div class="actions-section"> <div class="actions-section">
<div class="action-buttons"> <div class="action-buttons">
<button <button
@click="installService" v-if="serviceStatus !== 'running' && serviceStatus !== 'stopped'"
:disabled="actionLoading || serviceStatus === 'installed'" :disabled="actionLoading || serviceStatus === 'installed'"
class="action-btn install-btn" class="action-btn install-btn"
v-if="serviceStatus !== 'running' && serviceStatus !== 'stopped'" @click="installService"
> >
<component :is="actionLoading && currentAction === 'install' ? LoaderIcon : Download" :size="16" /> <component :is="actionLoading && currentAction === 'install' ? LoaderIcon : Download" :size="16" />
<span>{{ <span>{{
@@ -33,10 +33,10 @@
</button> </button>
<button <button
@click="startService" v-if="serviceStatus === 'installed' || serviceStatus === 'stopped'"
:disabled="actionLoading || (serviceStatus !== 'installed' && serviceStatus !== 'stopped')" :disabled="actionLoading || (serviceStatus !== 'installed' && serviceStatus !== 'stopped')"
class="action-btn start-btn" class="action-btn start-btn"
v-if="serviceStatus === 'installed' || serviceStatus === 'stopped'" @click="startService"
> >
<component :is="actionLoading && currentAction === 'start' ? LoaderIcon : Play" :size="16" /> <component :is="actionLoading && currentAction === 'start' ? LoaderIcon : Play" :size="16" />
<span>{{ <span>{{
@@ -45,10 +45,10 @@
</button> </button>
<button <button
@click="stopService" v-if="serviceStatus === 'running'"
:disabled="actionLoading" :disabled="actionLoading"
class="action-btn stop-btn" class="action-btn stop-btn"
v-if="serviceStatus === 'running'" @click="stopService"
> >
<component :is="actionLoading && currentAction === 'stop' ? LoaderIcon : Stop" :size="16" /> <component :is="actionLoading && currentAction === 'stop' ? LoaderIcon : Stop" :size="16" />
<span>{{ <span>{{
@@ -57,10 +57,10 @@
</button> </button>
<button <button
@click="showUninstallDialog = true" v-if="serviceStatus !== 'not-installed'"
:disabled="actionLoading" :disabled="actionLoading"
class="action-btn uninstall-btn" class="action-btn uninstall-btn"
v-if="serviceStatus !== 'not-installed'" @click="showUninstallDialog = true"
> >
<component :is="actionLoading && currentAction === 'uninstall' ? LoaderIcon : Trash2" :size="16" /> <component :is="actionLoading && currentAction === 'uninstall' ? LoaderIcon : Trash2" :size="16" />
<span>{{ <span>{{
@@ -87,24 +87,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useTranslation } from '../../composables/useI18n'
import { import {
CheckCircle2,
Circle,
Download, Download,
Loader2 as LoaderIcon,
Play, Play,
Server,
Square as Stop, Square as Stop,
Trash2, Trash2,
Loader2 as LoaderIcon, XCircle
CheckCircle2,
XCircle,
Circle,
Server
} from 'lucide-vue-next' } from 'lucide-vue-next'
import Card from '../ui/Card.vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import ConfirmDialog from '../ui/ConfirmDialog.vue'
import { TauriAPI } from '../../api/tauri'
import { useRcloneStore } from '@/stores/rclone' import { useRcloneStore } from '@/stores/rclone'
import { TauriAPI } from '../../api/tauri'
import { useTranslation } from '../../composables/useI18n'
import Card from '../ui/CardPage.vue'
import ConfirmDialog from '../ui/ConfirmDialog.vue'
const rcloneStore = useRcloneStore() const rcloneStore = useRcloneStore()
const { t } = useTranslation() const { t } = useTranslation()
@@ -114,7 +116,7 @@ const actionLoading = ref(false)
const currentAction = ref('') const currentAction = ref('')
const showUninstallDialog = ref(false) const showUninstallDialog = ref(false)
let statusCheckInterval: number | null = null const statusCheckInterval: number | null = null
const statusClass = computed(() => { const statusClass = computed(() => {
switch (serviceStatus.value) { switch (serviceStatus.value) {
@@ -225,7 +227,17 @@ const stopService = async () => {
if (!result) { if (!result) {
throw new Error('Service stop failed') 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) { } catch (error) {
console.error('Failed to stop service:', error) console.error('Failed to stop service:', error)
serviceStatus.value = 'error' serviceStatus.value = 'error'
@@ -265,7 +277,6 @@ const cancelUninstall = () => {
onMounted(async () => { onMounted(async () => {
await checkServiceStatus() await checkServiceStatus()
statusCheckInterval = window.setInterval(checkServiceStatus, 30 * 1000)
}) })
onUnmounted(() => { onUnmounted(() => {

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<Card <Card
:title="t('update.title')" :title="t('update.title')"
@@ -12,7 +13,7 @@
<h4>{{ t('update.currentVersion') }}</h4> <h4>{{ t('update.currentVersion') }}</h4>
<span class="version-tag">v{{ currentVersion }}</span> <span class="version-tag">v{{ currentVersion }}</span>
</div> </div>
<button @click="checkForUpdates" :disabled="checking || downloading || installing" class="check-update-btn"> <button :disabled="checking || downloading || installing" class="check-update-btn" @click="checkForUpdates">
<RefreshCw :size="16" /> <RefreshCw :size="16" />
{{ checking ? t('update.checking') : t('update.checkForUpdates') }} {{ checking ? t('update.checking') : t('update.checkForUpdates') }}
</button> </button>
@@ -21,7 +22,7 @@
<div class="settings-row"> <div class="settings-row">
<div class="auto-check-setting"> <div class="auto-check-setting">
<label class="checkbox-container"> <label class="checkbox-container">
<input type="checkbox" v-model="autoCheckEnabled" @change="toggleAutoCheck" :disabled="settingsLoading" /> <input v-model="autoCheckEnabled" type="checkbox" :disabled="settingsLoading" @change="toggleAutoCheck" />
<span class="label-text">{{ t('update.autoCheck') }}</span> <span class="label-text">{{ t('update.autoCheck') }}</span>
</label> </label>
</div> </div>
@@ -32,7 +33,7 @@
<AlertCircle :size="16" /> <AlertCircle :size="16" />
<span>{{ error }}</span> <span>{{ error }}</span>
</div> </div>
<button @click="clearError" class="clear-error-btn">×</button> <button class="clear-error-btn" @click="clearError">×</button>
</div> </div>
<div v-if="!updateCheck?.hasUpdate && lastChecked && !checking && !error" class="no-updates"> <div v-if="!updateCheck?.hasUpdate && lastChecked && !checking && !error" class="no-updates">
@@ -100,11 +101,11 @@
</div> </div>
</div> </div>
<div class="update-actions" v-if="!downloading"> <div v-if="!downloading" class="update-actions">
<button <button
@click="downloadAndInstall"
:disabled="!selectedAsset || checking || downloading || installing" :disabled="!selectedAsset || checking || downloading || installing"
class="install-btn" class="install-btn"
@click="downloadAndInstall"
> >
<Download :size="16" /> <Download :size="16" />
{{ t('update.downloadAndInstall') }} {{ t('update.downloadAndInstall') }}
@@ -124,7 +125,7 @@
<Info :size="20" class="notification-icon" /> <Info :size="20" class="notification-icon" />
<div class="notification-text"> <div class="notification-text">
<span>{{ t('update.backgroundUpdateAvailable') }}</span> <span>{{ t('update.backgroundUpdateAvailable') }}</span>
<button @click="showBackgroundUpdate" class="show-update-btn"> <button class="show-update-btn" @click="showBackgroundUpdate">
{{ t('update.showUpdate') }} {{ t('update.showUpdate') }}
</button> </button>
</div> </div>
@@ -135,13 +136,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue' import { AlertCircle, ArrowRight, CheckCircle, CheckCircle2, Download, Info, RefreshCw } from 'lucide-vue-next'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { formatBytes } from '@/utils/formatters'
import { TauriAPI } from '../../api/tauri'
import { useTranslation } from '../../composables/useI18n' import { useTranslation } from '../../composables/useI18n'
import { useAppStore } from '../../stores/app' import { useAppStore } from '../../stores/app'
import { TauriAPI } from '../../api/tauri' import Card from '../ui/CardPage.vue'
import Card from '../ui/Card.vue'
import { formatBytes } from '@/utils/formatters'
import { RefreshCw, Download, ArrowRight, CheckCircle, AlertCircle, Info, CheckCircle2 } from 'lucide-vue-next'
interface Props { interface Props {
isStandalone?: boolean isStandalone?: boolean
@@ -204,7 +207,7 @@ const checkForUpdates = async () => {
} }
} catch (err: any) { } catch (err: any) {
console.error('Failed to check for updates:', err) console.error('Failed to check for updates:', err)
error.value = err.message || t('update.checkError') error.value = t('update.checkError') + String(err ? `: ${err}` : '')
} finally { } finally {
checking.value = false checking.value = false
} }

View File

@@ -8,8 +8,12 @@
<h4>{{ t('dashboard.versionManager.openlist') }}</h4> <h4>{{ t('dashboard.versionManager.openlist') }}</h4>
<span class="current-version">{{ currentVersions.openlist }}</span> <span class="current-version">{{ currentVersions.openlist }}</span>
</div> </div>
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn"> <button :disabled="refreshing" class="refresh-icon-btn" @click="refreshVersions">
<component :is="refreshing ? LoaderIcon : RefreshCw" :size="16" /> <component
:is="refreshing ? Loader : RefreshCw"
:size="16"
:class="{ 'rotate-animation': refreshing && !loading.openlist }"
/>
</button> </button>
</div> </div>
<div class="version-controls"> <div class="version-controls">
@@ -20,14 +24,16 @@
</option> </option>
</select> </select>
<button <button
@click="updateVersion('openlist')"
:disabled=" :disabled="
!selectedVersions.openlist || loading.openlist || selectedVersions.openlist === currentVersions.openlist !selectedVersions.openlist || loading.openlist || selectedVersions.openlist === currentVersions.openlist
" "
class="update-btn" class="update-btn"
@click="updateVersion('openlist')"
> >
<component :is="loading.openlist ? LoaderIcon : Download" :size="14" /> <component :is="loading.openlist ? Loader : Download" :size="14" />
<span>{{ loading.openlist ? t('common.loading') : t('dashboard.versionManager.update') }}</span> <span>{{
loading.openlist ? t('dashboard.versionManager.updating') : t('dashboard.versionManager.update')
}}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -37,8 +43,12 @@
<h4>{{ t('dashboard.versionManager.rclone') }}</h4> <h4>{{ t('dashboard.versionManager.rclone') }}</h4>
<span class="current-version">{{ currentVersions.rclone }}</span> <span class="current-version">{{ currentVersions.rclone }}</span>
</div> </div>
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn"> <button :disabled="refreshing" class="refresh-icon-btn" @click="refreshVersions">
<component :is="refreshing ? LoaderIcon : RefreshCw" :size="16" /> <component
:is="refreshing ? Loader : RefreshCw"
:size="16"
:class="{ 'rotate-animation': refreshing && !loading.rclone }"
/>
</button> </button>
</div> </div>
<div class="version-controls"> <div class="version-controls">
@@ -49,14 +59,16 @@
</option> </option>
</select> </select>
<button <button
@click="updateVersion('rclone')"
:disabled=" :disabled="
!selectedVersions.rclone || loading.rclone || selectedVersions.rclone === currentVersions.rclone !selectedVersions.rclone || loading.rclone || selectedVersions.rclone === currentVersions.rclone
" "
class="update-btn" class="update-btn"
@click="updateVersion('rclone')"
> >
<component :is="loading.rclone ? LoaderIcon : Download" :size="14" /> <component :is="loading.rclone ? Loader : Download" :size="14" />
<span>{{ loading.rclone ? t('common.loading') : t('dashboard.versionManager.update') }}</span> <span>{{
loading.rclone ? t('dashboard.versionManager.updating') : t('dashboard.versionManager.update')
}}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -66,11 +78,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { Download, Loader, RefreshCw } from 'lucide-vue-next'
import { useTranslation } from '../../composables/useI18n' import { onMounted, ref } from 'vue'
import { Download, RefreshCw, Loader2 as LoaderIcon } from 'lucide-vue-next'
import Card from '../ui/Card.vue'
import { TauriAPI } from '../../api/tauri' import { TauriAPI } from '../../api/tauri'
import { useTranslation } from '../../composables/useI18n'
import Card from '../ui/CardPage.vue'
const { t } = useTranslation() const { t } = useTranslation()
@@ -144,20 +157,95 @@ const refreshVersions = async () => {
const updateVersion = async (type: 'openlist' | 'rclone') => { const updateVersion = async (type: 'openlist' | 'rclone') => {
loading.value[type] = true loading.value[type] = true
try { try {
const result = await TauriAPI.bin.updateVersion(type, selectedVersions.value[type]) const result = await TauriAPI.bin.updateVersion(type, selectedVersions.value[type])
currentVersions.value[type] = selectedVersions.value[type] currentVersions.value[type] = selectedVersions.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) console.log(`Updated ${type}:`, result)
} catch (error) { } catch (error) {
console.error(`Failed to update ${type}:`, 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 { } finally {
loading.value[type] = false 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(() => { onMounted(() => {
refreshVersions() refreshVersions()
}) })

View File

@@ -28,6 +28,7 @@ interface Props {
} }
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
title: '',
variant: 'default', variant: 'default',
hover: false, hover: false,
interactive: false interactive: false

View File

@@ -8,10 +8,10 @@
<p class="dialog-message">{{ message }}</p> <p class="dialog-message">{{ message }}</p>
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button @click="onCancel" class="dialog-btn cancel-btn"> <button class="dialog-btn cancel-btn" @click="onCancel">
{{ cancelText }} {{ cancelText }}
</button> </button>
<button @click="onConfirm" class="dialog-btn confirm-btn" :class="confirmButtonClass"> <button class="dialog-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
{{ confirmText }} {{ confirmText }}
</button> </button>
</div> </div>
@@ -79,7 +79,9 @@ export default {
.dialog-container { .dialog-container {
background: white; background: white;
border-radius: 0.75rem; border-radius: 0.75rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 28rem; max-width: 28rem;
width: 90%; width: 90%;
max-height: 80vh; max-height: 80vh;

View File

@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useTranslation } from '../../composables/useI18n'
import { ChevronDown } from 'lucide-vue-next' import { ChevronDown } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useTranslation } from '../../composables/useI18n'
const { currentLocale, switchLanguage } = useTranslation() const { currentLocale, switchLanguage } = useTranslation()
const isOpen = ref(false) const isOpen = ref(false)
@@ -35,7 +36,7 @@ onMounted(() => {
<template> <template>
<div ref="dropdownRef" class="language-switcher relative"> <div ref="dropdownRef" class="language-switcher relative">
<button @click="toggleDropdown" class="language-button"> <button class="language-button" @click="toggleDropdown">
<span class="language-label">{{ currentLanguage?.name }}</span> <span class="language-label">{{ currentLanguage?.name }}</span>
<ChevronDown :size="12" :class="{ flipped: isOpen }" /> <ChevronDown :size="12" :class="{ flipped: isOpen }" />
</button> </button>
@@ -44,9 +45,9 @@ onMounted(() => {
<div <div
v-for="language in languages" v-for="language in languages"
:key="language.code" :key="language.code"
@click="handleLanguageChange(language.code)"
class="language-option" class="language-option"
:class="{ active: language.code === currentLocale }" :class="{ active: language.code === currentLocale }"
@click="handleLanguageChange(language.code)"
> >
<span class="language-flag">{{ language.flag }}</span> <span class="language-flag">{{ language.flag }}</span>
<span class="language-name">{{ language.name }}</span> <span class="language-name">{{ language.name }}</span>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { Monitor, Moon, Sun } from 'lucide-vue-next'
import { computed } from 'vue' import { computed } from 'vue'
import { useAppStore } from '../../stores/app'
import { useTranslation } from '../../composables/useI18n' import { useTranslation } from '../../composables/useI18n'
import { Sun, Moon, Monitor } from 'lucide-vue-next' import { useAppStore } from '../../stores/app'
const appStore = useAppStore() const appStore = useAppStore()
const { t } = useTranslation() const { t } = useTranslation()
@@ -41,7 +42,7 @@ const toggleTheme = () => {
<template> <template>
<div class="theme-switcher"> <div class="theme-switcher">
<button @click="toggleTheme" class="theme-toggle-btn" :title="t('settings.theme.toggle')"> <button class="theme-toggle-btn" :title="t('settings.theme.toggle')" @click="toggleTheme">
<component :is="currentThemeOption.icon" :size="18" /> <component :is="currentThemeOption.icon" :size="18" />
<span class="theme-label">{{ currentThemeOption.label }}</span> <span class="theme-label">{{ currentThemeOption.label }}</span>
</button> </button>

View File

@@ -18,7 +18,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import { ref, onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import WindowControls from './WindowControls.vue' import WindowControls from './WindowControls.vue'
interface Props { interface Props {
appTitle?: string appTitle?: string

View File

@@ -10,10 +10,10 @@
<div v-if="message" class="notification-message">{{ message }}</div> <div v-if="message" class="notification-message">{{ message }}</div>
</div> </div>
<div class="notification-actions"> <div class="notification-actions">
<button v-if="showAction" @click="$emit('action')" class="action-btn"> <button v-if="showAction" class="action-btn" @click="$emit('action')">
{{ actionText }} {{ actionText }}
</button> </button>
<button @click="$emit('dismiss')" class="dismiss-btn"> <button class="dismiss-btn" @click="$emit('dismiss')">
<X :size="16" /> <X :size="16" />
</button> </button>
</div> </div>
@@ -23,13 +23,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Download, CheckCircle, AlertCircle, X } from 'lucide-vue-next' import { AlertCircle, CheckCircle, Download, X } from 'lucide-vue-next'
interface Props { interface Props {
visible: boolean visible: boolean
type: 'info' | 'success' | 'warning' | 'error' type?: 'info' | 'success' | 'warning' | 'error'
title: string title: string
message?: string message: string
showAction?: boolean showAction?: boolean
actionText?: string actionText?: string
} }

View File

@@ -1,19 +1,20 @@
<template> <template>
<div class="window-controls"> <div class="window-controls">
<button class="control-btn minimize" @click="$emit('minimize')" :title="t('common.minimize')"> <button class="control-btn minimize" :title="t('common.minimize')" @click="$emit('minimize')">
<Minimize2 :size="12" /> <Minimize2 :size="12" />
</button> </button>
<button class="control-btn maximize" @click="$emit('maximize')" :title="t('common.maximize')"> <button class="control-btn maximize" :title="t('common.maximize')" @click="$emit('maximize')">
<Maximize2 :size="12" /> <Maximize2 :size="12" />
</button> </button>
<button class="control-btn close" @click="$emit('close')" :title="t('common.close')"> <button class="control-btn close" :title="t('common.close')" @click="$emit('close')">
<X :size="12" /> <X :size="12" />
</button> </button>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Minimize2, Maximize2, X } from 'lucide-vue-next' import { Maximize2, Minimize2, X } from 'lucide-vue-next'
import { useTranslation } from '../../composables/useI18n' import { useTranslation } from '../../composables/useI18n'
const { t } = useTranslation() const { t } = useTranslation()

View File

@@ -2,6 +2,7 @@
"common": { "common": {
"save": "Save", "save": "Save",
"cancel": "Cancel", "cancel": "Cancel",
"confirm": "Confirm",
"reset": "Reset", "reset": "Reset",
"close": "Close", "close": "Close",
"minimize": "Minimize", "minimize": "Minimize",
@@ -23,10 +24,11 @@
"openlistService": "OpenList Core", "openlistService": "OpenList Core",
"rclone": "RClone", "rclone": "RClone",
"quickSettings": "Quick Settings", "quickSettings": "Quick Settings",
"startOpenListCore": "Start Core", "startOpenListCore": "Start",
"stopOpenListCore": "Stop Core", "stopOpenListCore": "Stop",
"processing": "Processing...",
"restart": "Restart", "restart": "Restart",
"openWeb": "Web UI", "openWeb": "Web",
"configRclone": "Configure RClone", "configRclone": "Configure RClone",
"startRclone": "Start RClone", "startRclone": "Start RClone",
"stopRclone": "Stop RClone", "stopRclone": "Stop RClone",
@@ -56,7 +58,10 @@
"openlist": "OpenList", "openlist": "OpenList",
"rclone": "Rclone", "rclone": "Rclone",
"selectVersion": "Select Version", "selectVersion": "Select Version",
"update": "Update" "update": "Update",
"updating": "Updating...",
"updateSuccess": "{type} updated successfully!",
"updateError": "Failed to update {type}: {error}"
}, },
"documentation": { "documentation": {
"title": "Documentation", "title": "Documentation",
@@ -98,7 +103,10 @@
"subtitle": "Configure your OpenList Desktop application", "subtitle": "Configure your OpenList Desktop application",
"saveChanges": "Save Changes", "saveChanges": "Save Changes",
"resetToDefaults": "Reset to defaults", "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!", "saved": "Settings saved successfully!",
"saveFailed": "Failed to save settings. Please try again.", "saveFailed": "Failed to save settings. Please try again.",
"resetSuccess": "Settings reset to defaults successfully!", "resetSuccess": "Settings reset to defaults successfully!",
@@ -139,7 +147,10 @@
"placeholder": "Optional. Custom data directory path", "placeholder": "Optional. Custom data directory path",
"help": "Optional. Specify a custom directory for OpenList data storage", "help": "Optional. Specify a custom directory for OpenList data storage",
"selectTitle": "Select Data Directory", "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": { "ssl": {
"title": "Enable SSL/HTTPS", "title": "Enable SSL/HTTPS",
@@ -149,7 +160,7 @@
"startup": { "startup": {
"autoLaunch": { "autoLaunch": {
"title": "Auto-launch on startup", "title": "Auto-launch on startup",
"description": "Automatically start OpenList service when the application launches" "description": "Automatically start OpenList core when the computer starts"
} }
}, },
"admin": { "admin": {
@@ -167,12 +178,24 @@
}, },
"rclone": { "rclone": {
"subtitle": "Configure remote storage connections", "subtitle": "Configure remote storage connections",
"api": {
"title": "API Configuration",
"subtitle": "Configure the Rclone API server settings",
"port": {
"label": "API Port",
"placeholder": "45572",
"help": "Port number for the Rclone API server (1-65535). Default: 45572"
}
},
"config": { "config": {
"title": "Remote Storage", "title": "Remote Storage",
"subtitle": "Configure rclone for remote storage access", "subtitle": "Configure rclone for remote storage access",
"label": "Rclone Configuration (JSON)", "label": "Rclone Configuration (JSON)",
"tips": "View your rclone configuration as JSON. This will be used to configure rclone remotes.", "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": { "app": {
@@ -185,6 +208,13 @@
"auto": "Auto", "auto": "Auto",
"autoDesc": "Follow system" "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": { "ghProxy": {
"title": "GitHub Proxy", "title": "GitHub Proxy",
"subtitle": "Accelerate GitHub with proxy service", "subtitle": "Accelerate GitHub with proxy service",
@@ -216,6 +246,10 @@
"title": "Auto-launch on startup(Immediate Effect)", "title": "Auto-launch on startup(Immediate Effect)",
"subtitle": "Automatically start OpenList Desktop application when the system starts", "subtitle": "Automatically start OpenList Desktop application when the system starts",
"description": "Automatically start OpenList service when the application launches" "description": "Automatically start OpenList service when the application launches"
},
"showWindowOnStartup": {
"title": "Show main window on startup",
"description": "Show the main application window when OpenList Desktop starts"
} }
} }
}, },
@@ -228,7 +262,9 @@
"copyFailed": "Failed to copy logs to clipboard", "copyFailed": "Failed to copy logs to clipboard",
"exportSuccess": "Successfully exported {count} logs entries to file", "exportSuccess": "Successfully exported {count} logs entries to file",
"clearSuccess": "Logs cleared successfully", "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": { "toolbar": {
"pause": "Pause (Space)", "pause": "Pause (Space)",
@@ -239,6 +275,7 @@
"copyToClipboard": "Copy to Clipboard (Ctrl+C)", "copyToClipboard": "Copy to Clipboard (Ctrl+C)",
"exportLogs": "Export Logs", "exportLogs": "Export Logs",
"clearLogs": "Clear Logs (Ctrl+Delete)", "clearLogs": "Clear Logs (Ctrl+Delete)",
"openLogsDirectory": "Open Logs Directory",
"toggleFullscreen": "Toggle Fullscreen (F11)", "toggleFullscreen": "Toggle Fullscreen (F11)",
"scrollToTop": "Scroll to Top (Home)", "scrollToTop": "Scroll to Top (Home)",
"scrollToBottom": "Scroll to Bottom (End)" "scrollToBottom": "Scroll to Bottom (End)"
@@ -263,7 +300,8 @@
"sources": { "sources": {
"all": "All Sources", "all": "All Sources",
"rclone": "Rclone", "rclone": "Rclone",
"openlist": "OpenList" "openlist": "OpenList",
"service": "Service"
}, },
"actions": { "actions": {
"selectAll": "Select All (Ctrl+A)", "selectAll": "Select All (Ctrl+A)",
@@ -283,7 +321,8 @@
"stripAnsiColors": "Strip ANSI Colors" "stripAnsiColors": "Strip ANSI Colors"
}, },
"messages": { "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": { "headers": {
"timestamp": "Timestamp", "timestamp": "Timestamp",
@@ -381,7 +420,7 @@
"checkers": "Number of checkers to run in parallel (default 8)", "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-age": "Max age of objects in cache (default 24h)",
"vfs-cache-max-size": "Max total size of cache (default 10G)", "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": "Bandwidth limit (e.g. 10M)",
"bwlimit-10M:100M": "Set separate upload and download bandwidth limits", "bwlimit-10M:100M": "Set separate upload and download bandwidth limits",
"bwlimit-schedule": "Time-based bandwidth limits", "bwlimit-schedule": "Time-based bandwidth limits",

View File

@@ -2,11 +2,12 @@
"common": { "common": {
"save": "保存", "save": "保存",
"cancel": "取消", "cancel": "取消",
"confirm": "确认",
"reset": "重置", "reset": "重置",
"close": "关闭", "close": "关闭",
"minimize": "最小化", "minimize": "最小化",
"maximize": "最大化", "maximize": "最大化",
"loading": "加载中...", "loading": "处理中...",
"saving": "保存中...", "saving": "保存中...",
"add": "添加" "add": "添加"
}, },
@@ -23,10 +24,11 @@
"openlistService": "OpenList 核心", "openlistService": "OpenList 核心",
"rclone": "RClone", "rclone": "RClone",
"quickSettings": "快速设置", "quickSettings": "快速设置",
"startOpenListCore": "启动核心", "startOpenListCore": "启动",
"stopOpenListCore": "停止核心", "stopOpenListCore": "停止",
"processing": "处理中...",
"restart": "重启", "restart": "重启",
"openWeb": "网页界面", "openWeb": "网页",
"configRclone": "配置 RClone", "configRclone": "配置 RClone",
"startRclone": "启动 RClone", "startRclone": "启动 RClone",
"stopRclone": "停止 RClone", "stopRclone": "停止 RClone",
@@ -56,7 +58,10 @@
"openlist": "OpenList", "openlist": "OpenList",
"rclone": "Rclone", "rclone": "Rclone",
"selectVersion": "选择版本", "selectVersion": "选择版本",
"update": "更新" "update": "更新",
"updating": "更新中...",
"updateSuccess": "{type} 更新成功!",
"updateError": "更新 {type} 失败:{error}"
}, },
"documentation": { "documentation": {
"title": "文档", "title": "文档",
@@ -98,7 +103,10 @@
"subtitle": "配置您的 OpenList 桌面应用程序", "subtitle": "配置您的 OpenList 桌面应用程序",
"saveChanges": "保存更改", "saveChanges": "保存更改",
"resetToDefaults": "重置为默认值", "resetToDefaults": "重置为默认值",
"confirmReset": "您确定要将所有设置重置为默认值吗?此操作无法撤消。", "confirmReset": {
"title": "重置设置",
"message": "您确定要将所有设置重置为默认值吗?此操作无法撤消。"
},
"saved": "设置保存成功!", "saved": "设置保存成功!",
"saveFailed": "保存设置失败,请重试。", "saveFailed": "保存设置失败,请重试。",
"resetSuccess": "设置已重置为默认值!", "resetSuccess": "设置已重置为默认值!",
@@ -139,7 +147,10 @@
"placeholder": "可选。自定义数据目录路径", "placeholder": "可选。自定义数据目录路径",
"help": "可选。为 OpenList 数据存储指定自定义目录", "help": "可选。为 OpenList 数据存储指定自定义目录",
"selectTitle": "选择数据目录", "selectTitle": "选择数据目录",
"selectError": "选择目录失败。请重试或手动输入路径。" "selectError": "选择目录失败。请重试或手动输入路径。",
"openTitle": "打开数据目录",
"openSuccess": "数据目录打开成功",
"openError": "打开数据目录失败"
}, },
"ssl": { "ssl": {
"title": "启用 SSL/HTTPS", "title": "启用 SSL/HTTPS",
@@ -149,7 +160,7 @@
"startup": { "startup": {
"autoLaunch": { "autoLaunch": {
"title": "开机自启", "title": "开机自启",
"description": "应用程序启动时自动启动 OpenList 服务" "description": "开机自动启动 OpenList 核心"
} }
}, },
"admin": { "admin": {
@@ -167,12 +178,24 @@
}, },
"rclone": { "rclone": {
"subtitle": "配置远程存储连接", "subtitle": "配置远程存储连接",
"api": {
"title": "API 配置",
"subtitle": "配置 Rclone API 服务器设置",
"port": {
"label": "API 端口",
"placeholder": "45572",
"help": "Rclone API 服务器的端口号 (1-65535)。默认45572"
}
},
"config": { "config": {
"title": "远程存储", "title": "远程存储",
"subtitle": "配置 rclone 远程存储访问", "subtitle": "配置 rclone 远程存储访问",
"label": "Rclone 配置 (JSON)", "label": "Rclone 配置 (JSON)",
"invalidJson": "无效的 JSON 配置。请检查您的语法。", "invalidJson": "无效的 JSON 配置。请检查您的语法。",
"tips": "查看你的JSON配置" "tips": "查看你的JSON配置",
"openFile": "打开 rclone.conf",
"openSuccess": "Rclone 配置文件打开成功",
"openError": "打开 Rclone 配置文件失败"
} }
}, },
"app": { "app": {
@@ -185,6 +208,13 @@
"auto": "自动", "auto": "自动",
"autoDesc": "跟随系统" "autoDesc": "跟随系统"
}, },
"config": {
"title": "配置文件",
"subtitle": "访问应用程序配置文件",
"openFile": "打开 settings.json",
"openSuccess": "设置文件打开成功",
"openError": "打开设置文件失败"
},
"ghProxy": { "ghProxy": {
"title": "GitHub 代理", "title": "GitHub 代理",
"subtitle": "使用代理服务加速 GitHub", "subtitle": "使用代理服务加速 GitHub",
@@ -216,6 +246,10 @@
"title": "开机自动启动应用(立即生效)", "title": "开机自动启动应用(立即生效)",
"subtitle": "在系统启动时自动启动 OpenList 桌面应用", "subtitle": "在系统启动时自动启动 OpenList 桌面应用",
"description": "在系统启动时自动启动 OpenList 桌面应用" "description": "在系统启动时自动启动 OpenList 桌面应用"
},
"showWindowOnStartup": {
"title": "启动时显示主窗口",
"description": "在 OpenList 桌面应用启动时显示主应用窗口"
} }
} }
}, },
@@ -228,7 +262,9 @@
"copyFailed": "复制日志到剪贴板失败", "copyFailed": "复制日志到剪贴板失败",
"exportSuccess": "成功导出 {count} 条日志到文件", "exportSuccess": "成功导出 {count} 条日志到文件",
"clearSuccess": "日志清理成功", "clearSuccess": "日志清理成功",
"clearFailed": "清理日志失败" "clearFailed": "清理日志失败",
"openDirectorySuccess": "日志目录打开成功",
"openDirectoryFailed": "打开日志目录失败"
}, },
"toolbar": { "toolbar": {
"pause": "暂停 (Space)", "pause": "暂停 (Space)",
@@ -239,6 +275,7 @@
"copyToClipboard": "复制到剪贴板 (Ctrl+C)", "copyToClipboard": "复制到剪贴板 (Ctrl+C)",
"exportLogs": "导出日志", "exportLogs": "导出日志",
"clearLogs": "清除日志 (Ctrl+Delete)", "clearLogs": "清除日志 (Ctrl+Delete)",
"openLogsDirectory": "打开日志目录",
"toggleFullscreen": "切换全屏 (F11)", "toggleFullscreen": "切换全屏 (F11)",
"scrollToTop": "滚动到顶部 (Home)", "scrollToTop": "滚动到顶部 (Home)",
"scrollToBottom": "滚动到底部 (End)" "scrollToBottom": "滚动到底部 (End)"
@@ -263,7 +300,8 @@
"sources": { "sources": {
"all": "所有来源", "all": "所有来源",
"rclone": "Rclone", "rclone": "Rclone",
"openlist": "OpenList" "openlist": "OpenList",
"service": "服务"
}, },
"actions": { "actions": {
"selectAll": "全选 (Ctrl+A)", "selectAll": "全选 (Ctrl+A)",
@@ -283,7 +321,8 @@
"stripAnsiColors": "去除 ANSI 颜色" "stripAnsiColors": "去除 ANSI 颜色"
}, },
"messages": { "messages": {
"confirmClear": "您确定要清除所有日志吗?" "confirmClear": "您确定要清除所有日志吗?",
"confirmTitle": "清除日志"
}, },
"headers": { "headers": {
"timestamp": "时间", "timestamp": "时间",
@@ -381,7 +420,7 @@
"checkers": "并行运行的检查器数量(默认 8", "checkers": "并行运行的检查器数量(默认 8",
"vfs-cache-max-age": "缓存的最大生命周期(默认 24h", "vfs-cache-max-age": "缓存的最大生命周期(默认 24h",
"vfs-cache-max-size": "缓存文件的最大大小(默认 10G", "vfs-cache-max-size": "缓存文件的最大大小(默认 10G",
"vfs-dir-cache-time": "缓存目录列表的时间(默认 5m", "dir-cache-time": "缓存目录列表的时间(默认 5m",
"bwlimit-10M": "带宽限制(例如 10M", "bwlimit-10M": "带宽限制(例如 10M",
"bwlimit-10M:100M": "分别设置上传和下载宽带限制", "bwlimit-10M:100M": "分别设置上传和下载宽带限制",
"bwlimit-schedule": "基于时间的带宽限制", "bwlimit-schedule": "基于时间的带宽限制",

View File

@@ -8,14 +8,15 @@ type ActionFn<T = any> = () => Promise<T>
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
const settings = ref<MergedSettings>({ const settings = ref<MergedSettings>({
openlist: { port: 5244, data_dir: '', auto_launch: false, ssl_enabled: false }, openlist: { port: 5244, data_dir: '', auto_launch: false, ssl_enabled: false },
rclone: { config: {} }, rclone: { config: {}, api_port: 45572 },
app: { app: {
theme: 'light', theme: 'light',
auto_update_enabled: true, auto_update_enabled: true,
gh_proxy: '', gh_proxy: '',
gh_proxy_api: false, gh_proxy_api: false,
open_links_in_browser: false, open_links_in_browser: false,
admin_password: undefined admin_password: undefined,
show_window_on_startup: true
} }
}) })
const openlistCoreStatus = ref<OpenListCoreStatus>({ running: false }) const openlistCoreStatus = ref<OpenListCoreStatus>({ running: false })
@@ -553,7 +554,7 @@ export const useAppStore = defineStore('app', () => {
} }
} }
async function loadLogs(source?: 'openlist' | 'rclone' | 'app') { async function loadLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
try { try {
source = source || 'openlist' source = source || 'openlist'
const logEntries = await TauriAPI.logs.get(source) const logEntries = await TauriAPI.logs.get(source)
@@ -563,9 +564,10 @@ export const useAppStore = defineStore('app', () => {
} }
} }
async function clearLogs(source?: 'openlist' | 'rclone' | 'app') { async function clearLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
try { try {
loading.value = true loading.value = true
source = source || 'openlist'
const result = await TauriAPI.logs.clear(source) const result = await TauriAPI.logs.clear(source)
if (result) { if (result) {
logs.value = [] logs.value = []
@@ -604,6 +606,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> { async function selectDirectory(title: string): Promise<string | null> {
try { try {
return await TauriAPI.util.selectDirectory(title) return await TauriAPI.util.selectDirectory(title)
@@ -789,6 +831,10 @@ export const useAppStore = defineStore('app', () => {
listFiles, listFiles,
openFile, openFile,
openFolder, openFolder,
openLogsDirectory,
openOpenListDataDir,
openRcloneConfigFile,
openSettingsFile,
selectDirectory, selectDirectory,
clearError, clearError,
init, init,

View File

@@ -13,6 +13,7 @@ interface OpenListCoreConfig {
interface RcloneConfig { interface RcloneConfig {
config?: any // Flexible JSON object for rclone configuration config?: any // Flexible JSON object for rclone configuration
api_port: number // Port for the Rclone API server
} }
interface RcloneWebdavConfig { interface RcloneWebdavConfig {
@@ -50,6 +51,9 @@ interface AppConfig {
gh_proxy_api?: boolean gh_proxy_api?: boolean
open_links_in_browser?: boolean open_links_in_browser?: boolean
admin_password?: string admin_password?: string
show_window_on_startup?: boolean
log_filter_level?: string
log_filter_source?: string
} }
interface MergedSettings { interface MergedSettings {

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, computed } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useAppStore } from '../stores/app'
import QuickActionsCard from '../components/dashboard/QuickActionsCard.vue'
import CoreMonitorCard from '../components/dashboard/CoreMonitorCard.vue' import CoreMonitorCard from '../components/dashboard/CoreMonitorCard.vue'
import VersionManagerCard from '../components/dashboard/VersionManagerCard.vue'
import DocumentationCard from '../components/dashboard/DocumentationCard.vue' import DocumentationCard from '../components/dashboard/DocumentationCard.vue'
import QuickActionsCard from '../components/dashboard/QuickActionsCard.vue'
import ServiceManagementCard from '../components/dashboard/ServiceManagementCard.vue' import ServiceManagementCard from '../components/dashboard/ServiceManagementCard.vue'
import VersionManagerCard from '../components/dashboard/VersionManagerCard.vue'
import { useAppStore } from '../stores/app'
const appStore = useAppStore() const appStore = useAppStore()

View File

@@ -1,26 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue' import * as chrono from 'chrono-node'
import { useAppStore } from '../stores/app'
import { useTranslation } from '../composables/useI18n'
import { import {
Search, AlertCircle,
Filter, AlertTriangle,
Download,
Copy,
Trash2,
Play,
Pause,
RotateCcw,
Settings,
ArrowUp,
ArrowDown, ArrowDown,
ArrowUp,
Copy,
Download,
Filter,
FolderOpen,
Info,
Maximize2, Maximize2,
Minimize2, Minimize2,
AlertCircle, Pause,
Info, Play,
AlertTriangle RotateCcw,
Search,
Settings,
Trash2
} from 'lucide-vue-next' } from 'lucide-vue-next'
import * as chrono from 'chrono-node' import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
import { useTranslation } from '../composables/useI18n'
import { useAppStore } from '../stores/app'
type filterSourceType = 'openlist' | 'rclone' | 'app' | 'service' | 'all'
const appStore = useAppStore() const appStore = useAppStore()
const { t } = useTranslation() const { t } = useTranslation()
@@ -28,11 +33,11 @@ const logContainer = ref<HTMLElement>()
const searchInputRef = ref<HTMLInputElement>() const searchInputRef = ref<HTMLInputElement>()
const autoScroll = ref(true) const autoScroll = ref(true)
const isPaused = ref(false) const isPaused = ref(false)
const filterLevel = ref<string>('all') const filterLevel = ref<string>(appStore.settings.app.log_filter_level || 'all')
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'all') const filterSource = ref<string>(appStore.settings.app.log_filter_source || 'openlist')
const searchQuery = ref('') const searchQuery = ref('')
const selectedEntries = ref<Set<number>>(new Set()) const selectedEntries = ref<Set<number>>(new Set())
const showFilters = ref(false) const showFilters = ref(true)
const showSettings = ref(false) const showSettings = ref(false)
const fontSize = ref(13) const fontSize = ref(13)
const maxLines = ref(1000) const maxLines = ref(1000)
@@ -42,14 +47,25 @@ const stripAnsiColors = ref(true)
const showNotification = ref(false) const showNotification = ref(false)
const notificationMessage = ref('') const notificationMessage = ref('')
const notificationType = ref<'success' | 'info' | 'warning' | 'error'>('success') const notificationType = ref<'success' | 'info' | 'warning' | 'error'>('success')
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
title: '',
message: '',
onConfirm: () => {},
onCancel: () => {}
})
watch( watch(filterLevel, async newValue => {
filterSource, appStore.settings.app.log_filter_level = newValue
newValue => { await appStore.saveSettings()
localStorage.setItem('logFilterSource', newValue) })
},
{ immediate: true } watch(filterSource, async newValue => {
) appStore.settings.app.log_filter_source = newValue
await appStore.saveSettings()
await appStore.loadLogs((newValue !== 'gin' ? newValue : 'openlist') as filterSourceType)
await scrollToBottom()
})
let logRefreshInterval: NodeJS.Timeout | null = null let logRefreshInterval: NodeJS.Timeout | null = null
@@ -63,6 +79,16 @@ const showNotificationMessage = (message: string, type: 'success' | 'info' | 'wa
}, 3000) }, 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 => { const stripAnsiCodes = (text: string): string => {
return text.replace(/\u001b\[[0-9;]*[mGKHF]/g, '') return text.replace(/\u001b\[[0-9;]*[mGKHF]/g, '')
} }
@@ -104,11 +130,8 @@ const parseLogEntry = (logText: string) => {
} }
} }
} }
} else if (cleanText.includes('openlist_desktop') || cleanText.includes('tao::')) { } else {
source = 'app' source = filterSource.value
level = 'info'
} else if (cleanText.toLowerCase().includes('rclone')) {
source = 'rclone'
} }
message = message message = message
@@ -187,21 +210,30 @@ const scrollToTop = () => {
} }
const clearLogs = async () => { const clearLogs = async () => {
if (confirm(t('logs.messages.confirmClear'))) { confirmDialogConfig.value = {
try { title: t('logs.messages.confirmTitle') || t('common.confirm'),
await appStore.clearLogs( message: t('logs.messages.confirmClear'),
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as onConfirm: async () => {
| 'openlist' showConfirmDialog.value = false
| 'rclone' try {
| 'app' await appStore.clearLogs(
) (filterSource.value !== 'all' && filterSource.value !== 'gin'
selectedEntries.value.clear() ? filterSource.value
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success') : 'openlist') as filterSourceType
} catch (error) { )
console.error('Failed to clear logs:', error) selectedEntries.value.clear()
showNotificationMessage(t('logs.notifications.clearFailed'), 'error') 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 () => { const copyLogsToClipboard = async () => {
@@ -273,7 +305,9 @@ const togglePause = () => {
} }
const refreshLogs = async () => { const refreshLogs = async () => {
await appStore.loadLogs() await appStore.loadLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType
)
await scrollToBottom() await scrollToBottom()
if (isPaused.value) { if (isPaused.value) {
isPaused.value = false isPaused.value = false
@@ -347,28 +381,16 @@ const handleKeydown = (event: KeyboardEvent) => {
} }
onMounted(async () => { onMounted(async () => {
appStore appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType).then(() => {
.loadLogs( scrollToBottom()
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as })
| 'openlist'
| 'rclone'
| 'app'
)
.then(() => {
scrollToBottom()
})
document.addEventListener('keydown', handleKeydown) document.addEventListener('keydown', handleKeydown)
logRefreshInterval = setInterval(async () => { logRefreshInterval = setInterval(async () => {
if (!isPaused.value) { if (!isPaused.value) {
const oldLength = appStore.logs.length const oldLength = appStore.logs.length
await appStore.loadLogs( await appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType)
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
| 'openlist'
| 'rclone'
| 'app'
)
if (appStore.logs.length > oldLength) { if (appStore.logs.length > oldLength) {
await scrollToBottom() await scrollToBottom()
@@ -405,14 +427,14 @@ onUnmounted(() => {
<button <button
class="toolbar-btn" class="toolbar-btn"
:class="{ active: isPaused }" :class="{ active: isPaused }"
@click="togglePause"
:title="isPaused ? t('logs.toolbar.resume') : t('logs.toolbar.pause')" :title="isPaused ? t('logs.toolbar.resume') : t('logs.toolbar.pause')"
@click="togglePause"
> >
<Pause v-if="!isPaused" :size="16" /> <Pause v-if="!isPaused" :size="16" />
<Play v-else :size="16" /> <Play v-else :size="16" />
</button> </button>
<button class="toolbar-btn" @click="refreshLogs" :title="t('logs.toolbar.refresh')"> <button class="toolbar-btn" :title="t('logs.toolbar.refresh')" @click="refreshLogs">
<RotateCcw :size="16" /> <RotateCcw :size="16" />
</button> </button>
@@ -421,8 +443,8 @@ onUnmounted(() => {
<button <button
class="toolbar-btn" class="toolbar-btn"
:class="{ active: showFilters }" :class="{ active: showFilters }"
@click="showFilters = !showFilters"
:title="t('logs.toolbar.showFilters')" :title="t('logs.toolbar.showFilters')"
@click="showFilters = !showFilters"
> >
<Filter :size="16" /> <Filter :size="16" />
</button> </button>
@@ -430,8 +452,8 @@ onUnmounted(() => {
<button <button
class="toolbar-btn" class="toolbar-btn"
:class="{ active: showSettings }" :class="{ active: showSettings }"
@click="showSettings = !showSettings"
:title="t('logs.toolbar.settings')" :title="t('logs.toolbar.settings')"
@click="showSettings = !showSettings"
> >
<Settings :size="16" /> <Settings :size="16" />
</button> </button>
@@ -465,29 +487,38 @@ onUnmounted(() => {
<button <button
class="toolbar-btn" class="toolbar-btn"
@click="copyLogsToClipboard"
:title="t('logs.toolbar.copyToClipboard')" :title="t('logs.toolbar.copyToClipboard')"
:disabled="filteredLogs.length === 0" :disabled="filteredLogs.length === 0"
@click="copyLogsToClipboard"
> >
<Copy :size="16" /> <Copy :size="16" />
</button> </button>
<button <button
class="toolbar-btn" class="toolbar-btn"
@click="exportLogs"
:title="t('logs.toolbar.exportLogs')" :title="t('logs.toolbar.exportLogs')"
:disabled="filteredLogs.length === 0" :disabled="filteredLogs.length === 0"
@click="exportLogs"
> >
<Download :size="16" /> <Download :size="16" />
</button> </button>
<button class="toolbar-btn danger" @click="clearLogs" :title="t('logs.toolbar.clearLogs')"> <button
class="toolbar-btn danger"
:disabled="filteredLogs.length === 0 || filterSource === 'gin' || filterSource === 'all'"
:title="t('logs.toolbar.clearLogs')"
@click="clearLogs"
>
<Trash2 :size="16" /> <Trash2 :size="16" />
</button> </button>
<button class="toolbar-btn" :title="t('logs.toolbar.openLogsDirectory')" @click="openLogsDirectory">
<FolderOpen :size="16" />
</button>
<div class="toolbar-separator"></div> <div class="toolbar-separator"></div>
<button class="toolbar-btn" @click="toggleFullscreen" :title="t('logs.toolbar.toggleFullscreen')"> <button class="toolbar-btn" :title="t('logs.toolbar.toggleFullscreen')" @click="toggleFullscreen">
<Maximize2 v-if="!isFullscreen" :size="16" /> <Maximize2 v-if="!isFullscreen" :size="16" />
<Minimize2 v-else :size="16" /> <Minimize2 v-else :size="16" />
</button> </button>
@@ -512,16 +543,17 @@ onUnmounted(() => {
<option value="openlist">{{ t('logs.filters.sources.openlist') }}</option> <option value="openlist">{{ t('logs.filters.sources.openlist') }}</option>
<option value="gin">GIN Server</option> <option value="gin">GIN Server</option>
<option value="rclone">{{ t('logs.filters.sources.rclone') }}</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> <option value="app">{{ t('logs.filters.app') }}</option>
</select> </select>
</div> </div>
<div class="filter-actions"> <div class="filter-actions">
<button class="filter-btn" @click="selectAllVisible" :disabled="filteredLogs.length === 0"> <button class="filter-btn" :disabled="filteredLogs.length === 0" @click="selectAllVisible">
{{ t('logs.filters.actions.selectAll') }} {{ t('logs.filters.actions.selectAll') }}
</button> </button>
<button class="filter-btn" @click="clearSelection" :disabled="selectedEntries.size === 0"> <button class="filter-btn" :disabled="selectedEntries.size === 0" @click="clearSelection">
{{ t('logs.filters.actions.clearSelection') }} {{ t('logs.filters.actions.clearSelection') }}
</button> </button>
@@ -562,10 +594,10 @@ onUnmounted(() => {
<div class="log-col source">{{ t('logs.headers.source') }}</div> <div class="log-col source">{{ t('logs.headers.source') }}</div>
<div class="log-col message">{{ t('logs.headers.message') }}</div> <div class="log-col message">{{ t('logs.headers.message') }}</div>
<div class="log-col actions"> <div class="log-col actions">
<button class="scroll-btn" @click="scrollToTop" :title="t('logs.toolbar.scrollToTop')"> <button class="scroll-btn" :title="t('logs.toolbar.scrollToTop')" @click="scrollToTop">
<ArrowUp :size="14" /> <ArrowUp :size="14" />
</button> </button>
<button class="scroll-btn" @click="scrollToBottom" :title="t('logs.toolbar.scrollToBottom')"> <button class="scroll-btn" :title="t('logs.toolbar.scrollToBottom')" @click="scrollToBottom">
<ArrowDown :size="14" /> <ArrowDown :size="14" />
</button> </button>
</div> </div>
@@ -639,6 +671,17 @@ onUnmounted(() => {
</div> </div>
</div> </div>
</Transition> </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> </div>
</template> </template>

View File

@@ -1,27 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, ComputedRef, Ref } from 'vue'
import { useTranslation } from '../composables/useI18n'
import { useRcloneStore } from '../stores/rclone'
import { import {
HardDrive,
Plus,
Edit,
Trash2,
Play,
Square,
CheckCircle, CheckCircle,
XCircle,
Loader,
Cloud, Cloud,
Search, Edit,
FolderOpen,
HardDrive,
Loader,
Play,
Plus,
RefreshCw, RefreshCw,
Save, Save,
X, Search,
Settings, Settings,
FolderOpen Square,
Trash2,
X,
XCircle
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useAppStore } from '@/stores/app' import { computed, ComputedRef, onMounted, onUnmounted, Ref, ref } from 'vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue' import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import { useAppStore } from '@/stores/app'
import { useTranslation } from '../composables/useI18n'
import { useRcloneStore } from '../stores/rclone'
const { t } = useTranslation() const { t } = useTranslation()
const rcloneStore = useRcloneStore() const rcloneStore = useRcloneStore()
@@ -67,7 +69,7 @@ const commonFlags = ref([
{ flag: '--vfs-cache-mode', value: 'minimal', descriptionKey: 'vfs-cache-mode-minimal' }, { 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-age', value: '24h', descriptionKey: 'vfs-cache-max-age' },
{ flag: '--vfs-cache-max-size', value: '10G', descriptionKey: 'vfs-cache-max-size' }, { 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' }
] ]
}, },
{ {
@@ -127,7 +129,7 @@ const commonFlags = ref([
const showFlagSelector = ref(false) const showFlagSelector = ref(false)
const filteredConfigs: ComputedRef<RcloneFormConfig[]> = computed(() => { const filteredConfigs: ComputedRef<RcloneFormConfig[]> = computed(() => {
let filtered: RcloneFormConfig[] = [] const filtered: RcloneFormConfig[] = []
const fullRemoteConfigs = appStore.fullRcloneConfigs const fullRemoteConfigs = appStore.fullRcloneConfigs
for (const config of fullRemoteConfigs) { for (const config of fullRemoteConfigs) {
@@ -456,7 +458,7 @@ const dismissWebdavTip = () => {
const isWindows = computed(() => { const isWindows = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32' return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
}) })
const showWinfspTip = ref(isWindows && !localStorage.getItem('winfsp_tip_dismissed')) const showWinfspTip = ref(isWindows.value && !localStorage.getItem('winfsp_tip_dismissed'))
const dismissWinfspTip = () => { const dismissWinfspTip = () => {
showWinfspTip.value = false showWinfspTip.value = false
@@ -464,7 +466,7 @@ const dismissWinfspTip = () => {
} }
const shouldShowWebdavTip = computed(() => { const shouldShowWebdavTip = computed(() => {
if (isWindows) { if (isWindows.value) {
return !showWinfspTip.value && showWebdavTip.value return !showWinfspTip.value && showWebdavTip.value
} }
return showWebdavTip.value return showWebdavTip.value
@@ -533,14 +535,14 @@ onUnmounted(() => {
{{ rcloneStore.serviceRunning ? t('mount.service.running') : t('mount.service.stopped') }} {{ rcloneStore.serviceRunning ? t('mount.service.running') : t('mount.service.stopped') }}
</span> </span>
<button <button
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
:class="['service-toggle', { active: rcloneStore.serviceRunning }]" :class="['service-toggle', { active: rcloneStore.serviceRunning }]"
:disabled="rcloneStore.loading" :disabled="rcloneStore.loading"
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
> >
<component :is="rcloneStore.serviceRunning ? Square : Play" class="btn-icon" /> <component :is="rcloneStore.serviceRunning ? Square : Play" class="btn-icon" />
</button> </button>
</div> </div>
<button @click="addNewConfig" class="primary-btn"> <button class="primary-btn" @click="addNewConfig">
<Plus class="btn-icon" /> <Plus class="btn-icon" />
<span>{{ t('mount.actions.addRemote') }}</span> <span>{{ t('mount.actions.addRemote') }}</span>
</button> </button>
@@ -557,7 +559,7 @@ onUnmounted(() => {
<h4 class="tip-title">{{ t('mount.tip.webdavTitle') }}</h4> <h4 class="tip-title">{{ t('mount.tip.webdavTitle') }}</h4>
<p class="tip-description">{{ t('mount.tip.webdavMessage') }}</p> <p class="tip-description">{{ t('mount.tip.webdavMessage') }}</p>
</div> </div>
<button @click="dismissWebdavTip" class="tip-close" :title="t('mount.tip.dismissForever')"> <button class="tip-close" :title="t('mount.tip.dismissForever')" @click="dismissWebdavTip">
<X class="close-icon" /> <X class="close-icon" />
</button> </button>
</div> </div>
@@ -572,7 +574,7 @@ onUnmounted(() => {
<h4 class="tip-title">{{ t('mount.tip.winfspTitle') }}</h4> <h4 class="tip-title">{{ t('mount.tip.winfspTitle') }}</h4>
<p class="tip-description">{{ t('mount.tip.winfspMessage') }}</p> <p class="tip-description">{{ t('mount.tip.winfspMessage') }}</p>
</div> </div>
<button @click="dismissWinfspTip" class="tip-close" :title="t('mount.tip.dismissForever')"> <button class="tip-close" :title="t('mount.tip.dismissForever')" @click="dismissWinfspTip">
<X class="close-icon" /> <X class="close-icon" />
</button> </button>
</div> </div>
@@ -596,7 +598,7 @@ onUnmounted(() => {
<option value="unmounted">{{ t('mount.status.unmounted') }}</option> <option value="unmounted">{{ t('mount.status.unmounted') }}</option>
<option value="error">{{ t('mount.status.error') }}</option> <option value="error">{{ t('mount.status.error') }}</option>
</select> </select>
<button @click="appStore.loadMountInfos" class="refresh-btn" :disabled="rcloneStore.loading"> <button class="refresh-btn" :disabled="rcloneStore.loading" @click="appStore.loadMountInfos">
<RefreshCw class="refresh-icon" :class="{ spinning: rcloneStore.loading }" /> <RefreshCw class="refresh-icon" :class="{ spinning: rcloneStore.loading }" />
</button> </button>
</div> </div>
@@ -605,7 +607,7 @@ onUnmounted(() => {
<div v-if="rcloneStore.error" class="error-alert"> <div v-if="rcloneStore.error" class="error-alert">
<XCircle class="alert-icon" /> <XCircle class="alert-icon" />
<span class="alert-message">{{ rcloneStore.error }}</span> <span class="alert-message">{{ rcloneStore.error }}</span>
<button @click="rcloneStore.clearError" class="alert-close"> <button class="alert-close" @click="rcloneStore.clearError">
<X class="close-icon" /> <X class="close-icon" />
</button> </button>
</div> </div>
@@ -617,7 +619,7 @@ onUnmounted(() => {
<Cloud class="empty-icon" /> <Cloud class="empty-icon" />
<h3 class="empty-title">{{ t('mount.empty.title') }}</h3> <h3 class="empty-title">{{ t('mount.empty.title') }}</h3>
<p class="empty-description">{{ t('mount.empty.description') }}</p> <p class="empty-description">{{ t('mount.empty.description') }}</p>
<button @click="addNewConfig" class="empty-action-btn"> <button class="empty-action-btn" @click="addNewConfig">
<Plus class="btn-icon" /> <Plus class="btn-icon" />
<span>{{ t('mount.actions.addRemote') }}</span> <span>{{ t('mount.actions.addRemote') }}</span>
</button> </button>
@@ -664,8 +666,8 @@ onUnmounted(() => {
<span <span
v-if="config.mountPoint" v-if="config.mountPoint"
class="meta-tag clickable-mount-point" class="meta-tag clickable-mount-point"
@click="openInFileExplorer(config.mountPoint)"
:title="t('mount.meta.openInExplorer')" :title="t('mount.meta.openInExplorer')"
@click="openInFileExplorer(config.mountPoint)"
> >
<FolderOpen class="mount-point-icon" /> <FolderOpen class="mount-point-icon" />
{{ config.mountPoint }} {{ config.mountPoint }}
@@ -679,19 +681,19 @@ onUnmounted(() => {
<div class="action-group"> <div class="action-group">
<button <button
v-if="!isConfigMounted(config)" v-if="!isConfigMounted(config)"
@click="mountConfig(config)"
class="action-btn primary" class="action-btn primary"
:disabled="isConfigMounting(config) || !config.mountPoint" :disabled="isConfigMounting(config) || !config.mountPoint"
:title="!config.mountPoint ? t('mount.messages.mountPointRequired') : ''" :title="!config.mountPoint ? t('mount.messages.mountPointRequired') : ''"
@click="mountConfig(config)"
> >
<Play class="btn-icon" /> <Play class="btn-icon" />
<span>{{ t('mount.actions.mount') }}</span> <span>{{ t('mount.actions.mount') }}</span>
</button> </button>
<button <button
v-else v-else
@click="unmountConfig(config)"
class="action-btn warning" class="action-btn warning"
:disabled="isConfigMounting(config)" :disabled="isConfigMounting(config)"
@click="unmountConfig(config)"
> >
<Square class="btn-icon" /> <Square class="btn-icon" />
<span>{{ t('mount.actions.unmount') }}</span> <span>{{ t('mount.actions.unmount') }}</span>
@@ -699,22 +701,22 @@ onUnmounted(() => {
</div> </div>
<div class="secondary-actions"> <div class="secondary-actions">
<button @click="editConfig(config)" class="secondary-btn" :title="t('mount.actions.edit')"> <button class="secondary-btn" :title="t('mount.actions.edit')" @click="editConfig(config)">
<Edit class="btn-icon" /> <Edit class="btn-icon" />
</button> </button>
<button <button
@click="deleteConfig(config)"
class="secondary-btn danger" class="secondary-btn danger"
:disabled="isConfigMounted(config)" :disabled="isConfigMounted(config)"
:title="t('mount.actions.delete')" :title="t('mount.actions.delete')"
@click="deleteConfig(config)"
> >
<Trash2 class="btn-icon" /> <Trash2 class="btn-icon" />
</button> </button>
<button <button
v-if="isConfigMounted(config)" v-if="isConfigMounted(config)"
@click="openInFileExplorer(config.mountPoint)"
class="secondary-btn" class="secondary-btn"
:title="t('mount.actions.openInExplorer')" :title="t('mount.actions.openInExplorer')"
@click="openInFileExplorer(config.mountPoint)"
> >
<FolderOpen class="btn-icon" /> <FolderOpen class="btn-icon" />
</button> </button>
@@ -733,7 +735,7 @@ onUnmounted(() => {
{{ editingConfig ? t('mount.config.editTitle') : t('mount.config.addTitle') }} {{ editingConfig ? t('mount.config.editTitle') : t('mount.config.addTitle') }}
</h2> </h2>
</div> </div>
<button @click="cancelForm" class="modal-close"> <button class="modal-close" @click="cancelForm">
<X class="close-icon" /> <X class="close-icon" />
</button> </button>
</div> </div>
@@ -847,10 +849,10 @@ onUnmounted(() => {
<div class="flags-header"> <div class="flags-header">
<button <button
@click="showFlagSelector = !showFlagSelector"
type="button" type="button"
class="quick-flags-btn" class="quick-flags-btn"
:title="t('mount.config.quickFlagsTooltip')" :title="t('mount.config.quickFlagsTooltip')"
@click="showFlagSelector = !showFlagSelector"
> >
<Settings class="btn-icon" /> <Settings class="btn-icon" />
<span>{{ t('mount.config.quickFlags') }}</span> <span>{{ t('mount.config.quickFlags') }}</span>
@@ -861,7 +863,7 @@ onUnmounted(() => {
<div class="flag-selector-popup" @click.stop> <div class="flag-selector-popup" @click.stop>
<div class="flag-selector-header"> <div class="flag-selector-header">
<h4>{{ t('mount.config.selectCommonFlags') }}</h4> <h4>{{ t('mount.config.selectCommonFlags') }}</h4>
<button @click="closeFlagSelector" class="close-selector-btn"> <button class="close-selector-btn" @click="closeFlagSelector">
<X class="btn-icon" /> <X class="btn-icon" />
</button> </button>
</div> </div>
@@ -880,13 +882,13 @@ onUnmounted(() => {
<div <div
v-for="flag in category.flags" v-for="flag in category.flags"
:key="`${flag.flag}-${flag.value}`" :key="`${flag.flag}-${flag.value}`"
@click="toggleFlag(flag)"
class="flag-option" class="flag-option"
:class="{ :class="{
selected: isFlagInConfig(flag), selected: isFlagInConfig(flag),
'in-config': isFlagInConfig(flag) 'in-config': isFlagInConfig(flag)
}" }"
:title="getFlagDescription(flag)" :title="getFlagDescription(flag)"
@click="toggleFlag(flag)"
> >
<div class="flag-checkbox"> <div class="flag-checkbox">
<div class="custom-checkbox" :class="{ checked: isFlagInConfig(flag) }"> <div class="custom-checkbox" :class="{ checked: isFlagInConfig(flag) }">
@@ -915,15 +917,15 @@ onUnmounted(() => {
:placeholder="t('mount.config.flagPlaceholder')" :placeholder="t('mount.config.flagPlaceholder')"
/> />
<button <button
@click="removeFlag(index)"
type="button" type="button"
class="remove-flag-btn" class="remove-flag-btn"
:title="t('mount.config.removeFlag')" :title="t('mount.config.removeFlag')"
@click="removeFlag(index)"
> >
<X class="btn-icon" /> <X class="btn-icon" />
</button> </button>
</div> </div>
<button @click="addFlag" type="button" class="add-flag-btn"> <button type="button" class="add-flag-btn" @click="addFlag">
<Plus class="btn-icon" /> <Plus class="btn-icon" />
<span>{{ t('mount.config.addFlag') }}</span> <span>{{ t('mount.config.addFlag') }}</span>
</button> </button>
@@ -934,11 +936,11 @@ onUnmounted(() => {
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button @click="cancelForm" class="cancel-btn"> <button class="cancel-btn" @click="cancelForm">
<X class="btn-icon" /> <X class="btn-icon" />
<span>{{ t('common.cancel') }}</span> <span>{{ t('common.cancel') }}</span>
</button> </button>
<button @click="saveConfig" class="save-btn" :disabled="appStore.loading"> <button class="save-btn" :disabled="appStore.loading" @click="saveConfig">
<Save class="btn-icon" /> <Save class="btn-icon" />
<span>{{ editingConfig ? t('common.save') : t('common.add') }}</span> <span>{{ editingConfig ? t('common.save') : t('common.add') }}</span>
</button> </button>

View File

@@ -1,11 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue' import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
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 { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import {
AlertCircle,
CheckCircle,
ExternalLink,
FolderOpen,
HardDrive,
RotateCcw,
Save,
Server,
Settings
} from 'lucide-vue-next'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
import { useTranslation } from '../composables/useI18n'
import { useAppStore } from '../stores/app'
const appStore = useAppStore() const appStore = useAppStore()
const route = useRoute() const route = useRoute()
@@ -17,12 +29,21 @@ const activeTab = ref('openlist')
const rcloneConfigJson = ref('') const rcloneConfigJson = ref('')
const autoStartApp = ref(false) const autoStartApp = ref(false)
const isResettingPassword = ref(false) const isResettingPassword = ref(false)
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
title: '',
message: '',
onConfirm: () => {},
onCancel: () => {}
})
const openlistCoreSettings = reactive({ ...appStore.settings.openlist }) const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
const rcloneSettings = reactive({ ...appStore.settings.rclone }) const rcloneSettings = reactive({ ...appStore.settings.rclone })
const appSettings = reactive({ ...appStore.settings.app }) const appSettings = reactive({ ...appStore.settings.app })
let originalOpenlistPort = openlistCoreSettings.port || 5244 let originalOpenlistPort = openlistCoreSettings.port || 5244
let originalDataDir = openlistCoreSettings.data_dir let originalDataDir = openlistCoreSettings.data_dir
let originalRcloneApiPort = rcloneSettings.api_port || 45572
let originalAdminPassword = appStore.settings.app.admin_password || ''
watch(autoStartApp, async newValue => { watch(autoStartApp, async newValue => {
if (newValue) { if (newValue) {
@@ -66,6 +87,7 @@ onMounted(async () => {
if (openlistCoreSettings.ssl_enabled === undefined) openlistCoreSettings.ssl_enabled = false if (openlistCoreSettings.ssl_enabled === undefined) openlistCoreSettings.ssl_enabled = false
if (!rcloneSettings.config) rcloneSettings.config = {} if (!rcloneSettings.config) rcloneSettings.config = {}
if (!rcloneSettings.api_port) rcloneSettings.api_port = 45572
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2) rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
if (!appSettings.theme) appSettings.theme = 'light' if (!appSettings.theme) appSettings.theme = 'light'
@@ -74,9 +96,11 @@ onMounted(async () => {
if (!appSettings.gh_proxy) appSettings.gh_proxy = '' if (!appSettings.gh_proxy) appSettings.gh_proxy = ''
if (appSettings.gh_proxy_api === undefined) appSettings.gh_proxy_api = false if (appSettings.gh_proxy_api === undefined) appSettings.gh_proxy_api = false
if (appSettings.open_links_in_browser === undefined) appSettings.open_links_in_browser = false if (appSettings.open_links_in_browser === undefined) appSettings.open_links_in_browser = false
if (appSettings.show_window_on_startup === undefined) appSettings.show_window_on_startup = true
if (!appSettings.admin_password) appSettings.admin_password = '' if (!appSettings.admin_password) appSettings.admin_password = ''
originalOpenlistPort = openlistCoreSettings.port || 5244 originalOpenlistPort = openlistCoreSettings.port || 5244
originalDataDir = openlistCoreSettings.data_dir originalDataDir = openlistCoreSettings.data_dir
originalRcloneApiPort = rcloneSettings.api_port || 45572
// Load current admin password // Load current admin password
await loadCurrentAdminPassword() await loadCurrentAdminPassword()
@@ -117,10 +141,13 @@ const handleSave = async () => {
appStore.settings.rclone = { ...rcloneSettings } appStore.settings.rclone = { ...rcloneSettings }
appStore.settings.app = { ...appSettings } appStore.settings.app = { ...appSettings }
const originalAdminPassword = appStore.settings.app.admin_password
const needsPasswordUpdate = originalAdminPassword !== appSettings.admin_password && appSettings.admin_password const needsPasswordUpdate = originalAdminPassword !== appSettings.admin_password && appSettings.admin_password
if (originalOpenlistPort !== openlistCoreSettings.port || originalDataDir !== openlistCoreSettings.data_dir) { if (
originalOpenlistPort !== openlistCoreSettings.port ||
originalDataDir !== openlistCoreSettings.data_dir ||
originalRcloneApiPort !== rcloneSettings.api_port
) {
await appStore.saveSettingsWithCoreUpdate() await appStore.saveSettingsWithCoreUpdate()
} else { } else {
await appStore.saveSettings() await appStore.saveSettings()
@@ -142,6 +169,7 @@ const handleSave = async () => {
} }
originalOpenlistPort = openlistCoreSettings.port || 5244 originalOpenlistPort = openlistCoreSettings.port || 5244
originalRcloneApiPort = rcloneSettings.api_port || 45572
originalDataDir = openlistCoreSettings.data_dir originalDataDir = openlistCoreSettings.data_dir
} catch (error) { } catch (error) {
message.value = t('settings.saveFailed') message.value = t('settings.saveFailed')
@@ -157,24 +185,33 @@ const handleSave = async () => {
} }
const handleReset = async () => { const handleReset = async () => {
if (!confirm(t('settings.confirmReset'))) { confirmDialogConfig.value = {
return 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 { showConfirmDialog.value = true
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'
}
} }
const handleSelectDataDir = async () => { const handleSelectDataDir = async () => {
@@ -199,6 +236,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 () => { const handleResetAdminPassword = async () => {
isResettingPassword.value = true isResettingPassword.value = true
try { try {
@@ -207,7 +264,6 @@ const handleResetAdminPassword = async () => {
appSettings.admin_password = newPassword appSettings.admin_password = newPassword
message.value = t('settings.service.admin.resetSuccess') message.value = t('settings.service.admin.resetSuccess')
messageType.value = 'success' messageType.value = 'success'
await navigator.clipboard.writeText(newPassword)
} else { } else {
message.value = t('settings.service.admin.resetFailed') message.value = t('settings.service.admin.resetFailed')
messageType.value = 'error' messageType.value = 'error'
@@ -224,11 +280,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 () => { const loadCurrentAdminPassword = async () => {
try { try {
const password = await appStore.getAdminPassword() const password = await appStore.getAdminPassword()
if (password) { if (password) {
appSettings.admin_password = password appSettings.admin_password = password
originalAdminPassword = password
} }
} catch (error) { } catch (error) {
console.error('Failed to load admin password:', error) console.error('Failed to load admin password:', error)
@@ -247,11 +336,11 @@ const loadCurrentAdminPassword = async () => {
</div> </div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button @click="handleReset" class="btn btn-secondary" :title="t('settings.resetToDefaults')"> <button class="btn btn-secondary" :title="t('settings.resetToDefaults')" @click="handleReset">
<RotateCcw :size="16" /> <RotateCcw :size="16" />
{{ t('common.reset') }} {{ t('common.reset') }}
</button> </button>
<button @click="handleSave" :disabled="!hasUnsavedChanges || isSaving" class="btn btn-primary"> <button :disabled="!hasUnsavedChanges || isSaving" class="btn btn-primary" @click="handleSave">
<Save :size="16" /> <Save :size="16" />
{{ isSaving ? t('common.saving') : t('settings.saveChanges') }} {{ isSaving ? t('common.saving') : t('settings.saveChanges') }}
</button> </button>
@@ -261,16 +350,16 @@ const loadCurrentAdminPassword = async () => {
<div v-if="message" class="message-banner" :class="messageType"> <div v-if="message" class="message-banner" :class="messageType">
<component :is="messageType === 'success' ? CheckCircle : AlertCircle" :size="16" /> <component :is="messageType === 'success' ? CheckCircle : AlertCircle" :size="16" />
<span>{{ message }}</span> <span>{{ message }}</span>
<button @click="message = ''" class="message-close">×</button> <button class="message-close" @click="message = ''">×</button>
</div> </div>
<div class="tab-navigation"> <div class="tab-navigation">
<button <button
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.id" :key="tab.id"
@click="activeTab = tab.id"
class="tab-button" class="tab-button"
:class="{ active: activeTab === tab.id }" :class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
> >
<component :is="tab.icon" :size="18" /> <component :is="tab.icon" :size="18" />
<span>{{ tab.label }}</span> <span>{{ tab.label }}</span>
@@ -307,12 +396,20 @@ const loadCurrentAdminPassword = async () => {
/> />
<button <button
type="button" type="button"
@click="handleSelectDataDir"
class="input-addon-btn" class="input-addon-btn"
:title="t('settings.service.network.dataDir.selectTitle')" :title="t('settings.service.network.dataDir.selectTitle')"
@click="handleSelectDataDir"
> >
<FolderOpen :size="16" /> <FolderOpen :size="16" />
</button> </button>
<button
type="button"
class="input-addon-btn"
:title="t('settings.service.network.dataDir.openTitle')"
@click="handleOpenDataDir"
>
<ExternalLink :size="16" />
</button>
</div> </div>
<small>{{ t('settings.service.network.dataDir.help') }}</small> <small>{{ t('settings.service.network.dataDir.help') }}</small>
</div> </div>
@@ -361,10 +458,10 @@ const loadCurrentAdminPassword = async () => {
/> />
<button <button
type="button" type="button"
@click="handleResetAdminPassword"
:disabled="isResettingPassword" :disabled="isResettingPassword"
class="input-addon-btn reset-password-btn" class="input-addon-btn reset-password-btn"
:title="t('settings.service.admin.resetTitle')" :title="t('settings.service.admin.resetTitle')"
@click="handleResetAdminPassword"
> >
<RotateCcw :size="16" /> <RotateCcw :size="16" />
</button> </button>
@@ -375,12 +472,43 @@ const loadCurrentAdminPassword = async () => {
</div> </div>
<div v-if="activeTab === 'rclone'" class="tab-content"> <div v-if="activeTab === 'rclone'" class="tab-content">
<div class="settings-section">
<h2>{{ t('settings.rclone.api.title') }}</h2>
<p>{{ t('settings.rclone.api.subtitle') }}</p>
<div class="form-grid">
<div class="form-group">
<label>{{ t('settings.rclone.api.port.label') }}</label>
<input
v-model.number="rcloneSettings.api_port"
type="number"
class="form-input"
:placeholder="t('settings.rclone.api.port.placeholder')"
min="1"
max="65535"
/>
<small>{{ t('settings.rclone.api.port.help') }}</small>
</div>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h2>{{ t('settings.rclone.config.title') }}</h2> <h2>{{ t('settings.rclone.config.title') }}</h2>
<p>{{ t('settings.rclone.config.subtitle') }}</p> <p>{{ t('settings.rclone.config.subtitle') }}</p>
<div class="form-group"> <div class="form-group">
<label>{{ t('settings.rclone.config.label') }}</label> <label>{{ t('settings.rclone.config.label') }}</label>
<div class="settings-section-actions">
<button
type="button"
class="btn btn-secondary"
:title="t('settings.rclone.config.openFile')"
@click="handleOpenRcloneConfig"
>
<ExternalLink :size="16" />
{{ t('settings.rclone.config.openFile') }}
</button>
</div>
<textarea <textarea
v-model="rcloneConfigJson" v-model="rcloneConfigJson"
class="form-textarea" class="form-textarea"
@@ -403,8 +531,8 @@ const loadCurrentAdminPassword = async () => {
<label>{{ t('settings.theme.title') }}</label> <label>{{ t('settings.theme.title') }}</label>
<select <select
v-model="appSettings.theme" v-model="appSettings.theme"
@change="appStore.setTheme(appSettings.theme || 'light')"
class="form-input" class="form-input"
@change="appStore.setTheme(appSettings.theme || 'light')"
> >
<option value="light">{{ t('settings.app.theme.light') }}</option> <option value="light">{{ t('settings.app.theme.light') }}</option>
<option value="dark">{{ t('settings.app.theme.dark') }}</option> <option value="dark">{{ t('settings.app.theme.dark') }}</option>
@@ -415,6 +543,25 @@ const loadCurrentAdminPassword = async () => {
</div> </div>
</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"
class="btn btn-secondary"
:title="t('settings.app.config.openFile')"
@click="handleOpenSettingsFile"
>
<ExternalLink :size="16" />
{{ t('settings.app.config.openFile') }}
</button>
</div>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h2>{{ t('settings.app.ghProxy.title') }}</h2> <h2>{{ t('settings.app.ghProxy.title') }}</h2>
<p>{{ t('settings.app.ghProxy.subtitle') }}</p> <p>{{ t('settings.app.ghProxy.subtitle') }}</p>
@@ -460,6 +607,21 @@ const loadCurrentAdminPassword = async () => {
</div> </div>
</div> </div>
<div class="settings-section">
<h2>{{ t('settings.app.showWindowOnStartup.title') }}</h2>
<div class="form-group">
<label class="switch-label">
<input v-model="appSettings.show_window_on_startup" type="checkbox" class="switch-input" />
<span class="switch-slider"></span>
<div class="switch-content">
<span class="switch-title">{{ t('settings.app.showWindowOnStartup.title') }}</span>
<span class="switch-description">{{ t('settings.app.showWindowOnStartup.description') }}</span>
</div>
</label>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h2>{{ t('settings.app.updates.title') }}</h2> <h2>{{ t('settings.app.updates.title') }}</h2>
<p>{{ t('settings.app.updates.subtitle') }}</p> <p>{{ t('settings.app.updates.subtitle') }}</p>
@@ -493,6 +655,17 @@ const loadCurrentAdminPassword = async () => {
</div> </div>
</div> </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> </div>
</template> </template>

View File

@@ -6,7 +6,7 @@
<p class="view-subtitle">{{ t('update.subtitle') }}</p> <p class="view-subtitle">{{ t('update.subtitle') }}</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button @click="goToSettings" class="settings-link"> <button class="settings-link" @click="goToSettings">
<Settings :size="16" /> <Settings :size="16" />
{{ t('navigation.settings') }} {{ t('navigation.settings') }}
</button> </button>
@@ -36,10 +36,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'
import { useTranslation } from '../composables/useI18n'
import { Settings } from 'lucide-vue-next' import { Settings } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import UpdateManagerCard from '../components/dashboard/UpdateManagerCard.vue' import UpdateManagerCard from '../components/dashboard/UpdateManagerCard.vue'
import { useTranslation } from '../composables/useI18n'
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()

View File

@@ -1,5 +1,5 @@
.dashboard-container { .dashboard-container {
padding: 0.25rem 0.5rem 0.25rem; padding: 0.25rem 0.5rem 1rem;
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, var(--color-background-secondary) 0%, var(--color-background-tertiary) 100%); background: linear-gradient(135deg, var(--color-background-secondary) 0%, var(--color-background-tertiary) 100%);
overflow-y: auto; overflow-y: auto;
@@ -93,13 +93,12 @@
.dashboard-grid.three-column { .dashboard-grid.three-column {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1.2fr; grid-template-columns: 1fr 1fr 1.2fr;
grid-template-rows: minmax(320px, 1fr) minmax(320px, 1fr); grid-template-rows: minmax(320px, auto) minmax(320px, auto);
gap: 0.5rem; gap: 0.5rem;
min-height: min-content; min-height: min-content;
align-items: stretch; align-items: stretch;
padding: 0.25rem; padding: 0.25rem 0.25rem 1rem;
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
@@ -108,6 +107,7 @@
grid-template-rows: repeat(3, minmax(280px, auto)); grid-template-rows: repeat(3, minmax(280px, auto));
gap: 0.5rem; gap: 0.5rem;
min-height: auto; min-height: auto;
padding-bottom: 1rem;
} }
.dashboard-grid.three-column .dashboard-card-wrapper:nth-child(1) { .dashboard-grid.three-column .dashboard-card-wrapper:nth-child(1) {
@@ -139,7 +139,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.dashboard-container { .dashboard-container {
padding: 0.25rem; padding: 0.25rem 0.25rem 1rem;
} }
.dashboard-grid, .dashboard-grid,
@@ -147,7 +147,7 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto; grid-template-rows: auto;
gap: 0.375rem; gap: 0.375rem;
padding: 0; padding: 0 0 1rem 0;
} }
.dashboard-grid.three-column .dashboard-card-wrapper:nth-child(1), .dashboard-grid.three-column .dashboard-card-wrapper:nth-child(1),
@@ -240,7 +240,7 @@
@media (max-width: 1200px) { @media (max-width: 1200px) {
.dashboard-container { .dashboard-container {
padding: 0.75rem 1rem; padding: 0.75rem 1rem 1.5rem;
gap: 0.5rem; gap: 0.5rem;
} }
@@ -282,7 +282,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.dashboard-container { .dashboard-container {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem 1.5rem;
gap: 0.5rem; gap: 0.5rem;
} }
@@ -314,7 +314,7 @@
@media (max-width: 480px) { @media (max-width: 480px) {
.dashboard-container { .dashboard-container {
padding: 0.5rem; padding: 0.5rem 0.5rem 1.5rem;
gap: 0.5rem; gap: 0.5rem;
} }

View File

@@ -527,3 +527,33 @@
padding: 1.5rem; 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;
}

14
src/vite-env.d.ts vendored
View File

@@ -1,12 +1,12 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module "*.vue" { declare module '*.vue' {
import type { DefineComponent } from "vue"; import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>; const component: DefineComponent<{}, {}, any>
export default component; export default component
} }
declare module "*.json" { declare module '*.json' {
const value: any; const value: any
export default value; export default value
} }

2392
yarn.lock

File diff suppressed because it is too large Load Diff