76 Commits

Author SHA1 Message Date
renovate[bot]
9b8d3cc196 Update actions/checkout action to v5 2025-11-10 16:08:14 +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
Kuingsmile
a88b17c92f fix: update mountPointPlaceholder in localization files 2025-07-18 17:38:17 +08:00
Kuingsmile
20aeb6a796 fix: add creation flags to netsh command in rule_stdout function close #62 2025-07-18 17:23:03 +08:00
Kuingsmile
13efd8a629 docs: update readme 2025-07-18 14:56:06 +08:00
GitHub Action
9998563110 chore: bump version to 0.5.0 [skip ci] 2025-07-18 06:19:09 +00:00
Kuingsmile
6f41bd708c fix: update GitHub token usage in release workflow 2025-07-18 13:44:18 +08:00
Kuingsmile
4837ee592f fix: fix an issue service and processes can't be stopped on macos, #60 2025-07-18 13:33:27 +08:00
Kuingsmile
8d25feefe0 fix: update vue-i18n 2025-07-17 17:47:27 +08:00
Kuingsmile
6628c7936b fix: update rclone download URL to use the official downloads site #60 2025-07-17 17:43:10 +08:00
Kuingsmile
9c53267589 fix: remove redundant confirmation message for admin password reset 2025-07-17 17:36:01 +08:00
Kuingsmile
e0d3250823 feat: optimize admin password management and add reset 2025-07-17 17:32:57 +08:00
Kuingsmile
c9ccf6d1ce feat: add data directory configuration for OpenList core close #58 2025-07-17 15:22:29 +08:00
Kuingsmile
a19e74ce1f feat: add Windows firewall management for openlist port #49 2025-07-17 14:03:40 +08:00
Kuingsmile
0231fa20d7 fix: update rclone configuration tips for clarity and set textarea to readonly #60 2025-07-17 10:48:02 +08:00
Kuingsmile
f69bfa6fd5 feat: disable modal close when click at blank space #60 2025-07-17 10:12:07 +08:00
Kuingsmile
bb0f091849 refactor: streamline exit status handling 2025-07-16 17:19:13 +08:00
Kuingsmile
9d95b6b46c refactor: simplify conditional checks in macOS service management 2025-07-16 17:10:03 +08:00
Kuingsmile
249612344e chore: update Rust toolchain to nightly in CI and release workflows 2025-07-16 16:59:03 +08:00
Kuingsmile
3c5f64b1b4 refactor: update open link handling 2025-07-16 16:53:01 +08:00
Kuingsmile
2911922403 refactor: remove unused RcloneRemoteParameters and TransferStats structs 2025-07-16 16:45:30 +08:00
Kuingsmile
0093f15524 feat: update package version to 0.4.0 and add winget manifests 2025-07-15 17:49:48 +08:00
GitHub Action
704d06ebe1 chore: bump version to 0.4.0 [skip ci] 2025-07-15 09:20:34 +00:00
Kuingsmile
7038a1a255 chore: cargo.toml 2025-07-15 16:57:35 +08:00
Kuingsmile
7476e29e2b feat: add macOS private API support to Tauri dependencies 2025-07-15 16:53:56 +08:00
Kuingsmile
08a9eb38cc fix: remove unused i18n 2025-07-15 16:46:41 +08:00
Kuingsmile
d0917ee550 feat: optimize front-end performance 2025-07-15 15:42:29 +08:00
Kuingsmile
ccb8b12f1e feat: remove tutorial functionality and related UI components 2025-07-15 14:24:15 +08:00
Kuingsmile
1d74daf8a5 refactor: optimize string creation in get_current_platform function 2025-07-14 21:51:45 +08:00
Kuingsmile
ffdf996cd0 refactor: simplify platform string formatting in get_current_platform function 2025-07-14 17:21:25 +08:00
Kuingsmile
eb82a49270 feat: removing service config file if select delete app data 2025-07-14 17:19:57 +08:00
Kuingsmile
0c09addb31 feat: termination rclone.exe, clean all related files after uninstall 2025-07-14 16:47:44 +08:00
Kuingsmile
731ff435a5 feat: update application configuration and monitoring intervals, remove monitor_interval setting
Fixes #48
2025-07-13 15:52:29 +08:00
Kuingsmile
3e1a5f121d fix: update parameter type for install_windows_update function to Path 2025-07-13 11:55:35 +08:00
Kuingsmile
9263ad9810 fix: fix a bug make auto update not woking on windows 2025-07-13 11:44:21 +08:00
Kuingsmile
7b0565f210 fix: run child process as user other than SYSTEM on windows, close #52 2025-07-13 10:57:51 +08:00
Kuingsmile
90f935e6bb fix: add WebDAV and WinFSP tips css 2025-07-12 14:56:51 +08:00
Kuingsmile
8aa4f93f6f docs: add installation instructions for Winget 2025-07-12 14:22:08 +08:00
Kuingsmile
4b08aeb4d3 fix: add missing scope and install modes to winget manifest 2025-07-11 15:34:20 +08:00
Kuingsmile
97be081c47 fix: change installer type to nullsoft in winget manifest 2025-07-11 11:54:48 +08:00
Kuingsmile
c68102fa20 feat: add version 0.3.0 winget manifest file 2025-07-10 16:30:18 +08:00
78 changed files with 6101 additions and 3958 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@v5
- 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@v5
- 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@v5
- 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

@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -63,10 +63,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@nightly
with: with:
targets: ${{ matrix.platform.target }} targets: ${{ matrix.platform.target }}
components: rustfmt, clippy components: rustfmt, clippy
@@ -116,7 +116,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -147,10 +147,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@nightly
with: with:
targets: ${{ matrix.platform.target }} targets: ${{ matrix.platform.target }}

View File

@@ -30,7 +30,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -169,7 +169,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@@ -26,6 +26,11 @@ 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 }}
concurrency: concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}" group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -40,7 +45,7 @@ jobs:
version-type: ${{ steps.check.outputs.version-type }} version-type: ${{ steps.check.outputs.version-type }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -89,7 +94,7 @@ jobs:
tag: ${{ steps.version.outputs.tag }} tag: ${{ steps.version.outputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -188,7 +193,7 @@ jobs:
if: github.event_name != 'workflow_dispatch' || inputs.version != '' if: github.event_name != 'workflow_dispatch' || inputs.version != ''
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Ensure jq and grep are installed - name: Ensure jq and grep are installed
run: sudo apt-get update && sudo apt-get install -y jq run: sudo apt-get update && sudo apt-get install -y jq
@@ -248,7 +253,7 @@ jobs:
tag: ${{ steps.tag.outputs.tag }} tag: ${{ steps.tag.outputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -315,10 +320,10 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust Stable - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@nightly
- name: Add Rust Target - name: Add Rust Target
run: rustup target add ${{ matrix.target }} run: rustup target add ${{ matrix.target }}
@@ -395,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
@@ -422,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 }}'
@@ -449,10 +485,10 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust Stable - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@nightly
- name: Add Rust Target - name: Add Rust Target
run: rustup target add ${{ matrix.target }} run: rustup target add ${{ matrix.target }}
@@ -572,7 +608,7 @@ jobs:
if: always() && needs.build.result == 'success' && needs.changelog.result == 'success' if: always() && needs.build.result == 'success' && needs.changelog.result == 'success'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Download changelog - name: Download changelog
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -607,7 +643,7 @@ jobs:
if: always() && needs.publish.result == 'success' if: always() && needs.publish.result == 'success'
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Get release version - name: Get release version
id: version id: version
@@ -624,7 +660,7 @@ jobs:
- name: Update WinGet package manifest - name: Update WinGet package manifest
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
run: | run: |
$version = "${{ steps.version.outputs.version }}" $version = "${{ steps.version.outputs.version }}"
# URLs for both x64 and arm64 installers # URLs for both x64 and arm64 installers

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

@@ -0,0 +1,18 @@
# Created using wingetcreate 1.9.14.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json
PackageIdentifier: OpenListTeam.OpenListDesktop
PackageVersion: 0.3.0
InstallerType: nullsoft
Scope: machine
InstallModes:
- interactive
Installers:
- Architecture: x64
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.3.0/OpenList.Desktop_0.3.0_x64-setup.exe
InstallerSha256: 43CC59B5E557F67A7D2F66ADBEF517FBB7CD4FD7E9032FA274FEF5373E38B885
- Architecture: arm64
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.3.0/OpenList.Desktop_0.3.0_arm64-setup.exe
InstallerSha256: 3F99A8F566242EE749A3463E3DCB7D6D1CE75C51D3E31970AE1A39C46287035F
ManifestType: installer
ManifestVersion: 1.9.0

View File

@@ -0,0 +1,30 @@
# Created using wingetcreate 1.9.14.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json
PackageIdentifier: OpenListTeam.OpenListDesktop
PackageVersion: 0.3.0
PackageLocale: en-US
Publisher: OpenList Team
PublisherUrl: https://github.com/OpenListTeam
PublisherSupportUrl: https://github.com/OpenListTeam/OpenList-Desktop/issues
Author: Kuingsmile
PackageName: OpenList Desktop
PackageUrl: https://github.com/OpenListTeam/OpenList-Desktop
License: GPL-3.0
LicenseUrl: https://github.com/OpenListTeam/OpenList-Desktop/blob/main/LICENSE
Copyright: Copyright (c) 2025 OpenList Team
ShortDescription: A cross-platform desktop application for OpenList with local disk mounting
Description: |
OpenList Desktop is a modern desktop application that provides a seamless interface for managing OpenList.
Features include local disk mounting, service management, real-time monitoring, and multi-language support.
Key Features:
- Cross-platform support (Windows, macOS, Linux)
- Local disk mounting
- Service management and monitoring
- Real-time log viewing
- Multi-language support
Tags:
- openlist
ManifestType: defaultLocale
ManifestVersion: 1.9.0

View File

@@ -0,0 +1,8 @@
# Created using wingetcreate 1.9.14.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json
PackageIdentifier: OpenListTeam.OpenListDesktop
PackageVersion: 0.3.0
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.9.0

View File

@@ -0,0 +1,18 @@
# Created using wingetcreate 1.9.14.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json
PackageIdentifier: OpenListTeam.OpenListDesktop
PackageVersion: 0.4.0
InstallerType: nullsoft
Scope: machine
InstallModes:
- interactive
Installers:
- Architecture: x64
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.4.0/OpenList.Desktop_0.4.0_x64-setup.exe
InstallerSha256: 500DDBC34C73A663C1CA5A82F55DF56321AE4E2E8B727BE26D6EBF4F9F19F881
- Architecture: arm64
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.4.0/OpenList.Desktop_0.4.0_arm64-setup.exe
InstallerSha256: 4D60544E1684AE3A90220DA6A044D57C144E9F566272D2D43A481DEC8ED573EA
ManifestType: installer
ManifestVersion: 1.9.0

View File

@@ -0,0 +1,30 @@
# Created using wingetcreate 1.9.14.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json
PackageIdentifier: OpenListTeam.OpenListDesktop
PackageVersion: 0.4.0
PackageLocale: en-US
Publisher: OpenList Team
PublisherUrl: https://github.com/OpenListTeam
PublisherSupportUrl: https://github.com/OpenListTeam/OpenList-Desktop/issues
Author: Kuingsmile
PackageName: OpenList Desktop
PackageUrl: https://github.com/OpenListTeam/OpenList-Desktop
License: GPL-3.0
LicenseUrl: https://github.com/OpenListTeam/OpenList-Desktop/blob/main/LICENSE
Copyright: Copyright (c) 2025 OpenList Team
ShortDescription: A cross-platform desktop application for OpenList with local disk mounting
Description: |
OpenList Desktop is a modern desktop application that provides a seamless interface for managing OpenList.
Features include local disk mounting, service management, real-time monitoring, and multi-language support.
Key Features:
- Cross-platform support (Windows, macOS, Linux)
- Local disk mounting
- Service management and monitoring
- Real-time log viewing
- Multi-language support
Tags:
- openlist
ManifestType: defaultLocale
ManifestVersion: 1.9.0

View File

@@ -0,0 +1,8 @@
# Created using wingetcreate 1.9.14.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json
PackageIdentifier: OpenListTeam.OpenListDesktop
PackageVersion: 0.4.0
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.9.0

View File

@@ -2,6 +2,7 @@ PackageIdentifier: OpenListTeam.OpenListDesktop
PackageName: OpenList Desktop PackageName: OpenList Desktop
Publisher: OpenList Team Publisher: OpenList Team
PackageUrl: https://github.com/OpenListTeam/OpenList-Desktop PackageUrl: https://github.com/OpenListTeam/OpenList-Desktop
Copyright: Copyright (c) 2025 OpenList Team
License: GPL-3.0 License: GPL-3.0
LicenseUrl: https://github.com/OpenListTeam/OpenList-Desktop/blob/main/LICENSE LicenseUrl: https://github.com/OpenListTeam/OpenList-Desktop/blob/main/LICENSE
ShortDescription: A cross-platform desktop application for OpenList with local disk mounting ShortDescription: A cross-platform desktop application for OpenList with local disk mounting

View File

@@ -138,11 +138,19 @@ yarn run tauri build
#### Windows #### Windows
##### 使用安装程序
1. 下载 `.exe` 安装程序 1. 下载 `.exe` 安装程序
2. 以管理员身份运行安装程序 2. 以管理员身份运行安装程序
3. 按照安装向导进行操作 3. 按照安装向导进行操作
4. 从开始菜单或桌面快捷方式启动 4. 从开始菜单或桌面快捷方式启动
##### 使用Winget
```bash
winget install OpenListTeam.OpenListDesktop
```
#### macOS #### macOS
1. 下载 `.dmg` 文件 1. 下载 `.dmg` 文件
@@ -170,7 +178,6 @@ yarn run tauri build
1. **初始设置**:首次启动时,应用程序将指导您完成初始配置 1. **初始设置**:首次启动时,应用程序将指导您完成初始配置
2. **服务安装**:在提示时安装 OpenList 服务 2. **服务安装**:在提示时安装 OpenList 服务
3. **存储配置**:配置您的第一个云存储连接 3. **存储配置**:配置您的第一个云存储连接
4. **教程**:完成交互式教程以学习关键功能
### 基本操作 ### 基本操作
@@ -235,9 +242,9 @@ yarn run tauri build
{ {
"openlist": { "openlist": {
"port": 5244, "port": 5244,
"api_token": "your-secure-token", "data_dir": "",
"auto_launch": true, "auto_launch": true,
"ssl_enabled": false "ssl_enabled": false,
} }
} }
``` ```
@@ -258,6 +265,7 @@ yarn run tauri build
"extraFlags": ["--vfs-cache-mode", "full"] "extraFlags": ["--vfs-cache-mode", "full"]
} }
}, },
"api_port": 45572
} }
} }
``` ```
@@ -269,18 +277,15 @@ yarn run tauri build
"app": { "app": {
"theme": "auto", "theme": "auto",
"auto_update_enabled": true, "auto_update_enabled": true,
"monitor_interval": 30000 "gh_proxy": "https://ghproxy.com/",
"gh_proxy_api": false,
"open_links_in_browser": true,
"admin_password": "",
"show_window_on_startup": true
} }
} }
``` ```
### 环境变量
- `OPENLIST_API_TOKEN`:覆盖默认 API 令牌
- `OPENLIST_PORT`覆盖默认端口5244
- `RCLONE_CONFIG_DIR`:自定义 Rclone 配置目录
- `LOG_LEVEL`设置日志级别debug、info、warn、error
## 🔧 开发 ## 🔧 开发
### 开发环境设置 ### 开发环境设置
@@ -288,7 +293,7 @@ yarn run tauri build
#### 先决条件 #### 先决条件
- **Node.js**v22+ 和 yarn - **Node.js**v22+ 和 yarn
- **Rust**:最新稳定版本 - **Rust**:最新nightly版本
- **Git**:版本控制 - **Git**:版本控制
#### 设置步骤 #### 设置步骤
@@ -301,35 +306,11 @@ cd openlist-desktop
# 安装 Node.js 依赖 # 安装 Node.js 依赖
yarn install yarn install
# 安装 Rust 依赖
cd src-tauri
cargo fetch
# 准备开发环境 # 准备开发环境
cd ..
yarn run prebuild:dev yarn run prebuild:dev
# 启动开发服务器 # 启动开发服务器
yarn run dev yarn tauri dev
```
#### 开发命令
```bash
# 启动带热重载的开发服务器
yarn run dev
# 启动不带文件监视的开发
yarn run nowatch
# 运行代码检查
yarn run lint
# 修复代码检查问题
yarn run lint:fix
# 类型检查
yarn run build --dry-run
``` ```
#### 提交PR #### 提交PR

View File

@@ -138,11 +138,19 @@ yarn run tauri build
#### Windows #### Windows
##### Installation via Installer
1. Download the `.exe` installer 1. Download the `.exe` installer
2. Run the installer as Administrator 2. Run the installer as Administrator
3. Follow the installation wizard 3. Follow the installation wizard
4. Launch from Start Menu or Desktop shortcut 4. Launch from Start Menu or Desktop shortcut
##### Winget
```bash
winget install OpenListTeam.OpenListDesktop
```
#### macOS #### macOS
1. Download the `.dmg` file 1. Download the `.dmg` file
@@ -170,7 +178,6 @@ yarn run tauri build
1. **Initial Setup**: On first launch, the application will guide you through initial configuration 1. **Initial Setup**: On first launch, the application will guide you through initial configuration
2. **Service Installation**: Install the OpenList service when prompted 2. **Service Installation**: Install the OpenList service when prompted
3. **Storage Configuration**: Configure your first cloud storage connection 3. **Storage Configuration**: Configure your first cloud storage connection
4. **Tutorial**: Complete the interactive tutorial to learn key features
### Basic Operations ### Basic Operations
@@ -235,7 +242,7 @@ Add custom Rclone flags for optimal performance:
{ {
"openlist": { "openlist": {
"port": 5244, "port": 5244,
"api_token": "your-secure-token", "data_dir": "",
"auto_launch": true, "auto_launch": true,
"ssl_enabled": false "ssl_enabled": false
} }
@@ -258,6 +265,7 @@ Add custom Rclone flags for optimal performance:
"extraFlags": ["--vfs-cache-mode", "full"] "extraFlags": ["--vfs-cache-mode", "full"]
} }
}, },
"api_port": 45572
} }
} }
``` ```
@@ -269,18 +277,15 @@ Add custom Rclone flags for optimal performance:
"app": { "app": {
"theme": "auto", "theme": "auto",
"auto_update_enabled": true, "auto_update_enabled": true,
"monitor_interval": 30000 "gh_proxy": "https://ghproxy.com/",
"gh_proxy_api": false,
"open_links_in_browser": true,
"admin_password": "",
"show_window_on_startup": true
} }
} }
``` ```
### Environment Variables
- `OPENLIST_API_TOKEN`: Override default API token
- `OPENLIST_PORT`: Override default port (5244)
- `RCLONE_CONFIG_DIR`: Custom Rclone configuration directory
- `LOG_LEVEL`: Set logging level (debug, info, warn, error)
## 🔧 Development ## 🔧 Development
### Development Environment Setup ### Development Environment Setup
@@ -288,7 +293,7 @@ Add custom Rclone flags for optimal performance:
#### Prerequisites #### Prerequisites
- **Node.js**: v22+ with yarn - **Node.js**: v22+ with yarn
- **Rust**: Latest stable version - **Rust**: Latest nightly version
- **Git**: Version control - **Git**: Version control
#### Setup Steps #### Setup Steps
@@ -301,35 +306,11 @@ cd openlist-desktop
# Install Node.js dependencies # Install Node.js dependencies
yarn install yarn install
# Install Rust dependencies
cd src-tauri
cargo fetch
# Prepare development environment # Prepare development environment
cd ..
yarn run prebuild:dev yarn run prebuild:dev
# Start development server # Start development server
yarn run dev yarn tauri dev
```
#### Development Commands
```bash
# Start development server with hot reload
yarn run dev
# Start development without file watching
yarn run nowatch
# Run linting
yarn run lint
# Fix linting issues
yarn run lint:fix
# Type checking
yarn run build --dry-run
``` ```
## 🤝 Contributing ## 🤝 Contributing

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.3.0", "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",
@@ -41,7 +42,7 @@
"check:all": "yarn check:frontend && yarn check:rust:all", "check:all": "yarn check:frontend && yarn check:rust:all",
"fix:rust": "cd src-tauri && cargo fmt --all && cargo clippy --all-targets --all-features --fix --allow-dirty", "fix:rust": "cd src-tauri && cargo fmt --all && cargo clippy --all-targets --all-features --fix --allow-dirty",
"fix:frontend": "yarn lint:fix", "fix:frontend": "yarn lint:fix",
"fix:all": "yarn fix:frontend && yarn fix:rust" "fix:all": "yarn fix:frontend && yarn fix:rust && yarn i18n:check"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
@@ -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.9", "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'
@@ -28,7 +29,7 @@ if (!getOpenlistArchMap[platformArch]) {
} }
// Rclone version management // Rclone version management
let rcloneVersion = 'v1.70.1' let rcloneVersion = 'v1.70.3'
const rcloneVersionUrl = 'https://github.com/rclone/rclone/releases/latest/download/version.txt' const rcloneVersionUrl = 'https://github.com/rclone/rclone/releases/latest/download/version.txt'
async function getLatestRcloneVersion() { async function getLatestRcloneVersion() {
@@ -42,7 +43,7 @@ async function getLatestRcloneVersion() {
} }
// openlist version management // openlist version management
let openlistVersion = 'v4.0.3' let openlistVersion = 'v4.0.8'
async function getLatestOpenlistVersion() { async function getLatestOpenlistVersion() {
try { try {
@@ -51,7 +52,7 @@ async function getLatestOpenlistVersion() {
getFetchOptions() getFetchOptions()
) )
const data = await response.json() const data = await response.json()
openlistVersion = data.tag_name || 'v4.0.3' openlistVersion = data.tag_name || 'v4.0.8'
console.log(`Latest OpenList version: ${openlistVersion}`) console.log(`Latest OpenList version: ${openlistVersion}`)
} catch (error) { } catch (error) {
console.log('Error fetching latest OpenList version:', error.message) console.log('Error fetching latest OpenList version:', error.message)
@@ -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 })
@@ -240,12 +252,7 @@ async function main() {
await retryTask('rclone', async () => { await retryTask('rclone', async () => {
await getLatestRcloneVersion() await getLatestRcloneVersion()
await resolveSidecar( await resolveSidecar(
createBinaryInfo( createBinaryInfo('rclone', getRcloneArchMap(rcloneVersion), `https://downloads.rclone.org`, rcloneVersion)
'rclone',
getRcloneArchMap(rcloneVersion),
'https://github.com/rclone/rclone/releases/download',
rcloneVersion
)
) )
}) })
if (isWin) { if (isWin) {

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.2.0" 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.3.0" 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

@@ -467,6 +467,22 @@ FunctionEnd
nsis_tauri_utils::KillProcess "openlist.exe" nsis_tauri_utils::KillProcess "openlist.exe"
!endif !endif
${EndIf} ${EndIf}
; Check if rclone.exe is running
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "rclone.exe"
!else
nsis_tauri_utils::FindProcess "rclone.exe"
!endif
Pop $R0
${If} $R0 = 0
DetailPrint "Kill rclone.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "rclone.exe"
!else
nsis_tauri_utils::KillProcess "rclone.exe"
!endif
${EndIf}
!macroend !macroend
!macro StartOpenListDesktopService !macro StartOpenListDesktopService
@@ -967,6 +983,13 @@ Section Uninstall
SetShellVarContext current SetShellVarContext current
RmDir /r "$APPDATA\${BUNDLEID}" RmDir /r "$APPDATA\${BUNDLEID}"
RmDir /r "$LOCALAPPDATA\${BUNDLEID}" RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
RmDir /r "$INSTDIR\data"
RmDir /r "$INSTDIR\logs"
Delete "$INSTDIR\settings.json"
Delete "$INSTDIR\openlist-desktop-service.log"
Delete "$INSTDIR\rclone.conf"
RMDir "$INSTDIR"
RMDir /r "C:\ProgramData\openlist-service-config"
${EndIf} ${EndIf}
SetShellVarContext current SetShellVarContext current

View File

@@ -4,10 +4,12 @@ use std::path::PathBuf;
use tauri::State; use tauri::State;
use tokio::time::{Duration, sleep}; use tokio::time::{Duration, sleep};
use crate::cmd::http_api::{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::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())?;
@@ -19,13 +21,13 @@ fn persist_app_settings(settings: &MergedSettings) -> Result<(), String> {
write_json_to_file(path, settings) write_json_to_file(path, settings)
} }
fn update_data_config_port(port: u16) -> Result<(), String> { fn update_data_config(port: u16, data_dir: Option<&str>) -> Result<(), String> {
let exe_dir = std::env::current_exe() let data_config_path = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
.map_err(|e| e.to_string())? PathBuf::from(dir).join("config.json")
.parent() } else {
.ok_or("Failed to get exe parent dir")? get_default_openlist_data_dir()?.join("config.json")
.to_path_buf(); };
let data_config_path = exe_dir.join("data").join("config.json");
if let Some(parent) = data_config_path.parent() { if let Some(parent) = data_config_path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?; fs::create_dir_all(parent).map_err(|e| e.to_string())?;
} }
@@ -63,6 +65,47 @@ async fn restart_openlist_core(state: State<'_, AppState>) -> Result<(), String>
Ok(()) Ok(())
} }
async fn recreate_openlist_core_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_openlist_core_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;
let auto_launch = state
.app_settings
.read()
.clone()
.map(|settings| settings.openlist.auto_launch)
.unwrap_or(false);
create_openlist_core_process(auto_launch, state.clone()).await?;
}
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()?;
@@ -85,14 +128,54 @@ pub async fn save_settings_with_update_port(
settings: MergedSettings, settings: MergedSettings,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let old_settings = state.get_settings();
let needs_openlist_recreation = if let Some(old) = &old_settings {
old.openlist.data_dir != settings.openlist.data_dir
} else {
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)?;
update_data_config_port(settings.openlist.port)?; let data_dir = if settings.openlist.data_dir.is_empty() {
if let Err(e) = restart_openlist_core(state.clone()).await { None
log::error!("{e}"); } else {
return Err(e); Some(settings.openlist.data_dir.as_str())
};
update_data_config(settings.openlist.port, data_dir)?;
if needs_openlist_recreation {
if let Err(e) = recreate_openlist_core_process(state.clone()).await {
log::error!("{e}");
return Err(e);
}
log::info!(
"Settings saved and OpenList core recreated with new data directory successfully"
);
} else {
if let Err(e) = restart_openlist_core(state.clone()).await {
log::error!("{e}");
return Err(e);
}
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

@@ -1,5 +1,5 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
@@ -68,13 +68,12 @@ pub struct DownloadProgress {
fn get_current_platform() -> String { fn get_current_platform() -> String {
let os = env::consts::OS; let os = env::consts::OS;
let arch = env::consts::ARCH;
match os { match os {
"windows" => format!("{arch}-pc-windows-msvc"), "windows" => "pc-windows-msvc".to_string(),
"macos" => format!("{arch}-apple-darwin"), "macos" => "apple-darwin".to_string(),
"linux" => format!("{arch}-unknown-linux-gnu"), "linux" => "unknown-linux-gnu".to_string(),
_ => format!("{arch}-{os}"), _ => os.to_string(),
} }
} }
@@ -398,6 +397,7 @@ pub async fn install_update_and_restart(
"linux" => install_linux_update(&path).await, "linux" => install_linux_update(&path).await,
_ => Err("Unsupported platform for auto-update".to_string()), _ => Err("Unsupported platform for auto-update".to_string()),
}; };
log::info!("Update installation result: {result:?}");
match result { match result {
Ok(_) => { Ok(_) => {
@@ -407,8 +407,8 @@ pub async fn install_update_and_restart(
log::error!("Failed to emit install completed event: {e}"); log::error!("Failed to emit install completed event: {e}");
} }
if let Err(e) = app.emit("app-restarting", ()) { if let Err(e) = app.emit("quit-app", ()) {
log::error!("Failed to emit app restarting event: {e}"); log::error!("Failed to emit app quit event: {e}");
} }
tokio::time::sleep(Duration::from_millis(1000)).await; tokio::time::sleep(Duration::from_millis(1000)).await;
@@ -423,29 +423,47 @@ pub async fn install_update_and_restart(
} }
} }
} }
async fn install_windows_update(installer_path: &Path) -> Result<(), String> {
async fn install_windows_update(installer_path: &PathBuf) -> Result<(), String> {
log::info!("Installing Windows update..."); log::info!("Installing Windows update...");
let mut cmd = Command::new(installer_path); let mut cmd = Command::new("powershell");
cmd.arg("/SILENT"); cmd.args([
"-Command",
#[cfg(windows)] &format!(
{ "Start-Process -FilePath '{}' -Verb runAs",
use std::os::windows::process::CommandExt; installer_path.display()
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW ),
} ]);
log::info!("Running command: {cmd:?}");
let _ = tokio::task::spawn_blocking(move || { let _ = tokio::task::spawn_blocking(move || {
cmd.spawn() let child = cmd
.map_err(|e| format!("Failed to start Windows installer: {e}")) .spawn()
.map_err(|e| format!("Failed to start Windows installer: {e}"))?;
log::info!("Started installer process with PID: {}", child.id());
let output = child
.wait_with_output()
.map_err(|e| format!("Failed to wait for installer: {e}"))?;
log::info!("Installer output: {output:?}");
if output.status.success() {
log::info!(
"Installer completed successfully. Output: {}",
String::from_utf8_lossy(&output.stdout)
);
Ok(())
} else {
log::error!(
"Installer failed. Error: {}",
String::from_utf8_lossy(&output.stderr)
);
Err(format!("Installer exited with status: {:?}", output.status))
}
}) })
.await .await
.map_err(|e| format!("Task error: {e}"))?; .map_err(|e| format!("Task error: {e}"))?;
Ok(()) Ok(())
} }
async fn install_macos_update(installer_path: &PathBuf) -> Result<(), String> { async fn install_macos_update(installer_path: &PathBuf) -> Result<(), String> {
log::info!("Installing macOS update..."); log::info!("Installing macOS update...");
@@ -453,8 +471,25 @@ async fn install_macos_update(installer_path: &PathBuf) -> Result<(), String> {
cmd.arg(installer_path); cmd.arg(installer_path);
let _ = tokio::task::spawn_blocking(move || { let _ = tokio::task::spawn_blocking(move || {
cmd.spawn() let child = cmd
.map_err(|e| format!("Failed to start macOS installer: {e}")) .spawn()
.map_err(|e| format!("Failed to start macOS installer: {e}"))?;
let output = child
.wait_with_output()
.map_err(|e| format!("Failed to wait for installer: {e}"))?;
if output.status.success() {
log::info!(
"Installer completed successfully. Output: {}",
String::from_utf8_lossy(&output.stdout)
);
Ok(())
} else {
log::error!(
"Installer failed. Error: {}",
String::from_utf8_lossy(&output.stderr)
);
Err(format!("Installer exited with status: {:?}", output.status))
}
}) })
.await .await
.map_err(|e| format!("Task error: {e}"))?; .map_err(|e| format!("Task error: {e}"))?;
@@ -489,8 +524,25 @@ async fn install_linux_update(installer_path: &PathBuf) -> Result<(), String> {
}; };
let _ = tokio::task::spawn_blocking(move || { let _ = tokio::task::spawn_blocking(move || {
cmd.spawn() let child = cmd
.map_err(|e| format!("Failed to start Linux installer: {e}")) .spawn()
.map_err(|e| format!("Failed to start Linux installer: {e}"))?;
let output = child
.wait_with_output()
.map_err(|e| format!("Failed to wait for installer: {e}"))?;
if output.status.success() {
log::info!(
"Installer completed successfully. Output: {}",
String::from_utf8_lossy(&output.stdout)
);
Ok(())
} else {
log::error!(
"Installer failed. Error: {}",
String::from_utf8_lossy(&output.stderr)
);
Err(format!("Installer exited with status: {:?}", output.status))
}
}) })
.await .await
.map_err(|e| format!("Task error: {e}"))?; .map_err(|e| format!("Task error: {e}"))?;
@@ -641,16 +693,3 @@ pub async fn perform_background_update_check(app: AppHandle) -> Result<(), Strin
} }
} }
} }
#[tauri::command]
pub async fn restart_app(app: AppHandle) {
log::info!("Restarting application...");
if let Err(e) = app.emit("app-restarting", ()) {
log::error!("Failed to emit app-restarting event: {e}");
}
tokio::time::sleep(Duration::from_millis(500)).await;
app.restart();
}

View File

@@ -0,0 +1,122 @@
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
use std::process::{Command, ExitStatus};
#[cfg(target_os = "windows")]
use deelevate::{PrivilegeLevel, Token};
#[cfg(target_os = "windows")]
use runas::Command as RunasCommand;
use tauri::State;
use crate::object::structs::AppState;
#[cfg(target_os = "windows")]
const RULE: &str = "OpenList Core";
#[cfg(target_os = "windows")]
fn netsh_with_elevation(args: &[String]) -> Result<ExitStatus, String> {
let token = Token::with_current_process().map_err(|e| format!("token: {e}"))?;
let elevated = !matches!(
token
.privilege_level()
.map_err(|e| format!("privilege: {e}"))?,
PrivilegeLevel::NotPrivileged
);
if elevated {
Command::new("netsh")
.args(args)
.creation_flags(0x08000000)
.status()
} else {
RunasCommand::new("netsh").args(args).show(false).status()
}
.map_err(|e| format!("netsh: {e}"))
}
#[cfg(target_os = "windows")]
fn firewall_rule(verb: &str, port: Option<u16>) -> Result<bool, String> {
let mut args: Vec<String> = vec![
"advfirewall".into(),
"firewall".into(),
verb.into(),
"rule".into(),
format!("name={RULE}"),
];
if let Some(p) = port {
args.extend([
"dir=in".into(),
"action=allow".into(),
"protocol=TCP".into(),
format!("localport={p}"),
"description=Allow OpenList Core web interface access".into(),
]);
}
Ok(netsh_with_elevation(&args)?.success())
}
#[cfg(target_os = "windows")]
fn rule_stdout() -> Result<Option<String>, String> {
let out = Command::new("netsh")
.args([
"advfirewall",
"firewall",
"show",
"rule",
&format!("name={RULE}"),
])
.creation_flags(0x08000000)
.output()
.map_err(|e| format!("netsh: {e}"))?;
if out.status.success() {
Ok(Some(String::from_utf8_lossy(&out.stdout).into()))
} else {
Ok(None)
}
}
#[cfg(not(target_os = "windows"))]
fn firewall_rule(_: &str, _: Option<u16>) -> Result<bool, String> {
Ok(true)
}
#[cfg(not(target_os = "windows"))]
fn rule_stdout() -> Result<Option<String>, String> {
Ok(None)
}
#[tauri::command]
pub async fn check_firewall_rule(state: State<'_, AppState>) -> Result<bool, String> {
let port = state
.app_settings
.read()
.clone()
.ok_or("read settings")?
.openlist
.port;
if let Some(out) = rule_stdout()? {
Ok(out.contains(&port.to_string()))
} else {
Ok(false)
}
}
#[tauri::command]
pub async fn add_firewall_rule(state: State<'_, AppState>) -> Result<bool, String> {
let port = state
.app_settings
.read()
.clone()
.ok_or("read settings")?
.openlist
.port;
let _ = firewall_rule("delete", None);
firewall_rule("add", Some(port))
}
#[tauri::command]
pub async fn remove_firewall_rule(_state: State<'_, AppState>) -> Result<bool, String> {
firewall_rule("delete", None)
}

View File

@@ -1,33 +1,142 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
use once_cell::sync::Lazy; use tauri::State;
use regex::Regex;
static ADMIN_PWD_REGEX: Lazy<Regex> = Lazy::new(|| { use crate::object::structs::AppState;
Regex::new(r"Successfully created the admin user and the initial password is: (\w+)") use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir, get_service_log_path};
.expect("Invalid regex pattern")
});
fn resolve_log_paths(source: Option<&str>) -> Result<Vec<PathBuf>, String> { fn generate_random_password() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};
let mut hasher = DefaultHasher::new();
if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
duration.as_nanos().hash(&mut hasher);
}
std::process::id().hash(&mut hasher);
let dummy = [1, 2, 3];
(dummy.as_ptr() as usize).hash(&mut hasher);
let hash = hasher.finish();
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut password = String::new();
let mut current_hash = hash;
for _ in 0..16 {
let index = (current_hash % chars.len() as u64) as usize;
password.push(chars.chars().nth(index).unwrap());
current_hash = current_hash.wrapping_mul(1103515245).wrapping_add(12345);
}
password
}
async fn execute_openlist_admin_set(
password: &str,
state: &State<'_, AppState>,
) -> Result<(), String> {
let exe_path = let exe_path =
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?; env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?;
let app_dir = exe_path let app_dir = exe_path
.parent() .parent()
.ok_or("Executable has no parent directory")? .ok_or("Executable has no parent directory")?;
.to_path_buf();
let possible_names = ["openlist", "openlist.exe"];
let mut openlist_exe = None;
for name in &possible_names {
let exe_path = app_dir.join(name);
if exe_path.exists() {
openlist_exe = Some(exe_path);
break;
}
}
let openlist_exe = openlist_exe.ok_or_else(|| {
format!(
"OpenList executable not found. Searched for: {:?} in {}",
possible_names,
app_dir.display()
)
})?;
log::info!(
"Setting new admin password using: {}",
openlist_exe.display()
);
let mut cmd = Command::new(&openlist_exe);
cmd.args(["admin", "set", password]);
cmd.current_dir(app_dir);
let effective_data_dir = if let Some(settings) = state.get_settings()
&& !settings.openlist.data_dir.is_empty()
{
settings.openlist.data_dir
} else {
get_default_openlist_data_dir()
.map_err(|e| format!("Failed to get default data directory: {e}"))?
.to_string_lossy()
.to_string()
};
cmd.arg("--data");
cmd.arg(&effective_data_dir);
log::info!("Using data directory: {effective_data_dir}");
log::info!("Executing command: {cmd:?}");
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
let output = cmd
.output()
.map_err(|e| format!("Failed to execute openlist command: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
log::error!("OpenList admin set command failed. stdout: {stdout}, stderr: {stderr}");
return Err(format!("OpenList admin set command failed: {stderr}"));
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::info!("Successfully set admin password. Output: {stdout}");
Ok(())
}
fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
let logs_dir = get_app_logs_dir()?;
let service_path = get_service_log_path()?;
let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
PathBuf::from(dir)
} else {
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(app_dir.join("data/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),
paths.push(app_dir.join("data/log/log.log")); Some("all") => {
paths.push(app_dir.join("logs/app.log")); paths.push(openlist_log_base.join("log/log.log"));
paths.push(app_dir.join("logs/process_rclone.log")); paths.push(logs_dir.join("app.log"));
paths.push(app_dir.join("logs/process_openlist_core.log")); paths.push(logs_dir.join("process_rclone.log"));
paths.push(logs_dir.join("process_openlist_core.log"));
paths.push(service_path);
} }
_ => return Err("Invalid log source".into()), _ => return Err("Invalid log source".into()),
} }
@@ -35,34 +144,117 @@ fn resolve_log_paths(source: Option<&str>) -> Result<Vec<PathBuf>, String> {
} }
#[tauri::command] #[tauri::command]
pub async fn get_admin_password() -> Result<String, String> { pub async fn get_admin_password(state: State<'_, AppState>) -> Result<String, String> {
let paths = resolve_log_paths(Some("openlist_core"))?; if let Some(settings) = state.get_settings()
let content = && let Some(ref stored_password) = settings.app.admin_password
std::fs::read_to_string(&paths[0]).map_err(|e| format!("Failed to read log file: {e}"))?; && !stored_password.is_empty()
{
log::info!("Found admin password in local settings");
return Ok(stored_password.clone());
}
ADMIN_PWD_REGEX let new_password = generate_random_password();
.captures_iter(&content)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string())) if let Err(e) = execute_openlist_admin_set(&new_password, &state).await {
.last() return Err(format!("Failed to set new admin password: {e}"));
.ok_or_else(|| "No admin password found in logs".into()) }
log::info!("Successfully generated and set new admin password");
if let Some(mut settings) = state.get_settings() {
settings.app.admin_password = Some(new_password.clone());
state.update_settings(settings.clone());
if let Err(e) = settings.save() {
log::warn!("Failed to save new admin password to settings: {e}");
}
}
Ok(new_password)
} }
#[tauri::command] #[tauri::command]
pub async fn get_logs(source: Option<String>) -> Result<Vec<String>, String> { pub async fn reset_admin_password(state: State<'_, AppState>) -> Result<String, String> {
let paths = resolve_log_paths(source.as_deref())?; log::info!("Forcing admin password reset");
let new_password = generate_random_password();
if let Err(e) = execute_openlist_admin_set(&new_password, &state).await {
return Err(format!("Failed to set new admin password: {e}"));
}
log::info!("Successfully generated and set new admin password via force reset");
if let Some(mut settings) = state.get_settings() {
settings.app.admin_password = Some(new_password.clone());
state.update_settings(settings.clone());
if let Err(e) = settings.save() {
log::warn!("Failed to save new admin password to settings: {e}");
}
}
Ok(new_password)
}
#[tauri::command]
pub async fn set_admin_password(
password: String,
state: State<'_, AppState>,
) -> Result<String, String> {
log::info!("Setting custom admin password");
if let Err(e) = execute_openlist_admin_set(&password, &state).await {
return Err(format!("Failed to set admin password: {e}"));
}
log::info!("Successfully set custom admin password");
if let Some(mut settings) = state.get_settings() {
settings.app.admin_password = Some(password.clone());
state.update_settings(settings.clone());
if let Err(e) = settings.save() {
log::warn!("Failed to save admin password to settings: {e}");
}
}
Ok(password)
}
#[tauri::command]
pub async fn get_logs(
source: Option<String>,
state: State<'_, AppState>,
) -> Result<Vec<String>, String> {
let data_dir = state
.get_settings()
.map(|s| s.openlist.data_dir)
.filter(|d| !d.is_empty());
let paths = resolve_log_paths(source.as_deref(), data_dir.as_deref())?;
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)
} }
#[tauri::command] #[tauri::command]
pub async fn clear_logs(source: Option<String>) -> Result<bool, String> { pub async fn clear_logs(
let paths = resolve_log_paths(source.as_deref())?; source: Option<String>,
state: State<'_, AppState>,
) -> Result<bool, String> {
let data_dir = state
.get_settings()
.map(|s| s.openlist.data_dir)
.filter(|d| !d.is_empty());
let paths = resolve_log_paths(source.as_deref(), data_dir.as_deref())?;
let mut cleared_count = 0; let mut cleared_count = 0;
for path in paths { for path in paths {

View File

@@ -1,6 +1,7 @@
pub mod binary; pub mod binary;
pub mod config; pub mod config;
pub mod custom_updater; pub mod custom_updater;
pub mod firewall;
pub mod http_api; pub mod http_api;
pub mod logs; pub mod logs;
pub mod openlist_core; pub mod openlist_core;

View File

@@ -4,13 +4,22 @@ 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(
auto_start: bool, auto_start: bool,
_state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<ProcessConfig, String> { ) -> Result<ProcessConfig, String> {
let data_dir = state
.app_settings
.read()
.clone()
.ok_or("Failed to read app settings")?
.openlist
.data_dir;
let binary_path = get_openlist_binary_path() let binary_path = get_openlist_binary_path()
.map_err(|e| format!("Failed to get OpenList binary path: {e}"))?; .map_err(|e| format!("Failed to get OpenList binary path: {e}"))?;
let log_file_path = let log_file_path =
@@ -19,12 +28,25 @@ 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()];
// Use custom data dir if set, otherwise use platform-specific default
let effective_data_dir = if !data_dir.is_empty() {
data_dir
} else {
get_default_openlist_data_dir()
.map_err(|e| format!("Failed to get default data directory: {e}"))?
.to_string_lossy()
.to_string()
};
args.push("--data".into());
args.push(effective_data_dir);
let config = ProcessConfig { let config = ProcessConfig {
id: "openlist_core".into(), id: "openlist_core".into(),
name: "single_openlist_core_process".into(), name: "single_openlist_core_process".into(),
bin_path: binary_path.to_string_lossy().into_owned(), bin_path: binary_path.to_string_lossy().into_owned(),
args: vec!["server".into()], args,
log_file: log_file_path.to_string_lossy().into_owned(), log_file: log_file_path.to_string_lossy().into_owned(),
working_dir: binary_path working_dir: binary_path
.parent() .parent()

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

@@ -3,22 +3,28 @@ use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Serialize, Deserialize, Clone)] #[derive(Default, Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig { pub struct AppConfig {
pub theme: Option<String>, pub theme: Option<String>,
pub monitor_interval: Option<u64>,
pub auto_update_enabled: Option<bool>, pub auto_update_enabled: Option<bool>,
pub gh_proxy: Option<String>, pub gh_proxy: Option<String>,
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 show_window_on_startup: Option<bool>,
pub log_filter_level: Option<String>,
pub log_filter_source: Option<String>,
} }
impl AppConfig { impl AppConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
theme: Some("light".to_string()), theme: Some("light".to_string()),
monitor_interval: Some(5),
auto_update_enabled: Some(true), auto_update_enabled: Some(true),
gh_proxy: None, gh_proxy: None,
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,
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 {
@@ -29,23 +29,22 @@ impl MergedSettings {
} }
} }
pub fn get_data_config_path() -> Result<PathBuf, String> { pub fn get_data_config_path_for_dir(data_dir: Option<&str>) -> Result<PathBuf, String> {
let exe = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
std::env::current_exe().map_err(|e| format!("Failed to get current exe path: {e}"))?; Ok(PathBuf::from(dir).join("config.json"))
let dir = exe } else {
.parent() Ok(get_default_openlist_data_dir()?.join("config.json"))
.ok_or_else(|| "Failed to get executable parent directory".to_string())?; }
Ok(dir.join("data").join("config.json"))
} }
pub fn read_data_config() -> Result<serde_json::Value, String> { pub fn read_data_config_for_dir(data_dir: Option<&str>) -> Result<serde_json::Value, String> {
let path = Self::get_data_config_path()?; let path = Self::get_data_config_path_for_dir(data_dir)?;
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?; let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
serde_json::from_str(&content).map_err(|e| e.to_string()) serde_json::from_str(&content).map_err(|e| e.to_string())
} }
fn get_port_from_data_config() -> Result<Option<u16>, String> { fn get_port_from_data_config_for_dir(data_dir: Option<&str>) -> Result<Option<u16>, String> {
let config = Self::read_data_config()?; let config = Self::read_data_config_for_dir(data_dir)?;
Ok(config Ok(config
.get("scheme") .get("scheme")
.and_then(|s| s.get("http_port")) .and_then(|s| s.get("http_port"))
@@ -77,7 +76,13 @@ impl MergedSettings {
default default
}; };
if let Ok(Some(port)) = Self::get_port_from_data_config() let data_dir = if settings.openlist.data_dir.is_empty() {
None
} else {
Some(settings.openlist.data_dir.as_str())
};
if let Ok(Some(port)) = Self::get_port_from_data_config_for_dir(data_dir)
&& settings.openlist.port != port && settings.openlist.port != port
{ {
settings.openlist.port = port; settings.openlist.port = port;

View File

@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OpenListCoreConfig { pub struct OpenListCoreConfig {
pub port: u16, pub port: u16,
pub api_token: String, pub data_dir: String,
pub auto_launch: bool, pub auto_launch: bool,
pub ssl_enabled: bool, pub ssl_enabled: bool,
} }
@@ -12,7 +12,7 @@ impl OpenListCoreConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
port: 5244, port: 5244,
api_token: "".to_string(), data_dir: "".to_string(),
auto_launch: false, auto_launch: false,
ssl_enabled: false, ssl_enabled: false,
} }

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)]
@@ -22,14 +23,6 @@ pub struct RcloneCreateRemoteRequest {
pub parameters: RcloneWebdavConfig, pub parameters: RcloneWebdavConfig,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RcloneRemoteParameters {
pub url: String,
pub vendor: Option<String>,
pub user: String,
pub pass: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RcloneMountRequest { pub struct RcloneMountRequest {
pub fs: String, pub fs: String,
@@ -49,17 +42,11 @@ pub struct RcloneMountOptions {
pub volume_name: Option<String>, pub volume_name: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct RcloneApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub error: Option<String>,
}
impl RcloneConfig { 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

@@ -732,28 +732,27 @@ pub async fn start_service() -> Result<bool, Box<dyn std::error::Error>> {
if let Some(pid_value) = extract_plist_value(&output_str, "PID") { if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
log::info!("Extracted PID value: {pid_value}"); log::info!("Extracted PID value: {pid_value}");
if let Ok(pid) = pid_value.parse::<i32>() { if let Ok(pid) = pid_value.parse::<i32>()
if pid > 0 { && pid > 0
log::info!("Service is running with PID: {pid}"); {
return Ok(true); log::info!("Service is running with PID: {pid}");
} return Ok(true);
} }
} }
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus") { if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus")
if let Ok(status) = exit_status.parse::<i32>() { && let Ok(status) = exit_status.parse::<i32>()
if status == 0 { {
log::info!( if status == 0 {
"Service is loaded but not running (clean exit), attempting to \ log::info!(
start" "Service is loaded but not running (clean exit), attempting to start"
); );
return start_macos_service(SERVICE_IDENTIFIER).await; return start_macos_service(SERVICE_IDENTIFIER).await;
} else { } else {
log::warn!( log::warn!(
"Service has non-zero exit status: {status}, attempting to restart" "Service has non-zero exit status: {status}, attempting to restart"
); );
return start_macos_service(SERVICE_IDENTIFIER).await; return start_macos_service(SERVICE_IDENTIFIER).await;
}
} }
} }
@@ -795,23 +794,23 @@ pub async fn check_service_status() -> Result<String, Box<dyn std::error::Error>
if let Some(pid_value) = extract_plist_value(&output_str, "PID") { if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
log::info!("Extracted PID value: {pid_value}"); log::info!("Extracted PID value: {pid_value}");
if let Ok(pid) = pid_value.parse::<i32>() { if let Ok(pid) = pid_value.parse::<i32>()
if pid > 0 { && pid > 0
log::info!("Service is running with PID: {pid}"); {
return Ok("running".to_string()); log::info!("Service is running with PID: {pid}");
} return Ok("running".to_string());
} }
} }
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus") { if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus")
if let Ok(status) = exit_status.parse::<i32>() { && let Ok(status) = exit_status.parse::<i32>()
if status == 0 { {
log::info!("Service is loaded but not running (clean exit)"); if status == 0 {
return Ok("stopped".to_string()); log::info!("Service is loaded but not running (clean exit)");
} else { return Ok("stopped".to_string());
log::warn!("Service has non-zero exit status: {status}"); } else {
return Ok("stopped".to_string()); log::warn!("Service has non-zero exit status: {status}");
} return Ok("stopped".to_string());
} }
} }
@@ -855,15 +854,15 @@ async fn start_macos_service(service_identifier: &str) -> Result<bool, Box<dyn s
let output_str = String::from_utf8_lossy(&verify_output.stdout); let output_str = String::from_utf8_lossy(&verify_output.stdout);
log::info!("Verification output: {output_str}"); log::info!("Verification output: {output_str}");
if let Some(pid_value) = extract_plist_value(&output_str, "PID") { if let Some(pid_value) = extract_plist_value(&output_str, "PID")
if let Ok(pid) = pid_value.parse::<i32>() { && let Ok(pid) = pid_value.parse::<i32>()
if pid > 0 { {
log::info!("Service verified as running with PID: {pid}"); if pid > 0 {
return Ok(true); log::info!("Service verified as running with PID: {pid}");
} else { return Ok(true);
log::warn!("Service has invalid PID: {pid}"); } else {
return Ok(false); log::warn!("Service has invalid PID: {pid}");
} return Ok(false);
} }
} }
@@ -887,19 +886,19 @@ fn extract_plist_value(plist_output: &str, key: &str) -> Option<String> {
for line in plist_output.lines() { for line in plist_output.lines() {
let trimmed = line.trim(); let trimmed = line.trim();
if trimmed.starts_with(&pattern) { if trimmed.starts_with(&pattern)
if let Some(equals_pos) = trimmed.find('=') { && let Some(equals_pos) = trimmed.find('=')
let value_part = &trimmed[equals_pos + 1..]; {
let value_trimmed = value_part.trim(); let value_part = &trimmed[equals_pos + 1..];
let value_trimmed = value_part.trim();
let value_clean = if let Some(stripped) = value_trimmed.strip_suffix(';') { let value_clean = if let Some(stripped) = value_trimmed.strip_suffix(';') {
stripped stripped
} else { } else {
value_trimmed value_trimmed
}; };
return Some(value_clean.trim().to_string()); return Some(value_clean.trim().to_string());
}
} }
} }

View File

@@ -11,16 +11,20 @@ use cmd::binary::get_binary_version;
use cmd::config::{load_settings, reset_settings, save_settings, save_settings_with_update_port}; use cmd::config::{load_settings, reset_settings, save_settings, save_settings_with_update_port};
use cmd::custom_updater::{ use cmd::custom_updater::{
check_for_updates, download_update, get_current_version, install_update_and_restart, check_for_updates, download_update, get_current_version, install_update_and_restart,
is_auto_check_enabled, restart_app, set_auto_check_enabled, is_auto_check_enabled, set_auto_check_enabled,
}; };
use cmd::firewall::{add_firewall_rule, check_firewall_rule, remove_firewall_rule};
use cmd::http_api::{ use cmd::http_api::{
delete_process, get_process_list, restart_process, start_process, stop_process, update_process, delete_process, get_process_list, restart_process, start_process, stop_process, update_process,
}; };
use cmd::logs::{clear_logs, get_admin_password, get_logs}; use cmd::logs::{
clear_logs, get_admin_password, get_logs, reset_admin_password, set_admin_password,
};
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,
@@ -65,7 +69,7 @@ async fn force_update_tray_menu(
fn setup_background_update_checker(app_handle: &tauri::AppHandle) { fn setup_background_update_checker(app_handle: &tauri::AppHandle) {
let app_handle_initial = app_handle.clone(); let app_handle_initial = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(10)).await; tokio::time::sleep(std::time::Duration::from_secs(300)).await;
let app_state = app_handle_initial.state::<AppState>(); let app_state = app_handle_initial.state::<AppState>();
match is_auto_check_enabled(app_state).await { match is_auto_check_enabled(app_state).await {
@@ -138,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,
@@ -147,6 +155,8 @@ pub fn run() {
get_logs, get_logs,
clear_logs, clear_logs,
get_admin_password, get_admin_password,
reset_admin_password,
set_admin_password,
get_binary_version, get_binary_version,
select_directory, select_directory,
get_available_versions, get_available_versions,
@@ -159,13 +169,15 @@ pub fn run() {
check_service_status, check_service_status,
stop_service, stop_service,
start_service, start_service,
check_firewall_rule,
add_firewall_rule,
remove_firewall_rule,
check_for_updates, check_for_updates,
download_update, download_update,
install_update_and_restart, install_update_and_restart,
get_current_version, get_current_version,
set_auto_check_enabled, set_auto_check_enabled,
is_auto_check_enabled, is_auto_check_enabled
restart_app,
]) ])
.setup(|app| { .setup(|app| {
let app_handle = app.app_handle(); let app_handle = app.app_handle();
@@ -173,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}");
@@ -189,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

@@ -31,13 +31,6 @@ pub struct RcloneMountInfo {
pub status: String, pub status: String,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TransferStats {
pub read: u64,
pub write: u64,
pub errors: u32,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RcloneRemoteListResponse { pub struct RcloneRemoteListResponse {
pub remotes: Vec<String>, pub remotes: Vec<String>,

View File

@@ -6,7 +6,7 @@ use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
static LAST_MENU_UPDATE: Mutex<Option<Instant>> = Mutex::new(None); static LAST_MENU_UPDATE: Mutex<Option<Instant>> = Mutex::new(None);
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(5000); const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(30000);
pub fn create_tray(app_handle: &AppHandle) -> tauri::Result<()> { pub fn create_tray(app_handle: &AppHandle) -> tauri::Result<()> {
let quit_i = MenuItem::with_id(app_handle, "quit", "退出", true, None::<&str>)?; let quit_i = MenuItem::with_id(app_handle, "quit", "退出", true, None::<&str>)?;

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.3.0", "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

@@ -2,6 +2,25 @@
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.openlistteam.openlist.desktop", "identifier": "io.github.openlistteam.openlist.desktop",
"productName": "OpenList Desktop", "productName": "OpenList Desktop",
"app": {
"windows": [
{
"title": "OpenList Desktop",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 400,
"resizable": true,
"center": true,
"decorations": true,
"titleBarStyle": "Transparent",
"visible": false
}
],
"security": {
"csp": null
}
},
"bundle": { "bundle": {
"targets": ["app", "dmg"], "targets": ["app", "dmg"],
"macOS": { "macOS": {

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,13 +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'
import TutorialOverlay from './components/ui/TutorialOverlay.vue'
const appStore = useAppStore() const appStore = useAppStore()
const { t } = useTranslation() const { t } = useTranslation()
@@ -58,16 +57,12 @@ onMounted(async () => {
console.log('Global update listener: Update available', updateInfo) console.log('Global update listener: Update available', updateInfo)
appStore.setUpdateAvailable(true, updateInfo) appStore.setUpdateAvailable(true, updateInfo)
}) })
console.log('Global update listener set up successfully')
} catch (err) { } catch (err) {
console.warn('Failed to set up global update listener:', err) console.warn('Failed to set up global update listener:', err)
} }
document.addEventListener('keydown', handleKeydown) document.addEventListener('keydown', handleKeydown)
} finally { } finally {
setTimeout(() => { isLoading.value = false
isLoading.value = false
}, 1000)
} }
}) })
@@ -147,8 +142,6 @@ onUnmounted(() => {
</router-view> </router-view>
</div> </div>
</main> </main>
<TutorialOverlay />
</div> </div>
</template> </template>
@@ -372,9 +365,9 @@ 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%);
animation: float 20s ease-in-out infinite;
} }
.loading-content { .loading-content {
@@ -402,7 +395,6 @@ body {
z-index: 2; z-index: 2;
color: white; color: white;
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.2)); filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.2));
animation: logoFloat 3s ease-in-out infinite;
} }
.logo-shimmer { .logo-shimmer {
@@ -410,7 +402,6 @@ body {
inset: -20px; inset: -20px;
background: conic-gradient(from 0deg, transparent, rgba(255, 255, 255, 0.2), transparent); background: conic-gradient(from 0deg, transparent, rgba(255, 255, 255, 0.2), transparent);
border-radius: 50%; border-radius: 50%;
animation: shimmer 2s linear infinite;
} }
.loading-title { .loading-title {
@@ -462,57 +453,6 @@ body {
.progress-fill { .progress-fill {
height: 100%; height: 100%;
border-radius: 1px; border-radius: 1px;
animation: progressFill 2s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(0deg);
}
33% {
transform: translateY(-10px) rotate(1deg);
}
66% {
transform: translateY(-5px) rotate(-1deg);
}
}
@keyframes logoFloat {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-8px);
}
}
@keyframes shimmer {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes progressFill {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(-50%);
}
100% {
transform: translateX(100%);
}
} }
.app-container { .app-container {
@@ -539,7 +479,6 @@ body {
height: 80%; height: 80%;
background: radial-gradient(circle, rgba(0, 122, 255, 0.05) 0%, transparent 70%); background: radial-gradient(circle, rgba(0, 122, 255, 0.05) 0%, transparent 70%);
border-radius: 50%; border-radius: 50%;
animation: gradientFloat 20s ease-in-out infinite;
} }
.bg-gradient-secondary { .bg-gradient-secondary {
@@ -550,22 +489,6 @@ body {
height: 60%; height: 60%;
background: radial-gradient(circle, rgba(175, 82, 222, 0.03) 0%, transparent 70%); background: radial-gradient(circle, rgba(175, 82, 222, 0.03) 0%, transparent 70%);
border-radius: 50%; border-radius: 50%;
animation: gradientFloat 25s ease-in-out infinite reverse;
}
@keyframes gradientFloat {
0%,
100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(-10px, -15px) scale(1.05);
}
66% {
transform: translate(10px, -10px) scale(0.95);
}
} }
.main-content { .main-content {
@@ -590,14 +513,6 @@ body {
margin: 0; margin: 0;
} }
.page-enter-active {
transition: all var(--transition-medium);
}
.page-leave-active {
transition: all 0.15s cubic-bezier(0.4, 0, 1, 1);
}
.page-enter-from { .page-enter-from {
opacity: 0; opacity: 0;
transform: translateY(24px) scale(0.98); transform: translateY(24px) scale(0.98);

View File

@@ -60,8 +60,12 @@ export class TauriAPI {
list: (path: string): Promise<FileItem[]> => call('list_files', { path }), list: (path: string): Promise<FileItem[]> => call('list_files', { path }),
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: (path: string): Promise<boolean> => call('open_url', { path }), url: (url: string): Promise<boolean> => call('open_url', { url }),
urlInBrowser: (url: string): Promise<boolean> => call('open_url_in_browser', { url }) urlInBrowser: (url: string): Promise<boolean> => call('open_url_in_browser', { url }),
openOpenListDataDir: (): Promise<boolean> => call('open_openlist_data_dir'),
openLogsDirectory: (): Promise<boolean> => call('open_logs_directory'),
openRcloneConfigFile: (): Promise<boolean> => call('open_rclone_config_file'),
openSettingsFile: (): Promise<boolean> => call('open_settings_file')
} }
// --- Settings management --- // --- Settings management ---
@@ -75,11 +79,13 @@ 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'),
setAdminPassword: (password: string): Promise<string> => call('set_admin_password', { password })
} }
// --- Binary management --- // --- Binary management ---
@@ -104,6 +110,13 @@ export class TauriAPI {
listen: (cb: (action: string) => void) => listen('tray-core-action', e => cb(e.payload as string)) listen: (cb: (action: string) => void) => listen('tray-core-action', e => cb(e.payload as string))
} }
// --- Firewall management ---
static firewall = {
check: (): Promise<boolean> => call('check_firewall_rule'),
add: (): Promise<boolean> => call('add_firewall_rule'),
remove: (): Promise<boolean> => call('remove_firewall_rule')
}
// --- Update management --- // --- Update management ---
static updater = { static updater = {
check: (): Promise<UpdateCheck> => call('check_for_updates'), check: (): Promise<UpdateCheck> => call('check_for_updates'),
@@ -119,6 +132,6 @@ export class TauriAPI {
listen('download-progress', e => cb(e.payload as DownloadProgress)), listen('download-progress', e => cb(e.payload as DownloadProgress)),
onInstallStarted: (cb: () => void) => listen('update-install-started', () => cb()), onInstallStarted: (cb: () => void) => listen('update-install-started', () => cb()),
onInstallError: (cb: (err: string) => void) => listen('update-install-error', e => cb(e.payload as string)), onInstallError: (cb: (err: string) => void) => listen('update-install-error', e => cb(e.payload as string)),
onAppRestarting: (cb: () => void) => listen('app-restarting', () => cb()) onAppQuit: (cb: () => void) => listen('quit-app', () => cb())
} }
} }

View File

@@ -1,40 +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 {
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url)
} 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">
@@ -73,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>
@@ -83,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;
@@ -189,7 +199,6 @@ const openLink = async (url: string) => {
text-decoration: none; text-decoration: none;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
transition: all 0.2s ease-in-out;
} }
:root.dark .nav-item, :root.dark .nav-item,
@@ -237,7 +246,6 @@ const openLink = async (url: string) => {
background: rgb(220 38 38); background: rgb(220 38 38);
border-radius: 50%; border-radius: 50%;
border: 2px solid var(--color-background-secondary); border: 2px solid var(--color-background-secondary);
animation: pulse 2s infinite;
} }
.nav-item.has-notification .nav-icon-container { .nav-item.has-notification .nav-icon-container {
@@ -272,7 +280,6 @@ const openLink = async (url: string) => {
color: rgb(75 85 99); color: rgb(75 85 99);
text-decoration: none; text-decoration: none;
border-radius: 0.5rem; border-radius: 0.5rem;
transition: all 0.2s ease-in-out;
} }
:root.dark .github-link, :root.dark .github-link,
@@ -283,7 +290,6 @@ const openLink = async (url: string) => {
.github-link:hover { .github-link:hover {
background: rgb(243 244 246); background: rgb(243 244 246);
color: rgb(17 24 39); color: rgb(17 24 39);
transform: scale(1.1);
} }
:root.dark .github-link:hover, :root.dark .github-link:hover,
@@ -291,14 +297,4 @@ const openLink = async (url: string) => {
background: rgb(55 65 81); background: rgb(55 65 81);
color: rgb(243 244 246); color: rgb(243 244 246);
} }
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style> </style>

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>()
@@ -223,12 +224,13 @@ const updateChartSize = () => {
onMounted(async () => { onMounted(async () => {
await nextTick() await nextTick()
updateChartSize() updateChartSize()
await appStore.refreshOpenListCoreStatus()
if (isCoreRunning.value) { if (isCoreRunning.value) {
startTime.value = Date.now() startTime.value = Date.now()
} }
monitoringInterval.value = window.setInterval(checkCoreHealth, (appStore.settings.app.monitor_interval || 5) * 1000) monitoringInterval.value = window.setInterval(checkCoreHealth, 15 * 1000)
window.addEventListener('resize', updateChartSize) window.addEventListener('resize', updateChartSize)
}) })
@@ -256,9 +258,10 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
padding: 0.5rem 0.875rem; padding: 0.5rem 0.875rem;
border-radius: 20px; border-radius: 20px;
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(226, 232, 240, 0.6); border: 1px solid rgba(226, 232, 240, 0.6);
transition: all 0.2s ease; transition:
background-color 0.15s ease,
border-color 0.15s ease;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -287,20 +290,6 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
background: currentColor; background: currentColor;
} }
.status-indicator.online .pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.heartbeat-section { .heartbeat-section {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@@ -348,9 +337,7 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
padding: 0.5rem 0.875rem; padding: 0.5rem 0.875rem;
border-radius: 20px; border-radius: 20px;
font-weight: 600; font-weight: 600;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
} }
.metric:hover { .metric:hover {
@@ -422,7 +409,6 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(226, 232, 240, 0.6); border: 1px solid rgba(226, 232, 240, 0.6);
backdrop-filter: blur(10px);
} }
@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,13 +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 {
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url) if (appStore.settings.app.open_links_in_browser || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url)
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>
@@ -128,12 +139,11 @@ const openLink = async (url: string) => {
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem; padding: 1rem;
background: rgb(249 250 251); background: rgb(249 250 251);
transition: all 0.2s;
} }
.doc-section:hover { .doc-section:hover {
border-color: rgb(209 213 219); border-color: rgb(209 213 219);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); background: rgb(243 244 246);
} }
:root.dark .doc-section, :root.dark .doc-section,
@@ -145,7 +155,7 @@ const openLink = async (url: string) => {
:root.dark .doc-section:hover, :root.dark .doc-section:hover,
:root.auto.dark .doc-section:hover { :root.auto.dark .doc-section:hover {
border-color: rgb(75 85 99); border-color: rgb(75 85 99);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); background: rgb(55 65 81);
} }
.doc-header { .doc-header {
@@ -220,7 +230,6 @@ const openLink = async (url: string) => {
border: none; border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s;
text-decoration: none; text-decoration: none;
flex: 1; flex: 1;
justify-content: center; justify-content: center;
@@ -298,7 +307,6 @@ const openLink = async (url: string) => {
border: 1px solid rgb(209 213 219); border: 1px solid rgb(209 213 219);
border-radius: 0.5rem; border-radius: 0.5rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s;
text-align: center; text-align: center;
} }

View File

@@ -4,66 +4,93 @@
<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 <button
:disabled="isCoreLoading"
:class="['action-btn', 'service-btn', { running: isCoreRunning, loading: isCoreLoading }]"
@click="toggleCore" @click="toggleCore"
:class="['action-btn', 'service-btn', { running: isCoreRunning }]"
:disabled="loading"
> >
<component :is="serviceButtonIcon" :size="20" /> <component :is="serviceButtonIcon" v-if="!isCoreLoading" :size="20" />
<span>{{ serviceButtonText }}</span> <Loader v-else :size="20" class="loading-icon" />
<span>{{ isCoreLoading ? t('dashboard.quickActions.processing') : serviceButtonText }}</span>
</button> </button>
<button @click="restartCore" :disabled="!isCoreRunning || loading" 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="showAdminPassword"
class="action-btn password-btn icon-only-btn" class="action-btn password-btn icon-only-btn"
:title="t('dashboard.quickActions.showAdminPassword')" :title="t('dashboard.quickActions.copyAdminPassword')"
@click="copyAdminPassword"
> >
<Key :size="16" /> <Key :size="16" />
</button> </button>
<button
class="action-btn reset-password-btn icon-only-btn"
:title="t('dashboard.quickActions.resetAdminPassword')"
@click="resetAdminPassword"
>
<RotateCcw :size="16" />
</button>
</div> </div>
</div> </div>
<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()"
:disabled="loading || rcloneStore.loading"
: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>
@@ -77,9 +104,31 @@
</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-->
<button
v-if="isWindows"
:class="['firewall-toggle-btn', { active: firewallEnabled }]"
:disabled="firewallLoading"
:title="
firewallEnabled
? t('dashboard.quickActions.firewall.disable')
: t('dashboard.quickActions.firewall.enable')
"
@click="toggleFirewallRule"
>
<Shield :size="18" />
<span>
{{
firewallEnabled
? t('dashboard.quickActions.firewall.disable')
: t('dashboard.quickActions.firewall.enable')
}}
</span>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -87,14 +136,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue' import { ExternalLink, HardDrive, Key, Loader, Play, RotateCcw, Settings, Shield, Square } from 'lucide-vue-next'
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, Loader, Key } from 'lucide-vue-next'
import { TauriAPI } from '@/api/tauri'
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
@@ -102,17 +153,22 @@ const appStore = useAppStore()
const rcloneStore = useRcloneStore() const rcloneStore = useRcloneStore()
const isCoreRunning = computed(() => appStore.isCoreRunning) const isCoreRunning = computed(() => appStore.isCoreRunning)
const loading = computed(() => appStore.loading) 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
const firewallEnabled = ref(false)
const firewallLoading = ref(false)
const isWindows = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
})
const serviceButtonIcon = computed(() => { const serviceButtonIcon = computed(() => {
if (loading.value) return Loader
return isCoreRunning.value ? Square : Play return isCoreRunning.value ? Square : Play
}) })
const serviceButtonText = computed(() => { const serviceButtonText = computed(() => {
if (loading.value) return t('dashboard.quickActions.processing')
return isCoreRunning.value return isCoreRunning.value
? t('dashboard.quickActions.stopOpenListCore') ? t('dashboard.quickActions.stopOpenListCore')
: t('dashboard.quickActions.startOpenListCore') : t('dashboard.quickActions.startOpenListCore')
@@ -144,7 +200,7 @@ const viewMounts = () => {
router.push({ name: 'Mount' }) router.push({ name: 'Mount' })
} }
const showAdminPassword = async () => { const copyAdminPassword = async () => {
try { try {
const password = await appStore.getAdminPassword() const password = await appStore.getAdminPassword()
if (password) { if (password) {
@@ -217,38 +273,54 @@ const showAdminPassword = async () => {
} }
} catch (error) { } catch (error) {
console.error('Failed to get admin password:', error) console.error('Failed to get admin password:', error)
showNotification('error', 'Failed to get admin password. Please check the logs.')
}
}
const notification = document.createElement('div') const resetAdminPassword = async () => {
notification.innerHTML = ` try {
<div style=" const newPassword = await appStore.resetAdminPassword()
position: fixed; if (newPassword) {
top: 20px; await navigator.clipboard.writeText(newPassword)
right: 20px;
background: linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38)); const notification = document.createElement('div')
color: white; notification.innerHTML = `
padding: 12px 20px; <div style="
border-radius: 8px; position: fixed;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); top: 20px;
z-index: 10000; right: 20px;
font-weight: 500; background: linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105));
max-width: 300px; color: white;
"> padding: 12px 20px;
<div style="display: flex; align-items: center; gap: 8px;"> border-radius: 8px;
<div style="font-size: 18px;">✗</div> box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
<div> z-index: 10000;
<div style="font-size: 14px; margin-bottom: 4px;">Failed to get admin password</div> font-weight: 500;
<div style="font-size: 12px; opacity: 0.9;">Please check the logs.</div> max-width: 300px;
word-break: break-all;
">
<div style="display: flex; align-items: center; gap: 8px;">
<div style="font-size: 18px;">✓</div>
<div>
<div style="font-size: 14px; margin-bottom: 4px;">Admin password reset and copied!</div>
<div style="font-size: 12px; opacity: 0.9; font-family: monospace;">${newPassword}</div>
</div>
</div> </div>
</div> </div>
</div> `
` document.body.appendChild(notification)
document.body.appendChild(notification)
setTimeout(() => { setTimeout(() => {
if (notification.parentNode) { if (notification.parentNode) {
notification.parentNode.removeChild(notification) notification.parentNode.removeChild(notification)
} }
}, 4000) }, 4000)
} else {
showNotification('error', 'Failed to reset admin password. Please check the logs.')
}
} catch (error) {
console.error('Failed to reset admin password:', error)
showNotification('error', 'Failed to reset admin password. Please check the logs.')
} }
} }
@@ -282,21 +354,103 @@ const stopBackend = async () => {
} }
} }
const checkFirewallStatus = async () => {
if (!isWindows.value) return
try {
firewallEnabled.value = await TauriAPI.firewall.check()
} catch (error) {
console.error('Failed to check firewall status:', error)
}
}
const toggleFirewallRule = async () => {
if (!isWindows.value) return
try {
firewallLoading.value = true
if (firewallEnabled.value) {
await TauriAPI.firewall.remove()
firewallEnabled.value = false
showNotification('success', t('dashboard.quickActions.firewall.removed'))
} else {
await TauriAPI.firewall.add()
firewallEnabled.value = true
showNotification('success', t('dashboard.quickActions.firewall.added'))
}
} catch (error: any) {
console.error('Failed to toggle firewall rule:', error)
const message = firewallEnabled.value
? t('dashboard.quickActions.firewall.failedToRemove')
: t('dashboard.quickActions.firewall.failedToAdd')
showNotification('error', message + ': ' + (error.message || error))
} finally {
firewallLoading.value = 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: 300px;
word-break: break-word;
">
<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>
`
document.body.appendChild(notification)
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification)
}
}, 4000)
}
const isMacOs = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'darwin'
})
const openLink = async (url: string) => { const openLink = async (url: string) => {
try { try {
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url) if (appStore.settings.app.open_links_in_browser || isMacOs.value) {
await TauriAPI.files.urlInBrowser(url)
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 () => {
await rcloneStore.checkRcloneBackendStatus() await rcloneStore.checkRcloneBackendStatus()
statusCheckInterval = window.setInterval( statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 15 * 1000)
rcloneStore.checkRcloneBackendStatus,
(appStore.settings.app.monitor_interval || 5) * 1000 await checkFirewallStatus()
)
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -332,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;
@@ -360,13 +524,10 @@ onUnmounted(() => {
border: 1px solid var(--color-border-secondary); border: 1px solid var(--color-border-secondary);
border-radius: 10px; border-radius: 10px;
background: var(--color-surface); background: var(--color-surface);
backdrop-filter: blur(10px);
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-sm);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
text-align: center; text-align: center;
@@ -380,122 +541,126 @@ onUnmounted(() => {
} }
.action-btn:hover:not(:disabled) { .action-btn:hover:not(:disabled) {
transform: translateY(-1px);
background: var(--color-surface-elevated); background: var(--color-surface-elevated);
border-color: rgba(59, 130, 246, 0.3); border-color: rgba(59, 130, 246, 0.3);
box-shadow: var(--shadow-md);
} }
.action-btn:active { .action-btn:active {
transform: translateY(0); opacity: 0.8;
} }
.action-btn:disabled { .action-btn:disabled {
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
transform: none; }
.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: linear-gradient(135deg, rgb(239, 68, 68) 0%, rgb(220, 38, 38) 100%); background: rgb(239, 68, 68);
color: white; color: white;
border-color: rgba(220, 38, 38, 0.3); border-color: rgba(220, 38, 38, 0.3);
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.service-btn.running:hover:not(:disabled) { .service-btn.running:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(220, 38, 38) 0%, rgb(185, 28, 28) 100%); background: rgb(220, 38, 38);
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.service-btn:not(.running) { .service-btn:not(.running) {
background: linear-gradient(135deg, rgb(16, 185, 129) 0%, rgb(5, 150, 105) 100%); background: rgb(16, 185, 129);
color: white; color: white;
border-color: rgba(5, 150, 105, 0.3); border-color: rgba(5, 150, 105, 0.3);
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.service-btn:not(.running):hover:not(:disabled) { .service-btn:not(.running):hover:not(:disabled) {
background: linear-gradient(135deg, rgb(5, 150, 105) 0%, rgb(4, 120, 87) 100%); background: rgb(5, 150, 105);
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.restart-btn:hover:not(:disabled) { .restart-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(251, 191, 36) 0%, rgb(245, 158, 11) 100%); background: rgb(251, 191, 36);
color: white; color: white;
border-color: rgba(245, 158, 11, 0.3); border-color: rgba(245, 158, 11, 0.3);
} }
.web-btn:hover:not(:disabled) { .web-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(59, 130, 246) 0%, rgb(37, 99, 235) 100%); background: rgb(59, 130, 246);
color: white; color: white;
border-color: rgba(37, 99, 235, 0.3); border-color: rgba(37, 99, 235, 0.3);
} }
.config-btn:hover:not(:disabled) { .config-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(139, 92, 246) 0%, rgb(124, 58, 237) 100%); background: rgb(139, 92, 246);
color: white; color: white;
border-color: rgba(124, 58, 237, 0.3); border-color: rgba(124, 58, 237, 0.3);
} }
.test-btn:hover:not(:disabled) { .test-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(6, 182, 212) 0%, rgb(8, 145, 178) 100%); background: rgb(6, 182, 212);
color: white; color: white;
border-color: rgba(8, 145, 178, 0.3); border-color: rgba(8, 145, 178, 0.3);
} }
.mount-btn:hover:not(:disabled) { .mount-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(249, 115, 22) 0%, rgb(234, 88, 12) 100%); background: rgb(249, 115, 22);
color: white; color: white;
border-color: rgba(234, 88, 12, 0.3); border-color: rgba(234, 88, 12, 0.3);
} }
.password-btn:hover:not(:disabled) { .password-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(168, 85, 247) 0%, rgb(147, 51, 234) 100%); background: rgb(168, 85, 247);
color: white; color: white;
border-color: rgba(147, 51, 234, 0.3); border-color: rgba(147, 51, 234, 0.3);
} }
.reset-password-btn:hover:not(:disabled) {
background: rgb(239, 68, 68);
color: white;
border-color: rgba(220, 38, 38, 0.3);
}
.service-indicator-btn { .service-indicator-btn {
background: var(--color-surface); background: var(--color-surface);
border-color: var(--color-border-secondary); border-color: var(--color-border-secondary);
} }
.service-indicator-btn.active { .service-indicator-btn.active {
background: linear-gradient(135deg, rgb(239, 68, 68) 0%, rgb(220, 38, 38) 100%); background: rgb(239, 68, 68);
color: white; color: white;
border-color: rgba(5, 150, 105, 0.3); border-color: rgba(220, 38, 38, 0.3);
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.service-indicator-btn.active:hover:not(:disabled) { .service-indicator-btn.active:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(220, 38, 38) 0%, rgb(185, 28, 28) 100%); background: rgb(220, 38, 38);
border-color: rgba(220, 38, 38, 0.3); border-color: rgba(220, 38, 38, 0.3);
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.service-indicator-btn:not(.active):not(:disabled) { .service-indicator-btn:not(.active):not(:disabled) {
background: linear-gradient(135deg, rgb(16, 185, 129) 0%, rgb(5, 150, 105) 100%); background: rgb(16, 185, 129);
color: white; color: white;
border-color: rgba(5, 150, 105, 0.3); border-color: rgba(5, 150, 105, 0.3);
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.service-indicator-btn:not(.active):hover:not(:disabled) { .service-indicator-btn:not(.active):hover:not(:disabled) {
background: linear-gradient(135deg, rgb(5, 150, 105) 0%, rgb(4, 120, 87) 100%); background: rgb(5, 150, 105);
color: white; color: white;
border-color: rgba(5, 150, 105, 0.3); border-color: rgba(5, 150, 105, 0.3);
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.settings-btn:hover:not(:disabled) { .settings-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(100, 116, 139) 0%, rgb(71, 85, 105) 100%); background: rgb(100, 116, 139);
color: white; color: white;
border-color: rgba(71, 85, 105, 0.3); border-color: rgba(71, 85, 105, 0.3);
} }
.custom-services-btn:hover:not(:disabled) { .custom-services-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgb(139, 92, 246) 0%, rgb(124, 58, 237) 100%); background: rgb(139, 92, 246);
color: white; color: white;
border-color: rgba(124, 58, 237, 0.3); border-color: rgba(124, 58, 237, 0.3);
} }
@@ -518,7 +683,6 @@ onUnmounted(() => {
cursor: pointer; cursor: pointer;
padding: 0.375rem; padding: 0.375rem;
border-radius: 8px; border-radius: 8px;
transition: background-color 0.2s ease;
flex: 1; flex: 1;
white-space: nowrap; white-space: nowrap;
} }
@@ -542,6 +706,41 @@ onUnmounted(() => {
user-select: none; user-select: none;
} }
.firewall-toggle-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border-secondary);
border-radius: 8px;
background: var(--color-surface);
color: var(--color-text-primary);
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
}
.firewall-toggle-btn:hover:not(:disabled) {
background: var(--color-surface-elevated);
border-color: rgba(59, 130, 246, 0.3);
}
.firewall-toggle-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.firewall-toggle-btn.active {
background: rgb(16, 185, 129);
color: white;
border-color: rgba(5, 150, 105, 0.3);
}
.firewall-toggle-btn.active:hover:not(:disabled) {
background: rgb(5, 150, 105);
}
@media (max-width: 768px) { @media (max-width: 768px) {
.actions-grid { .actions-grid {
gap: 1.5rem; gap: 1.5rem;

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,26 +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 { useAppStore } from '../../stores/app' import { TauriAPI } from '../../api/tauri'
import { useTranslation } from '../../composables/useI18n'
import Card from '../ui/CardPage.vue'
import ConfirmDialog from '../ui/ConfirmDialog.vue'
const appStore = useAppStore()
const rcloneStore = useRcloneStore() const rcloneStore = useRcloneStore()
const { t } = useTranslation() const { t } = useTranslation()
@@ -116,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) {
@@ -227,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'
@@ -267,7 +277,6 @@ const cancelUninstall = () => {
onMounted(async () => { onMounted(async () => {
await checkServiceStatus() await checkServiceStatus()
statusCheckInterval = window.setInterval(checkServiceStatus, (appStore.settings.app.monitor_interval || 5) * 1000)
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -438,7 +447,6 @@ onUnmounted(() => {
border: none; border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s;
flex: 1; flex: 1;
justify-content: center; justify-content: center;
min-width: 7rem; min-width: 7rem;
@@ -494,21 +502,6 @@ onUnmounted(() => {
cursor: not-allowed; cursor: not-allowed;
} }
/* Loading animation */
.action-btn [data-lucide='loader-2'],
.logs-refresh-btn [data-lucide='loader-2'] {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Responsive design */ /* Responsive design */
@media (max-width: 768px) { @media (max-width: 768px) {
.action-buttons { .action-buttons {

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,8 +13,8 @@
<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 :class="{ 'animate-spin': checking }" :size="16" /> <RefreshCw :size="16" />
{{ checking ? t('update.checking') : t('update.checkForUpdates') }} {{ checking ? t('update.checking') : t('update.checkForUpdates') }}
</button> </button>
</div> </div>
@@ -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
@@ -177,7 +180,7 @@ let backgroundUpdateUnlisten: (() => void) | null = null
let downloadProgressUnlisten: (() => void) | null = null let downloadProgressUnlisten: (() => void) | null = null
let installStartedUnlisten: (() => void) | null = null let installStartedUnlisten: (() => void) | null = null
let installErrorUnlisten: (() => void) | null = null let installErrorUnlisten: (() => void) | null = null
let appRestartingUnlisten: (() => void) | null = null let appQuitEventUnsubscriber: (() => void) | null = null
const checkForUpdates = async () => { const checkForUpdates = async () => {
if (checking.value || downloading.value || installing.value) return if (checking.value || downloading.value || installing.value) return
@@ -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
} }
@@ -358,13 +361,13 @@ onMounted(async () => {
} }
try { try {
appRestartingUnlisten = await TauriAPI.updater.onAppRestarting(() => { appQuitEventUnsubscriber = await TauriAPI.updater.onAppQuit(() => {
installationStatus.value = t('update.restartingApp') installationStatus.value = t('update.quitApp')
installationStatusType.value = 'success' installationStatusType.value = 'success'
}) })
} catch (err) { } catch (err) {
console.warn('App restarting listener not available:', err) console.warn('App restarting listener not available:', err)
appRestartingUnlisten = null appQuitEventUnsubscriber = null
} }
if (autoCheckEnabled.value) { if (autoCheckEnabled.value) {
await checkForUpdates() await checkForUpdates()
@@ -400,7 +403,7 @@ onUnmounted(() => {
} }
try { try {
appRestartingUnlisten?.() appQuitEventUnsubscriber?.()
} catch (err) { } catch (err) {
console.warn('Error unregistering app restarting listener:', err) console.warn('Error unregistering app restarting listener:', err)
} }
@@ -456,7 +459,6 @@ onUnmounted(() => {
border: none; border: none;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s;
} }
.check-update-btn:hover:not(:disabled) { .check-update-btn:hover:not(:disabled) {
@@ -468,19 +470,6 @@ onUnmounted(() => {
cursor: not-allowed; cursor: not-allowed;
} }
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.settings-row { .settings-row {
padding: 1rem; padding: 1rem;
background: var(--color-surface); background: var(--color-surface);
@@ -673,7 +662,6 @@ onUnmounted(() => {
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all 0.2s;
} }
.asset-item:hover { .asset-item:hover {
@@ -773,7 +761,6 @@ onUnmounted(() => {
.progress-fill { .progress-fill {
height: 100%; height: 100%;
background: var(--color-success); background: var(--color-success);
transition: width 0.3s ease;
} }
.progress-details { .progress-details {
@@ -799,7 +786,6 @@ onUnmounted(() => {
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
transition: background-color 0.2s;
} }
.install-btn:hover:not(:disabled) { .install-btn:hover:not(:disabled) {
@@ -902,7 +888,6 @@ onUnmounted(() => {
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 0.8rem; font-size: 0.8rem;
transition: background-color 0.2s;
} }
.show-update-btn:hover { .show-update-btn:hover {

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()
}) })
@@ -184,7 +272,6 @@ onMounted(() => {
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 0.875rem; padding: 0.875rem;
background: var(--color-background-tertiary, rgb(249 250 251)); background: var(--color-background-tertiary, rgb(249 250 251));
transition: all 0.2s ease;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
@@ -192,7 +279,7 @@ onMounted(() => {
.version-item:hover { .version-item:hover {
border-color: var(--color-border, rgb(209 213 219)); border-color: var(--color-border, rgb(209 213 219));
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); background: var(--color-background-secondary, rgb(243 244 246));
} }
:root.dark .version-item, :root.dark .version-item,
@@ -204,7 +291,7 @@ onMounted(() => {
:root.dark .version-item:hover, :root.dark .version-item:hover,
:root.auto.dark .version-item:hover { :root.auto.dark .version-item:hover {
border-color: var(--color-border, rgb(75 85 99)); border-color: var(--color-border, rgb(75 85 99));
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); background: var(--color-background-primary, rgb(55 65 81));
} }
.version-header { .version-header {
@@ -234,7 +321,6 @@ onMounted(() => {
border: 1px solid var(--color-border-secondary, rgb(209 213 219)); border: 1px solid var(--color-border-secondary, rgb(209 213 219));
border-radius: 0.5rem; border-radius: 0.5rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -308,7 +394,6 @@ onMounted(() => {
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-text-primary, rgb(17 24 39)); color: var(--color-text-primary, rgb(17 24 39));
width: 100%; width: 100%;
transition: border-color 0.2s ease;
} }
:root.dark .version-select, :root.dark .version-select,
@@ -337,7 +422,6 @@ onMounted(() => {
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
} }
@@ -365,18 +449,4 @@ onMounted(() => {
padding: 0.75rem; padding: 0.75rem;
} }
} }
.refresh-icon-btn [data-lucide='loader-2'],
.update-btn [data-lucide='loader-2'] {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style> </style>

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
@@ -37,29 +38,22 @@ withDefaults(defineProps<Props>(), {
<style scoped> <style scoped>
.card { .card {
background: var(--color-surface); background: var(--color-surface);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 16px; border-radius: 16px;
border: 1px solid var(--color-border-secondary); border: 1px solid var(--color-border-secondary);
box-shadow: var(--shadow-md);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.card--elevated { .card--elevated {
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-sm);
} }
.card--outlined { .card--outlined {
border: 2px solid var(--color-border); border: 2px solid var(--color-border);
box-shadow: var(--shadow-sm);
} }
.card--glass { .card--glass {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
@@ -73,16 +67,15 @@ withDefaults(defineProps<Props>(), {
/* Interactive states */ /* Interactive states */
.card--hover:hover, .card--hover:hover,
.card--interactive:hover { .card--interactive:hover {
transform: translateY(-4px); background: var(--color-surface-elevated);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04), border-color: rgba(59, 130, 246, 0.2);
inset 0 1px 0 rgba(255, 255, 255, 0.3);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.card--hover:hover, .card--hover:hover,
.card--interactive:hover { .card--interactive:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3), background: var(--color-surface-elevated);
inset 0 1px 0 rgba(255, 255, 255, 0.08); border-color: rgba(59, 130, 246, 0.2);
} }
} }
@@ -91,7 +84,7 @@ withDefaults(defineProps<Props>(), {
} }
.card--interactive:active { .card--interactive:active {
transform: translateY(-2px); opacity: 0.9;
} }
/* Card structure */ /* Card structure */

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>
@@ -74,13 +74,14 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
backdrop-filter: blur(4px);
} }
.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;
@@ -138,7 +139,6 @@ export default {
font-weight: 500; font-weight: 500;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.2s;
min-width: 4rem; min-width: 4rem;
} }

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,18 +36,18 @@ 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="{ 'rotate-180': isOpen }" class="transition-transform" /> <ChevronDown :size="12" :class="{ flipped: isOpen }" />
</button> </button>
<div v-if="isOpen" class="language-dropdown"> <div v-if="isOpen" class="language-dropdown">
<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>
@@ -72,7 +73,6 @@ onMounted(() => {
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 0.875rem; font-size: 0.875rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
min-width: 120px; min-width: 120px;
} }
@@ -86,6 +86,10 @@ onMounted(() => {
text-align: center; text-align: center;
} }
.flipped {
opacity: 0.7;
}
.language-dropdown { .language-dropdown {
position: absolute; position: absolute;
top: 100%; top: 100%;
@@ -107,7 +111,6 @@ onMounted(() => {
gap: 0.75rem; gap: 0.75rem;
padding: 0.75rem; padding: 0.75rem;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease;
font-size: 0.875rem; font-size: 0.875rem;
} }

View File

@@ -56,7 +56,6 @@ defineProps<Props>()
.status-indicator.active .status-dot { .status-indicator.active .status-dot {
background-color: rgb(34 197 94); background-color: rgb(34 197 94);
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
.status-text { .status-text {

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>
@@ -65,14 +66,12 @@ const toggleTheme = () => {
color: var(--color-text-secondary); color: var(--color-text-secondary);
border-radius: var(--radius-md); border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
font-size: 0.875rem; font-size: 0.875rem;
} }
.theme-toggle-btn:hover { .theme-toggle-btn:hover {
background: var(--color-surface-elevated); background: var(--color-surface-elevated);
color: var(--color-text-primary); color: var(--color-text-primary);
transform: translateY(-1px);
} }
.theme-label { .theme-label {

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

@@ -1,636 +0,0 @@
<script setup lang="ts">
import { computed, ref, onMounted, nextTick, watch } from 'vue'
import { useAppStore } from '../../stores/app'
import { useTranslation } from '../../composables/useI18n'
import { ChevronLeft, ChevronRight, X, Check, Play, FileText, Settings, HardDrive, Home } from 'lucide-vue-next'
const appStore = useAppStore()
const { t } = useTranslation()
const tutorialSteps = computed(() => [
{
title: t('tutorial.welcome.title'),
content: t('tutorial.welcome.content'),
target: '.app-title',
position: 'center',
showNext: true,
showSkip: true,
icon: Home
},
{
title: t('tutorial.navigation.title'),
content: t('tutorial.navigation.content'),
target: '.nav-menu',
position: 'right',
showNext: true,
showPrev: true,
showSkip: true,
icon: HardDrive
},
{
title: t('tutorial.service.title'),
content: t('tutorial.service.content'),
target: '.service-management-card',
position: 'top',
showNext: true,
showPrev: true,
showSkip: true,
icon: Play
},
{
title: t('tutorial.openlist.title'),
content: t('tutorial.openlist.content'),
target: '.quick-actions-card',
position: 'top',
showNext: true,
showPrev: true,
showSkip: true,
icon: HardDrive
},
{
title: t('tutorial.documentation.title'),
content: t('tutorial.documentation.content'),
target: '.documentation-card',
position: 'bottom',
showNext: true,
showPrev: true,
showSkip: true,
icon: FileText
},
{
title: t('tutorial.settings.title'),
content: t('tutorial.settings.content'),
target: '.nav-item[href="/settings"]',
position: 'right',
showPrev: true,
showComplete: true,
icon: Settings
}
])
const currentStep = computed(() => tutorialSteps.value[appStore.tutorialStep] || tutorialSteps.value[0])
const highlightStyle = ref({})
const updateHighlight = async () => {
await nextTick()
if (!currentStep.value.target) {
highlightStyle.value = {}
return
}
const targetElement = document.querySelector(currentStep.value.target) as HTMLElement
if (!targetElement) {
highlightStyle.value = {}
return
}
const rect = targetElement.getBoundingClientRect()
const padding = 8
highlightStyle.value = {
top: `${rect.top - padding}px`,
left: `${rect.left - padding}px`,
width: `${rect.width + padding * 2}px`,
height: `${rect.height + padding * 2}px`
}
}
const getTooltipStyle = () => {
if (!currentStep.value.target)
return {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
const targetElement = document.querySelector(currentStep.value.target) as HTMLElement
if (!targetElement)
return {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
const rect = targetElement.getBoundingClientRect()
const position = currentStep.value.position || 'bottom'
const offset = 16
const tooltipWidth = 320
const tooltipHeight = 200
let style: any = {}
let adjustedPosition = position
if (position === 'left' && rect.left < tooltipWidth + offset) {
adjustedPosition = 'right'
} else if (position === 'right' && rect.right + tooltipWidth + offset > window.innerWidth) {
adjustedPosition = 'left'
} else if (position === 'top' && rect.top < tooltipHeight + offset) {
adjustedPosition = 'bottom'
} else if (position === 'bottom' && rect.bottom + tooltipHeight + offset > window.innerHeight) {
adjustedPosition = 'top'
}
switch (adjustedPosition) {
case 'center':
style = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
break
case 'top':
style = {
bottom: `${window.innerHeight - rect.top + offset}px`,
left: `${rect.left + rect.width / 2}px`,
transform: 'translateX(-50%)'
}
break
case 'bottom':
style = {
top: `${rect.bottom + offset}px`,
left: `${rect.left + rect.width / 2}px`,
transform: 'translateX(-50%)'
}
break
case 'left':
style = {
top: `${rect.top + rect.height / 2}px`,
right: `${window.innerWidth - rect.left + offset}px`,
transform: 'translateY(-50%)'
}
break
case 'right':
style = {
top: `${rect.top + rect.height / 2}px`,
left: `${rect.right + offset}px`,
transform: 'translateY(-50%)'
}
break
case 'bottom-right':
style = {
top: `${rect.bottom + offset}px`,
left: `${Math.max(16, rect.right - tooltipWidth)}px`
}
break
default:
style = {
top: `${rect.bottom + offset}px`,
left: `${rect.left + rect.width / 2}px`,
transform: 'translateX(-50%)'
}
}
if (style.left && !style.transform?.includes('translateX')) {
const leftPos = parseInt(style.left)
if (leftPos + tooltipWidth > window.innerWidth) {
style.left = `${window.innerWidth - tooltipWidth - 16}px`
}
if (leftPos < 16) {
style.left = '16px'
}
}
if (style.top && !style.transform?.includes('translateY')) {
const topPos = parseInt(style.top)
if (topPos + tooltipHeight > window.innerHeight) {
style.top = `${window.innerHeight - tooltipHeight - 16}px`
}
if (topPos < 16) {
style.top = '16px'
}
}
if (style.bottom) {
const bottomPos = parseInt(style.bottom)
if (window.innerHeight - bottomPos - tooltipHeight < 16) {
delete style.bottom
style.top = '16px'
}
}
if (style.right) {
const rightPos = parseInt(style.right)
if (window.innerWidth - rightPos - tooltipWidth < 16) {
delete style.right
style.left = '16px'
delete style.transform
}
}
if (style.transform?.includes('translate(-50%, -50%)')) {
return style
}
if (style.transform?.includes('translateX(-50%)') && style.left) {
const leftPos = parseInt(style.left)
const halfWidth = tooltipWidth / 2
if (leftPos - halfWidth < 16) {
style.left = `${halfWidth + 16}px`
}
if (leftPos + halfWidth > window.innerWidth - 16) {
style.left = `${window.innerWidth - halfWidth - 16}px`
}
}
if (style.transform?.includes('translateY(-50%)') && style.top) {
const topPos = parseInt(style.top)
const halfHeight = tooltipHeight / 2
if (topPos - halfHeight < 16) {
style.top = `${halfHeight + 16}px`
}
if (topPos + halfHeight > window.innerHeight - 16) {
style.top = `${window.innerHeight - halfHeight - 16}px`
}
}
return style
}
const handleNext = () => {
if (appStore.tutorialStep < tutorialSteps.value.length - 1) {
appStore.nextTutorialStep()
updateHighlight()
}
}
const handlePrev = () => {
if (appStore.tutorialStep > 0) {
appStore.prevTutorialStep()
updateHighlight()
}
}
const handleSkip = () => {
appStore.skipTutorial()
}
const handleComplete = () => {
appStore.completeTutorial()
}
const handleClose = () => {
appStore.closeTutorial()
}
onMounted(() => {
updateHighlight()
watch(
() => appStore.tutorialStep,
() => {
setTimeout(() => {
updateHighlight()
}, 100)
}
)
const handleResize = () => {
updateHighlight()
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
})
</script>
<template>
<Teleport to="body">
<div v-if="appStore.showTutorial" class="tutorial-overlay">
<div class="tutorial-backdrop" @click="handleClose" />
<div
v-if="currentStep.target && currentStep.position !== 'center'"
class="tutorial-highlight"
:style="highlightStyle"
/>
<div class="tutorial-tooltip" :style="getTooltipStyle()">
<div class="tooltip-header">
<div class="tooltip-icon">
<component :is="currentStep.icon" :size="20" />
</div>
<h3 class="tooltip-title">{{ currentStep.title }}</h3>
<button class="tooltip-close" @click="handleClose" :title="t('common.close')">
<X :size="18" />
</button>
</div>
<div class="tooltip-content">
<p>{{ currentStep.content }}</p>
</div>
<div class="tooltip-footer">
<div class="step-indicator">
<span class="step-current">{{ appStore.tutorialStep + 1 }}</span>
<span class="step-divider">/</span>
<span class="step-total">{{ tutorialSteps.length }}</span>
</div>
<div class="tutorial-actions">
<button v-if="currentStep.showSkip" class="btn-skip" @click="handleSkip">
{{ t('tutorial.skip') }}
</button>
<button v-if="currentStep.showPrev" class="btn-prev" @click="handlePrev">
<ChevronLeft :size="16" />
{{ t('tutorial.previous') }}
</button>
<button v-if="currentStep.showNext" class="btn-next" @click="handleNext">
{{ t('tutorial.next') }}
<ChevronRight :size="16" />
</button>
<button v-if="currentStep.showComplete" class="btn-complete" @click="handleComplete">
<Check :size="16" />
{{ t('tutorial.complete') }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.tutorial-overlay {
position: fixed;
inset: 0;
z-index: 10000;
pointer-events: none;
}
.tutorial-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
pointer-events: all;
}
.tutorial-highlight {
position: absolute;
border: 2px solid var(--color-accent);
border-radius: var(--radius-md);
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.2), var(--shadow-lg);
background: rgba(255, 255, 255, 0.05);
pointer-events: none;
transition: all var(--transition-medium);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.2), var(--shadow-lg);
}
50% {
box-shadow: 0 0 0 8px rgba(0, 122, 255, 0.1), var(--shadow-xl);
}
}
.tutorial-tooltip {
position: absolute;
width: 320px;
max-width: calc(100vw - 32px);
background: var(--color-surface-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
backdrop-filter: blur(20px);
pointer-events: all;
animation: tooltipEnter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 10001;
}
@keyframes tooltipEnter {
0% {
opacity: 0;
transform: translateY(16px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.tooltip-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px 12px 20px;
border-bottom: 1px solid var(--color-border-secondary);
}
.tooltip-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: var(--color-accent);
color: white;
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.tooltip-title {
flex: 1;
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.tooltip-close {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
border: none;
color: var(--color-text-tertiary);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-fast);
}
.tooltip-close:hover {
background: var(--color-background-secondary);
color: var(--color-text-primary);
}
.tooltip-content {
padding: 16px 20px;
}
.tooltip-content p {
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-text-secondary);
}
.tooltip-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px 16px 20px;
border-top: 1px solid var(--color-border-secondary);
}
.step-indicator {
display: flex;
align-items: center;
font-size: 0.75rem;
color: var(--color-text-tertiary);
}
.step-current {
font-weight: 600;
color: var(--color-accent);
}
.step-divider {
margin: 0 4px;
}
.tutorial-actions {
display: flex;
align-items: center;
gap: 8px;
}
.btn-skip {
padding: 6px 12px;
font-size: 0.75rem;
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-tertiary);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-skip:hover {
background: var(--color-background-secondary);
color: var(--color-text-secondary);
}
.btn-prev,
.btn-next,
.btn-complete {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
font-size: 0.75rem;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-fast);
font-weight: 500;
}
.btn-prev {
background: var(--color-background-secondary);
color: var(--color-text-primary);
}
.btn-prev:hover {
background: var(--color-background-tertiary);
}
.btn-next,
.btn-complete {
background: var(--color-accent);
color: white;
}
.btn-next:hover,
.btn-complete:hover {
background: var(--color-accent-hover);
}
.btn-complete {
background: var(--color-success);
}
.btn-complete:hover {
background: #2fb344;
}
:root.dark .tutorial-backdrop {
background: rgba(0, 0, 0, 0.8);
}
:root.dark .tutorial-highlight {
border-color: var(--color-accent);
box-shadow: 0 0 0 4px rgba(10, 132, 255, 0.3), var(--shadow-lg);
}
:root.dark .tutorial-tooltip {
background: var(--color-surface-elevated);
border-color: var(--color-border);
}
@media (max-width: 768px) {
.tutorial-tooltip {
width: 280px;
max-width: calc(100vw - 24px);
position: fixed !important;
top: auto !important;
bottom: 20px !important;
left: 50% !important;
right: auto !important;
transform: translateX(-50%) !important;
}
.tooltip-header {
padding: 12px 16px 8px 16px;
}
.tooltip-content {
padding: 12px 16px;
}
.tooltip-footer {
padding: 8px 16px 12px 16px;
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.tutorial-actions {
justify-content: space-between;
width: 100%;
}
.step-indicator {
align-self: center;
}
}
@media (max-width: 480px) {
.tutorial-tooltip {
width: calc(100vw - 32px);
}
.tutorial-actions {
flex-direction: column;
gap: 8px;
}
.btn-prev,
.btn-next,
.btn-complete,
.btn-skip {
width: 100%;
justify-content: center;
}
}
</style>

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
} }
@@ -133,7 +133,6 @@ const getIcon = () => {
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 0.85rem; font-size: 0.85rem;
transition: background-color 0.2s;
} }
.action-btn:hover { .action-btn:hover {
@@ -147,7 +146,6 @@ const getIcon = () => {
padding: 0.25rem; padding: 0.25rem;
border-radius: 4px; border-radius: 4px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
transition: color 0.2s, background-color 0.2s;
} }
.dismiss-btn:hover { .dismiss-btn:hover {
@@ -155,19 +153,11 @@ const getIcon = () => {
background: var(--color-surface); background: var(--color-surface);
} }
/* Transitions */
.notification-enter-active,
.notification-leave-active {
transition: all 0.3s ease;
}
.notification-enter-from { .notification-enter-from {
opacity: 0; opacity: 0;
transform: translateX(100%) scale(0.95);
} }
.notification-leave-to { .notification-leave-to {
opacity: 0; opacity: 0;
transform: translateX(100%) scale(0.95);
} }
</style> </style>

View File

@@ -1,30 +1,20 @@
<template> <template>
<div class="window-controls"> <button <div class="window-controls">
class="control-btn minimize" <button class="control-btn minimize" :title="t('common.minimize')" @click="$emit('minimize')">
@click="$emit('minimize')"
:title="t('common.minimize')"
>
<Minimize2 :size="12" /> <Minimize2 :size="12" />
</button> </button>
<button <button class="control-btn maximize" :title="t('common.maximize')" @click="$emit('maximize')">
class="control-btn maximize"
@click="$emit('maximize')"
:title="t('common.maximize')"
>
<Maximize2 :size="12" /> <Maximize2 :size="12" />
</button> </button>
<button <button class="control-btn close" :title="t('common.close')" @click="$emit('close')">
class="control-btn close"
@click="$emit('close')"
:title="t('common.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()
@@ -53,7 +43,6 @@ defineEmits<{
color: rgb(107 114 128); color: rgb(107 114 128);
border-radius: 0.25rem; border-radius: 0.25rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease-in-out;
} }
.control-btn:hover { .control-btn:hover {
@@ -65,7 +54,7 @@ defineEmits<{
.control-btn { .control-btn {
color: rgb(156 163 175); color: rgb(156 163 175);
} }
.control-btn:hover { .control-btn:hover {
background: rgb(55 65 81); background: rgb(55 65 81);
color: rgb(209 213 219); color: rgb(209 213 219);

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,17 +24,26 @@
"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",
"manageMounts": "Manage Mounts", "manageMounts": "Manage Mounts",
"autoLaunch": "Auto Launch Core(not app)", "autoLaunch": "Auto Launch Core(not app)",
"processing": "Processing...", "copyAdminPassword": "Copy admin password",
"showAdminPassword": "Show/Copy admin password from logs" "resetAdminPassword": "Reset admin password",
"firewall": {
"enable": "Allow Port",
"disable": "Remove Port Allow",
"added": "Port allowed successfully",
"removed": "Port removed successfully",
"failedToAdd": "Failed to allow port",
"failedToRemove": "Failed to remove port allow"
}
}, },
"coreMonitor": { "coreMonitor": {
"title": "Core Monitor", "title": "Core Monitor",
@@ -48,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",
@@ -90,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!",
@@ -126,10 +142,15 @@
"placeholder": "5244", "placeholder": "5244",
"help": "Port number for the web interface (1-65535)" "help": "Port number for the web interface (1-65535)"
}, },
"apiToken": { "dataDir": {
"label": "API Token", "label": "Data Directory",
"placeholder": "Optional. Secure API access with authentication", "placeholder": "Optional. Custom data directory path",
"help": "Optional. Secure API access with authentication" "help": "Optional. Specify a custom directory for OpenList data storage",
"selectTitle": "Select Data Directory",
"selectError": "Failed to select directory. Please try again or enter path manually.",
"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",
@@ -139,18 +160,42 @@
"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": {
"title": "Admin Password",
"subtitle": "Manage the admin password for OpenList Core web interface",
"currentPassword": "Admin Password",
"passwordPlaceholder": "Enter admin password or click reset to generate",
"resetTitle": "Reset admin password to a new random value",
"resetSuccess": "Admin password reset successfully! New password has been generated and saved.",
"resetFailed": "Failed to reset admin password. Please check the logs for more details.",
"passwordUpdated": "Admin password updated successfully!",
"passwordUpdateFailed": "Failed to update admin password. Please check the logs for more details.",
"help": "Enter a custom admin password or click the reset button to generate a new random password. Click 'Save Changes' to apply the password to OpenList Core."
} }
}, },
"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": "Enter 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": {
@@ -163,14 +208,12 @@
"auto": "Auto", "auto": "Auto",
"autoDesc": "Follow system" "autoDesc": "Follow system"
}, },
"monitor": { "config": {
"title": "Monitoring", "title": "Configuration Files",
"subtitle": "System monitoring and refresh settings", "subtitle": "Access application configuration files",
"interval": { "openFile": "Open settings.json",
"label": "Monitor Interval (seconds)", "openSuccess": "Settings file opened successfully",
"placeholder": "5", "openError": "Failed to open settings file"
"help": "How often to refresh system metrics and status"
}
}, },
"ghProxy": { "ghProxy": {
"title": "GitHub Proxy", "title": "GitHub Proxy",
@@ -191,12 +234,6 @@
"description": "Use system default browser instead of built-in window" "description": "Use system default browser instead of built-in window"
} }
}, },
"tutorial": {
"title": "Tutorial",
"subtitle": "Learn how to use OpenList Desktop",
"restart": "Start Tutorial",
"help": "Restart the tutorial to learn about app features and navigation"
},
"updates": { "updates": {
"title": "Updates", "title": "Updates",
"subtitle": "Manage automatic updates and notifications", "subtitle": "Manage automatic updates and notifications",
@@ -209,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"
} }
} }
}, },
@@ -221,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)",
@@ -232,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)"
@@ -256,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)",
@@ -276,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",
@@ -330,7 +376,7 @@
"authentication": "Authentication", "authentication": "Authentication",
"mountSettings": "Mount Settings", "mountSettings": "Mount Settings",
"name": "Name", "name": "Name",
"namePlaceholder": "e.g., my-webdav-remote", "namePlaceholder": "e.g.mount1",
"type": "Type", "type": "Type",
"url": "URL", "url": "URL",
"urlPlaceholder": "e.g., http://localhost:5264/dav/189", "urlPlaceholder": "e.g., http://localhost:5264/dav/189",
@@ -341,7 +387,7 @@
"password": "Password", "password": "Password",
"passwordPlaceholder": "Password", "passwordPlaceholder": "Password",
"mountPoint": "Mount Path", "mountPoint": "Mount Path",
"mountPointPlaceholder": "e.g., T: (Windows) or /mnt/remote (Linux)", "mountPointPlaceholder": "e.g., T: or /mnt/remote",
"volumeName": "Remote Path", "volumeName": "Remote Path",
"volumeNamePlaceholder": "e.g., /", "volumeNamePlaceholder": "e.g., /",
"autoMount": "Auto-mount on startup", "autoMount": "Auto-mount on startup",
@@ -374,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",
@@ -429,36 +475,6 @@
"title": "OpenList", "title": "OpenList",
"loading": "Initializing OpenList Desktop..." "loading": "Initializing OpenList Desktop..."
}, },
"tutorial": {
"welcome": {
"title": "Welcome to OpenList Desktop",
"content": "Welcome to OpenList Desktop! This tutorial will guide you through the key features and help you get started quickly."
},
"navigation": {
"title": "Navigation Panel",
"content": "Use the navigation panel to access different sections: Dashboard for monitoring, Mount for storage management, Logs for troubleshooting, and Settings for configuration."
},
"service": {
"title": "Install & Start Service",
"content": "First, you need to install and start the OpenList service. This is the core component that manages your cloud storage connections."
},
"openlist": {
"title": "OpenList Core Access",
"content": "Once the service is running, you can access the OpenList web interface to manage your files and configurations."
},
"documentation": {
"title": "Read Documentation",
"content": "For detailed information and advanced configurations, check out the documentation section. You'll find guides, API docs, and troubleshooting tips."
},
"settings": {
"title": "Settings & Configuration",
"content": "Customize your OpenList experience in the Settings section. Configure themes, service options, and storage preferences."
},
"skip": "Skip Tutorial",
"next": "Next",
"previous": "Previous",
"complete": "Complete Tutorial"
},
"update": { "update": {
"title": "App Updates", "title": "App Updates",
"subtitle": "Keep your application up to date with the latest features and security improvements", "subtitle": "Keep your application up to date with the latest features and security improvements",
@@ -480,7 +496,7 @@
"startingDownload": "Starting download...", "startingDownload": "Starting download...",
"downloading": "Downloading", "downloading": "Downloading",
"installingUpdate": "Installing update...", "installingUpdate": "Installing update...",
"restartingApp": "Restarting application...", "quitApp": "Quitting application...",
"noUpdatesFound": "No updates available", "noUpdatesFound": "No updates available",
"aboutUpdates": "About Updates", "aboutUpdates": "About Updates",
"autoCheckInfo": "Automatic update checks keep your app secure and up-to-date", "autoCheckInfo": "Automatic update checks keep your app secure and up-to-date",

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,17 +24,26 @@
"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",
"manageMounts": "管理挂载", "manageMounts": "管理挂载",
"autoLaunch": "自动启动核心(非桌面app)", "autoLaunch": "自动启动核心(非桌面app)",
"processing": "处理中...", "copyAdminPassword": "复制管理员密码",
"showAdminPassword": "显示/复制日志中的管理员密码" "resetAdminPassword": "重置管理员密码",
"firewall": {
"enable": "放行端口",
"disable": "移除端口放行",
"added": "端口放行成功",
"removed": "端口移除成功",
"failedToAdd": "添加端口放行失败",
"failedToRemove": "移除端口放行失败"
}
}, },
"coreMonitor": { "coreMonitor": {
"title": "核心监控", "title": "核心监控",
@@ -48,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": "文档",
@@ -90,7 +103,10 @@
"subtitle": "配置您的 OpenList 桌面应用程序", "subtitle": "配置您的 OpenList 桌面应用程序",
"saveChanges": "保存更改", "saveChanges": "保存更改",
"resetToDefaults": "重置为默认值", "resetToDefaults": "重置为默认值",
"confirmReset": "您确定要将所有设置重置为默认值吗?此操作无法撤消。", "confirmReset": {
"title": "重置设置",
"message": "您确定要将所有设置重置为默认值吗?此操作无法撤消。"
},
"saved": "设置保存成功!", "saved": "设置保存成功!",
"saveFailed": "保存设置失败,请重试。", "saveFailed": "保存设置失败,请重试。",
"resetSuccess": "设置已重置为默认值!", "resetSuccess": "设置已重置为默认值!",
@@ -126,10 +142,15 @@
"placeholder": "5244", "placeholder": "5244",
"help": "Web 界面的端口号 (1-65535)" "help": "Web 界面的端口号 (1-65535)"
}, },
"apiToken": { "dataDir": {
"label": "API 令牌", "label": "数据目录",
"placeholder": "可选。用于 API 访问的身份验证", "placeholder": "可选。自定义数据目录路径",
"help": "可选。用于 API 访问的身份验证" "help": "可选。为 OpenList 数据存储指定自定义目录",
"selectTitle": "选择数据目录",
"selectError": "选择目录失败。请重试或手动输入路径。",
"openTitle": "打开数据目录",
"openSuccess": "数据目录打开成功",
"openError": "打开数据目录失败"
}, },
"ssl": { "ssl": {
"title": "启用 SSL/HTTPS", "title": "启用 SSL/HTTPS",
@@ -139,18 +160,42 @@
"startup": { "startup": {
"autoLaunch": { "autoLaunch": {
"title": "开机自启", "title": "开机自启",
"description": "应用程序启动时自动启动 OpenList 服务" "description": "开机自动启动 OpenList 核心"
} }
},
"admin": {
"title": "管理员密码",
"subtitle": "管理 OpenList 核心网页界面的管理员密码",
"currentPassword": "管理员密码",
"passwordPlaceholder": "输入管理员密码或点击重置生成",
"resetTitle": "重置管理员密码为新的随机值",
"resetSuccess": "管理员密码重置成功!已生成新密码并保存。",
"resetFailed": "重置管理员密码失败。请查看日志了解详细信息。",
"passwordUpdated": "管理员密码更新成功!",
"passwordUpdateFailed": "更新管理员密码失败。请查看日志了解详细信息。",
"help": "输入自定义管理员密码或点击重置按钮生成新的随机密码。点击'保存更改'将密码应用到 OpenList 核心。"
} }
}, },
"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": {
@@ -163,14 +208,12 @@
"auto": "自动", "auto": "自动",
"autoDesc": "跟随系统" "autoDesc": "跟随系统"
}, },
"monitor": { "config": {
"title": "监控", "title": "配置文件",
"subtitle": "系统监控和刷新设置", "subtitle": "访问应用程序配置文件",
"interval": { "openFile": "打开 settings.json",
"label": "监控间隔(秒)", "openSuccess": "设置文件打开成功",
"placeholder": "5", "openError": "打开设置文件失败"
"help": "刷新系统指标和状态的频率"
}
}, },
"ghProxy": { "ghProxy": {
"title": "GitHub 代理", "title": "GitHub 代理",
@@ -191,12 +234,6 @@
"description": "使用系统默认浏览器而不是内置窗口" "description": "使用系统默认浏览器而不是内置窗口"
} }
}, },
"tutorial": {
"title": "教程",
"subtitle": "学习如何使用 OpenList 桌面版",
"restart": "开始教程",
"help": "重新启动教程以了解应用功能和导航"
},
"updates": { "updates": {
"title": "更新", "title": "更新",
"subtitle": "管理自动更新和通知", "subtitle": "管理自动更新和通知",
@@ -209,6 +246,10 @@
"title": "开机自动启动应用(立即生效)", "title": "开机自动启动应用(立即生效)",
"subtitle": "在系统启动时自动启动 OpenList 桌面应用", "subtitle": "在系统启动时自动启动 OpenList 桌面应用",
"description": "在系统启动时自动启动 OpenList 桌面应用" "description": "在系统启动时自动启动 OpenList 桌面应用"
},
"showWindowOnStartup": {
"title": "启动时显示主窗口",
"description": "在 OpenList 桌面应用启动时显示主应用窗口"
} }
} }
}, },
@@ -221,7 +262,9 @@
"copyFailed": "复制日志到剪贴板失败", "copyFailed": "复制日志到剪贴板失败",
"exportSuccess": "成功导出 {count} 条日志到文件", "exportSuccess": "成功导出 {count} 条日志到文件",
"clearSuccess": "日志清理成功", "clearSuccess": "日志清理成功",
"clearFailed": "清理日志失败" "clearFailed": "清理日志失败",
"openDirectorySuccess": "日志目录打开成功",
"openDirectoryFailed": "打开日志目录失败"
}, },
"toolbar": { "toolbar": {
"pause": "暂停 (Space)", "pause": "暂停 (Space)",
@@ -232,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)"
@@ -256,7 +300,8 @@
"sources": { "sources": {
"all": "所有来源", "all": "所有来源",
"rclone": "Rclone", "rclone": "Rclone",
"openlist": "OpenList" "openlist": "OpenList",
"service": "服务"
}, },
"actions": { "actions": {
"selectAll": "全选 (Ctrl+A)", "selectAll": "全选 (Ctrl+A)",
@@ -276,7 +321,8 @@
"stripAnsiColors": "去除 ANSI 颜色" "stripAnsiColors": "去除 ANSI 颜色"
}, },
"messages": { "messages": {
"confirmClear": "您确定要清除所有日志吗?" "confirmClear": "您确定要清除所有日志吗?",
"confirmTitle": "清除日志"
}, },
"headers": { "headers": {
"timestamp": "时间", "timestamp": "时间",
@@ -330,7 +376,7 @@
"authentication": "身份认证", "authentication": "身份认证",
"mountSettings": "挂载设置", "mountSettings": "挂载设置",
"name": "名称", "name": "名称",
"namePlaceholder": "例如:我的webdav远程", "namePlaceholder": "例如:mount1",
"type": "类型", "type": "类型",
"url": "URL", "url": "URL",
"urlPlaceholder": "例如http://localhost:5264/dav/189", "urlPlaceholder": "例如http://localhost:5264/dav/189",
@@ -341,7 +387,7 @@
"password": "密码", "password": "密码",
"passwordPlaceholder": "密码", "passwordPlaceholder": "密码",
"mountPoint": "挂载点", "mountPoint": "挂载点",
"mountPointPlaceholder": "例如T: (Windows) 或 /mnt/remote (Linux)", "mountPointPlaceholder": "例如T: 或 /mnt/remote",
"volumeName": "远程路径", "volumeName": "远程路径",
"volumeNamePlaceholder": "例如:/", "volumeNamePlaceholder": "例如:/",
"autoMount": "开机自动挂载", "autoMount": "开机自动挂载",
@@ -374,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": "基于时间的带宽限制",
@@ -429,36 +475,6 @@
"title": "OpenList", "title": "OpenList",
"loading": "正在初始化" "loading": "正在初始化"
}, },
"tutorial": {
"welcome": {
"title": "欢迎使用 OpenList 桌面版",
"content": "欢迎使用 OpenList 桌面版!本教程将引导您了解主要功能,帮助您快速上手。"
},
"navigation": {
"title": "导航面板",
"content": "使用导航面板访问不同部分:仪表板用于监控,挂载用于存储管理,日志用于故障排除,设置用于配置。"
},
"service": {
"title": "安装并启动服务",
"content": "首先,您需要安装并启动 OpenList 服务。这是管理云存储连接的核心组件。"
},
"openlist": {
"title": "OpenList 核心访问",
"content": "服务运行后,您可以访问 OpenList 网页界面来管理文件和配置。"
},
"documentation": {
"title": "阅读文档",
"content": "有关详细信息和高级配置请查看文档部分。您会找到指南、API 文档和故障排除提示。"
},
"settings": {
"title": "设置和配置",
"content": "在设置部分自定义您的 OpenList 体验。配置主题、服务选项和存储首选项。"
},
"skip": "跳过教程",
"next": "下一步",
"previous": "上一步",
"complete": "完成教程"
},
"update": { "update": {
"title": "应用更新", "title": "应用更新",
"subtitle": "保持应用程序最新,获取最新功能和安全改进", "subtitle": "保持应用程序最新,获取最新功能和安全改进",
@@ -480,7 +496,7 @@
"startingDownload": "开始下载...", "startingDownload": "开始下载...",
"downloading": "下载中...", "downloading": "下载中...",
"installingUpdate": "安装更新中...", "installingUpdate": "安装更新中...",
"restartingApp": "重启应用中...", "quitApp": "退出应用中...",
"noUpdatesFound": "没有可用更新", "noUpdatesFound": "没有可用更新",
"aboutUpdates": "关于更新", "aboutUpdates": "关于更新",
"autoCheckInfo": "自动更新检查让您的应用保持安全和最新状态", "autoCheckInfo": "自动更新检查让您的应用保持安全和最新状态",

View File

@@ -7,9 +7,17 @@ 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, api_token: '', 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: { theme: 'light', monitor_interval: 5000, auto_update_enabled: true } app: {
theme: 'light',
auto_update_enabled: true,
gh_proxy: '',
gh_proxy_api: false,
open_links_in_browser: false,
admin_password: undefined,
show_window_on_startup: true
}
}) })
const openlistCoreStatus = ref<OpenListCoreStatus>({ running: false }) const openlistCoreStatus = ref<OpenListCoreStatus>({ running: false })
const remoteConfigs = ref<IRemoteConfig>({}) const remoteConfigs = ref<IRemoteConfig>({})
@@ -97,7 +105,7 @@ export const useAppStore = defineStore('app', () => {
const saveSettings = () => withLoading(() => TauriAPI.settings.save(settings.value), 'Failed to save settings') const saveSettings = () => withLoading(() => TauriAPI.settings.save(settings.value), 'Failed to save settings')
async function saveSettingsWithUpdatePort(): Promise<boolean> { async function saveSettingsWithCoreUpdate(): Promise<boolean> {
try { try {
await TauriAPI.settings.saveWithUpdatePort(settings.value) await TauriAPI.settings.saveWithUpdatePort(settings.value)
return true return true
@@ -273,13 +281,10 @@ export const useAppStore = defineStore('app', () => {
async function loadRemoteConfigs() { async function loadRemoteConfigs() {
try { try {
loading.value = true
remoteConfigs.value = await TauriAPI.rclone.remotes.listConfig('webdav') remoteConfigs.value = await TauriAPI.rclone.remotes.listConfig('webdav')
} catch (err: any) { } catch (err: any) {
error.value = 'Failed to load remote configurations' error.value = 'Failed to load remote configurations'
console.error('Failed to load remote configs:', err) console.error('Failed to load remote configs:', err)
} finally {
loading.value = false
} }
} }
@@ -375,10 +380,6 @@ export const useAppStore = defineStore('app', () => {
const openlistProcessId = ref<string | undefined>(undefined) const openlistProcessId = ref<string | undefined>(undefined)
const showTutorial = ref(false)
const tutorialStep = ref(0)
const tutorialSkipped = ref(false)
async function getRcloneMountProcessId(name: string): Promise<string | undefined> { async function getRcloneMountProcessId(name: string): Promise<string | undefined> {
try { try {
const processList = await TauriAPI.process.list() const processList = await TauriAPI.process.list()
@@ -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)
@@ -683,7 +725,6 @@ export const useAppStore = defineStore('app', () => {
async function init() { async function init() {
try { try {
initTutorial()
await loadSettings() await loadSettings()
await refreshOpenListCoreStatus() await refreshOpenListCoreStatus()
await TauriAPI.tray.updateDelayed(openlistCoreStatus.value.running) await TauriAPI.tray.updateDelayed(openlistCoreStatus.value.running)
@@ -697,43 +738,6 @@ export const useAppStore = defineStore('app', () => {
} }
} }
function initTutorial() {
const hasSeenTutorial = localStorage.getItem('openlist-tutorial-completed')
const tutorialDisabled = localStorage.getItem('openlist-tutorial-disabled')
if (!hasSeenTutorial && tutorialDisabled !== 'true') {
showTutorial.value = true
tutorialStep.value = 0
}
}
function startTutorial() {
showTutorial.value = true
tutorialStep.value = 0
localStorage.removeItem('openlist-tutorial-disabled')
}
function nextTutorialStep() {
tutorialStep.value++
}
function prevTutorialStep() {
if (tutorialStep.value > 0) {
tutorialStep.value--
}
}
function skipTutorial() {
showTutorial.value = false
tutorialSkipped.value = true
localStorage.setItem('openlist-tutorial-disabled', 'true')
}
function completeTutorial() {
showTutorial.value = false
localStorage.setItem('openlist-tutorial-completed', 'true')
}
async function getAdminPassword(): Promise<string | null> { async function getAdminPassword(): Promise<string | null> {
try { try {
return await TauriAPI.logs.adminPassword() return await TauriAPI.logs.adminPassword()
@@ -743,8 +747,34 @@ export const useAppStore = defineStore('app', () => {
} }
} }
function closeTutorial() { async function resetAdminPassword(): Promise<string | null> {
showTutorial.value = false try {
const newPassword = await TauriAPI.logs.resetAdminPassword()
if (newPassword) {
settings.value.app.admin_password = newPassword
await saveSettings()
}
return newPassword
} catch (err) {
console.error('Failed to reset admin password:', err)
return null
}
}
async function setAdminPassword(password: string): Promise<boolean> {
try {
await TauriAPI.logs.setAdminPassword(password)
settings.value.app.admin_password = password
await saveSettings()
return true
} catch (err) {
console.error('Failed to set admin password:', err)
return false
}
} }
// Update management functions // Update management functions
@@ -783,16 +813,12 @@ export const useAppStore = defineStore('app', () => {
updateAvailable, updateAvailable,
updateCheck, updateCheck,
showTutorial,
tutorialStep,
tutorialSkipped,
isCoreRunning, isCoreRunning,
openListCoreUrl, openListCoreUrl,
loadSettings, loadSettings,
saveSettings, saveSettings,
saveSettingsWithUpdatePort, saveSettingsWithCoreUpdate,
resetSettings, resetSettings,
startOpenListCore, startOpenListCore,
@@ -805,22 +831,21 @@ export const useAppStore = defineStore('app', () => {
listFiles, listFiles,
openFile, openFile,
openFolder, openFolder,
openLogsDirectory,
openOpenListDataDir,
openRcloneConfigFile,
openSettingsFile,
selectDirectory, selectDirectory,
clearError, clearError,
init, init,
getAdminPassword, getAdminPassword,
resetAdminPassword,
setAdminPassword,
setTheme, setTheme,
toggleTheme, toggleTheme,
applyTheme, applyTheme,
initTutorial,
startTutorial,
nextTutorialStep,
prevTutorialStep,
skipTutorial,
completeTutorial,
closeTutorial,
setUpdateAvailable, setUpdateAvailable,
clearUpdateStatus clearUpdateStatus
} }

View File

@@ -6,13 +6,14 @@ interface IRemoteConfig {
interface OpenListCoreConfig { interface OpenListCoreConfig {
port: number port: number
api_token: string data_dir: string
auto_launch: boolean auto_launch: boolean
ssl_enabled: boolean ssl_enabled: boolean
} }
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 {
@@ -45,11 +46,14 @@ interface RcloneMountInfo {
interface AppConfig { interface AppConfig {
theme?: 'light' | 'dark' | 'auto' theme?: 'light' | 'dark' | 'auto'
monitor_interval?: number
auto_update_enabled?: boolean auto_update_enabled?: boolean
gh_proxy?: string gh_proxy?: string
gh_proxy_api?: boolean gh_proxy_api?: boolean
open_links_in_browser?: boolean open_links_in_browser?: boolean
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,31 +381,22 @@ const handleKeydown = (event: KeyboardEvent) => {
} }
onMounted(async () => { onMounted(async () => {
await appStore.loadLogs( appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType).then(() => {
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as scrollToBottom()
| 'openlist' })
| 'rclone'
| 'app'
)
await 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()
} }
} }
}, (appStore.settings.app.monitor_interval || 5) * 1000) }, 30 * 1000)
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -402,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>
@@ -418,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>
@@ -427,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>
@@ -462,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>
@@ -509,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>
@@ -559,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>
@@ -636,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) {
@@ -453,8 +455,10 @@ const dismissWebdavTip = () => {
localStorage.setItem('webdav_tip_dismissed', 'true') localStorage.setItem('webdav_tip_dismissed', 'true')
} }
const isWindows = /win/i.test(navigator.platform) || /win/i.test(navigator.userAgent) const isWindows = computed(() => {
const showWinfspTip = ref(isWindows && !localStorage.getItem('winfsp_tip_dismissed')) return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
})
const showWinfspTip = ref(isWindows.value && !localStorage.getItem('winfsp_tip_dismissed'))
const dismissWinfspTip = () => { const dismissWinfspTip = () => {
showWinfspTip.value = false showWinfspTip.value = false
@@ -462,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
@@ -470,14 +474,14 @@ const shouldShowWebdavTip = computed(() => {
onMounted(async () => { onMounted(async () => {
document.addEventListener('keydown', handleKeydown) document.addEventListener('keydown', handleKeydown)
await rcloneStore.checkRcloneBackendStatus() rcloneStore.checkRcloneBackendStatus()
await appStore.loadRemoteConfigs() appStore.loadRemoteConfigs()
await appStore.loadMountInfos() appStore.loadMountInfos()
mountRefreshInterval = setInterval(appStore.loadMountInfos, (appStore.settings.app.monitor_interval || 5) * 1000) mountRefreshInterval = setInterval(appStore.loadMountInfos, 15 * 1000)
backendStatusCheckInterval = setInterval(() => { backendStatusCheckInterval = setInterval(() => {
rcloneStore.checkRcloneBackendStatus() rcloneStore.checkRcloneBackendStatus()
}, (appStore.settings.app.monitor_interval || 5) * 1000) }, 15 * 1000)
await rcloneStore.init() rcloneStore.init()
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -531,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>
@@ -555,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>
@@ -570,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>
@@ -594,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>
@@ -603,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>
@@ -615,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>
@@ -662,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 }}
@@ -677,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>
@@ -697,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>
@@ -722,7 +726,7 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- Configuration Modal --> <!-- Configuration Modal -->
<div v-if="showAddForm" class="modal-backdrop" @click="cancelForm"> <div v-if="showAddForm" class="modal-backdrop">
<div class="config-modal" @click.stop> <div class="config-modal" @click.stop>
<div class="modal-header"> <div class="modal-header">
<div class="modal-title-section"> <div class="modal-title-section">
@@ -731,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>
@@ -845,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>
@@ -859,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>
@@ -878,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) }">
@@ -913,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>
@@ -932,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,14 +1,26 @@
<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, useRouter } from 'vue-router' import { open } from '@tauri-apps/plugin-dialog'
import { useAppStore } from '../stores/app' 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 { useTranslation } from '../composables/useI18n'
import { Settings, Server, HardDrive, Save, RotateCcw, AlertCircle, CheckCircle, Play } from 'lucide-vue-next' import { useAppStore } from '../stores/app'
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
const appStore = useAppStore() const appStore = useAppStore()
const route = useRoute() const route = useRoute()
const router = useRouter()
const { t } = useTranslation() const { t } = useTranslation()
const isSaving = ref(false) const isSaving = ref(false)
const message = ref('') const message = ref('')
@@ -16,11 +28,22 @@ const messageType = ref<'success' | 'error' | 'info'>('info')
const activeTab = ref('openlist') const activeTab = ref('openlist')
const rcloneConfigJson = ref('') const rcloneConfigJson = ref('')
const autoStartApp = ref(false) const autoStartApp = ref(false)
const isResettingPassword = ref(false)
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
title: '',
message: '',
onConfirm: () => {},
onCancel: () => {}
})
const openlistCoreSettings = reactive({ ...appStore.settings.openlist }) const 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 originalRcloneApiPort = rcloneSettings.api_port || 45572
let originalAdminPassword = appStore.settings.app.admin_password || ''
watch(autoStartApp, async newValue => { watch(autoStartApp, async newValue => {
if (newValue) { if (newValue) {
@@ -59,21 +82,28 @@ onMounted(async () => {
} }
if (!openlistCoreSettings.port) openlistCoreSettings.port = 5244 if (!openlistCoreSettings.port) openlistCoreSettings.port = 5244
if (!openlistCoreSettings.api_token) openlistCoreSettings.api_token = '' if (!openlistCoreSettings.data_dir) openlistCoreSettings.data_dir = ''
if (openlistCoreSettings.auto_launch === undefined) openlistCoreSettings.auto_launch = false if (openlistCoreSettings.auto_launch === undefined) openlistCoreSettings.auto_launch = false
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'
if (!appSettings.monitor_interval) appSettings.monitor_interval = 5
if (appSettings.auto_update_enabled === undefined) appSettings.auto_update_enabled = true if (appSettings.auto_update_enabled === undefined) appSettings.auto_update_enabled = true
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 = ''
originalOpenlistPort = openlistCoreSettings.port || 5244 originalOpenlistPort = openlistCoreSettings.port || 5244
originalDataDir = openlistCoreSettings.data_dir
originalRcloneApiPort = rcloneSettings.api_port || 45572
// Load current admin password
await loadCurrentAdminPassword()
}) })
const hasUnsavedChanges = computed(() => { const hasUnsavedChanges = computed(() => {
@@ -110,13 +140,37 @@ const handleSave = async () => {
appStore.settings.openlist = { ...openlistCoreSettings } appStore.settings.openlist = { ...openlistCoreSettings }
appStore.settings.rclone = { ...rcloneSettings } appStore.settings.rclone = { ...rcloneSettings }
appStore.settings.app = { ...appSettings } appStore.settings.app = { ...appSettings }
if (originalOpenlistPort !== openlistCoreSettings.port) {
await appStore.saveSettingsWithUpdatePort() const needsPasswordUpdate = originalAdminPassword !== appSettings.admin_password && appSettings.admin_password
if (
originalOpenlistPort !== openlistCoreSettings.port ||
originalDataDir !== openlistCoreSettings.data_dir ||
originalRcloneApiPort !== rcloneSettings.api_port
) {
await appStore.saveSettingsWithCoreUpdate()
} else { } else {
await appStore.saveSettings() await appStore.saveSettings()
} }
message.value = t('settings.saved')
messageType.value = 'success' if (needsPasswordUpdate) {
try {
await appStore.setAdminPassword(appSettings.admin_password!)
message.value = t('settings.service.admin.passwordUpdated')
messageType.value = 'success'
} catch (error) {
console.error('Failed to update admin password:', error)
message.value = t('settings.service.admin.passwordUpdateFailed')
messageType.value = 'error'
}
} else {
message.value = t('settings.saved')
messageType.value = 'success'
}
originalOpenlistPort = openlistCoreSettings.port || 5244
originalRcloneApiPort = rcloneSettings.api_port || 45572
originalDataDir = openlistCoreSettings.data_dir
} catch (error) { } catch (error) {
message.value = t('settings.saveFailed') message.value = t('settings.saveFailed')
messageType.value = 'error' messageType.value = 'error'
@@ -130,29 +184,143 @@ const handleSave = async () => {
} }
} }
async function startTutorial() {
router.push({ name: 'Dashboard' })
appStore.startTutorial()
}
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
}
} }
showConfirmDialog.value = true
}
const handleSelectDataDir = async () => {
try { try {
await appStore.resetSettings() const selected = await open({
Object.assign(openlistCoreSettings, appStore.settings.openlist) directory: true,
Object.assign(rcloneSettings, appStore.settings.rclone) multiple: false,
Object.assign(appSettings, appStore.settings.app) title: t('settings.service.network.dataDir.selectTitle'),
defaultPath: openlistCoreSettings.data_dir || undefined
})
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2) if (selected && typeof selected === 'string') {
openlistCoreSettings.data_dir = selected
message.value = t('settings.resetSuccess') }
messageType.value = 'info'
} catch (error) { } catch (error) {
message.value = t('settings.resetFailed') console.error('Failed to select directory:', error)
message.value = t('settings.service.network.dataDir.selectError')
messageType.value = 'error' messageType.value = 'error'
setTimeout(() => {
message.value = ''
}, 3000)
}
}
const handleOpenDataDir = async () => {
try {
if (openlistCoreSettings.data_dir) {
await appStore.openFolder(openlistCoreSettings.data_dir)
} else {
await appStore.openOpenListDataDir()
}
message.value = t('settings.service.network.dataDir.openSuccess')
messageType.value = 'success'
} catch (error) {
console.error('Failed to open data directory:', error)
message.value = t('settings.service.network.dataDir.openError')
messageType.value = 'error'
} finally {
setTimeout(() => {
message.value = ''
}, 3000)
}
}
const handleResetAdminPassword = async () => {
isResettingPassword.value = true
try {
const newPassword = await appStore.resetAdminPassword()
if (newPassword) {
appSettings.admin_password = newPassword
message.value = t('settings.service.admin.resetSuccess')
messageType.value = 'success'
} else {
message.value = t('settings.service.admin.resetFailed')
messageType.value = 'error'
}
} catch (error) {
console.error('Failed to reset admin password:', error)
message.value = t('settings.service.admin.resetFailed')
messageType.value = 'error'
} finally {
isResettingPassword.value = false
setTimeout(() => {
message.value = ''
}, 3000)
}
}
const handleOpenRcloneConfig = async () => {
try {
await appStore.openRcloneConfigFile()
message.value = t('settings.rclone.config.openSuccess')
messageType.value = 'success'
} catch (error) {
console.error('Failed to open rclone config file:', error)
message.value = t('settings.rclone.config.openError')
messageType.value = 'error'
} finally {
setTimeout(() => {
message.value = ''
}, 3000)
}
}
const handleOpenSettingsFile = async () => {
try {
await appStore.openSettingsFile()
message.value = t('settings.app.config.openSuccess')
messageType.value = 'success'
} catch (error) {
console.error('Failed to open settings file:', error)
message.value = t('settings.app.config.openError')
messageType.value = 'error'
} finally {
setTimeout(() => {
message.value = ''
}, 3000)
}
}
const loadCurrentAdminPassword = async () => {
try {
const password = await appStore.getAdminPassword()
if (password) {
appSettings.admin_password = password
originalAdminPassword = password
}
} catch (error) {
console.error('Failed to load admin password:', error)
} }
} }
</script> </script>
@@ -168,11 +336,11 @@ const handleReset = 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>
@@ -182,16 +350,16 @@ const handleReset = 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>
@@ -218,14 +386,32 @@ const handleReset = async () => {
<small>{{ t('settings.service.network.port.help') }}</small> <small>{{ t('settings.service.network.port.help') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ t('settings.service.network.apiToken.label') }}</label> <label>{{ t('settings.service.network.dataDir.label') }}</label>
<input <div class="input-group">
v-model="openlistCoreSettings.api_token" <input
type="password" v-model="openlistCoreSettings.data_dir"
class="form-input" type="text"
:placeholder="t('settings.service.network.apiToken.placeholder')" class="form-input"
/> :placeholder="t('settings.service.network.dataDir.placeholder')"
<small>{{ t('settings.service.network.apiToken.help') }}</small> />
<button
type="button"
class="input-addon-btn"
:title="t('settings.service.network.dataDir.selectTitle')"
@click="handleSelectDataDir"
>
<FolderOpen :size="16" />
</button>
<button
type="button"
class="input-addon-btn"
:title="t('settings.service.network.dataDir.openTitle')"
@click="handleOpenDataDir"
>
<ExternalLink :size="16" />
</button>
</div>
<small>{{ t('settings.service.network.dataDir.help') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -256,20 +442,79 @@ const handleReset = async () => {
</label> </label>
</div> </div>
</div> </div>
<div class="settings-section">
<h2>{{ t('settings.service.admin.title') }}</h2>
<p>{{ t('settings.service.admin.subtitle') }}</p>
<div class="form-group">
<label>{{ t('settings.service.admin.currentPassword') }}</label>
<div class="input-group">
<input
v-model="appSettings.admin_password"
type="text"
class="form-input"
:placeholder="t('settings.service.admin.passwordPlaceholder')"
/>
<button
type="button"
:disabled="isResettingPassword"
class="input-addon-btn reset-password-btn"
:title="t('settings.service.admin.resetTitle')"
@click="handleResetAdminPassword"
>
<RotateCcw :size="16" />
</button>
</div>
<small>{{ t('settings.service.admin.help') }}</small>
</div>
</div>
</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"
placeholder='{ "remote1": { "type": "s3", "provider": "AWS" } }' placeholder='{ "remote1": { "type": "s3", "provider": "AWS" } }'
rows="10" rows="10"
readonly
></textarea> ></textarea>
<small>{{ t('settings.rclone.config.tips') }}</small> <small>{{ t('settings.rclone.config.tips') }}</small>
</div> </div>
@@ -286,8 +531,8 @@ const handleReset = 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>
@@ -299,21 +544,20 @@ const handleReset = async () => {
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h2>{{ t('settings.app.monitor.title') }}</h2> <h2>{{ t('settings.app.config.title') }}</h2>
<p>{{ t('settings.app.monitor.subtitle') }}</p> <p>{{ t('settings.app.config.subtitle') }}</p>
<div class="form-grid"> <div class="form-group">
<div class="form-group"> <div class="settings-section-actions">
<label>{{ t('settings.app.monitor.interval.label') }}</label> <button
<input type="button"
v-model.number="appSettings.monitor_interval" class="btn btn-secondary"
type="number" :title="t('settings.app.config.openFile')"
class="form-input" @click="handleOpenSettingsFile"
:placeholder="t('settings.app.monitor.interval.placeholder')" >
min="1" <ExternalLink :size="16" />
max="60" {{ t('settings.app.config.openFile') }}
/> </button>
<small>{{ t('settings.app.monitor.interval.help') }}</small>
</div> </div>
</div> </div>
</div> </div>
@@ -363,6 +607,21 @@ const handleReset = 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>
@@ -394,23 +653,19 @@ const handleReset = async () => {
</label> </label>
</div> </div>
</div> </div>
<div class="settings-section">
<h2>{{ t('settings.app.tutorial.title') }}</h2>
<p>{{ t('settings.app.tutorial.subtitle') }}</p>
<div class="form-grid">
<div class="form-group">
<button @click="startTutorial" class="tutorial-btn" type="button">
<Play :size="16" />
{{ t('settings.app.tutorial.restart') }}
</button>
<small>{{ t('settings.app.tutorial.help') }}</small>
</div>
</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()
@@ -100,7 +101,6 @@ const goToSettings = () => {
text-decoration: none; text-decoration: none;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -128,7 +128,6 @@ const goToSettings = () => {
background: var(--color-surface); background: var(--color-surface);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 12px; border-radius: 12px;
transition: all 0.2s ease;
} }
.info-card:hover { .info-card:hover {

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;
@@ -56,20 +56,9 @@
rgba(139, 92, 246, 0.3) 80%, rgba(139, 92, 246, 0.3) 80%,
transparent 100% transparent 100%
); );
animation: shimmer 8s ease-in-out infinite;
z-index: 0; z-index: 0;
} }
@keyframes shimmer {
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 0.8;
}
}
.metrics-overview { .metrics-overview {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -104,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) {
@@ -119,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) {
@@ -150,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,
@@ -158,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),
@@ -196,22 +185,17 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
transform-origin: center;
position: relative; position: relative;
border-radius: 16px; border-radius: 16px;
background: transparent; background: transparent;
will-change: transform;
} }
.dashboard-card-wrapper:hover { .dashboard-card-wrapper:hover {
transform: translateY(-2px) scale(1.01); background: rgba(255, 255, 255, 0.5);
z-index: 10;
} }
.dashboard-card-wrapper:hover > * { .dashboard-card-wrapper:hover > * {
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.15), 0 8px 16px -5px rgba(0, 0, 0, 0.1), border-color: rgba(59, 130, 246, 0.3);
inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
:root.dark .dashboard-card-wrapper:hover > *, :root.dark .dashboard-card-wrapper:hover > *,
@@ -232,7 +216,6 @@
.dashboard-grid:focus-within > *:not(:focus-within) { .dashboard-grid:focus-within > *:not(:focus-within) {
opacity: 0.7; opacity: 0.7;
transform: scale(0.98);
} }
.dashboard-grid > *:focus-within { .dashboard-grid > *:focus-within {
@@ -241,49 +224,6 @@
border-radius: 20px; border-radius: 20px;
} }
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.dashboard-loading {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.dashboard-ready .metrics-overview {
animation: slideInDown 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.dashboard-ready .dashboard-grid {
animation: fadeIn 0.8s cubic-bezier(0.4, 0, 0.2, 1) 0.2s forwards;
opacity: 0;
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (max-width: 1400px) { @media (max-width: 1400px) {
.dashboard-grid { .dashboard-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@@ -300,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;
} }
@@ -342,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;
} }
@@ -370,15 +310,11 @@
.dashboard-subtitle { .dashboard-subtitle {
font-size: 0.875rem; font-size: 0.875rem;
} }
.dashboard-grid > *:hover {
transform: translateY(-2px) scale(1.001);
}
} }
@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;
} }
@@ -421,51 +357,6 @@
} }
} }
.dashboard-grid > *:nth-child(1) {
animation: slideInLeft 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.dashboard-grid > *:nth-child(2) {
animation: slideInRight 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.1s forwards;
}
.dashboard-grid > *:nth-child(n + 3) {
animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.2s forwards;
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dashboard-header { .dashboard-header {
text-align: center; text-align: center;
margin-bottom: 1rem; margin-bottom: 1rem;

View File

@@ -22,7 +22,6 @@
padding: 12px 20px; padding: 12px 20px;
background: var(--color-surface-elevated); background: var(--color-surface-elevated);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
backdrop-filter: blur(20px) saturate(180%);
gap: 20px; gap: 20px;
flex-shrink: 0; flex-shrink: 0;
min-height: 56px; min-height: 56px;
@@ -58,7 +57,6 @@
background: transparent; background: transparent;
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.toolbar-btn:hover:not(:disabled) { .toolbar-btn:hover:not(:disabled) {
@@ -111,7 +109,6 @@
background: var(--color-background-secondary); background: var(--color-background-secondary);
font-size: 13px; font-size: 13px;
color: var(--color-text-primary); color: var(--color-text-primary);
transition: all var(--transition-medium);
} }
.search-input:focus { .search-input:focus {
@@ -154,19 +151,6 @@
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
gap: 20px; gap: 20px;
flex-wrap: wrap; flex-wrap: wrap;
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
transform: translateY(-10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
} }
.filter-group, .filter-group,
@@ -220,7 +204,6 @@
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.filter-btn:hover:not(:disabled) { .filter-btn:hover:not(:disabled) {
@@ -318,7 +301,6 @@
background: transparent; background: transparent;
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.scroll-btn:hover { .scroll-btn:hover {
@@ -370,7 +352,6 @@
padding: 8px 16px; padding: 8px 16px;
border-bottom: 1px solid var(--color-border-secondary); border-bottom: 1px solid var(--color-border-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.log-entry:hover { .log-entry:hover {
@@ -684,7 +665,6 @@
padding: 16px 20px; padding: 16px 20px;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
@@ -747,21 +727,11 @@
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
} }
.notification-enter-active {
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.notification-leave-active {
transition: all 0.3s ease-in;
}
.notification-enter-from { .notification-enter-from {
transform: translateX(100%) scale(0.8);
opacity: 0; opacity: 0;
} }
.notification-leave-to { .notification-leave-to {
transform: translateX(100%) scale(0.8);
opacity: 0; opacity: 0;
} }

View File

@@ -38,7 +38,6 @@
z-index: 1; z-index: 1;
padding: 24px 28px 20px; padding: 24px 28px 20px;
background: var(--color-surface-elevated); background: var(--color-surface-elevated);
backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
} }
@@ -146,7 +145,6 @@
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: var(--color-danger); background: var(--color-danger);
transition: background-color var(--transition-fast);
} }
.service-indicator.active .indicator-dot { .service-indicator.active .indicator-dot {
@@ -170,7 +168,6 @@
background: var(--color-background-secondary); background: var(--color-background-secondary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.service-toggle:hover { .service-toggle:hover {
@@ -200,7 +197,6 @@
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
@@ -219,7 +215,6 @@
z-index: 1; z-index: 1;
padding: 20px 28px; padding: 20px 28px;
background: var(--color-surface); background: var(--color-surface);
backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
display: flex; display: flex;
align-items: center; align-items: center;
@@ -255,7 +250,6 @@
background: var(--color-background-primary); background: var(--color-background-primary);
font-size: 14px; font-size: 14px;
color: var(--color-text-primary); color: var(--color-text-primary);
transition: all var(--transition-medium);
} }
.search-input:focus { .search-input:focus {
@@ -282,7 +276,6 @@
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.status-filter:focus { .status-filter:focus {
@@ -301,7 +294,6 @@
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.refresh-btn:hover:not(:disabled) { .refresh-btn:hover:not(:disabled) {
@@ -318,20 +310,6 @@
.refresh-icon { .refresh-icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
transition: transform var(--transition-medium);
}
.refresh-icon.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }
.error-alert { .error-alert {
@@ -369,7 +347,6 @@
background: transparent; background: transparent;
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
transition: background-color var(--transition-fast);
} }
.alert-close:hover { .alert-close:hover {
@@ -438,7 +415,6 @@
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
@@ -459,10 +435,8 @@
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
padding: 24px; padding: 24px;
transition: all var(--transition-medium);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
backdrop-filter: blur(20px) saturate(180%);
} }
.config-card:hover { .config-card:hover {
@@ -562,10 +536,6 @@
color: var(--color-danger); color: var(--color-danger);
} }
.status-icon.spinning {
animation: spin 1s linear infinite;
}
.card-meta { .card-meta {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -596,7 +566,6 @@
.meta-tag.clickable-mount-point { .meta-tag.clickable-mount-point {
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
background: var(--color-primary-50); background: var(--color-primary-50);
color: var(--color-primary-600); color: var(--color-primary-600);
border: 1px solid var(--color-primary-200); border: 1px solid var(--color-primary-200);
@@ -655,7 +624,6 @@
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
width: 100%; width: 100%;
justify-content: center; justify-content: center;
} }
@@ -701,7 +669,6 @@
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.secondary-btn:hover:not(:disabled) { .secondary-btn:hover:not(:disabled) {
@@ -734,7 +701,6 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -752,7 +718,6 @@
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
backdrop-filter: blur(20px) saturate(180%);
} }
.modal-header { .modal-header {
@@ -793,7 +758,6 @@
background: var(--color-background-secondary); background: var(--color-background-secondary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.modal-close:hover { .modal-close:hover {
@@ -854,7 +818,6 @@
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 14px; font-size: 14px;
transition: all var(--transition-fast);
} }
.field-input:focus, .field-input:focus,
@@ -908,7 +871,6 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.cancel-btn:hover { .cancel-btn:hover {
@@ -928,7 +890,6 @@
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.save-btn:hover:not(:disabled) { .save-btn:hover:not(:disabled) {
@@ -960,7 +921,6 @@
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 13px; font-size: 13px;
transition: all var(--transition-fast);
} }
.flag-input:focus { .flag-input:focus {
@@ -984,7 +944,6 @@
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
} }
.remove-flag-btn:hover { .remove-flag-btn:hover {
@@ -1010,7 +969,6 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast);
margin-top: 4px; margin-top: 4px;
} }
@@ -1045,7 +1003,6 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
} }
.quick-flags-btn:hover { .quick-flags-btn:hover {
@@ -1068,7 +1025,6 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
z-index: 9999; z-index: 9999;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1087,18 +1043,6 @@
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: flagSelectorFadeIn 0.3s ease-out;
}
@keyframes flagSelectorFadeIn {
from {
opacity: 0;
transform: scale(0.96) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
} }
.flag-selector-header { .flag-selector-header {
@@ -1129,13 +1073,11 @@
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
} }
.close-selector-btn:hover { .close-selector-btn:hover {
background: var(--color-background-tertiary); background: var(--color-background-tertiary);
color: var(--color-text-primary); color: var(--color-text-primary);
transform: scale(1.05);
} }
.close-selector-btn .btn-icon { .close-selector-btn .btn-icon {
@@ -1218,7 +1160,6 @@
overflow: hidden; overflow: hidden;
background: var(--color-background-primary); background: var(--color-background-primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
} }
.flag-category:hover { .flag-category:hover {
@@ -1260,7 +1201,6 @@
color: var(--color-text-primary); color: var(--color-text-primary);
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent; border-left: 3px solid transparent;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
position: relative; position: relative;
@@ -1304,19 +1244,16 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
position: relative; position: relative;
} }
.custom-checkbox:hover { .custom-checkbox:hover {
border-color: #22c55e; border-color: #22c55e;
transform: scale(1.05);
} }
.custom-checkbox.checked { .custom-checkbox.checked {
background: #22c55e; background: #22c55e;
border-color: #22c55e; border-color: #22c55e;
transform: scale(1.05);
} }
.check-icon { .check-icon {
@@ -1431,3 +1368,385 @@
background: var(--color-background-primary); background: var(--color-background-primary);
border-color: var(--color-border); border-color: var(--color-border);
} }
.webdav-tip {
position: relative;
z-index: 1;
margin: 0 28px 12px;
background: linear-gradient(135deg, #fef3cd 0%, #fff3cd 100%);
border: 1px solid #f9cc33;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(249, 204, 51, 0.1);
overflow: hidden;
}
.winfsp-tip {
position: relative;
z-index: 1;
margin: 0 28px 12px;
background: linear-gradient(135deg, #dbeafe 0%, #e0f2fe 100%);
border: 1px solid #3b82f6;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(59, 130, 246, 0.1);
overflow: hidden;
}
.tip-content {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
}
.tip-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
background: rgba(249, 204, 51, 0.1);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.winfsp-tip .tip-icon {
background: rgba(59, 130, 246, 0.1);
}
.tip-icon .icon {
width: 16px;
height: 16px;
color: #b45309;
}
.winfsp-tip .tip-icon .icon {
color: #1d4ed8;
}
.tip-message {
flex: 1;
min-width: 0;
}
.tip-title {
margin: 0 0 4px 0;
font-size: 13px;
font-weight: 600;
color: #92400e;
line-height: 1.3;
}
.winfsp-tip .tip-title {
color: #1e40af;
}
.tip-description {
margin: 0;
font-size: 12px;
color: #a16207;
line-height: 1.4;
}
.winfsp-tip .tip-description {
color: #1d4ed8;
}
.tip-close {
flex-shrink: 0;
width: 28px;
height: 28px;
background: rgba(249, 204, 51, 0.1);
border: none;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.winfsp-tip .tip-close {
background: rgba(59, 130, 246, 0.1);
}
.tip-close:hover {
background: rgba(249, 204, 51, 0.2);
}
.winfsp-tip .tip-close:hover {
background: rgba(59, 130, 246, 0.2);
}
.tip-close .close-icon {
width: 14px;
height: 14px;
color: #a16207;
}
.winfsp-tip .tip-close .close-icon {
color: #1d4ed8;
}
:root.dark .webdav-tip,
:root.auto.dark .webdav-tip {
background: linear-gradient(135deg, #451a03 0%, #541c15 100%);
border-color: #a16207;
box-shadow: 0 1px 4px rgba(161, 98, 7, 0.1);
}
:root.dark .winfsp-tip,
:root.auto.dark .winfsp-tip {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
border-color: #3b82f6;
box-shadow: 0 1px 4px rgba(59, 130, 246, 0.1);
}
:root.dark .tip-icon,
:root.auto.dark .tip-icon {
background: rgba(161, 98, 7, 0.1);
}
:root.dark .winfsp-tip .tip-icon,
:root.auto.dark .winfsp-tip .tip-icon {
background: rgba(59, 130, 246, 0.1);
}
:root.dark .tip-icon .icon,
:root.auto.dark .tip-icon .icon {
color: #f59e0b;
}
:root.dark .winfsp-tip .tip-icon .icon,
:root.auto.dark .winfsp-tip .tip-icon .icon {
color: #60a5fa;
}
:root.dark .tip-title,
:root.auto.dark .tip-title {
color: #fbbf24;
}
:root.dark .winfsp-tip .tip-title,
:root.auto.dark .winfsp-tip .tip-title {
color: #93c5fd;
}
:root.dark .tip-description,
:root.auto.dark .tip-description {
color: #d97706;
}
:root.dark .winfsp-tip .tip-description,
:root.auto.dark .winfsp-tip .tip-description {
color: #60a5fa;
}
:root.dark .tip-close,
:root.auto.dark .tip-close {
background: rgba(161, 98, 7, 0.1);
}
:root.dark .winfsp-tip .tip-close,
:root.auto.dark .winfsp-tip .tip-close {
background: rgba(59, 130, 246, 0.1);
}
:root.dark .tip-close:hover,
:root.auto.dark .tip-close:hover {
background: rgba(161, 98, 7, 0.2);
}
:root.dark .winfsp-tip .tip-close:hover,
:root.auto.dark .winfsp-tip .tip-close:hover {
background: rgba(59, 130, 246, 0.2);
}
:root.dark .tip-close .close-icon,
:root.auto.dark .tip-close .close-icon {
color: #d97706;
}
:root.dark .winfsp-tip .tip-close .close-icon,
:root.auto.dark .winfsp-tip .tip-close .close-icon {
color: #60a5fa;
}
@media (max-width: 1024px) {
.header-content {
grid-template-columns: 1fr;
gap: 20px;
}
.header-actions {
justify-content: flex-start;
}
.controls-section {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
.search-container {
max-width: none;
}
.config-grid {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
}
@media (max-width: 768px) {
.mount-header {
padding: 20px 16px;
}
.controls-section {
padding: 16px;
}
.configs-container {
padding: 20px 16px;
}
.page-title {
font-size: 24px;
}
.stats-overview {
flex-wrap: wrap;
gap: 12px;
}
.webdav-tip,
.winfsp-tip {
margin: 0 16px 8px;
}
.tip-content {
padding: 10px 12px;
gap: 10px;
}
.tip-icon {
width: 28px;
height: 28px;
}
.tip-icon .icon {
width: 14px;
height: 14px;
}
.tip-title {
font-size: 12px;
margin-bottom: 2px;
}
.tip-description {
font-size: 11px;
}
.tip-close {
width: 24px;
height: 24px;
}
.tip-close .close-icon {
width: 12px;
height: 12px;
}
.config-grid {
grid-template-columns: 1fr;
}
.form-grid {
grid-template-columns: 1fr;
}
.modal-backdrop {
padding: 10px;
}
.modal-header {
padding: 20px 16px;
}
.modal-content {
padding: 20px 16px;
}
.modal-footer {
padding: 16px;
}
}
@media (max-width: 480px) {
.header-actions {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.primary-btn {
justify-content: center;
}
.card-actions {
flex-direction: column;
gap: 12px;
}
.secondary-actions {
justify-content: center;
}
}
/* Dark mode specific adjustments */
:root.dark .config-card.mounted,
:root.auto.dark .config-card.mounted {
background: rgba(52, 199, 89, 0.1);
}
:root.dark .config-card.error,
:root.auto.dark .config-card.error {
background: rgba(255, 59, 48, 0.1);
}
:root.dark .config-card.loading,
:root.auto.dark .config-card.loading {
background: rgba(10, 132, 255, 0.1);
}
:root.dark .error-alert,
:root.auto.dark .error-alert {
background: rgba(255, 59, 48, 0.2);
border-bottom-color: rgba(255, 59, 48, 0.3);
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
.config-card,
.refresh-icon,
.status-icon.spinning {
animation: none;
}
* {
transition: none !important;
}
}
/* Focus styles for accessibility */
.search-input:focus-visible,
.status-filter:focus-visible,
.refresh-btn:focus-visible,
.primary-btn:focus-visible,
.action-btn:focus-visible,
.secondary-btn:focus-visible,
.cancel-btn:focus-visible,
.save-btn:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}

View File

@@ -117,7 +117,6 @@
font-weight: 500; font-weight: 500;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
} }
.btn:disabled { .btn:disabled {
@@ -208,7 +207,6 @@
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
flex: 1; flex: 1;
justify-content: center; justify-content: center;
} }
@@ -298,7 +296,6 @@
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
font-size: 0.875rem; font-size: 0.875rem;
transition: all 0.2s ease;
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
@@ -314,7 +311,6 @@
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
font-size: 0.875rem; font-size: 0.875rem;
transition: all 0.2s ease;
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text-primary); color: var(--color-text-primary);
resize: vertical; resize: vertical;
@@ -381,7 +377,6 @@
background: var(--color-background-tertiary); background: var(--color-background-tertiary);
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
} }
.input-addon-btn:hover { .input-addon-btn:hover {
@@ -389,6 +384,22 @@
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.input-addon-btn.reset-password-btn {
background: var(--color-error-background, #fef2f2);
color: var(--color-error, #dc2626);
border-color: var(--color-error-border, #fecaca);
}
.input-addon-btn.reset-password-btn:hover:not(:disabled) {
background: var(--color-error, #dc2626);
color: white;
}
.input-addon-btn.reset-password-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Switch */ /* Switch */
.switch-label { .switch-label {
display: flex; display: flex;
@@ -399,7 +410,6 @@
border: 1px solid var(--color-border-secondary); border: 1px solid var(--color-border-secondary);
border-radius: 8px; border-radius: 8px;
background: var(--color-background-tertiary); background: var(--color-background-tertiary);
transition: all 0.2s ease;
min-height: auto; min-height: auto;
} }
@@ -432,7 +442,6 @@
height: 24px; height: 24px;
background: var(--color-border); background: var(--color-border);
border-radius: 12px; border-radius: 12px;
transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -445,7 +454,6 @@
height: 20px; height: 20px;
background: var(--color-surface); background: var(--color-surface);
border-radius: 50%; border-radius: 50%;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
@@ -487,91 +495,6 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
/* Flags */
.flags-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.flag-item {
display: flex;
gap: 0.5rem;
}
.flag-item .form-input {
flex: 1;
}
.remove-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: 1px solid rgb(239 68 68);
border-radius: 6px;
background: rgb(254 242 242);
color: rgb(239 68 68);
cursor: pointer;
transition: all 0.2s ease;
}
.remove-btn:hover {
background: rgb(254 226 226);
}
.add-flag-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
border: 1px dashed rgb(209 213 219);
border-radius: 6px;
background: transparent;
color: rgb(107 114 128);
cursor: pointer;
transition: all 0.2s ease;
align-self: flex-start;
}
.add-flag-btn:hover {
border-color: rgb(156 163 175);
color: rgb(55 65 81);
}
@media (prefers-color-scheme: dark) {
.add-flag-btn {
border-color: rgb(75 85 99);
color: rgb(156 163 175);
}
.add-flag-btn:hover {
border-color: rgb(107 114 128);
color: rgb(209 213 219);
}
}
/* Info Message */
.info-message {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: rgba(0, 122, 255, 0.1);
color: var(--color-accent);
border: 1px solid rgba(0, 122, 255, 0.2);
border-radius: 8px;
font-size: 0.875rem;
}
:root.dark .info-message,
:root.auto.dark .info-message {
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.2);
color: rgb(147, 197, 253);
}
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.settings-container { .settings-container {
@@ -605,28 +528,32 @@
} }
} }
.tutorial-btn { /* Settings Section Actions */
.settings-section-actions {
display: flex;
justify-content: flex-start;
margin: 0;
gap: 0.5rem;
}
.settings-section-actions .btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 0.5rem;
padding: 12px 20px; white-space: nowrap;
background: var(--color-accent);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
} }
.tutorial-btn:hover { .input-group .input-addon-btn:last-child:not(:only-child) {
background: var(--color-accent-hover); border-radius: 0 8px 8px 0;
transform: translateY(-1px); border-left: none;
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
} }
.tutorial-btn:active { .input-group .input-addon-btn:not(:first-child):not(:last-child) {
transform: translateY(0); border-radius: 0;
box-shadow: 0 2px 6px rgba(0, 122, 255, 0.3); 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
} }

2686
yarn.lock

File diff suppressed because it is too large Load Diff