mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 19:27:33 +08:00
Compare commits
97 Commits
v0.2.0
...
renovate/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b533bf43b4 | ||
|
|
0dae544d54 | ||
|
|
f3a5e2556f | ||
|
|
a3a6ef03a5 | ||
|
|
e4ab2184eb | ||
|
|
4c76f31885 | ||
|
|
68411aaaf3 | ||
|
|
e00c0d5ffd | ||
|
|
ca0b9251d3 | ||
|
|
94027733fc | ||
|
|
674c9b0041 | ||
|
|
a81f0ed9eb | ||
|
|
d4a42de814 | ||
|
|
bf0f481086 | ||
|
|
6d6c38eab9 | ||
|
|
d2c834d936 | ||
|
|
31980949a3 | ||
|
|
58b7128e4e | ||
|
|
62f2710a7c | ||
|
|
15d789ef3a | ||
|
|
d6ff164b0c | ||
|
|
7f7cc8c8ce | ||
|
|
9277c9380c | ||
|
|
a6c83fe289 | ||
|
|
c47fc1443b | ||
|
|
954ee010c1 | ||
|
|
c219afa54e | ||
|
|
06e54d1b01 | ||
|
|
386147d5ff | ||
|
|
2eeba5f428 | ||
|
|
dc1cb41e61 | ||
|
|
99c426c15c | ||
|
|
77f9f81dea | ||
|
|
5cc2c1640c | ||
|
|
24b45446cc | ||
|
|
f3cc4a021b | ||
|
|
b6cfda7648 | ||
|
|
3b9910da0a | ||
|
|
a88b17c92f | ||
|
|
20aeb6a796 | ||
|
|
13efd8a629 | ||
|
|
9998563110 | ||
|
|
6f41bd708c | ||
|
|
4837ee592f | ||
|
|
8d25feefe0 | ||
|
|
6628c7936b | ||
|
|
9c53267589 | ||
|
|
e0d3250823 | ||
|
|
c9ccf6d1ce | ||
|
|
a19e74ce1f | ||
|
|
0231fa20d7 | ||
|
|
f69bfa6fd5 | ||
|
|
bb0f091849 | ||
|
|
9d95b6b46c | ||
|
|
249612344e | ||
|
|
3c5f64b1b4 | ||
|
|
2911922403 | ||
|
|
0093f15524 | ||
|
|
704d06ebe1 | ||
|
|
7038a1a255 | ||
|
|
7476e29e2b | ||
|
|
08a9eb38cc | ||
|
|
d0917ee550 | ||
|
|
ccb8b12f1e | ||
|
|
1d74daf8a5 | ||
|
|
ffdf996cd0 | ||
|
|
eb82a49270 | ||
|
|
0c09addb31 | ||
|
|
731ff435a5 | ||
|
|
3e1a5f121d | ||
|
|
9263ad9810 | ||
|
|
7b0565f210 | ||
|
|
90f935e6bb | ||
|
|
8aa4f93f6f | ||
|
|
4b08aeb4d3 | ||
|
|
97be081c47 | ||
|
|
c68102fa20 | ||
|
|
5c978d43f6 | ||
|
|
cb3a4cb995 | ||
|
|
340b117956 | ||
|
|
a70b576e1e | ||
|
|
23095dba6b | ||
|
|
64b12e6c3b | ||
|
|
f9cf0b59ed | ||
|
|
522e1f8059 | ||
|
|
c83b63eb03 | ||
|
|
84ef6de7db | ||
|
|
96e995051e | ||
|
|
83b3caf521 | ||
|
|
bff5a44bfc | ||
|
|
cdb0f608e0 | ||
|
|
3685013ca6 | ||
|
|
f00f1da349 | ||
|
|
08c860c02a | ||
|
|
a8360bf228 | ||
|
|
4f38ebb85d | ||
|
|
9b85453ed0 |
230
.github/scripts/Connect-SimplySign-Enhanced.ps1
vendored
Normal file
230
.github/scripts/Connect-SimplySign-Enhanced.ps1
vendored
Normal 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"
|
||||||
252
.github/scripts/configure-simplysign-registry.ps1
vendored
Normal file
252
.github/scripts/configure-simplysign-registry.ps1
vendored
Normal 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
112
.github/scripts/install-simplysign.sh
vendored
Normal 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
450
.github/workflows/build-test.yml
vendored
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
name: 'Build Test - All Platforms'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
test_version:
|
||||||
|
description: 'Test version (e.g., 1.0.0-test). Leave empty to use package.json version with -test suffix'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions: write-all
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
# macOS signing and notarization (optional for testing)
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
# Certum cloud code signing for Windows
|
||||||
|
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
|
||||||
|
CERTUM_USERNAME: ${{ secrets.CERTUM_USERNAME }}
|
||||||
|
CERTUM_CERTIFICATE_SHA1: ${{ secrets.CERTUM_CERTIFICATE_SHA1 }}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare:
|
||||||
|
name: Prepare Build Information
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
build-date: ${{ steps.version.outputs.build-date }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 'lts/*'
|
||||||
|
|
||||||
|
- name: Calculate test version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
# If manual test version is provided, use it
|
||||||
|
if [ -n "${{ inputs.test_version }}" ]; then
|
||||||
|
TEST_VERSION="${{ inputs.test_version }}"
|
||||||
|
echo "Using manual test version: $TEST_VERSION"
|
||||||
|
echo "version=$TEST_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
# Use package.json version with test suffix
|
||||||
|
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
TEST_VERSION="$CURRENT_VERSION-test"
|
||||||
|
echo "Using auto test version: $TEST_VERSION"
|
||||||
|
echo "version=$TEST_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add build date for reference
|
||||||
|
BUILD_DATE=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
echo "build-date=$BUILD_DATE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
needs: prepare
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
platform: windows
|
||||||
|
arch: x64
|
||||||
|
- os: windows-latest
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
platform: windows
|
||||||
|
arch: arm64
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
platform: macos
|
||||||
|
arch: arm64
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
platform: macos
|
||||||
|
arch: x64
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
platform: linux
|
||||||
|
arch: x64
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
save-if: false
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
uses: borales/actions-yarn@v5
|
||||||
|
with:
|
||||||
|
cmd: install
|
||||||
|
|
||||||
|
- name: Prebuild and check
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
yarn run prebuild:dev --target=${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Update version for test build (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
# Update package.json
|
||||||
|
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
|
||||||
|
|
||||||
|
# Update Cargo.toml
|
||||||
|
(Get-Content src-tauri/Cargo.toml) -replace '^version = ".*"', 'version = "${{ needs.prepare.outputs.version }}"' | Set-Content src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
# Update tauri.conf.json
|
||||||
|
(Get-Content src-tauri/tauri.conf.json) -replace '"version": ".*"', '"version": "${{ needs.prepare.outputs.version }}"' | Set-Content src-tauri/tauri.conf.json
|
||||||
|
|
||||||
|
- name: Update version for test build (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Update package.json
|
||||||
|
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
|
||||||
|
|
||||||
|
# Update Cargo.toml
|
||||||
|
sed -i '' 's/^version = "[^"]*"/version = "${{ needs.prepare.outputs.version }}"/' src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
# Update tauri.conf.json
|
||||||
|
sed -i '' 's/"version": "[^"]*"/"version": "${{ needs.prepare.outputs.version }}"/' src-tauri/tauri.conf.json
|
||||||
|
|
||||||
|
- name: Update version for test build (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Update package.json
|
||||||
|
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
|
||||||
|
|
||||||
|
# Update Cargo.toml
|
||||||
|
sed -i 's/^version = "[^"]*"/version = "${{ needs.prepare.outputs.version }}"/' src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
# Update tauri.conf.json
|
||||||
|
sed -i 's/"version": "[^"]*"/"version": "${{ needs.prepare.outputs.version }}"/' src-tauri/tauri.conf.json
|
||||||
|
|
||||||
|
- name: Import Apple Developer Certificate (macOS only)
|
||||||
|
if: matrix.os == 'macos-latest' && env.APPLE_CERTIFICATE != ''
|
||||||
|
uses: apple-actions/import-codesign-certs@v5
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Setup Certum Code Signing (Windows)
|
||||||
|
if: matrix.platform == 'windows'
|
||||||
|
run: |
|
||||||
|
echo "=== SETTING UP CERTUM CODE SIGNING FOR WINDOWS ==="
|
||||||
|
echo "Installing SimplySign Desktop and configuring for automatic authentication"
|
||||||
|
|
||||||
|
# Install SimplySign Desktop
|
||||||
|
chmod +x ./.github/scripts/install-simplysign.sh
|
||||||
|
./.github/scripts/install-simplysign.sh
|
||||||
|
|
||||||
|
# Configure registry for auto-login dialog
|
||||||
|
echo "Configuring registry for automatic login dialog..."
|
||||||
|
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign-registry.ps1"
|
||||||
|
|
||||||
|
echo "Certum signing environment ready"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Authenticate Certum (Windows)
|
||||||
|
if: matrix.platform == 'windows'
|
||||||
|
env:
|
||||||
|
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
|
||||||
|
CERTUM_USERNAME: ${{ secrets.CERTUM_USERNAME }}
|
||||||
|
run: |
|
||||||
|
echo "=== CERTUM AUTHENTICATION ==="
|
||||||
|
echo "Authenticating with Certum cloud certificate using TOTP"
|
||||||
|
|
||||||
|
# Authenticate with Certum using our enhanced script
|
||||||
|
powershell -ExecutionPolicy Bypass -File "./.github/scripts/Connect-SimplySign-Enhanced.ps1"
|
||||||
|
|
||||||
|
echo "Authentication completed"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Configure Certum Certificate Thumbprint (Windows)
|
||||||
|
if: matrix.platform == 'windows'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "=== CONFIGURING CERTUM CERTIFICATE THUMBPRINT ==="
|
||||||
|
CONFIG_PATH="src-tauri/tauri.windows.conf.json"
|
||||||
|
THUMBPRINT="${{ secrets.CERTUM_CERTIFICATE_SHA1 }}"
|
||||||
|
|
||||||
|
# Update the certificateThumbprint field using jq
|
||||||
|
jq --arg thumbprint "$THUMBPRINT" '.bundle.windows.certificateThumbprint = $thumbprint' "$CONFIG_PATH" > tmp.$$ && mv tmp.$$ "$CONFIG_PATH"
|
||||||
|
|
||||||
|
echo "Certificate thumbprint configured: $THUMBPRINT"
|
||||||
|
|
||||||
|
- name: Build the app
|
||||||
|
if: matrix.platform == 'windows' || matrix.platform == 'linux'
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build the app (macOS)
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
if: matrix.platform == 'macos'
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
# macOS signing and notarization environment variables
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
TAURI_SKIP_SIDECAR_SIGNATURE_CHECK: "true"
|
||||||
|
with:
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.platform }}-${{ matrix.arch }}-build
|
||||||
|
path: |
|
||||||
|
src-tauri/target/${{ matrix.target }}/release/bundle/**/*
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
build-linux-arm:
|
||||||
|
name: Build Linux ARM
|
||||||
|
needs: prepare
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
arch: arm64
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: armv7-unknown-linux-gnueabihf
|
||||||
|
arch: armhf
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
save-if: false
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
uses: borales/actions-yarn@v5
|
||||||
|
with:
|
||||||
|
cmd: install
|
||||||
|
|
||||||
|
- name: Prebuild and check
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
yarn run prebuild:dev --target=${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Update version for test build
|
||||||
|
run: |
|
||||||
|
# Update package.json
|
||||||
|
yarn version --new-version ${{ needs.prepare.outputs.version }} --no-git-tag-version
|
||||||
|
|
||||||
|
# Update Cargo.toml
|
||||||
|
sed -i 's/^version = "[^"]*"/version = "${{ needs.prepare.outputs.version }}"/' src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
# Update tauri.conf.json
|
||||||
|
sed -i 's/"version": "[^"]*"/"version": "${{ needs.prepare.outputs.version }}"/' src-tauri/tauri.conf.json
|
||||||
|
|
||||||
|
- name: Setup for Linux ARM cross-compilation
|
||||||
|
run: |-
|
||||||
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
|
cat > /tmp/sources.list << EOF
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
|
||||||
|
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
|
||||||
|
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||||
|
|
||||||
|
sudo dpkg --add-architecture ${{ matrix.arch }}
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
sudo apt install -y \
|
||||||
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
|
libssl-dev:${{ matrix.arch }} \
|
||||||
|
patchelf:${{ matrix.arch }} \
|
||||||
|
librsvg2-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: Install aarch64 tools
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||||
|
run: |
|
||||||
|
sudo apt install -y \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
|
- name: Install armv7 tools
|
||||||
|
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||||
|
run: |
|
||||||
|
sudo apt install -y \
|
||||||
|
gcc-arm-linux-gnueabihf \
|
||||||
|
g++-arm-linux-gnueabihf
|
||||||
|
|
||||||
|
- name: Build for Linux ARM
|
||||||
|
run: |
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
|
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
||||||
|
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
|
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
|
||||||
|
fi
|
||||||
|
yarn build --target ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload Linux ARM artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-${{ matrix.target }}-build
|
||||||
|
path: |
|
||||||
|
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
summary:
|
||||||
|
name: Build Summary
|
||||||
|
needs: [prepare, build, build-linux-arm]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Create build summary
|
||||||
|
run: |
|
||||||
|
echo "# Build Test Summary" > build-summary.md
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> build-summary.md
|
||||||
|
echo "**Build Date:** ${{ needs.prepare.outputs.build-date }}" >> build-summary.md
|
||||||
|
echo "**Commit:** ${{ github.sha }}" >> build-summary.md
|
||||||
|
echo "**Branch:** ${{ github.ref_name }}" >> build-summary.md
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
echo "## Build Status" >> build-summary.md
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
|
||||||
|
# Check build status
|
||||||
|
if [ "${{ needs.build.result }}" == "success" ]; then
|
||||||
|
echo "✅ **Main platforms build:** Success" >> build-summary.md
|
||||||
|
else
|
||||||
|
echo "❌ **Main platforms build:** Failed" >> build-summary.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.build-linux-arm.result }}" == "success" ]; then
|
||||||
|
echo "✅ **Linux ARM build:** Success" >> build-summary.md
|
||||||
|
else
|
||||||
|
echo "❌ **Linux ARM build:** Failed" >> build-summary.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
echo "## Available Artifacts" >> build-summary.md
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
echo "The following build artifacts are available for download from the Actions tab:" >> build-summary.md
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
|
||||||
|
# List artifacts
|
||||||
|
if [ -d "artifacts" ]; then
|
||||||
|
find artifacts -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.pkg" -o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | while read file; do
|
||||||
|
echo "- $(basename "$file")" >> build-summary.md
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
echo "## Testing Instructions" >> build-summary.md
|
||||||
|
echo "" >> build-summary.md
|
||||||
|
echo "1. Download the appropriate artifact for your platform from the GitHub Actions page" >> build-summary.md
|
||||||
|
echo "2. Extract and install the application" >> build-summary.md
|
||||||
|
echo "3. Test the functionality" >> build-summary.md
|
||||||
|
echo "4. Report any issues found" >> build-summary.md
|
||||||
|
|
||||||
|
- name: Upload build summary
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-summary
|
||||||
|
path: build-summary.md
|
||||||
|
retention-days: 30
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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
|
||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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 }}
|
||||||
|
|
||||||
|
|||||||
114
.github/workflows/release.yml
vendored
114
.github/workflows/release.yml
vendored
@@ -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' }}
|
||||||
@@ -317,8 +322,8 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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 }}
|
||||||
@@ -341,7 +346,7 @@ jobs:
|
|||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
|
||||||
- name: Run install
|
- name: Run install
|
||||||
uses: borales/actions-yarn@v4
|
uses: borales/actions-yarn@v5
|
||||||
with:
|
with:
|
||||||
cmd: install
|
cmd: install
|
||||||
- name: install and check
|
- name: install and check
|
||||||
@@ -390,11 +395,56 @@ jobs:
|
|||||||
|
|
||||||
- name: Import Apple Developer Certificate (macOS only)
|
- name: Import Apple Developer Certificate (macOS only)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
uses: apple-actions/import-codesign-certs@v2
|
uses: apple-actions/import-codesign-certs@v5
|
||||||
with:
|
with:
|
||||||
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: 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'
|
||||||
|
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.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
|
||||||
env:
|
env:
|
||||||
@@ -437,8 +487,8 @@ jobs:
|
|||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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 }}
|
||||||
@@ -455,7 +505,7 @@ jobs:
|
|||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
|
||||||
- name: Run install
|
- name: Run install
|
||||||
uses: borales/actions-yarn@v4
|
uses: borales/actions-yarn@v5
|
||||||
with:
|
with:
|
||||||
cmd: install
|
cmd: install
|
||||||
|
|
||||||
@@ -573,7 +623,7 @@ jobs:
|
|||||||
path: arm-artifacts
|
path: arm-artifacts
|
||||||
|
|
||||||
- name: Update release with ARM artifacts
|
- name: Update release with ARM artifacts
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.changelog.outputs.tag }}
|
tag_name: ${{ needs.changelog.outputs.tag }}
|
||||||
name: 'OpenList Desktop ${{ needs.changelog.outputs.tag }}'
|
name: 'OpenList Desktop ${{ needs.changelog.outputs.tag }}'
|
||||||
@@ -585,3 +635,51 @@ jobs:
|
|||||||
arm-artifacts/**/*.rpm
|
arm-artifacts/**/*.rpm
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
winget-submit:
|
||||||
|
name: Submit to WinGet
|
||||||
|
needs: [publish, changelog, auto-version]
|
||||||
|
runs-on: windows-latest
|
||||||
|
if: always() && needs.publish.result == 'success'
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get release version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
$version = "${{ needs.auto-version.outputs.version }}"
|
||||||
|
echo "version=$version" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Download WinGet Create CLI
|
||||||
|
run: |
|
||||||
|
Write-Host "Downloading wingetcreate CLI..."
|
||||||
|
$url = "https://aka.ms/wingetcreate/latest"
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile "wingetcreate.exe"
|
||||||
|
Write-Host "Downloaded wingetcreate.exe"
|
||||||
|
|
||||||
|
- name: Update WinGet package manifest
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
$version = "${{ steps.version.outputs.version }}"
|
||||||
|
# URLs for both x64 and arm64 installers
|
||||||
|
$x64InstallerUrl = "https://github.com/${{ github.repository }}/releases/download/v$version/OpenList.Desktop_$version`_x64-setup.exe"
|
||||||
|
$arm64InstallerUrl = "https://github.com/${{ github.repository }}/releases/download/v$version/OpenList.Desktop_$version`_arm64-setup.exe"
|
||||||
|
|
||||||
|
Write-Host "Updating WinGet package for version: $version"
|
||||||
|
Write-Host "x64 Installer URL: $x64InstallerUrl"
|
||||||
|
Write-Host "arm64 Installer URL: $arm64InstallerUrl"
|
||||||
|
|
||||||
|
Write-Host "Attempting to update existing package..."
|
||||||
|
./wingetcreate.exe update OpenListTeam.OpenListDesktop `
|
||||||
|
--version $version `
|
||||||
|
--urls $x64InstallerUrl $arm64InstallerUrl `
|
||||||
|
--token $env:GITHUB_TOKEN `
|
||||||
|
--submit
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "First submit, will do manually..."
|
||||||
|
} else {
|
||||||
|
Write-Host "Successfully updated existing WinGet package"
|
||||||
|
}
|
||||||
|
|||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
18
.winget/metadata.yml
Normal file
18
.winget/metadata.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
PackageIdentifier: OpenListTeam.OpenListDesktop
|
||||||
|
PackageName: OpenList Desktop
|
||||||
|
Publisher: OpenList Team
|
||||||
|
PackageUrl: https://github.com/OpenListTeam/OpenList-Desktop
|
||||||
|
Copyright: Copyright (c) 2025 OpenList Team
|
||||||
|
License: GPL-3.0
|
||||||
|
LicenseUrl: https://github.com/OpenListTeam/OpenList-Desktop/blob/main/LICENSE
|
||||||
|
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
|
||||||
61
README.md
61
README.md
@@ -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. **教程**:完成交互式教程以学习关键功能
|
|
||||||
|
|
||||||
### 基本操作
|
### 基本操作
|
||||||
|
|
||||||
@@ -216,9 +223,9 @@ yarn run tauri build
|
|||||||
|
|
||||||
添加自定义 Rclone 标志以获得最佳性能:
|
添加自定义 Rclone 标志以获得最佳性能:
|
||||||
|
|
||||||
- `--vfs-cache-mode full`:启用完整 VFS 缓存
|
- `--vfs-cache-mode=full`:启用完整 VFS 缓存
|
||||||
- `--buffer-size 256M`:增加缓冲区大小
|
- `--buffer-size=256M`:增加缓冲区大小
|
||||||
- `--transfers 10`:并发传输限制
|
- `--transfers=10`:并发传输限制
|
||||||
|
|
||||||
#### 系统托盘操作
|
#### 系统托盘操作
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
59
README_en.md
59
README_en.md
@@ -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
|
||||||
|
|
||||||
@@ -216,9 +223,9 @@ Dashboard → Quick Actions → Start Rclone Backend
|
|||||||
|
|
||||||
Add custom Rclone flags for optimal performance:
|
Add custom Rclone flags for optimal performance:
|
||||||
|
|
||||||
- `--vfs-cache-mode full`: Enable full VFS caching
|
- `--vfs-cache-mode=full`: Enable full VFS caching
|
||||||
- `--buffer-size 256M`: Increase buffer size
|
- `--buffer-size=256M`: Increase buffer size
|
||||||
- `--transfers 10`: Concurrent transfer limit
|
- `--transfers=10`: Concurrent transfer limit
|
||||||
|
|
||||||
#### System Tray Operations
|
#### System Tray 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
63
package.json
63
package.json
@@ -9,7 +9,7 @@
|
|||||||
"tauri"
|
"tauri"
|
||||||
],
|
],
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.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.8",
|
"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.35.1",
|
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||||
"@typescript-eslint/parser": "^8.35.1",
|
"@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.35.1",
|
"typescript-eslint": "^8.41.0",
|
||||||
"vite": "^7.0.0",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,288 +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 = []
|
||||||
keys.forEach(key => {
|
|
||||||
usage[key] = {
|
keys.forEach(key => {
|
||||||
used: false,
|
usage[key] = {
|
||||||
files: [],
|
used: false,
|
||||||
patterns: []
|
files: [],
|
||||||
}
|
patterns: [],
|
||||||
})
|
dynamicMatch: false
|
||||||
|
}
|
||||||
console.log(colorize('\n🔍 Searching for key usage in source files...', 'blue'))
|
})
|
||||||
|
|
||||||
const sourceFiles = findFiles(SRC_DIR)
|
console.log(colorize('\n🔍 Searching for key usage in source files...', 'blue'))
|
||||||
|
|
||||||
console.log(` Found ${sourceFiles.length} source files to analyze`)
|
const sourceFiles = findFiles(SRC_DIR)
|
||||||
|
|
||||||
const searchPatterns = [
|
console.log(` Found ${sourceFiles.length} source files to analyze`)
|
||||||
/\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
||||||
/(?:^|[^a-zA-Z])t\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
const searchPatterns = [
|
||||||
/\{\{\s*\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g
|
/\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
||||||
]
|
/(?:^|[^a-zA-Z])t\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
||||||
|
/\{\{\s*\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g
|
||||||
sourceFiles.forEach(filePath => {
|
]
|
||||||
try {
|
|
||||||
const content = readFileSync(filePath, 'utf8')
|
const dynamicPattern = /\$?t\s*\(\s*`([^`]*\$\{[^}]+\}[^`]*)`/g
|
||||||
const relativePath = relative(join(__dirname, '..'), filePath)
|
|
||||||
|
sourceFiles.forEach(filePath => {
|
||||||
searchPatterns.forEach((pattern, patternIndex) => {
|
try {
|
||||||
let match
|
const content = readFileSync(filePath, 'utf8')
|
||||||
while ((match = pattern.exec(content)) !== null) {
|
const relativePath = relative(join(__dirname, '..'), filePath)
|
||||||
const key = match[1]
|
|
||||||
if (usage[key]) {
|
searchPatterns.forEach((pattern, patternIndex) => {
|
||||||
usage[key].used = true
|
let match
|
||||||
if (!usage[key].files.includes(relativePath)) {
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
usage[key].files.push(relativePath)
|
const key = match[1]
|
||||||
}
|
if (usage[key]) {
|
||||||
if (!usage[key].patterns.includes(patternIndex)) {
|
usage[key].used = true
|
||||||
usage[key].patterns.push(patternIndex)
|
if (!usage[key].files.includes(relativePath)) {
|
||||||
}
|
usage[key].files.push(relativePath)
|
||||||
}
|
}
|
||||||
}
|
if (!usage[key].patterns.includes(patternIndex)) {
|
||||||
})
|
usage[key].patterns.push(patternIndex)
|
||||||
} catch (error) {
|
}
|
||||||
console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red'))
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return usage
|
let dynamicMatch
|
||||||
}
|
while ((dynamicMatch = dynamicPattern.exec(content)) !== null) {
|
||||||
|
const templateString = dynamicMatch[1]
|
||||||
function findLocaleInconsistencies(localeData) {
|
|
||||||
const locales = Object.keys(localeData)
|
const staticParts = templateString.split(/\$\{[^}]+\}/)
|
||||||
const inconsistencies = {}
|
|
||||||
|
const patternInfo = {
|
||||||
if (locales.length < 2) {
|
template: templateString,
|
||||||
return inconsistencies
|
file: relativePath,
|
||||||
}
|
staticParts
|
||||||
|
}
|
||||||
locales.forEach(locale => {
|
|
||||||
const currentKeys = new Set(localeData[locale].keys)
|
if (!dynamicPatterns.some(p => p.template === templateString && p.file === relativePath)) {
|
||||||
inconsistencies[locale] = {
|
dynamicPatterns.push(patternInfo)
|
||||||
missing: [],
|
}
|
||||||
extra: []
|
|
||||||
}
|
keys.forEach(key => {
|
||||||
|
if (matchesDynamicPattern(key, staticParts)) {
|
||||||
locales.forEach(otherLocale => {
|
if (usage[key]) {
|
||||||
if (locale !== otherLocale) {
|
usage[key].used = true
|
||||||
localeData[otherLocale].keys.forEach(key => {
|
usage[key].dynamicMatch = true
|
||||||
if (!currentKeys.has(key) && !inconsistencies[locale].missing.includes(key)) {
|
if (!usage[key].files.includes(relativePath)) {
|
||||||
inconsistencies[locale].missing.push(key)
|
usage[key].files.push(relativePath)
|
||||||
}
|
}
|
||||||
})
|
if (!usage[key].patterns.includes('dynamic')) {
|
||||||
}
|
usage[key].patterns.push('dynamic')
|
||||||
})
|
}
|
||||||
|
}
|
||||||
localeData[locale].keys.forEach(key => {
|
}
|
||||||
const existsInOthers = locales.some(
|
})
|
||||||
otherLocale => locale !== otherLocale && localeData[otherLocale].keys.includes(key)
|
}
|
||||||
)
|
} catch (error) {
|
||||||
if (!existsInOthers) {
|
console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red'))
|
||||||
inconsistencies[locale].extra.push(key)
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
})
|
usage._dynamicPatterns = dynamicPatterns
|
||||||
|
|
||||||
return inconsistencies
|
return usage
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function matchesDynamicPattern(key, staticParts) {
|
||||||
console.log(colorize('🌐 OpenList Desktop - I18n Usage Analyzer', 'cyan'))
|
if (staticParts.length === 0) return false
|
||||||
console.log(colorize('==========================================', 'cyan'))
|
|
||||||
|
let keyIndex = 0
|
||||||
const { allKeys, localeData } = getAllI18nKeys()
|
|
||||||
|
for (let i = 0; i < staticParts.length; i++) {
|
||||||
console.log(colorize(`\n📊 Total unique keys found: ${allKeys.length}`, 'yellow'))
|
const part = staticParts[i]
|
||||||
const usage = findKeyUsage(allKeys)
|
|
||||||
const usedKeys = allKeys.filter(key => usage[key].used)
|
if (part === '') {
|
||||||
const unusedKeys = allKeys.filter(key => !usage[key].used)
|
if (i < staticParts.length - 1) {
|
||||||
|
const nextPart = staticParts[i + 1]
|
||||||
const inconsistencies = findLocaleInconsistencies(localeData)
|
if (nextPart) {
|
||||||
|
const nextIndex = key.indexOf(nextPart, keyIndex)
|
||||||
console.log(colorize('\n📈 Usage Summary:', 'blue'))
|
if (nextIndex === -1) return false
|
||||||
console.log(` ${colorize('✓', 'green')} Used keys: ${usedKeys.length}`)
|
keyIndex = nextIndex
|
||||||
console.log(` ${colorize('✗', 'red')} Unused keys: ${unusedKeys.length}`)
|
}
|
||||||
console.log(` ${colorize('📊', 'yellow')} Usage rate: ${((usedKeys.length / allKeys.length) * 100).toFixed(1)}%`)
|
}
|
||||||
|
continue
|
||||||
if (unusedKeys.length > 0) {
|
}
|
||||||
console.log(colorize('\n🗑️ Unused I18n Keys:', 'red'))
|
|
||||||
console.log(colorize('====================', 'red'))
|
if (i === 0) {
|
||||||
|
if (!key.startsWith(part)) return false
|
||||||
const groupedUnused = {}
|
keyIndex = part.length
|
||||||
unusedKeys.forEach(key => {
|
} else if (i === staticParts.length - 1) {
|
||||||
const namespace = key.split('.')[0]
|
if (part && !key.endsWith(part)) return false
|
||||||
if (!groupedUnused[namespace]) {
|
} else {
|
||||||
groupedUnused[namespace] = []
|
const index = key.indexOf(part, keyIndex)
|
||||||
}
|
if (index === -1) return false
|
||||||
groupedUnused[namespace].push(key)
|
keyIndex = index + part.length
|
||||||
})
|
}
|
||||||
|
}
|
||||||
Object.entries(groupedUnused).forEach(([namespace, keys]) => {
|
|
||||||
console.log(colorize(`\n[${namespace}] - ${keys.length} unused keys:`, 'yellow'))
|
return true
|
||||||
keys.forEach(key => {
|
}
|
||||||
console.log(` ${colorize('✗', 'red')} ${key}`)
|
|
||||||
})
|
function findLocaleInconsistencies(localeData) {
|
||||||
})
|
const locales = Object.keys(localeData)
|
||||||
} else {
|
const inconsistencies = {}
|
||||||
console.log(colorize('\n🎉 No unused keys found! All i18n keys are being used.', 'green'))
|
|
||||||
}
|
if (locales.length < 2) {
|
||||||
|
return inconsistencies
|
||||||
const hasInconsistencies = Object.values(inconsistencies).some(inc => inc.missing.length > 0 || inc.extra.length > 0)
|
}
|
||||||
|
|
||||||
if (hasInconsistencies) {
|
locales.forEach(locale => {
|
||||||
console.log(colorize('\n⚠️ Locale Inconsistencies:', 'yellow'))
|
const currentKeys = new Set(localeData[locale].keys)
|
||||||
console.log(colorize('=========================', 'yellow'))
|
inconsistencies[locale] = {
|
||||||
|
missing: [],
|
||||||
Object.entries(inconsistencies).forEach(([locale, data]) => {
|
extra: []
|
||||||
if (data.missing.length > 0 || data.extra.length > 0) {
|
}
|
||||||
console.log(colorize(`\n[${locale}.json]:`, 'cyan'))
|
|
||||||
|
locales.forEach(otherLocale => {
|
||||||
if (data.missing.length > 0) {
|
if (locale !== otherLocale) {
|
||||||
console.log(colorize(` Missing ${data.missing.length} keys:`, 'red'))
|
localeData[otherLocale].keys.forEach(key => {
|
||||||
data.missing.forEach(key => {
|
if (!currentKeys.has(key) && !inconsistencies[locale].missing.includes(key)) {
|
||||||
console.log(` ${colorize('✗', 'red')} ${key}`)
|
inconsistencies[locale].missing.push(key)
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
}
|
||||||
if (data.extra.length > 0) {
|
})
|
||||||
console.log(colorize(` Extra ${data.extra.length} keys:`, 'blue'))
|
|
||||||
data.extra.forEach(key => {
|
localeData[locale].keys.forEach(key => {
|
||||||
console.log(` ${colorize('!', 'blue')} ${key}`)
|
const existsInOthers = locales.some(
|
||||||
})
|
otherLocale => locale !== otherLocale && localeData[otherLocale].keys.includes(key)
|
||||||
}
|
)
|
||||||
}
|
if (!existsInOthers) {
|
||||||
})
|
inconsistencies[locale].extra.push(key)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if (process.argv.includes('--verbose') || process.argv.includes('-v')) {
|
})
|
||||||
console.log(colorize('\n📋 Sample Used Keys (first 10):', 'blue'))
|
|
||||||
console.log(colorize('=================================', 'blue'))
|
return inconsistencies
|
||||||
|
}
|
||||||
usedKeys.slice(0, 10).forEach(key => {
|
|
||||||
const files = usage[key].files.slice(0, 3) // Show first 3 files
|
function main() {
|
||||||
const moreFiles = usage[key].files.length > 3 ? ` (+${usage[key].files.length - 3} more)` : ''
|
console.log(colorize('🌐 OpenList Desktop - I18n Usage Analyzer', 'cyan'))
|
||||||
console.log(` ${colorize('✓', 'green')} ${key}`)
|
console.log(colorize('==========================================', 'cyan'))
|
||||||
console.log(` Used in: ${files.join(', ')}${moreFiles}`)
|
|
||||||
})
|
const { allKeys, localeData } = getAllI18nKeys()
|
||||||
}
|
|
||||||
|
console.log(colorize(`\n📊 Total unique keys found: ${allKeys.length}`, 'yellow'))
|
||||||
console.log(colorize('\n✨ Analysis complete!', 'cyan'))
|
const usage = findKeyUsage(allKeys)
|
||||||
|
const dynamicPatterns = usage._dynamicPatterns || []
|
||||||
if (unusedKeys.length > 0) {
|
delete usage._dynamicPatterns
|
||||||
console.log(colorize('\n💡 Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue'))
|
|
||||||
}
|
const usedKeys = allKeys.filter(key => usage[key].used)
|
||||||
}
|
const unusedKeys = allKeys.filter(key => !usage[key].used)
|
||||||
|
const dynamicallyUsedKeys = usedKeys.filter(key => usage[key].dynamicMatch)
|
||||||
main()
|
const staticUsedKeys = usedKeys.filter(key => !usage[key].dynamicMatch)
|
||||||
|
|
||||||
|
const inconsistencies = findLocaleInconsistencies(localeData)
|
||||||
|
|
||||||
|
console.log(colorize('\n📈 Usage Summary:', 'blue'))
|
||||||
|
console.log(` ${colorize('✓', 'green')} Used keys: ${usedKeys.length}`)
|
||||||
|
console.log(` ${colorize('→', 'cyan')} Static usage: ${staticUsedKeys.length}`)
|
||||||
|
console.log(` ${colorize('→', 'magenta')} Dynamic usage: ${dynamicallyUsedKeys.length}`)
|
||||||
|
console.log(` ${colorize('✗', 'red')} Unused keys: ${unusedKeys.length}`)
|
||||||
|
console.log(` ${colorize('📊', 'yellow')} Usage rate: ${((usedKeys.length / allKeys.length) * 100).toFixed(1)}%`)
|
||||||
|
|
||||||
|
if (dynamicPatterns.length > 0) {
|
||||||
|
console.log(colorize('\n🔮 Dynamic I18n Patterns Detected:', 'magenta'))
|
||||||
|
console.log(colorize('===================================', 'magenta'))
|
||||||
|
|
||||||
|
dynamicPatterns.forEach((pattern, index) => {
|
||||||
|
console.log(colorize(`\n${index + 1}. Template: \`${pattern.template}\``, 'cyan'))
|
||||||
|
console.log(` File: ${pattern.file}`)
|
||||||
|
console.log(` Static parts: [${pattern.staticParts.map(p => `"${p}"`).join(', ')}]`)
|
||||||
|
|
||||||
|
const matchingKeys = allKeys.filter(key => matchesDynamicPattern(key, pattern.staticParts))
|
||||||
|
if (matchingKeys.length > 0) {
|
||||||
|
console.log(
|
||||||
|
` ${colorize('Matches', 'green')} (${matchingKeys.length}): ${matchingKeys.slice(0, 5).join(', ')}${
|
||||||
|
matchingKeys.length > 5 ? '...' : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unusedKeys.length > 0) {
|
||||||
|
console.log(colorize('\n🗑️ Unused I18n Keys:', 'red'))
|
||||||
|
console.log(colorize('====================', 'red'))
|
||||||
|
|
||||||
|
const groupedUnused = {}
|
||||||
|
unusedKeys.forEach(key => {
|
||||||
|
const namespace = key.split('.')[0]
|
||||||
|
if (!groupedUnused[namespace]) {
|
||||||
|
groupedUnused[namespace] = []
|
||||||
|
}
|
||||||
|
groupedUnused[namespace].push(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(groupedUnused).forEach(([namespace, keys]) => {
|
||||||
|
console.log(colorize(`\n[${namespace}] - ${keys.length} unused keys:`, 'yellow'))
|
||||||
|
keys.forEach(key => {
|
||||||
|
console.log(` ${colorize('✗', 'red')} ${key}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (hasInconsistencies) {
|
||||||
|
console.log(colorize('\n⚠️ Locale Inconsistencies:', 'yellow'))
|
||||||
|
console.log(colorize('=========================', 'yellow'))
|
||||||
|
|
||||||
|
Object.entries(inconsistencies).forEach(([locale, data]) => {
|
||||||
|
if (data.missing.length > 0 || data.extra.length > 0) {
|
||||||
|
console.log(colorize(`\n[${locale}.json]:`, 'cyan'))
|
||||||
|
|
||||||
|
if (data.missing.length > 0) {
|
||||||
|
console.log(colorize(` Missing ${data.missing.length} keys:`, 'red'))
|
||||||
|
data.missing.forEach(key => {
|
||||||
|
console.log(` ${colorize('✗', 'red')} ${key}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.extra.length > 0) {
|
||||||
|
console.log(colorize(` Extra ${data.extra.length} keys:`, 'blue'))
|
||||||
|
data.extra.forEach(key => {
|
||||||
|
console.log(` ${colorize('!', 'blue')} ${key}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.includes('--verbose') || process.argv.includes('-v')) {
|
||||||
|
console.log(colorize('\n📋 Sample Used Keys (first 10):', 'blue'))
|
||||||
|
console.log(colorize('=================================', 'blue'))
|
||||||
|
|
||||||
|
usedKeys.slice(0, 10).forEach(key => {
|
||||||
|
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 usageType = usage[key].dynamicMatch ? colorize('(dynamic)', 'magenta') : colorize('(static)', 'cyan')
|
||||||
|
console.log(` ${colorize('✓', 'green')} ${key} ${usageType}`)
|
||||||
|
console.log(` Used in: ${files.join(', ')}${moreFiles}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dynamicallyUsedKeys.length > 0) {
|
||||||
|
console.log(colorize('\n🔮 Dynamic Key Usage Details:', 'magenta'))
|
||||||
|
console.log(colorize('=============================', 'magenta'))
|
||||||
|
|
||||||
|
dynamicallyUsedKeys.slice(0, 5).forEach(key => {
|
||||||
|
const files = usage[key].files.slice(0, 2)
|
||||||
|
console.log(` ${colorize('✨', 'magenta')} ${key}`)
|
||||||
|
console.log(` Files: ${files.join(', ')}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dynamicallyUsedKeys.length > 5) {
|
||||||
|
console.log(` ... and ${dynamicallyUsedKeys.length - 5} more dynamic keys`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(colorize('\n✨ Analysis complete!', 'cyan'))
|
||||||
|
|
||||||
|
if (unusedKeys.length > 0) {
|
||||||
|
console.log(colorize('\n💡 Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -81,24 +82,29 @@ const getServiceInfo = exeName => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimpleSC.dll
|
const resolvePlugins = async () => {
|
||||||
const resolvePlugin = async () => {
|
const pluginDir = path.join(process.env.APPDATA, 'Local/NSIS')
|
||||||
|
await fs.mkdir(pluginDir, { recursive: true })
|
||||||
|
await resolveSimpleServicePlugin(pluginDir)
|
||||||
|
await resolveAccessControlPlugin(pluginDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveSimpleServicePlugin = async pluginDir => {
|
||||||
const url = 'https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip'
|
const url = 'https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip'
|
||||||
const TEMP_DIR = path.join(cwd, 'temp')
|
const TEMP_DIR = path.join(cwd, 'temp')
|
||||||
const tempDir = path.join(TEMP_DIR, 'SimpleSC')
|
const tempDir = path.join(TEMP_DIR, 'SimpleSC')
|
||||||
const tempZip = path.join(tempDir, 'NSIS_Simple_Service_Plugin_Unicode_1.30.zip')
|
const tempZip = path.join(tempDir, 'NSIS_Simple_Service_Plugin_Unicode_1.30.zip')
|
||||||
const tempDll = path.join(tempDir, 'SimpleSC.dll')
|
const tempDll = path.join(tempDir, 'SimpleSC.dll')
|
||||||
const pluginDir = path.join(process.env.APPDATA, 'Local/NSIS')
|
|
||||||
const pluginPath = path.join(pluginDir, 'SimpleSC.dll')
|
const pluginPath = path.join(pluginDir, 'SimpleSC.dll')
|
||||||
await fs.mkdir(pluginDir, { recursive: true })
|
|
||||||
await fs.mkdir(tempDir, { recursive: true })
|
await fs.mkdir(tempDir, { recursive: true })
|
||||||
if (fs.existsSync(pluginPath)) return
|
if (fs.existsSync(pluginPath)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(tempZip)) {
|
if (!fs.existsSync(tempZip)) {
|
||||||
await downloadFile(url, tempZip)
|
await downloadFile(url, tempZip)
|
||||||
}
|
}
|
||||||
const zip = new AdmZip(tempZip)
|
const zip = new AdmZip(tempZip)
|
||||||
|
|
||||||
zip.extractAllTo(tempDir, true)
|
zip.extractAllTo(tempDir, true)
|
||||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true })
|
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true })
|
||||||
console.log(`SimpleSC.dll copied to ${pluginPath}`)
|
console.log(`SimpleSC.dll copied to ${pluginPath}`)
|
||||||
@@ -107,6 +113,60 @@ const resolvePlugin = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calculateSha256 = async filePath => {
|
||||||
|
const hash = crypto.createHash('sha256')
|
||||||
|
const fileStream = fs.createReadStream(filePath)
|
||||||
|
fileStream.on('data', chunk => hash.update(chunk))
|
||||||
|
fileStream.on('end', () => {
|
||||||
|
const digest = hash.digest('hex')
|
||||||
|
console.log(`SHA-256 hash of ${filePath}: ${digest}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveAccessControlPlugin = async pluginDir => {
|
||||||
|
const url = 'https://nsis.sourceforge.io/mediawiki/images/4/4a/AccessControl.zip'
|
||||||
|
const TEMP_DIR = path.join(cwd, 'temp')
|
||||||
|
const tempDir = path.join(TEMP_DIR, 'AccessControl')
|
||||||
|
const tempZip = path.join(tempDir, 'AccessControl.zip')
|
||||||
|
const tempDll = path.join(tempDir, 'Plugins', 'AccessControl.dll')
|
||||||
|
const pluginPath = path.join(pluginDir, 'Plugins', 'x86-unicode', 'AccessControl.dll')
|
||||||
|
const pluginPathB = path.join(pluginDir, 'AccessControl.dll')
|
||||||
|
|
||||||
|
await fs.mkdir(tempDir, { recursive: true })
|
||||||
|
if (fs.existsSync(pluginPath)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(tempZip)) {
|
||||||
|
await downloadFile(url, tempZip)
|
||||||
|
}
|
||||||
|
const zip = new AdmZip(tempZip)
|
||||||
|
zip.extractAllTo(tempDir, true)
|
||||||
|
|
||||||
|
let sourcePath = tempDll
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
const altPaths = [
|
||||||
|
path.join(tempDir, 'AccessControl.dll'),
|
||||||
|
path.join(tempDir, 'Plugins', 'i386-unicode', 'AccessControl.dll')
|
||||||
|
]
|
||||||
|
for (const altPath of altPaths) {
|
||||||
|
if (fs.existsSync(altPath)) {
|
||||||
|
sourcePath = altPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fs.existsSync(sourcePath)) {
|
||||||
|
await fsp.cp(sourcePath, pluginPath, { recursive: true, force: true })
|
||||||
|
await fsp.cp(sourcePath, pluginPathB, { recursive: true, force: true })
|
||||||
|
console.log(`AccessControl.dll copied to ${pluginPath}`)
|
||||||
|
} else {
|
||||||
|
console.warn('AccessControl.dll not found in the extracted archive')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fsp.rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveSidecar(binInfo) {
|
async function resolveSidecar(binInfo) {
|
||||||
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo
|
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo
|
||||||
const binaryDir = path.join(cwd, 'src-tauri', 'binary')
|
const binaryDir = path.join(cwd, 'src-tauri', 'binary')
|
||||||
@@ -135,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 })
|
||||||
@@ -191,16 +252,11 @@ 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) {
|
||||||
await resolvePlugin()
|
await resolvePlugins()
|
||||||
}
|
}
|
||||||
await resolveService(getServiceInfo('install-openlist-service'))
|
await resolveService(getServiceInfo('install-openlist-service'))
|
||||||
await resolveService(getServiceInfo('openlist-desktop-service'))
|
await resolveService(getServiceInfo('openlist-desktop-service'))
|
||||||
|
|||||||
275
src-tauri/Cargo.lock
generated
275
src-tauri/Cargo.lock
generated
@@ -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",
|
||||||
@@ -2082,6 +2082,17 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-uring"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@@ -2590,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"
|
||||||
@@ -2779,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"
|
||||||
@@ -2790,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"
|
||||||
@@ -2826,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"
|
||||||
@@ -2850,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]]
|
||||||
@@ -2887,7 +2940,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openlist-desktop"
|
name = "openlist-desktop"
|
||||||
version = "0.1.0"
|
version = "0.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -2906,6 +2959,7 @@ dependencies = [
|
|||||||
"runas",
|
"runas",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sysinfo",
|
||||||
"tar",
|
"tar",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
@@ -3439,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",
|
||||||
@@ -3476,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",
|
||||||
]
|
]
|
||||||
@@ -3665,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",
|
||||||
@@ -3694,9 +3748,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.20"
|
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 = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
|
checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4071,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",
|
||||||
@@ -4101,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"
|
||||||
@@ -4159,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",
|
||||||
@@ -4170,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]]
|
||||||
@@ -4331,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"
|
||||||
@@ -4481,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"
|
||||||
@@ -4511,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",
|
||||||
@@ -4584,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",
|
||||||
@@ -4607,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",
|
||||||
@@ -4634,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",
|
||||||
@@ -4650,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",
|
||||||
@@ -4683,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",
|
||||||
@@ -4697,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",
|
||||||
@@ -4708,7 +4798,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"toml",
|
"toml 0.9.5",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4728,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",
|
||||||
@@ -4746,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",
|
||||||
@@ -4762,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",
|
||||||
@@ -4821,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",
|
||||||
@@ -4836,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",
|
||||||
@@ -4847,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",
|
||||||
@@ -4885,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",
|
||||||
@@ -4914,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",
|
||||||
@@ -4929,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]]
|
||||||
@@ -5118,21 +5211,23 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.45.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 = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"io-uring",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"slab",
|
||||||
|
"socket2 0.6.0",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5186,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"
|
||||||
@@ -5200,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"
|
||||||
@@ -5207,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5218,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5230,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"
|
||||||
@@ -6349,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",
|
||||||
@@ -6448,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",
|
||||||
@@ -6473,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",
|
||||||
@@ -6482,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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "openlist-desktop"
|
name = "openlist-desktop"
|
||||||
version = "0.2.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.45.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.20", 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"
|
||||||
|
|||||||
@@ -401,17 +401,11 @@ Function .onInit
|
|||||||
${If} $INSTDIR == ""
|
${If} $INSTDIR == ""
|
||||||
; Set default install location
|
; Set default install location
|
||||||
!if "${INSTALLMODE}" == "perMachine"
|
!if "${INSTALLMODE}" == "perMachine"
|
||||||
${If} ${RunningX64}
|
IfFileExists "D:\" 0 +3
|
||||||
!if "${ARCH}" == "x64"
|
StrCpy $INSTDIR "D:\Program\${PRODUCTNAME}"
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
Goto instdir_set
|
||||||
!else if "${ARCH}" == "arm64"
|
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
instdir_set:
|
||||||
!else
|
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
|
||||||
!endif
|
|
||||||
${Else}
|
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
|
||||||
${EndIf}
|
|
||||||
!else if "${INSTALLMODE}" == "currentUser"
|
!else if "${INSTALLMODE}" == "currentUser"
|
||||||
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"
|
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"
|
||||||
!endif
|
!endif
|
||||||
@@ -473,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
|
||||||
@@ -501,6 +511,34 @@ FunctionEnd
|
|||||||
${EndIf}
|
${EndIf}
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
|
!macro SetDirectoryPermissions
|
||||||
|
DetailPrint "Setting permissions for installation directory..."
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
AccessControl::GrantOnFile "$INSTDIR" "(S-1-5-32-545)" "FullAccess"
|
||||||
|
Pop $R0
|
||||||
|
${If} $R0 == "ok"
|
||||||
|
DetailPrint "Successfully granted permissions to Users group"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "Warning: Failed to set permissions - $R0"
|
||||||
|
${EndIf}
|
||||||
|
!else
|
||||||
|
AccessControl::GrantOnFile "$INSTDIR" "(S-1-5-32-545)" "FullAccess"
|
||||||
|
Pop $R0
|
||||||
|
${If} $R0 == "ok"
|
||||||
|
DetailPrint "Successfully granted permissions to Users group"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "Warning: Failed to set permissions - $R0"
|
||||||
|
${EndIf}
|
||||||
|
AccessControl::GrantOnFile "$INSTDIR" "(S-1-5-11)" "FullAccess"
|
||||||
|
Pop $R0
|
||||||
|
${If} $R0 == "ok"
|
||||||
|
DetailPrint "Successfully granted permissions to Authenticated Users"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "Warning: Failed to set permissions for Authenticated Users - $R0"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
!macro RemoveOpenListService
|
!macro RemoveOpenListService
|
||||||
; Check if the service exists
|
; Check if the service exists
|
||||||
SimpleSC::ExistsService "openlist_desktop_service"
|
SimpleSC::ExistsService "openlist_desktop_service"
|
||||||
@@ -731,6 +769,8 @@ Section Install
|
|||||||
!insertmacro CheckIfAppIsRunning
|
!insertmacro CheckIfAppIsRunning
|
||||||
!insertmacro CheckAllOpenListProcesses
|
!insertmacro CheckAllOpenListProcesses
|
||||||
|
|
||||||
|
!insertmacro SetDirectoryPermissions
|
||||||
|
|
||||||
DetailPrint "Cleaning auto-launch registry entries..."
|
DetailPrint "Cleaning auto-launch registry entries..."
|
||||||
|
|
||||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
@@ -943,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
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_binary_version(binary_name: Option<String>) -> Result<String, String> {
|
pub async fn get_binary_version(binary_name: Option<String>) -> Result<String, String> {
|
||||||
let app_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
let bin = binary_name.as_deref().unwrap_or("openlist");
|
||||||
let binary_path = if cfg!(windows) {
|
let mut binary_path: PathBuf =
|
||||||
app_dir.join(format!(
|
env::current_exe().map_err(|e| format!("Failed to get current exe path: {e}"))?;
|
||||||
"{}.exe",
|
binary_path.pop();
|
||||||
binary_name.unwrap_or("openlist".to_string())
|
|
||||||
))
|
#[cfg(windows)]
|
||||||
} else {
|
let file_name = format!("{bin}.exe");
|
||||||
app_dir.join(binary_name.unwrap_or("openlist".to_string()))
|
#[cfg(not(windows))]
|
||||||
};
|
let file_name = bin.to_string();
|
||||||
let mut cmd = Command::new(binary_path);
|
|
||||||
|
binary_path.push(file_name);
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&binary_path);
|
||||||
cmd.arg("version");
|
cmd.arg("version");
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -21,19 +25,25 @@ pub async fn get_binary_version(binary_name: Option<String>) -> Result<String, S
|
|||||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = cmd.output().map_err(|e| e.to_string())?;
|
let output = cmd
|
||||||
if output.status.success() {
|
.output()
|
||||||
let version_output = String::from_utf8_lossy(&output.stdout);
|
.map_err(|e| format!("Failed to spawn {:?}: {}", &binary_path, e))?;
|
||||||
let version_line = version_output
|
|
||||||
.lines()
|
if !output.status.success() {
|
||||||
.find(|line| line.starts_with("Version:") || line.starts_with("rclone"))
|
return Err(format!(
|
||||||
.ok_or("Version not found in output")?;
|
"{:?} exited with status: {}",
|
||||||
let version = version_line
|
&binary_path, output.status
|
||||||
.split_whitespace()
|
));
|
||||||
.nth(1)
|
|
||||||
.ok_or("Failed to parse version")?;
|
|
||||||
Ok(version.to_string())
|
|
||||||
} else {
|
|
||||||
Err("Failed to get OpenList binary version".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
let version = stdout
|
||||||
|
.lines()
|
||||||
|
.filter(|l| l.starts_with("Version:") || l.starts_with("rclone"))
|
||||||
|
.filter_map(|l| l.split_whitespace().nth(1))
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "Version not found in output".to_string())?;
|
||||||
|
|
||||||
|
Ok(version.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,109 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
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};
|
||||||
|
|
||||||
#[tauri::command]
|
fn write_json_to_file<T: serde::Serialize>(path: PathBuf, value: &T) -> Result<(), String> {
|
||||||
pub async fn save_settings(
|
let json = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
|
||||||
settings: MergedSettings,
|
fs::write(path, json).map_err(|e| e.to_string())
|
||||||
state: State<'_, AppState>,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
state.update_settings(settings.clone());
|
|
||||||
let settings_path = app_config_file_path().map_err(|e| e.to_string())?;
|
|
||||||
let settings_json = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
|
|
||||||
fs::write(settings_path, settings_json).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
log::info!("Settings saved successfully");
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
fn persist_app_settings(settings: &MergedSettings) -> Result<(), String> {
|
||||||
pub async fn save_settings_with_update_port(
|
let path = app_config_file_path().map_err(|e| e.to_string())?;
|
||||||
settings: MergedSettings,
|
write_json_to_file(path, settings)
|
||||||
state: State<'_, AppState>,
|
}
|
||||||
) -> Result<bool, String> {
|
|
||||||
save_settings(settings.clone(), state.clone()).await?;
|
fn update_data_config(port: u16, data_dir: Option<&str>) -> Result<(), String> {
|
||||||
let app_dir = std::env::current_exe()
|
let data_config_path = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||||
.map_err(|e| format!("Failed to get current exe path: {e}"))?
|
PathBuf::from(dir).join("config.json")
|
||||||
.parent()
|
|
||||||
.ok_or("Failed to get parent directory")?
|
|
||||||
.to_path_buf();
|
|
||||||
let data_config_path = app_dir.join("data").join("config.json");
|
|
||||||
if let Some(parent) = data_config_path.parent() {
|
|
||||||
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
|
||||||
}
|
|
||||||
let mut config = if data_config_path.exists() {
|
|
||||||
let content =
|
|
||||||
std::fs::read_to_string(data_config_path.clone()).map_err(|e| e.to_string())?;
|
|
||||||
serde_json::from_str(&content).map_err(|e| e.to_string())?
|
|
||||||
} else {
|
} else {
|
||||||
serde_json::json!({
|
get_default_openlist_data_dir()?.join("config.json")
|
||||||
"scheme": {
|
|
||||||
"http_port": settings.openlist.port,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
if let Some(scheme) = config.get_mut("scheme") {
|
|
||||||
if let Some(scheme_obj) = scheme.as_object_mut() {
|
if let Some(parent) = data_config_path.parent() {
|
||||||
scheme_obj.insert(
|
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||||
"http_port".to_string(),
|
|
||||||
serde_json::Value::Number(serde_json::Number::from(settings.openlist.port)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config["scheme"] = serde_json::json!({
|
|
||||||
"http_port": settings.openlist.port
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let content = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?;
|
|
||||||
std::fs::write(data_config_path, content).map_err(|e| e.to_string())?;
|
|
||||||
// Stop the OpenList core process
|
|
||||||
let process_list = get_process_list(state.clone()).await?;
|
|
||||||
if let Some(existing_process) = process_list
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.config.name == "single_openlist_core_process")
|
|
||||||
{
|
|
||||||
match stop_process(existing_process.config.id.clone(), state.clone()).await {
|
|
||||||
Ok(_) => log::info!("OpenList core process stopped successfully"),
|
|
||||||
Err(e) => log::warn!("Failed to stop OpenList core process: {e}"),
|
|
||||||
}
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
|
|
||||||
match start_process(existing_process.config.id.clone(), state.clone()).await {
|
|
||||||
Ok(_) => log::info!("OpenList core process started successfully with new port"),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to start OpenList core process: {e}");
|
|
||||||
return Err(format!(
|
|
||||||
"Failed to restart OpenList core with new port: {e}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Settings saved and OpenList core restarted with new port successfully");
|
let mut cfg_value = if data_config_path.exists() {
|
||||||
Ok(true)
|
let s = fs::read_to_string(&data_config_path).map_err(|e| e.to_string())?;
|
||||||
|
serde_json::from_str::<serde_json::Value>(&s).map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
serde_json::json!({ "scheme": { "http_port": port } })
|
||||||
|
};
|
||||||
|
|
||||||
|
let scheme = cfg_value.get_mut("scheme").and_then(|v| v.as_object_mut());
|
||||||
|
if let Some(obj) = scheme {
|
||||||
|
obj.insert("http_port".into(), serde_json::json!(port));
|
||||||
|
} else {
|
||||||
|
cfg_value["scheme"] = serde_json::json!({ "http_port": port });
|
||||||
|
}
|
||||||
|
|
||||||
|
write_json_to_file(data_config_path, &cfg_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restart_openlist_core(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(1_000)).await;
|
||||||
|
start_process(id, state)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to restart OpenList core: {e}"))?;
|
||||||
|
}
|
||||||
|
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]
|
||||||
@@ -94,15 +113,77 @@ pub async fn load_settings(state: State<'_, AppState>) -> Result<Option<MergedSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn reset_settings(state: State<'_, AppState>) -> Result<Option<MergedSettings>, String> {
|
pub async fn save_settings(
|
||||||
let default_settings = MergedSettings::default();
|
settings: MergedSettings,
|
||||||
state.update_settings(default_settings.clone());
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
let settings_path = app_config_file_path().map_err(|e| e.to_string())?;
|
state.update_settings(settings.clone());
|
||||||
let settings_json =
|
persist_app_settings(&settings)?;
|
||||||
serde_json::to_string_pretty(&default_settings).map_err(|e| e.to_string())?;
|
log::info!("Settings saved successfully");
|
||||||
fs::write(settings_path, settings_json).map_err(|e| e.to_string())?;
|
Ok(true)
|
||||||
|
}
|
||||||
log::info!("Settings reset to default");
|
|
||||||
Ok(Some(default_settings))
|
#[tauri::command]
|
||||||
|
pub async fn save_settings_with_update_port(
|
||||||
|
settings: MergedSettings,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> 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());
|
||||||
|
persist_app_settings(&settings)?;
|
||||||
|
let data_dir = if settings.openlist.data_dir.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reset_settings(state: State<'_, AppState>) -> Result<Option<MergedSettings>, String> {
|
||||||
|
let base_settings = MergedSettings::default();
|
||||||
|
state.update_settings(base_settings.clone());
|
||||||
|
persist_app_settings(&base_settings)?;
|
||||||
|
log::info!("Settings reset to default");
|
||||||
|
Ok(Some(base_settings))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
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;
|
||||||
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Emitter, State};
|
use tauri::{AppHandle, Emitter, Manager, State};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
use crate::cmd::config::save_settings;
|
use crate::cmd::config::save_settings;
|
||||||
use crate::object::structs::AppState;
|
use crate::object::structs::AppState;
|
||||||
|
use crate::utils::github_proxy::apply_github_proxy;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct GitHubRelease {
|
pub struct GitHubRelease {
|
||||||
@@ -67,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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +168,25 @@ fn compare_versions(current: &str, latest: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn check_for_updates() -> Result<UpdateCheck, String> {
|
pub async fn check_for_updates(state: State<'_, AppState>) -> Result<UpdateCheck, String> {
|
||||||
log::info!("Checking for updates...");
|
log::info!("Checking for updates...");
|
||||||
|
|
||||||
|
let gh_proxy = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy.clone());
|
||||||
|
|
||||||
|
let gh_proxy_api = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy_api);
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let url = "https://api.github.com/repos/OpenListTeam/openlist-desktop/releases/latest";
|
let url = "https://api.github.com/repos/OpenListTeam/openlist-desktop/releases/latest";
|
||||||
|
let proxied_url = apply_github_proxy(url, &gh_proxy, &gh_proxy_api);
|
||||||
|
|
||||||
|
log::info!("Fetching updates from: {proxied_url}");
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.get(url)
|
.get(&proxied_url)
|
||||||
.header("User-Agent", "OpenList-Desktop")
|
.header("User-Agent", "OpenList-Desktop")
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.send()
|
.send()
|
||||||
@@ -212,7 +223,14 @@ pub async fn check_for_updates() -> Result<UpdateCheck, String> {
|
|||||||
let latest_version = release.tag_name.as_str();
|
let latest_version = release.tag_name.as_str();
|
||||||
|
|
||||||
let has_update = compare_versions(current_version, latest_version);
|
let has_update = compare_versions(current_version, latest_version);
|
||||||
let assets = filter_assets_for_platform(&release.assets);
|
let mut assets = filter_assets_for_platform(&release.assets);
|
||||||
|
|
||||||
|
// Apply GitHub proxy to asset URLs if proxy is configured
|
||||||
|
if gh_proxy.is_some() {
|
||||||
|
for asset in &mut assets {
|
||||||
|
asset.url = apply_github_proxy(&asset.url, &gh_proxy, &gh_proxy_api);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Update check result: current={}, latest={}, has_update={}, assets_count={}",
|
"Update check result: current={}, latest={}, has_update={}, assets_count={}",
|
||||||
@@ -237,9 +255,21 @@ pub async fn download_update(
|
|||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
asset_url: String,
|
asset_url: String,
|
||||||
asset_name: String,
|
asset_name: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
log::info!("Starting download of update: {asset_name}");
|
log::info!("Starting download of update: {asset_name}");
|
||||||
|
|
||||||
|
let gh_proxy = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy.clone());
|
||||||
|
|
||||||
|
let gh_proxy_api = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy_api);
|
||||||
|
|
||||||
|
let proxied_url = apply_github_proxy(&asset_url, &gh_proxy, &gh_proxy_api);
|
||||||
|
log::info!("Downloading from: {proxied_url}");
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
@@ -248,7 +278,7 @@ pub async fn download_update(
|
|||||||
log::info!("Downloading to: {file_path:?}");
|
log::info!("Downloading to: {file_path:?}");
|
||||||
|
|
||||||
let mut response = client
|
let mut response = client
|
||||||
.get(&asset_url)
|
.get(&proxied_url)
|
||||||
.header("User-Agent", "OpenList-Desktop")
|
.header("User-Agent", "OpenList-Desktop")
|
||||||
.timeout(Duration::from_secs(9000))
|
.timeout(Duration::from_secs(9000))
|
||||||
.send()
|
.send()
|
||||||
@@ -367,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(_) => {
|
||||||
@@ -376,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;
|
||||||
@@ -392,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...");
|
||||||
|
|
||||||
@@ -422,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}"))?;
|
||||||
@@ -458,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}"))?;
|
||||||
@@ -502,10 +585,92 @@ pub async fn is_auto_check_enabled(state: State<'_, AppState>) -> Result<bool, S
|
|||||||
Ok(settings.app.auto_update_enabled.unwrap_or(true))
|
Ok(settings.app.auto_update_enabled.unwrap_or(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_for_updates_internal(app: &AppHandle) -> Result<UpdateCheck, String> {
|
||||||
|
log::info!("Checking for updates (background check)...");
|
||||||
|
|
||||||
|
let app_state = app.state::<AppState>();
|
||||||
|
let gh_proxy = app_state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy.clone());
|
||||||
|
|
||||||
|
let gh_proxy_api = app_state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy_api);
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
let url = "https://api.github.com/repos/OpenListTeam/openlist-desktop/releases/latest";
|
||||||
|
let proxied_url = apply_github_proxy(url, &gh_proxy, &gh_proxy_api);
|
||||||
|
|
||||||
|
log::info!("Fetching updates from: {proxied_url}");
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(&proxied_url)
|
||||||
|
.header("User-Agent", "OpenList-Desktop")
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Network error while checking for updates: {e}");
|
||||||
|
log::error!("{error_msg}");
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_msg = if status.as_u16() == 404 {
|
||||||
|
"Repository not found. Please check the repository URL.".to_string()
|
||||||
|
} else if status.as_u16() == 403 {
|
||||||
|
"API rate limit exceeded. Please try again later.".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"GitHub API returned status: {} ({})",
|
||||||
|
status.as_u16(),
|
||||||
|
status.canonical_reason().unwrap_or("Unknown")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
log::error!("{error_msg}");
|
||||||
|
return Err(error_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let release: GitHubRelease = response.json().await.map_err(|e| {
|
||||||
|
log::error!("Failed to parse GitHub response: {e}");
|
||||||
|
format!("Failed to parse update information: {e}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let current_version = env!("CARGO_PKG_VERSION");
|
||||||
|
let latest_version = release.tag_name.as_str();
|
||||||
|
|
||||||
|
let has_update = compare_versions(current_version, latest_version);
|
||||||
|
let mut assets = filter_assets_for_platform(&release.assets);
|
||||||
|
|
||||||
|
if gh_proxy.is_some() {
|
||||||
|
for asset in &mut assets {
|
||||||
|
asset.url = apply_github_proxy(&asset.url, &gh_proxy, &gh_proxy_api);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Update check result: current={}, latest={}, has_update={}, assets_count={}",
|
||||||
|
current_version,
|
||||||
|
latest_version,
|
||||||
|
has_update,
|
||||||
|
assets.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(UpdateCheck {
|
||||||
|
has_update,
|
||||||
|
current_version: current_version.to_string(),
|
||||||
|
latest_version: latest_version.to_string(),
|
||||||
|
release_date: release.published_at,
|
||||||
|
release_notes: release.body,
|
||||||
|
assets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn perform_background_update_check(app: AppHandle) -> Result<(), String> {
|
pub async fn perform_background_update_check(app: AppHandle) -> Result<(), String> {
|
||||||
log::debug!("Performing background update check...");
|
log::debug!("Performing background update check...");
|
||||||
|
|
||||||
match check_for_updates().await {
|
match check_for_updates_internal(&app).await {
|
||||||
Ok(update_check) => {
|
Ok(update_check) => {
|
||||||
if update_check.has_update {
|
if update_check.has_update {
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -528,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();
|
|
||||||
}
|
|
||||||
|
|||||||
122
src-tauri/src/cmd/firewall.rs
Normal file
122
src-tauri/src/cmd/firewall.rs
Normal 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)
|
||||||
|
}
|
||||||
@@ -1,120 +1,90 @@
|
|||||||
use std::str::FromStr;
|
use reqwest::Client;
|
||||||
|
|
||||||
use reqwest;
|
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::object::structs::AppState;
|
use crate::object::structs::AppState;
|
||||||
use crate::utils::api::{ListProcessResponse, ProcessStatus, get_api_key, get_server_port};
|
use crate::utils::api::{ListProcessResponse, ProcessStatus, get_api_key, get_server_port};
|
||||||
|
use crate::utils::args::split_args_vec;
|
||||||
|
|
||||||
|
fn create_client() -> (Client, String, u16) {
|
||||||
|
let client = Client::new();
|
||||||
|
let api_key = get_api_key();
|
||||||
|
let port = get_server_port();
|
||||||
|
(client, api_key, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_operation(id: &str, operation: &str) -> Result<bool, String> {
|
||||||
|
let (client, api_key, port) = create_client();
|
||||||
|
let url = match operation {
|
||||||
|
"start" => format!("http://127.0.0.1:{port}/api/v1/processes/{id}/start"),
|
||||||
|
"stop" => format!("http://127.0.0.1:{port}/api/v1/processes/{id}/stop"),
|
||||||
|
"delete" => format!("http://127.0.0.1:{port}/api/v1/processes/{id}"),
|
||||||
|
_ => return Err("Invalid operation".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = match operation {
|
||||||
|
"delete" => client.delete(&url),
|
||||||
|
_ => client.post(&url),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = request
|
||||||
|
.header("Authorization", format!("Bearer {api_key}"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to send request: {e}"))?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to {operation} process: {}",
|
||||||
|
response.status()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_process_list(_state: State<'_, AppState>) -> Result<Vec<ProcessStatus>, String> {
|
pub async fn get_process_list(_state: State<'_, AppState>) -> Result<Vec<ProcessStatus>, String> {
|
||||||
let api_key = get_api_key();
|
let (client, api_key, port) = create_client();
|
||||||
let port = get_server_port();
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = client
|
let response = client
|
||||||
.get(format!("http://127.0.0.1:{port}/api/v1/processes"))
|
.get(format!("http://127.0.0.1:{port}/api/v1/processes"))
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
.header("Authorization", format!("Bearer {api_key}"))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
.map_err(|e| format!("Failed to send request: {e}"))?;
|
||||||
if response.status().is_success() {
|
|
||||||
let response_text = response
|
if !response.status().is_success() {
|
||||||
.text()
|
return Err(format!("Failed to get process list: {}", response.status()));
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to read response text: {e}"))?;
|
|
||||||
let process_list = match serde_json::from_str::<ListProcessResponse>(&response_text) {
|
|
||||||
Ok(process_list) => process_list,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(format!(
|
|
||||||
"Failed to parse response: {e}, response: {response_text}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(process_list.data)
|
|
||||||
} else {
|
|
||||||
Err(format!("Failed to get process list: {}", response.status()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let response_text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read response text: {e}"))?;
|
||||||
|
|
||||||
|
serde_json::from_str::<ListProcessResponse>(&response_text)
|
||||||
|
.map(|process_list| process_list.data)
|
||||||
|
.map_err(|e| format!("Failed to parse response: {e}, response: {response_text}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn start_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
pub async fn start_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
let api_key = get_api_key();
|
process_operation(&id, "start").await
|
||||||
let port = get_server_port();
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = client
|
|
||||||
.post(format!(
|
|
||||||
"http://127.0.0.1:{port}/api/v1/processes/{id}/start"
|
|
||||||
))
|
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Err(format!("Failed to start process: {}", response.status()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn stop_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
pub async fn stop_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
let api_key = get_api_key();
|
process_operation(&id, "stop").await
|
||||||
let port = get_server_port();
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = client
|
|
||||||
.post(format!(
|
|
||||||
"http://127.0.0.1:{port}/api/v1/processes/{id}/stop"
|
|
||||||
))
|
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Err(format!("Failed to stop process: {}", response.status()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restart_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
pub async fn restart_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
let api_key = get_api_key();
|
process_operation(&id, "stop")
|
||||||
let port = get_server_port();
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let stop_response = client
|
|
||||||
.post(format!(
|
|
||||||
"http://127.0.0.1:{port}/api/v1/processes/{id}/stop"
|
|
||||||
))
|
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
|
||||||
.send()
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
.map_err(|e| format!("Failed to stop OpenList Core process: {e}"))?;
|
||||||
if stop_response.status().is_success() {
|
|
||||||
let start_response = client
|
process_operation(&id, "start")
|
||||||
.post(
|
.await
|
||||||
url::Url::from_str(&format!(
|
.map_err(|e| format!("Failed to start OpenList Core process: {e}"))
|
||||||
"http://127.0.0.1:{port}/api/v1/processes/{id}/start"
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
|
||||||
if start_response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"Failed to start OpenList Core process: {}",
|
|
||||||
start_response.status()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"Failed to stop OpenList Core process: {}",
|
|
||||||
stop_response.status()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -123,16 +93,26 @@ pub async fn update_process(
|
|||||||
update_config: serde_json::Value,
|
update_config: serde_json::Value,
|
||||||
_state: State<'_, AppState>,
|
_state: State<'_, AppState>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let api_key = get_api_key();
|
let (client, api_key, port) = create_client();
|
||||||
let port = get_server_port();
|
|
||||||
let client = reqwest::Client::new();
|
let mut processed_config = update_config;
|
||||||
|
if let Some(args) = processed_config.get("args").and_then(|v| v.as_array()) {
|
||||||
|
let args_strings: Vec<String> = args
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
processed_config["args"] = serde_json::json!(split_args_vec(args_strings));
|
||||||
|
}
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.put(format!("http://127.0.0.1:{port}/api/v1/processes/{id}"))
|
.put(format!("http://127.0.0.1:{port}/api/v1/processes/{id}"))
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
.header("Authorization", format!("Bearer {api_key}"))
|
||||||
.json(&update_config)
|
.json(&processed_config)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
.map_err(|e| format!("Failed to send request: {e}"))?;
|
||||||
|
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
@@ -142,18 +122,5 @@ pub async fn update_process(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
pub async fn delete_process(id: String, _state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
let api_key = get_api_key();
|
process_operation(&id, "delete").await
|
||||||
let port = get_server_port();
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = client
|
|
||||||
.delete(format!("http://127.0.0.1:{port}/api/v1/processes/{id}"))
|
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Err(format!("Failed to delete process: {}", response.status()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,272 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
use regex::Regex;
|
use tauri::State;
|
||||||
|
|
||||||
#[tauri::command]
|
use crate::object::structs::AppState;
|
||||||
pub async fn get_admin_password() -> Result<String, String> {
|
use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir, get_service_log_path};
|
||||||
let app_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
|
||||||
let logs_dir = app_dir.join("logs/process_openlist_core.log");
|
|
||||||
|
|
||||||
let logs_content =
|
fn generate_random_password() -> String {
|
||||||
std::fs::read_to_string(logs_dir).map_err(|e| format!("Failed to read log file: {e}"))?;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
let re = Regex::new(r"Successfully created the admin user and the initial password is: (\w+)")
|
let mut hasher = DefaultHasher::new();
|
||||||
.map_err(|e| format!("Failed to create regex: {e}"))?;
|
|
||||||
|
|
||||||
let mut last_password = None;
|
if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
for line in logs_content.lines() {
|
duration.as_nanos().hash(&mut hasher);
|
||||||
if let Some(captures) = re.captures(line)
|
}
|
||||||
&& let Some(password) = captures.get(1)
|
|
||||||
{
|
std::process::id().hash(&mut hasher);
|
||||||
last_password = Some(password.as_str().to_string());
|
|
||||||
|
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 =
|
||||||
|
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?;
|
||||||
|
let app_dir = exe_path
|
||||||
|
.parent()
|
||||||
|
.ok_or("Executable has no parent directory")?;
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
last_password.ok_or("No admin password found in logs".to_string())
|
let openlist_exe = openlist_exe.ok_or_else(|| {
|
||||||
}
|
format!(
|
||||||
|
"OpenList executable not found. Searched for: {:?} in {}",
|
||||||
|
possible_names,
|
||||||
|
app_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
#[tauri::command]
|
log::info!(
|
||||||
pub async fn get_logs(source: Option<String>) -> Result<Vec<String>, String> {
|
"Setting new admin password using: {}",
|
||||||
match source.as_deref() {
|
openlist_exe.display()
|
||||||
Some("openlist") => {
|
);
|
||||||
let app_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
|
||||||
let logs_dir = app_dir.join("data/log/log.log");
|
|
||||||
let logs = std::fs::read_to_string(logs_dir)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.lines()
|
|
||||||
.map(|line| line.to_string())
|
|
||||||
.collect();
|
|
||||||
Ok(logs)
|
|
||||||
}
|
|
||||||
Some("app") => {
|
|
||||||
let app_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
|
||||||
let logs_dir = app_dir.join("logs/app.log");
|
|
||||||
let logs = std::fs::read_to_string(logs_dir)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.lines()
|
|
||||||
.map(|line| line.to_string())
|
|
||||||
.collect();
|
|
||||||
Ok(logs)
|
|
||||||
}
|
|
||||||
Some("rclone") => {
|
|
||||||
let app_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
|
||||||
let logs_dir = app_dir.join("logs/process_rclone.log");
|
|
||||||
let logs = std::fs::read_to_string(logs_dir)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.lines()
|
|
||||||
.map(|line| line.to_string())
|
|
||||||
.collect();
|
|
||||||
Ok(logs)
|
|
||||||
}
|
|
||||||
Some("openlist_core") => {
|
|
||||||
let app_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
|
||||||
let logs_dir = app_dir.join("logs/process_openlist_core.log");
|
|
||||||
let logs = std::fs::read_to_string(logs_dir)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.lines()
|
|
||||||
.map(|line| line.to_string())
|
|
||||||
.collect();
|
|
||||||
Ok(logs)
|
|
||||||
}
|
|
||||||
_ => Err("Invalid log source".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
let mut cmd = Command::new(&openlist_exe);
|
||||||
pub async fn clear_logs(source: Option<String>) -> Result<bool, String> {
|
cmd.args(["admin", "set", password]);
|
||||||
let app_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
cmd.current_dir(app_dir);
|
||||||
|
|
||||||
let log_files = match source.as_deref() {
|
let effective_data_dir = if let Some(settings) = state.get_settings()
|
||||||
Some("openlist") => vec![app_dir.join("data/log/log.log")],
|
&& !settings.openlist.data_dir.is_empty()
|
||||||
Some("app") => vec![app_dir.join("logs/app.log")],
|
{
|
||||||
Some("rclone") => vec![app_dir.join("logs/process_rclone.log")],
|
settings.openlist.data_dir
|
||||||
Some("openlist_core") => vec![app_dir.join("logs/process_openlist_core.log")],
|
} else {
|
||||||
None => vec![
|
get_default_openlist_data_dir()
|
||||||
app_dir.join("data/log/log.log"),
|
.map_err(|e| format!("Failed to get default data directory: {e}"))?
|
||||||
app_dir.join("logs/app.log"),
|
.to_string_lossy()
|
||||||
app_dir.join("logs/process_rclone.log"),
|
.to_string()
|
||||||
app_dir.join("logs/process_openlist_core.log"),
|
|
||||||
],
|
|
||||||
_ => return Err("Invalid log source".to_string()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cleared_count = 0;
|
cmd.arg("--data");
|
||||||
let mut errors = Vec::new();
|
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}"))?;
|
||||||
|
|
||||||
for log_file in log_files {
|
if !output.status.success() {
|
||||||
if log_file.exists() {
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
match std::fs::write(&log_file, "") {
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
Ok(_) => {
|
log::error!("OpenList admin set command failed. stdout: {stdout}, stderr: {stderr}");
|
||||||
cleared_count += 1;
|
return Err(format!("OpenList admin set command failed: {stderr}"));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
let error_msg = format!("Failed to clear {log_file:?}: {e}");
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
errors.push(error_msg);
|
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();
|
||||||
|
match source {
|
||||||
|
Some("openlist") => paths.push(openlist_log_base.join("log/log.log")),
|
||||||
|
Some("app") => paths.push(logs_dir.join("app.log")),
|
||||||
|
Some("rclone") => paths.push(logs_dir.join("process_rclone.log")),
|
||||||
|
Some("openlist_core") => paths.push(logs_dir.join("process_openlist_core.log")),
|
||||||
|
Some("service") => paths.push(service_path),
|
||||||
|
Some("all") => {
|
||||||
|
paths.push(openlist_log_base.join("log/log.log"));
|
||||||
|
paths.push(logs_dir.join("app.log"));
|
||||||
|
paths.push(logs_dir.join("process_rclone.log"));
|
||||||
|
paths.push(logs_dir.join("process_openlist_core.log"));
|
||||||
|
paths.push(service_path);
|
||||||
|
}
|
||||||
|
_ => return Err("Invalid log source".into()),
|
||||||
|
}
|
||||||
|
Ok(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_admin_password(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
|
if let Some(settings) = state.get_settings()
|
||||||
|
&& let Some(ref stored_password) = settings.app.admin_password
|
||||||
|
&& !stored_password.is_empty()
|
||||||
|
{
|
||||||
|
log::info!("Found admin password in local settings");
|
||||||
|
return Ok(stored_password.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.is_empty() {
|
Ok(new_password)
|
||||||
return Err(format!(
|
}
|
||||||
"Some log files could not be cleared: {}",
|
|
||||||
errors.join(", ")
|
#[tauri::command]
|
||||||
));
|
pub async fn reset_admin_password(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
|
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();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
if path.exists() {
|
||||||
|
let content = std::fs::read_to_string(&path)
|
||||||
|
.map_err(|e| format!("Failed to read {path:?}: {e}"))?;
|
||||||
|
logs.extend(content.lines().map(str::to_string));
|
||||||
|
} else {
|
||||||
|
log::info!("Log file does not exist: {path:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn clear_logs(
|
||||||
|
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;
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::write(&path, "").map_err(|e| format!("Failed to clear {path:?}: {e}"))?;
|
||||||
|
cleared_count += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cleared_count == 0 {
|
if cleared_count == 0 {
|
||||||
return Err("No log files found to clear".to_string());
|
Err("No log files found to clear".into())
|
||||||
|
} else {
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ 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::path::{get_openlist_binary_path, get_rclone_binary_path};
|
use crate::utils::github_proxy::apply_github_proxy;
|
||||||
|
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")]
|
||||||
@@ -60,6 +64,17 @@ pub async fn open_url(url: String) -> Result<bool, String> {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_url_in_browser(url: String, app_handle: AppHandle) -> Result<bool, String> {
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
|
app_handle
|
||||||
|
.opener()
|
||||||
|
.open_url(url, None::<&str>)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn select_directory(title: String, app_handle: AppHandle) -> Result<Option<String>, String> {
|
pub fn select_directory(title: String, app_handle: AppHandle) -> Result<Option<String>, String> {
|
||||||
use tauri_plugin_dialog::DialogExt;
|
use tauri_plugin_dialog::DialogExt;
|
||||||
@@ -127,19 +142,37 @@ pub async fn list_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_available_versions(tool: String) -> Result<Vec<String>, String> {
|
pub async fn get_available_versions(
|
||||||
|
tool: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
let url = match tool.as_str() {
|
let url = match tool.as_str() {
|
||||||
"openlist" => "https://api.github.com/repos/OpenListTeam/OpenList/releases",
|
"openlist" => "https://api.github.com/repos/OpenListTeam/OpenList/releases",
|
||||||
"rclone" => "https://api.github.com/repos/rclone/rclone/releases",
|
"rclone" => "https://api.github.com/repos/rclone/rclone/releases",
|
||||||
_ => return Err("Unsupported tool".to_string()),
|
_ => return Err("Unsupported tool".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let gh_proxy = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy.clone());
|
||||||
|
|
||||||
|
let gh_proxy_api = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy_api);
|
||||||
|
|
||||||
|
let proxied_url = apply_github_proxy(url, &gh_proxy, &gh_proxy_api);
|
||||||
|
log::info!("Fetching {tool} versions from: {proxied_url}");
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.user_agent("OpenList Desktop/1.0")
|
.user_agent("OpenList Desktop/1.0")
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let response = client.get(url).send().await.map_err(|e| e.to_string())?;
|
let response = client
|
||||||
|
.get(&proxied_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
let releases: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
|
let releases: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let versions = releases
|
let versions = releases
|
||||||
@@ -162,21 +195,29 @@ pub async fn update_tool_version(
|
|||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
log::info!("Updating {tool} to version {version}");
|
log::info!("Updating {tool} to version {version}");
|
||||||
|
|
||||||
let process_list = get_process_list(state.clone())
|
let process_list_result = get_process_list(state.clone()).await;
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to get process list: {e}"))?;
|
|
||||||
|
|
||||||
let process_name = match tool.as_str() {
|
let (was_running, process_id) = match process_list_result {
|
||||||
"openlist" => "single_openlist_core_process",
|
Ok(process_list) => {
|
||||||
"rclone" => "single_rclone_backend_process",
|
let process_name = match tool.as_str() {
|
||||||
_ => return Err("Unsupported tool".to_string()),
|
"openlist" => "single_openlist_core_process",
|
||||||
|
"rclone" => "single_rclone_backend_process",
|
||||||
|
_ => return Err("Unsupported tool".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let running_process = process_list.iter().find(|p| p.config.name == process_name);
|
||||||
|
let was_running = running_process.map(|p| p.is_running).unwrap_or(false);
|
||||||
|
let process_id = running_process.map(|p| p.config.id.clone());
|
||||||
|
|
||||||
|
(was_running, process_id)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to get process list (service may not be installed): {e}");
|
||||||
|
log::info!("Proceeding with update without stopping processes");
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let running_process = process_list.iter().find(|p| p.config.name == process_name);
|
|
||||||
|
|
||||||
let was_running = running_process.map(|p| p.is_running).unwrap_or(false);
|
|
||||||
let process_id = running_process.map(|p| p.config.id.clone());
|
|
||||||
|
|
||||||
if was_running && let Some(pid) = &process_id {
|
if was_running && let Some(pid) = &process_id {
|
||||||
log::info!("Stopping {tool} process with ID: {pid}");
|
log::info!("Stopping {tool} process with ID: {pid}");
|
||||||
match tool.as_str() {
|
match tool.as_str() {
|
||||||
@@ -190,7 +231,15 @@ pub async fn update_tool_version(
|
|||||||
log::info!("Successfully stopped {tool} process");
|
log::info!("Successfully stopped {tool} process");
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = download_and_replace_binary(&tool, &version).await;
|
let gh_proxy = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy.clone());
|
||||||
|
|
||||||
|
let gh_proxy_api = state
|
||||||
|
.get_settings()
|
||||||
|
.and_then(|settings| settings.app.gh_proxy_api);
|
||||||
|
|
||||||
|
let result = download_and_replace_binary(&tool, &version, &gh_proxy, &gh_proxy_api).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -207,6 +256,10 @@ pub async fn update_tool_version(
|
|||||||
_ => return Err("Unsupported tool".to_string()),
|
_ => return Err("Unsupported tool".to_string()),
|
||||||
}
|
}
|
||||||
log::info!("Successfully restarted {tool} process");
|
log::info!("Successfully restarted {tool} process");
|
||||||
|
} else if process_id.is_none() {
|
||||||
|
log::info!(
|
||||||
|
"Update completed successfully. Service is not currently installed or running."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(format!("Successfully updated {tool} to {version}"))
|
Ok(format!("Successfully updated {tool} to {version}"))
|
||||||
@@ -231,7 +284,12 @@ pub async fn update_tool_version(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_and_replace_binary(tool: &str, version: &str) -> Result<(), String> {
|
async fn download_and_replace_binary(
|
||||||
|
tool: &str,
|
||||||
|
version: &str,
|
||||||
|
gh_proxy: &Option<String>,
|
||||||
|
gh_proxy_api: &Option<bool>,
|
||||||
|
) -> Result<(), String> {
|
||||||
let platform = std::env::consts::OS;
|
let platform = std::env::consts::OS;
|
||||||
let arch = std::env::consts::ARCH;
|
let arch = std::env::consts::ARCH;
|
||||||
|
|
||||||
@@ -258,13 +316,13 @@ async fn download_and_replace_binary(tool: &str, version: &str) -> Result<(), St
|
|||||||
"openlist" => {
|
"openlist" => {
|
||||||
let path = get_openlist_binary_path()
|
let 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 info = get_openlist_download_info(&platform_arch, version)?;
|
let info = get_openlist_download_info(&platform_arch, version, gh_proxy, gh_proxy_api)?;
|
||||||
(path, info)
|
(path, info)
|
||||||
}
|
}
|
||||||
"rclone" => {
|
"rclone" => {
|
||||||
let path = get_rclone_binary_path()
|
let path = get_rclone_binary_path()
|
||||||
.map_err(|e| format!("Failed to get Rclone binary path: {e}"))?;
|
.map_err(|e| format!("Failed to get Rclone binary path: {e}"))?;
|
||||||
let info = get_rclone_download_info(&platform_arch, version)?;
|
let info = get_rclone_download_info(&platform_arch, version, gh_proxy, gh_proxy_api)?;
|
||||||
(path, info)
|
(path, info)
|
||||||
}
|
}
|
||||||
_ => return Err("Unsupported tool".to_string()),
|
_ => return Err("Unsupported tool".to_string()),
|
||||||
@@ -335,7 +393,12 @@ struct DownloadInfo {
|
|||||||
executable_name: String,
|
executable_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_openlist_download_info(platform_arch: &str, version: &str) -> Result<DownloadInfo, String> {
|
fn get_openlist_download_info(
|
||||||
|
platform_arch: &str,
|
||||||
|
version: &str,
|
||||||
|
gh_proxy: &Option<String>,
|
||||||
|
gh_proxy_api: &Option<bool>,
|
||||||
|
) -> Result<DownloadInfo, String> {
|
||||||
let arch_map = get_openlist_arch_mapping(platform_arch)?;
|
let arch_map = get_openlist_arch_mapping(platform_arch)?;
|
||||||
let is_windows = platform_arch.starts_with("win32");
|
let is_windows = platform_arch.starts_with("win32");
|
||||||
let is_unix = platform_arch.starts_with("darwin") || platform_arch.starts_with("linux");
|
let is_unix = platform_arch.starts_with("darwin") || platform_arch.starts_with("linux");
|
||||||
@@ -348,15 +411,21 @@ fn get_openlist_download_info(platform_arch: &str, version: &str) -> Result<Down
|
|||||||
let download_url = format!(
|
let download_url = format!(
|
||||||
"https://github.com/OpenListTeam/OpenList/releases/download/{version}/{archive_name}"
|
"https://github.com/OpenListTeam/OpenList/releases/download/{version}/{archive_name}"
|
||||||
);
|
);
|
||||||
|
let proxied_url = apply_github_proxy(&download_url, gh_proxy, gh_proxy_api);
|
||||||
|
|
||||||
Ok(DownloadInfo {
|
Ok(DownloadInfo {
|
||||||
download_url,
|
download_url: proxied_url,
|
||||||
archive_name,
|
archive_name,
|
||||||
executable_name,
|
executable_name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rclone_download_info(platform_arch: &str, version: &str) -> Result<DownloadInfo, String> {
|
fn get_rclone_download_info(
|
||||||
|
platform_arch: &str,
|
||||||
|
version: &str,
|
||||||
|
gh_proxy: &Option<String>,
|
||||||
|
gh_proxy_api: &Option<bool>,
|
||||||
|
) -> Result<DownloadInfo, String> {
|
||||||
let arch_map = get_rclone_arch_mapping(platform_arch)?;
|
let arch_map = get_rclone_arch_mapping(platform_arch)?;
|
||||||
let is_windows = platform_arch.starts_with("win32");
|
let is_windows = platform_arch.starts_with("win32");
|
||||||
|
|
||||||
@@ -365,9 +434,10 @@ fn get_rclone_download_info(platform_arch: &str, version: &str) -> Result<Downlo
|
|||||||
let executable_name = format!("rclone{exe_ext}");
|
let executable_name = format!("rclone{exe_ext}");
|
||||||
let download_url =
|
let download_url =
|
||||||
format!("https://github.com/rclone/rclone/releases/download/{version}/{archive_name}");
|
format!("https://github.com/rclone/rclone/releases/download/{version}/{archive_name}");
|
||||||
|
let proxied_url = apply_github_proxy(&download_url, gh_proxy, gh_proxy_api);
|
||||||
|
|
||||||
Ok(DownloadInfo {
|
Ok(DownloadInfo {
|
||||||
download_url,
|
download_url: proxied_url,
|
||||||
archive_name,
|
archive_name,
|
||||||
executable_name,
|
executable_name,
|
||||||
})
|
})
|
||||||
@@ -548,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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(1))
|
|
||||||
.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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,114 +2,127 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde::de::DeserializeOwned;
|
||||||
|
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::path::{get_app_logs_dir, get_rclone_binary_path};
|
use crate::utils::args::split_args_vec;
|
||||||
|
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 {
|
||||||
|
client: Client,
|
||||||
|
api_base: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RcloneApi {
|
||||||
|
fn new(api_base: String) -> Self {
|
||||||
|
Self {
|
||||||
|
client: Client::new(),
|
||||||
|
api_base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_json<T: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
endpoint: &str,
|
||||||
|
body: Option<Value>,
|
||||||
|
) -> Result<T, String> {
|
||||||
|
let url = format!("{}/{endpoint}", self.api_base);
|
||||||
|
let mut req = self.client.post(&url).header("Authorization", RCLONE_AUTH);
|
||||||
|
if let Some(b) = body {
|
||||||
|
req = req.json(&b).header("Content-Type", "application/json");
|
||||||
|
}
|
||||||
|
let resp = req
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Request failed: {e}"))?;
|
||||||
|
let status = resp.status();
|
||||||
|
if status.is_success() {
|
||||||
|
resp.json::<T>()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to parse JSON: {e}"))
|
||||||
|
} else {
|
||||||
|
let txt = resp.text().await.unwrap_or_default();
|
||||||
|
Err(format!("API error {status}: {txt}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_text(&self, endpoint: &str) -> Result<String, String> {
|
||||||
|
let url = format!("{}/{endpoint}", self.api_base);
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.header("Authorization", RCLONE_AUTH)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Request failed: {e}"))?;
|
||||||
|
let status = resp.status();
|
||||||
|
if status.is_success() {
|
||||||
|
resp.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read text: {e}"))
|
||||||
|
} else {
|
||||||
|
let txt = resp.text().await.unwrap_or_default();
|
||||||
|
Err(format!("API error {status}: {txt}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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<serde_json::Value, String> {
|
) -> Result<Value, String> {
|
||||||
let client = Client::new();
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
let response = client
|
let text = api.post_text("config/dump").await?;
|
||||||
.post(format!("{RCLONE_API_BASE}/config/dump"))
|
let all: Value = serde_json::from_str(&text).map_err(|e| format!("Invalid JSON: {e}"))?;
|
||||||
.header("Authorization", RCLONE_AUTH)
|
let remotes = match (remote_type.as_str(), all.as_object()) {
|
||||||
.send()
|
("", _) => all.clone(),
|
||||||
.await
|
(t, Some(map)) => {
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
let filtered = map
|
||||||
if response.status().is_success() {
|
.iter()
|
||||||
let response_text = response
|
.filter_map(|(name, cfg)| {
|
||||||
.text()
|
cfg.get("type")
|
||||||
.await
|
.and_then(Value::as_str)
|
||||||
.map_err(|e| format!("Failed to read response text: {e}"))?;
|
.filter(|&ty| ty == t)
|
||||||
let json: serde_json::Value = serde_json::from_str(&response_text)
|
.map(|_| (name.clone(), cfg.clone()))
|
||||||
.map_err(|e| format!("Failed to parse JSON: {e}"))?;
|
})
|
||||||
let remotes = if remote_type.is_empty() {
|
.collect();
|
||||||
json.clone()
|
Value::Object(filtered)
|
||||||
} else if let Some(obj) = json.as_object() {
|
}
|
||||||
let mut filtered_map = serde_json::Map::new();
|
_ => Value::Object(Default::default()),
|
||||||
for (remote_name, remote_config) in obj {
|
};
|
||||||
if let Some(config_obj) = remote_config.as_object()
|
Ok(remotes)
|
||||||
&& let Some(remote_type_value) = config_obj.get("type")
|
|
||||||
&& let Some(type_str) = remote_type_value.as_str()
|
|
||||||
&& type_str == remote_type
|
|
||||||
{
|
|
||||||
filtered_map.insert(remote_name.clone(), remote_config.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serde_json::Value::Object(filtered_map)
|
|
||||||
} else {
|
|
||||||
serde_json::Value::Object(serde_json::Map::new())
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(remotes)
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"Failed to list Rclone config: {}",
|
|
||||||
response.status()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 client = Client::new();
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
|
let resp: RcloneRemoteListResponse = api.post_json("config/listremotes", None).await?;
|
||||||
let response = client
|
Ok(resp.remotes)
|
||||||
.post(format!("{RCLONE_API_BASE}/config/listremotes"))
|
|
||||||
.header("Authorization", RCLONE_AUTH)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to list remotes: {e}"))?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let remote_list: RcloneRemoteListResponse = response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to parse remote list response: {e}"))?;
|
|
||||||
Ok(remote_list.remotes)
|
|
||||||
} else {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(format!("Failed to list remotes: {error_text}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn rclone_list_mounts() -> Result<RcloneMountListResponse, String> {
|
pub async fn rclone_list_mounts(
|
||||||
let client = Client::new();
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<RcloneMountListResponse, String> {
|
||||||
let response = client
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
.post(format!("{RCLONE_API_BASE}/mount/listmounts"))
|
api.post_json("mount/listmounts", None).await
|
||||||
.header("Authorization", RCLONE_AUTH)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to list mounts: {e}"))?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let mount_list: RcloneMountListResponse = response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to parse mount list response: {e}"))?;
|
|
||||||
Ok(mount_list)
|
|
||||||
} else {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(format!("Failed to list mounts: {error_text}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -117,39 +130,17 @@ 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 client = Client::new();
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
|
let req = RcloneCreateRemoteRequest {
|
||||||
let create_request = RcloneCreateRemoteRequest {
|
name,
|
||||||
name: name.clone(),
|
r#type,
|
||||||
r#type: r#type.clone(),
|
parameters: config,
|
||||||
parameters: crate::conf::rclone::RcloneWebdavConfig {
|
|
||||||
url: config.url.clone(),
|
|
||||||
vendor: config.vendor.clone(),
|
|
||||||
user: config.user.clone(),
|
|
||||||
pass: config.pass.clone(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
api.post_json::<Value>("config/create", Some(json!(req)))
|
||||||
let response = client
|
|
||||||
.post(format!("{RCLONE_API_BASE}/config/create"))
|
|
||||||
.header("Authorization", RCLONE_AUTH)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&create_request)
|
|
||||||
.send()
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to create remote config: {e}"))?;
|
.map(|_| true)
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(format!("Failed to create remote config: {error_text}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -157,109 +148,47 @@ 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 client = Client::new();
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
|
let body = json!({ "name": name, "type": r#type, "parameters": config });
|
||||||
let response = client
|
api.post_json::<Value>("config/update", Some(body))
|
||||||
.post(format!("{RCLONE_API_BASE}/config/update"))
|
|
||||||
.header("Authorization", RCLONE_AUTH)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&json!({ "name": name, "type": r#type, "parameters": config }))
|
|
||||||
.send()
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to update remote config: {e}"))?;
|
.map(|_| true)
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(format!("Failed to update remote config: {error_text}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 client = Client::new();
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
|
let body = json!({ "name": name });
|
||||||
let response = client
|
api.post_json::<Value>("config/delete", Some(body))
|
||||||
.post(format!("{RCLONE_API_BASE}/config/delete"))
|
|
||||||
.header("Authorization", RCLONE_AUTH)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&json!({ "name": name }))
|
|
||||||
.send()
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to delete remote config: {e}"))?;
|
.map(|_| true)
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(format!("Failed to delete remote config: {error_text}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 client = Client::new();
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
|
api.post_json::<Value>("mount/mount", Some(json!(mount_request)))
|
||||||
let response = client
|
|
||||||
.post(format!("{RCLONE_API_BASE}/mount/mount"))
|
|
||||||
.header("Authorization", RCLONE_AUTH)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&mount_request)
|
|
||||||
.send()
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to mount remote: {e}"))?;
|
.map(|_| true)
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(format!("Failed to mount remote: {error_text}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 client = Client::new();
|
let api = RcloneApi::new(get_rclone_api_base_url(&state));
|
||||||
|
api.post_json::<Value>("mount/unmount", Some(json!({ "mountPoint": mount_point })))
|
||||||
let response = client
|
|
||||||
.post(format!("{RCLONE_API_BASE}/mount/unmount"))
|
|
||||||
.header("Authorization", RCLONE_AUTH)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&json!({ "mountPoint": mount_point }))
|
|
||||||
.send()
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to unmount remote: {e}"))?;
|
.map(|_| true)
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(format!("Failed to unmount remote: {error_text}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -272,10 +201,25 @@ 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())?;
|
// Extract mount point from args and create directory if it doesn't exist.
|
||||||
|
// The mount point is the second non-flag argument (first is remote:path).
|
||||||
|
let args_vec = split_args_vec(config.args.clone());
|
||||||
|
let mount_point_opt = args_vec.iter().filter(|arg| !arg.starts_with('-')).nth(1); // 0th is remote:path, 1st is mount_point
|
||||||
|
|
||||||
|
if let Some(mount_point) = mount_point_opt {
|
||||||
|
let mount_path = Path::new(mount_point);
|
||||||
|
if !mount_path.exists()
|
||||||
|
&& let Err(e) = fs::create_dir_all(mount_path)
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to create mount point directory '{}': {}",
|
||||||
|
mount_point, e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let api_key = get_api_key();
|
let api_key = get_api_key();
|
||||||
let port = get_server_port();
|
let port = get_server_port();
|
||||||
@@ -284,7 +228,7 @@ pub async fn create_rclone_mount_remote_process(
|
|||||||
"--config".into(),
|
"--config".into(),
|
||||||
rclone_conf_path.to_string_lossy().into_owned(),
|
rclone_conf_path.to_string_lossy().into_owned(),
|
||||||
];
|
];
|
||||||
args.extend(config.args.clone());
|
args.extend(args_vec);
|
||||||
|
|
||||||
let config = ProcessConfig {
|
let config = ProcessConfig {
|
||||||
id: config.id.clone(),
|
id: config.id.clone(),
|
||||||
@@ -384,9 +328,10 @@ pub async fn get_mount_info_list(
|
|||||||
Ok(is_mounted) => {
|
Ok(is_mounted) => {
|
||||||
if process.is_running {
|
if process.is_running {
|
||||||
if is_mounted { "mounted" } else { "mounting" }
|
if is_mounted { "mounted" } else { "mounting" }
|
||||||
} else if is_mounted {
|
|
||||||
"unmounting"
|
|
||||||
} else {
|
} else {
|
||||||
|
// If process is not running, the mount point should be considered
|
||||||
|
// unmounted regardless of whether
|
||||||
|
// the directory exists or not
|
||||||
"unmounted"
|
"unmounted"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +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_api: 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_api: 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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +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};
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub static OPENLIST_CORE_CONFIG: &str = "data/config.json";
|
|
||||||
#[allow(unused)]
|
|
||||||
pub static OPENLIST_DESKTOP_SETTINGS_FILE_NAME: &str = "settings.json";
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct MergedSettings {
|
pub struct MergedSettings {
|
||||||
@@ -19,6 +14,12 @@ pub struct MergedSettings {
|
|||||||
pub app: AppConfig,
|
pub app: AppConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for MergedSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MergedSettings {
|
impl MergedSettings {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -28,69 +29,66 @@ 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 app_dir = std::env::current_exe()
|
if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||||
.map_err(|e| format!("Failed to get current exe path: {e}"))?
|
Ok(PathBuf::from(dir).join("config.json"))
|
||||||
.parent()
|
} else {
|
||||||
.ok_or("Failed to get parent directory")?
|
Ok(get_default_openlist_data_dir()?.join("config.json"))
|
||||||
.to_path_buf();
|
}
|
||||||
Ok(app_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)?;
|
||||||
if !path.exists() {
|
|
||||||
return Err("data/config.json does not exist".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
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(|scheme| scheme.get("http_port"))
|
.and_then(|s| s.get("http_port"))
|
||||||
.and_then(|port| port.as_u64())
|
.and_then(|p| p.as_u64())
|
||||||
.map(|port| port as u16))
|
.map(|p| p as u16))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<(), String> {
|
pub fn save(&self) -> Result<(), String> {
|
||||||
let path = app_config_file_path().map_err(|e| e.to_string())?;
|
let path = app_config_file_path().map_err(|e| e.to_string())?;
|
||||||
std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
|
if let Some(dir) = path.parent() {
|
||||||
let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||||
std::fs::write(&path, json).map_err(|e| e.to_string())?;
|
}
|
||||||
Ok(())
|
let file = std::fs::File::create(&path).map_err(|e| e.to_string())?;
|
||||||
|
serde_json::to_writer_pretty(file, &self).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load() -> Result<Self, String> {
|
pub fn load() -> Result<Self, String> {
|
||||||
let path = app_config_file_path().map_err(|e| e.to_string())?;
|
let path = app_config_file_path().map_err(|e| e.to_string())?;
|
||||||
let mut merged_settings = if !path.exists() {
|
|
||||||
std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
|
let mut settings = if path.exists() {
|
||||||
let new_settings = Self::new();
|
let data = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||||
std::fs::write(
|
serde_json::from_str(&data).map_err(|e| e.to_string())?
|
||||||
&path,
|
|
||||||
serde_json::to_string_pretty(&new_settings).map_err(|e| e.to_string())?,
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
new_settings
|
|
||||||
} else {
|
} else {
|
||||||
let config = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
|
let default = Self::new();
|
||||||
serde_json::from_str(&config).map_err(|e| e.to_string())?
|
if let Some(dir) = path.parent() {
|
||||||
|
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
default.save()?;
|
||||||
|
default
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(Some(port)) = Self::get_port_from_data_config()
|
let data_dir = if settings.openlist.data_dir.is_empty() {
|
||||||
&& merged_settings.openlist.port != port
|
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
|
||||||
{
|
{
|
||||||
merged_settings.openlist.port = port;
|
settings.openlist.port = port;
|
||||||
merged_settings.save()?;
|
settings.save()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(merged_settings)
|
Ok(settings)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, select_directory,
|
get_available_versions, list_files, open_file, open_folder, open_logs_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,7 +142,12 @@ 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,
|
||||||
save_settings,
|
save_settings,
|
||||||
save_settings_with_update_port,
|
save_settings_with_update_port,
|
||||||
load_settings,
|
load_settings,
|
||||||
@@ -146,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,
|
||||||
@@ -158,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();
|
||||||
@@ -172,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}");
|
||||||
@@ -188,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 {
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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>)?;
|
||||||
|
|||||||
67
src-tauri/src/utils/args.rs
Normal file
67
src-tauri/src/utils/args.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
pub fn split_args(input: &str) -> Vec<String> {
|
||||||
|
let mut args = Vec::new();
|
||||||
|
let mut current_arg = String::new();
|
||||||
|
let mut in_quotes = false;
|
||||||
|
let mut quote_char = '"';
|
||||||
|
let mut escape_next = false;
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if escape_next {
|
||||||
|
current_arg.push(ch);
|
||||||
|
escape_next = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match ch {
|
||||||
|
'\\' => {
|
||||||
|
if let Some(&next_ch) = chars.peek() {
|
||||||
|
if next_ch == '"' || next_ch == '\'' || next_ch == '\\' {
|
||||||
|
escape_next = true;
|
||||||
|
} else {
|
||||||
|
current_arg.push(ch);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current_arg.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'"' | '\'' if !in_quotes => {
|
||||||
|
in_quotes = true;
|
||||||
|
quote_char = ch;
|
||||||
|
}
|
||||||
|
ch if in_quotes && ch == quote_char => {
|
||||||
|
in_quotes = false;
|
||||||
|
}
|
||||||
|
' ' | '\t' if !in_quotes => {
|
||||||
|
if !current_arg.is_empty() {
|
||||||
|
args.push(current_arg.clone());
|
||||||
|
current_arg.clear();
|
||||||
|
}
|
||||||
|
while let Some(&next_ch) = chars.peek() {
|
||||||
|
if next_ch == ' ' || next_ch == '\t' {
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
current_arg.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current_arg.is_empty() {
|
||||||
|
args.push(current_arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
args
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split_args_vec(args: Vec<String>) -> Vec<String> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for arg in args {
|
||||||
|
result.extend(split_args(&arg));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
26
src-tauri/src/utils/github_proxy.rs
Normal file
26
src-tauri/src/utils/github_proxy.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pub fn apply_github_proxy(
|
||||||
|
url: &str,
|
||||||
|
gh_proxy: &Option<String>,
|
||||||
|
gh_proxy_api: &Option<bool>,
|
||||||
|
) -> String {
|
||||||
|
if let Some(proxy) = gh_proxy
|
||||||
|
&& !proxy.is_empty()
|
||||||
|
&& should_proxy_url(url, gh_proxy_api)
|
||||||
|
{
|
||||||
|
let proxy_clean = proxy.trim_end_matches('/');
|
||||||
|
return format!("{proxy_clean}/{url}");
|
||||||
|
}
|
||||||
|
url.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_proxy_url(url: &str, gh_proxy_api: &Option<bool>) -> bool {
|
||||||
|
if url.starts_with("https://github.com/") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.starts_with("https://api.github.com/") {
|
||||||
|
return gh_proxy_api.unwrap_or(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
|
pub mod args;
|
||||||
|
pub mod github_proxy;
|
||||||
pub mod init_log;
|
pub mod init_log;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
|
|||||||
@@ -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,48 +41,85 @@ fn get_app_dir() -> Result<PathBuf, String> {
|
|||||||
Ok(app_dir)
|
Ok(app_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_openlist_binary_path() -> Result<PathBuf, String> {
|
fn get_user_data_dir() -> Result<PathBuf, String> {
|
||||||
let app_dir = get_app_dir()?;
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
let binary_name = if cfg!(target_os = "windows") {
|
#[cfg(target_os = "linux")]
|
||||||
"openlist.exe"
|
{
|
||||||
} else {
|
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||||
"openlist"
|
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")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let binary_path = app_dir.join(binary_name);
|
|
||||||
|
|
||||||
if !binary_path.exists() {
|
fs::create_dir_all(&data_dir).map_err(|e| format!("Failed to create data directory: {e}"))?;
|
||||||
return Err(format!(
|
|
||||||
"OpenList service binary not found at: {binary_path:?}"
|
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> {
|
||||||
|
let mut name = binary.to_string();
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
name.push_str(".exe");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(binary_path)
|
let path = get_app_dir()?.join(&name);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!(
|
||||||
|
"{service_name} service binary not found at: {path:?}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_openlist_binary_path() -> Result<PathBuf, String> {
|
||||||
|
get_binary_path("openlist", "OpenList")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rclone_binary_path() -> Result<PathBuf, String> {
|
pub fn get_rclone_binary_path() -> Result<PathBuf, String> {
|
||||||
let app_dir = get_app_dir()?;
|
get_binary_path("rclone", "Rclone")
|
||||||
|
|
||||||
let binary_name = if cfg!(target_os = "windows") {
|
|
||||||
"rclone.exe"
|
|
||||||
} else {
|
|
||||||
"rclone"
|
|
||||||
};
|
|
||||||
let binary_path = app_dir.join(binary_name);
|
|
||||||
|
|
||||||
if !binary_path.exists() {
|
|
||||||
return Err(format!(
|
|
||||||
"Rclone service binary not found at: {binary_path:?}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(binary_path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_app_config_dir() -> Result<PathBuf, String> {
|
pub fn get_app_config_dir() -> Result<PathBuf, String> {
|
||||||
let app_dir = get_app_dir()?;
|
get_user_data_dir()
|
||||||
fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
|
|
||||||
Ok(app_dir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_config_file_path() -> Result<PathBuf, String> {
|
pub fn app_config_file_path() -> Result<PathBuf, String> {
|
||||||
@@ -65,8 +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 app_dir = get_app_dir()?;
|
get_user_logs_dir()
|
||||||
let logs_dir = app_dir.join("logs");
|
}
|
||||||
fs::create_dir_all(&logs_dir).map_err(|e| e.to_string())?;
|
|
||||||
Ok(logs_dir)
|
pub fn get_rclone_config_path() -> Result<PathBuf, String> {
|
||||||
|
Ok(get_user_data_dir()?.join("rclone.conf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_openlist_data_dir() -> Result<PathBuf, String> {
|
||||||
|
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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.2.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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"targets": ["nsis"],
|
"targets": ["nsis"],
|
||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": "",
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": "",
|
"timestampUrl": "http://time.certum.pl",
|
||||||
"webviewInstallMode": {
|
"webviewInstallMode": {
|
||||||
"type": "embedBootstrapper",
|
"type": "embedBootstrapper",
|
||||||
"silent": true
|
"silent": true
|
||||||
|
|||||||
109
src/App.vue
109
src/App.vue
@@ -2,15 +2,14 @@
|
|||||||
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 store = useAppStore()
|
const appStore = useAppStore()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { updateTrayMenu } = useTray()
|
const { updateTrayMenu } = useTray()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -49,25 +48,21 @@ const handleKeydown = (event: KeyboardEvent) => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
store.init()
|
appStore.init()
|
||||||
store.applyTheme(store.settings.app.theme || 'light')
|
appStore.applyTheme(appStore.settings.app.theme || 'light')
|
||||||
await updateTrayMenu(store.openlistCoreStatus.running)
|
await updateTrayMenu(appStore.openlistCoreStatus.running)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateUnlisten = await TauriAPI.updater.onBackgroundUpdate(updateInfo => {
|
updateUnlisten = await TauriAPI.updater.onBackgroundUpdate(updateInfo => {
|
||||||
console.log('Global update listener: Update available', updateInfo)
|
console.log('Global update listener: Update available', updateInfo)
|
||||||
store.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);
|
||||||
|
|||||||
@@ -60,7 +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 }),
|
||||||
|
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 ---
|
||||||
@@ -74,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 ---
|
||||||
@@ -103,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'),
|
||||||
@@ -118,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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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>
|
||||||
@@ -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,19 +70,20 @@
|
|||||||
</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 store = useAppStore()
|
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>()
|
||||||
@@ -97,8 +98,8 @@ const tooltip = ref({
|
|||||||
statusText: ''
|
statusText: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const isCoreRunning = computed(() => store.isCoreRunning)
|
const isCoreRunning = computed(() => appStore.isCoreRunning)
|
||||||
const openlistCoreStatus = computed(() => store.openlistCoreStatus)
|
const openlistCoreStatus = computed(() => appStore.openlistCoreStatus)
|
||||||
|
|
||||||
const avgResponseTime = computed(() => {
|
const avgResponseTime = computed(() => {
|
||||||
if (dataPoints.value.length === 0) return 0
|
if (dataPoints.value.length === 0) return 0
|
||||||
@@ -158,7 +159,7 @@ const gridColor = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const checkCoreHealth = async () => {
|
const checkCoreHealth = async () => {
|
||||||
await store.refreshOpenListCoreStatus()
|
await appStore.refreshOpenListCoreStatus()
|
||||||
if (!isCoreRunning.value) {
|
if (!isCoreRunning.value) {
|
||||||
dataPoints.value.push({
|
dataPoints.value.push({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -172,7 +173,7 @@ const checkCoreHealth = async () => {
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await store.refreshOpenListCoreStatus()
|
await appStore.refreshOpenListCoreStatus()
|
||||||
|
|
||||||
const endTime = Date.now()
|
const endTime = Date.now()
|
||||||
const responseTimeMs = endTime - startTime
|
const responseTimeMs = endTime - startTime
|
||||||
@@ -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, (store.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) {
|
||||||
|
|||||||
@@ -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,12 +75,16 @@
|
|||||||
</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 Card from '../ui/CardPage.vue'
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const openOpenListDocs = () => {
|
const openOpenListDocs = () => {
|
||||||
openLink('https://docs.oplist.org/')
|
openLink('https://docs.oplist.org/')
|
||||||
@@ -98,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 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>
|
||||||
|
|
||||||
@@ -126,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,
|
||||||
@@ -143,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 {
|
||||||
@@ -218,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;
|
||||||
@@ -296,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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="store.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
|
||||||
@click="rcloneStore.serviceRunning ? rcloneStore.stopRcloneBackend() : rcloneStore.startRcloneBackend()"
|
:disabled="isRcloneLoading"
|
||||||
:disabled="loading || rcloneStore.loading"
|
:class="[
|
||||||
:class="['action-btn', 'service-indicator-btn', { active: rcloneStore.serviceRunning }]"
|
'action-btn',
|
||||||
|
'service-indicator-btn',
|
||||||
|
{ active: rcloneStore.serviceRunning, loading: isRcloneLoading }
|
||||||
|
]"
|
||||||
|
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
|
||||||
>
|
>
|
||||||
<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,31 +136,39 @@
|
|||||||
</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'
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useAppStore()
|
const appStore = useAppStore()
|
||||||
const rcloneStore = useRcloneStore()
|
const rcloneStore = useRcloneStore()
|
||||||
|
|
||||||
const isCoreRunning = computed(() => store.isCoreRunning)
|
const isCoreRunning = computed(() => appStore.isCoreRunning)
|
||||||
const loading = computed(() => store.loading)
|
const isCoreLoading = computed(() => appStore.loading)
|
||||||
const settings = computed(() => store.settings)
|
const isRcloneLoading = computed(() => rcloneStore.loading)
|
||||||
|
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')
|
||||||
@@ -119,19 +176,19 @@ const serviceButtonText = computed(() => {
|
|||||||
|
|
||||||
const toggleCore = async () => {
|
const toggleCore = async () => {
|
||||||
if (isCoreRunning.value) {
|
if (isCoreRunning.value) {
|
||||||
await store.stopOpenListCore()
|
await appStore.stopOpenListCore()
|
||||||
} else {
|
} else {
|
||||||
await store.startOpenListCore()
|
await appStore.startOpenListCore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const restartCore = async () => {
|
const restartCore = async () => {
|
||||||
await store.restartOpenListCore()
|
await appStore.restartOpenListCore()
|
||||||
}
|
}
|
||||||
|
|
||||||
const openWebUI = () => {
|
const openWebUI = () => {
|
||||||
if (store.openListCoreUrl) {
|
if (appStore.openListCoreUrl) {
|
||||||
window.open(store.openListCoreUrl, '_blank')
|
openLink(appStore.openListCoreUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +200,9 @@ const viewMounts = () => {
|
|||||||
router.push({ name: 'Mount' })
|
router.push({ name: 'Mount' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAdminPassword = async () => {
|
const copyAdminPassword = async () => {
|
||||||
try {
|
try {
|
||||||
const password = await store.getAdminPassword()
|
const password = await appStore.getAdminPassword()
|
||||||
if (password) {
|
if (password) {
|
||||||
await navigator.clipboard.writeText(password)
|
await navigator.clipboard.writeText(password)
|
||||||
|
|
||||||
@@ -216,56 +273,184 @@ 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.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAutoLaunchToggle = () => {
|
const handleAutoLaunchToggle = () => {
|
||||||
store.enableAutoLaunch(settings.value.openlist.auto_launch)
|
appStore.enableAutoLaunch(settings.value.openlist.auto_launch)
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
await store.saveSettings()
|
await appStore.saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
const startBackend = async () => {
|
||||||
|
try {
|
||||||
|
await rcloneStore.startRcloneBackend()
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
await rcloneStore.checkRcloneBackendStatus()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error.message || t('mount.messages.failedToStartService'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopBackend = async () => {
|
||||||
|
try {
|
||||||
|
const stopped = await rcloneStore.stopRcloneBackend()
|
||||||
|
if (!stopped) {
|
||||||
|
throw new Error(t('mount.messages.failedToStopService'))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error.message || t('mount.messages.failedToStopService'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await rcloneStore.checkRcloneBackendStatus()
|
await rcloneStore.checkRcloneBackendStatus()
|
||||||
statusCheckInterval = window.setInterval(
|
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 15 * 1000)
|
||||||
rcloneStore.checkRcloneBackendStatus,
|
|
||||||
(store.settings.app.monitor_interval || 5) * 1000
|
await checkFirewallStatus()
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -301,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;
|
||||||
@@ -329,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;
|
||||||
@@ -349,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);
|
||||||
}
|
}
|
||||||
@@ -487,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;
|
||||||
}
|
}
|
||||||
@@ -511,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;
|
||||||
|
|||||||
@@ -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,36 +87,36 @@
|
|||||||
</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 { useAppStore } from '../../stores/app'
|
|
||||||
|
|
||||||
const store = useAppStore()
|
import { useRcloneStore } from '@/stores/rclone'
|
||||||
|
|
||||||
|
import { TauriAPI } from '../../api/tauri'
|
||||||
|
import { useTranslation } from '../../composables/useI18n'
|
||||||
|
import Card from '../ui/CardPage.vue'
|
||||||
|
import ConfirmDialog from '../ui/ConfirmDialog.vue'
|
||||||
|
|
||||||
|
const rcloneStore = useRcloneStore()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const rcloneStore = useRcloneStore()
|
|
||||||
|
|
||||||
const serviceStatus = ref<'not-installed' | 'installed' | 'running' | 'error' | 'stopped'>('not-installed')
|
const serviceStatus = ref<'not-installed' | 'installed' | 'running' | 'error' | 'stopped'>('not-installed')
|
||||||
const actionLoading = ref(false)
|
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, (store.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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<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 { Sun, Moon, Monitor } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const store = useAppStore()
|
import { useTranslation } from '../../composables/useI18n'
|
||||||
|
import { useAppStore } from '../../stores/app'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const currentTheme = computed(() => store.settings.app.theme || 'light')
|
const currentTheme = computed(() => appStore.settings.app.theme || 'light')
|
||||||
|
|
||||||
const themeOptions = computed(() => [
|
const themeOptions = computed(() => [
|
||||||
{
|
{
|
||||||
@@ -35,13 +36,13 @@ const currentThemeOption = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
store.toggleTheme()
|
appStore.toggleTheme()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 store = 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[store.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 (store.tutorialStep < tutorialSteps.value.length - 1) {
|
|
||||||
store.nextTutorialStep()
|
|
||||||
updateHighlight()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePrev = () => {
|
|
||||||
if (store.tutorialStep > 0) {
|
|
||||||
store.prevTutorialStep()
|
|
||||||
updateHighlight()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSkip = () => {
|
|
||||||
store.skipTutorial()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleComplete = () => {
|
|
||||||
store.completeTutorial()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
store.closeTutorial()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateHighlight()
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => store.tutorialStep,
|
|
||||||
() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
updateHighlight()
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
updateHighlight()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="store.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">{{ store.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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
|
|
||||||
export const useCoreActions = () => {
|
export const useCoreActions = () => {
|
||||||
const store = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const startOpenListCore = async () => {
|
const startOpenListCore = async () => {
|
||||||
try {
|
try {
|
||||||
await store.startOpenListCore()
|
await appStore.startOpenListCore()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start service:', error)
|
console.error('Failed to start service:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -14,7 +14,7 @@ export const useCoreActions = () => {
|
|||||||
|
|
||||||
const stopOpenListCore = async () => {
|
const stopOpenListCore = async () => {
|
||||||
try {
|
try {
|
||||||
await store.stopOpenListCore()
|
await appStore.stopOpenListCore()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop service:', error)
|
console.error('Failed to stop service:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -23,7 +23,7 @@ export const useCoreActions = () => {
|
|||||||
|
|
||||||
const restartOpenListCore = async () => {
|
const restartOpenListCore = async () => {
|
||||||
try {
|
try {
|
||||||
await store.restartOpenListCore()
|
await appStore.restartOpenListCore()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restart service:', error)
|
console.error('Failed to restart service:', error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
export function useKeyboardShortcuts() {
|
|
||||||
const handleKeydown = (event: KeyboardEvent, callbacks: Record<string, () => void>) => {
|
|
||||||
const { metaKey, ctrlKey, shiftKey, key } = event
|
|
||||||
const modifier = metaKey || ctrlKey
|
|
||||||
|
|
||||||
if (modifier && key === 'k' && callbacks.search) {
|
|
||||||
event.preventDefault()
|
|
||||||
callbacks.search()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modifier && shiftKey && key === 'L' && callbacks.logs) {
|
|
||||||
event.preventDefault()
|
|
||||||
callbacks.logs()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modifier && shiftKey && key === 'M' && callbacks.metrics) {
|
|
||||||
event.preventDefault()
|
|
||||||
callbacks.metrics()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modifier && key === '1' && callbacks.dashboard) {
|
|
||||||
event.preventDefault()
|
|
||||||
callbacks.dashboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'F11' && callbacks.fullscreen) {
|
|
||||||
event.preventDefault()
|
|
||||||
callbacks.fullscreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleKeydown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
import { useAppStore } from '../stores/app'
|
|
||||||
|
|
||||||
export function useLogs() {
|
|
||||||
const store = useAppStore()
|
|
||||||
|
|
||||||
const logContainer = ref<HTMLElement>()
|
|
||||||
const autoScroll = ref(true)
|
|
||||||
const filterLevel = ref<string>('all')
|
|
||||||
const filterSource = ref<string>('all')
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const selectedLogEntry = ref<any>(null)
|
|
||||||
|
|
||||||
let logRefreshInterval: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
|
||||||
let logs = store.logs || []
|
|
||||||
|
|
||||||
if (filterLevel.value !== 'all') {
|
|
||||||
logs = logs.filter((log: any) => log.level === filterLevel.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterSource.value !== 'all') {
|
|
||||||
logs = logs.filter((log: any) => log.source === filterSource.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery.value.trim()) {
|
|
||||||
const query = searchQuery.value.toLowerCase()
|
|
||||||
logs = logs.filter(
|
|
||||||
(log: any) => log.message.toLowerCase().includes(query) || log.source.toLowerCase().includes(query)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return logs.slice(-500)
|
|
||||||
})
|
|
||||||
|
|
||||||
const logLevelClass = (level: string) => {
|
|
||||||
switch (level) {
|
|
||||||
case 'error':
|
|
||||||
return 'log-error'
|
|
||||||
case 'warn':
|
|
||||||
return 'log-warning'
|
|
||||||
case 'info':
|
|
||||||
return 'log-info'
|
|
||||||
case 'debug':
|
|
||||||
return 'log-debug'
|
|
||||||
default:
|
|
||||||
return 'log-info'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string) => {
|
|
||||||
return new Date(timestamp).toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollToBottom = async () => {
|
|
||||||
if (autoScroll.value && logContainer.value) {
|
|
||||||
await nextTick()
|
|
||||||
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearLogs = async (source?: 'openlist' | 'rclone' | 'app') => {
|
|
||||||
try {
|
|
||||||
await store.clearLogs(source)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear logs:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyLogsToClipboard = async () => {
|
|
||||||
const logsText = filteredLogs.value
|
|
||||||
.map((log: any) => `[${log.timestamp}] [${log.level.toUpperCase()}] [${log.source}] ${log.message}`)
|
|
||||||
.join('\n')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(logsText)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to copy logs:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportLogs = () => {
|
|
||||||
const logsText = filteredLogs.value
|
|
||||||
.map((log: any) => `[${log.timestamp}] [${log.level.toUpperCase()}] [${log.source}] ${log.message}`)
|
|
||||||
.join('\n')
|
|
||||||
|
|
||||||
const blob = new Blob([logsText], { type: 'text/plain' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = `openlist-logs-${new Date().toISOString().split('T')[0]}.txt`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const startLogRefresh = (interval = 2000) => {
|
|
||||||
if (logRefreshInterval) return
|
|
||||||
|
|
||||||
logRefreshInterval = setInterval(async () => {
|
|
||||||
const oldLength = store.logs?.length || 0
|
|
||||||
await store.loadLogs()
|
|
||||||
if (store.logs?.length > oldLength) {
|
|
||||||
await scrollToBottom()
|
|
||||||
}
|
|
||||||
}, interval)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopLogRefresh = () => {
|
|
||||||
if (logRefreshInterval) {
|
|
||||||
clearInterval(logRefreshInterval)
|
|
||||||
logRefreshInterval = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await store.loadLogs()
|
|
||||||
await scrollToBottom()
|
|
||||||
startLogRefresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopLogRefresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
logContainer,
|
|
||||||
autoScroll,
|
|
||||||
filterLevel,
|
|
||||||
filterSource,
|
|
||||||
searchQuery,
|
|
||||||
selectedLogEntry,
|
|
||||||
filteredLogs,
|
|
||||||
logLevelClass,
|
|
||||||
formatTimestamp,
|
|
||||||
scrollToBottom,
|
|
||||||
clearLogs,
|
|
||||||
copyLogsToClipboard,
|
|
||||||
exportLogs,
|
|
||||||
startLogRefresh,
|
|
||||||
stopLogRefresh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
export interface MenuItem {
|
|
||||||
label?: string
|
|
||||||
shortcut?: string
|
|
||||||
action?: () => void
|
|
||||||
enabled?: boolean
|
|
||||||
type?: 'separator'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MenuSection {
|
|
||||||
label: string
|
|
||||||
items: MenuItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMenu() {
|
|
||||||
const showMenuDropdown = ref<string | false>(false)
|
|
||||||
const showUserMenu = ref(false)
|
|
||||||
|
|
||||||
const closeAllMenus = () => {
|
|
||||||
showMenuDropdown.value = false
|
|
||||||
showUserMenu.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleMenu = (menuName: string) => {
|
|
||||||
if (showMenuDropdown.value === menuName) {
|
|
||||||
showMenuDropdown.value = false
|
|
||||||
} else {
|
|
||||||
showMenuDropdown.value = menuName
|
|
||||||
showUserMenu.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
showMenuDropdown,
|
|
||||||
showUserMenu,
|
|
||||||
closeAllMenus,
|
|
||||||
toggleMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
export function useResizable() {
|
|
||||||
const isResizing = ref(false)
|
|
||||||
|
|
||||||
const startResize = (
|
|
||||||
event: MouseEvent,
|
|
||||||
initialValue: number,
|
|
||||||
onResize: (newValue: number) => void,
|
|
||||||
options: { min?: number; max?: number; direction?: 'horizontal' | 'vertical' } = {}
|
|
||||||
) => {
|
|
||||||
const { min = 100, max = 1000, direction = 'vertical' } = options
|
|
||||||
|
|
||||||
isResizing.value = true
|
|
||||||
const startPos = direction === 'vertical' ? event.clientY : event.clientX
|
|
||||||
const startValue = initialValue
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
const delta = (direction === 'vertical' ? e.clientY : e.clientX) - startPos
|
|
||||||
const newValue = Math.max(min, Math.min(max, startValue + delta))
|
|
||||||
onResize(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
isResizing.value = false
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isResizing,
|
|
||||||
startResize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import { useCoreActions } from './useCoreActions'
|
|||||||
|
|
||||||
export const useTray = () => {
|
export const useTray = () => {
|
||||||
const { startOpenListCore, stopOpenListCore, restartOpenListCore } = useCoreActions()
|
const { startOpenListCore, stopOpenListCore, restartOpenListCore } = useCoreActions()
|
||||||
const store = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
let unlistenTrayActions: (() => void) | null = null
|
let unlistenTrayActions: (() => void) | null = null
|
||||||
const updateTrayMenu = async (serviceRunning: boolean) => {
|
const updateTrayMenu = async (serviceRunning: boolean) => {
|
||||||
@@ -23,19 +23,19 @@ export const useTray = () => {
|
|||||||
case 'start':
|
case 'start':
|
||||||
await startOpenListCore()
|
await startOpenListCore()
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await updateTrayMenu(store.openlistCoreStatus.running)
|
await updateTrayMenu(appStore.openlistCoreStatus.running)
|
||||||
}, 5000)
|
}, 5000)
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
await stopOpenListCore()
|
await stopOpenListCore()
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await updateTrayMenu(store.openlistCoreStatus.running)
|
await updateTrayMenu(appStore.openlistCoreStatus.running)
|
||||||
}, 5000)
|
}, 5000)
|
||||||
break
|
break
|
||||||
case 'restart':
|
case 'restart':
|
||||||
await restartOpenListCore()
|
await restartOpenListCore()
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await updateTrayMenu(store.openlistCoreStatus.running)
|
await updateTrayMenu(appStore.openlistCoreStatus.running)
|
||||||
}, 5000)
|
}, 5000)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@@ -44,7 +44,7 @@ export const useTray = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to execute tray action '${action}':`, error)
|
console.error(`Failed to execute tray action '${action}':`, error)
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await updateTrayMenu(store.openlistCoreStatus.running)
|
await updateTrayMenu(appStore.openlistCoreStatus.running)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ export const useTray = () => {
|
|||||||
try {
|
try {
|
||||||
unlistenTrayActions = await TauriAPI.tray.listen(handleTrayServiceAction)
|
unlistenTrayActions = await TauriAPI.tray.listen(handleTrayServiceAction)
|
||||||
|
|
||||||
await TauriAPI.tray.forceUpdate(store.openlistCoreStatus.running)
|
await TauriAPI.tray.forceUpdate(appStore.openlistCoreStatus.running)
|
||||||
console.log('Tray listeners initialized and menu updated')
|
console.log('Tray listeners initialized and menu updated')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize tray listeners:', error)
|
console.error('Failed to initialize tray listeners:', error)
|
||||||
|
|||||||
@@ -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,20 +208,31 @@
|
|||||||
"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": {
|
||||||
|
"title": "GitHub Proxy",
|
||||||
|
"subtitle": "Accelerate GitHub with proxy service",
|
||||||
|
"label": "GitHub Proxy URL",
|
||||||
|
"placeholder": "https://ghfast.top",
|
||||||
|
"help": "Optional. Enter a proxy URL to accelerate GitHub. Example: https://ghfast.top",
|
||||||
|
"api": {
|
||||||
|
"title": "Apply proxy to API URLs",
|
||||||
|
"description": "Also use proxy for api.github.com URLs "
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tutorial": {
|
"links": {
|
||||||
"title": "Tutorial",
|
"title": "Link Handling",
|
||||||
"subtitle": "Learn how to use OpenList Desktop",
|
"subtitle": "Configure how links are opened",
|
||||||
"restart": "Start Tutorial",
|
"openInBrowser": {
|
||||||
"help": "Restart the tutorial to learn about app features and navigation"
|
"title": "Open links in external browser",
|
||||||
|
"description": "Use system default browser instead of built-in window"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"title": "Updates",
|
"title": "Updates",
|
||||||
@@ -190,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -202,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)",
|
||||||
@@ -213,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)"
|
||||||
@@ -237,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)",
|
||||||
@@ -257,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",
|
||||||
@@ -311,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",
|
||||||
@@ -322,15 +387,58 @@
|
|||||||
"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",
|
||||||
"advancedSettings": "Advanced Settings",
|
"advancedSettings": "Advanced Settings",
|
||||||
"extraFlags": "Extra Flags",
|
"extraFlags": "Extra Flags",
|
||||||
"flagPlaceholder": "e.g., --vfs-cache-mode",
|
"flagPlaceholder": "e.g., --vfs-cache-mode=full",
|
||||||
"addFlag": "Add Flag",
|
"addFlag": "Add Flag",
|
||||||
"removeFlag": "Remove Flag",
|
"removeFlag": "Remove Flag",
|
||||||
|
"quickFlags": "Common Used Flags",
|
||||||
|
"quickFlagsTooltip": "Quick select common rclone flags",
|
||||||
|
"selectCommonFlags": "Select Common Flags",
|
||||||
|
"clickToToggleFlags": "Click on flags to instantly add or remove them from your configuration",
|
||||||
|
"flagCategories": {
|
||||||
|
"Performance": "Performance",
|
||||||
|
"Caching": "Caching",
|
||||||
|
"Bandwidth": "Bandwidth",
|
||||||
|
"Network": "Network",
|
||||||
|
"Security": "Security",
|
||||||
|
"WebDAV Specific": "WebDAV Specific",
|
||||||
|
"Debugging": "Debugging"
|
||||||
|
},
|
||||||
|
"flagDescriptions": {
|
||||||
|
"vfs-cache-mode-full": "Cache full file contents",
|
||||||
|
"vfs-cache-mode-writes": "Cache local writes, upload file contents after writing completes",
|
||||||
|
"vfs-cache-mode-minimal": "Cache only file metadata",
|
||||||
|
"buffer-size-16M": "Buffer size for reading files (default 16M)",
|
||||||
|
"buffer-size-32M": "Larger buffer for better performance",
|
||||||
|
"vfs-read-chunk-size": "Read chunk size (default 128M)",
|
||||||
|
"transfers": "Number of parallel transfers (default 4)",
|
||||||
|
"checkers": "Number of checkers to run in parallel (default 8)",
|
||||||
|
"vfs-cache-max-age": "Max age of objects in cache (default 24h)",
|
||||||
|
"vfs-cache-max-size": "Max total size of cache (default 10G)",
|
||||||
|
"dir-cache-time": "How long to cache directory listings (default 5m)",
|
||||||
|
"bwlimit-10M": "Bandwidth limit (e.g. 10M)",
|
||||||
|
"bwlimit-10M:100M": "Set separate upload and download bandwidth limits",
|
||||||
|
"bwlimit-schedule": "Time-based bandwidth limits",
|
||||||
|
"timeout": "IO idle timeout (default 5m)",
|
||||||
|
"contimeout": "Connection timeout (default 1m)",
|
||||||
|
"low-level-retries": "Number of low level retries (default 10)",
|
||||||
|
"retries": "Retry operations this many times (default 3)",
|
||||||
|
"read-only": "Mount read-only",
|
||||||
|
"allow-other": "Allow other users to access the mount",
|
||||||
|
"allow-root": "Allow root to access the mount",
|
||||||
|
"umask": "Override file permissions (default 022)",
|
||||||
|
"webdav-headers": "Set custom HTTP headers",
|
||||||
|
"webdav-bearer-token": "Custom bearer token",
|
||||||
|
"log-level": "Log level: ERROR, NOTICE, INFO, DEBUG",
|
||||||
|
"verbose": "Print lots more stuff",
|
||||||
|
"use-json-log": "Use JSON format for logging",
|
||||||
|
"progress": "Show progress during transfer"
|
||||||
|
},
|
||||||
"types": {
|
"types": {
|
||||||
"webdav": "WebDAV"
|
"webdav": "WebDAV"
|
||||||
}
|
}
|
||||||
@@ -358,6 +466,8 @@
|
|||||||
"tip": {
|
"tip": {
|
||||||
"webdavTitle": "Enable WebDAV Management Required",
|
"webdavTitle": "Enable WebDAV Management Required",
|
||||||
"webdavMessage": "Before mounting remotes, please ensure WebDAV management for specific user is enabled in OpenList Core.",
|
"webdavMessage": "Before mounting remotes, please ensure WebDAV management for specific user is enabled in OpenList Core.",
|
||||||
|
"winfspTitle": "WinFSP Installation Required",
|
||||||
|
"winfspMessage": "On Windows, you need to install WinFSP first to use mount functionality. Please download and install it from GitHub: https://github.com/winfsp/winfsp/releases",
|
||||||
"dismissForever": "Dismiss forever"
|
"dismissForever": "Dismiss forever"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -365,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",
|
||||||
@@ -416,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",
|
||||||
|
|||||||
@@ -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,20 +208,31 @@
|
|||||||
"auto": "自动",
|
"auto": "自动",
|
||||||
"autoDesc": "跟随系统"
|
"autoDesc": "跟随系统"
|
||||||
},
|
},
|
||||||
"monitor": {
|
"config": {
|
||||||
"title": "监控",
|
"title": "配置文件",
|
||||||
"subtitle": "系统监控和刷新设置",
|
"subtitle": "访问应用程序配置文件",
|
||||||
"interval": {
|
"openFile": "打开 settings.json",
|
||||||
"label": "监控间隔(秒)",
|
"openSuccess": "设置文件打开成功",
|
||||||
"placeholder": "5",
|
"openError": "打开设置文件失败"
|
||||||
"help": "刷新系统指标和状态的频率"
|
},
|
||||||
|
"ghProxy": {
|
||||||
|
"title": "GitHub 代理",
|
||||||
|
"subtitle": "使用代理服务加速 GitHub",
|
||||||
|
"label": "GitHub 代理地址",
|
||||||
|
"placeholder": "https://ghfast.top",
|
||||||
|
"help": "可选。输入代理地址以加速 GitHub。例如:https://ghfast.top",
|
||||||
|
"api": {
|
||||||
|
"title": "代理 API 地址",
|
||||||
|
"description": "同时为 api.github.com 地址使用代理"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tutorial": {
|
"links": {
|
||||||
"title": "教程",
|
"title": "链接处理",
|
||||||
"subtitle": "学习如何使用 OpenList 桌面版",
|
"subtitle": "配置链接的打开方式",
|
||||||
"restart": "开始教程",
|
"openInBrowser": {
|
||||||
"help": "重新启动教程以了解应用功能和导航"
|
"title": "在外部浏览器中打开链接",
|
||||||
|
"description": "使用系统默认浏览器而不是内置窗口"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"title": "更新",
|
"title": "更新",
|
||||||
@@ -190,6 +246,10 @@
|
|||||||
"title": "开机自动启动应用(立即生效)",
|
"title": "开机自动启动应用(立即生效)",
|
||||||
"subtitle": "在系统启动时自动启动 OpenList 桌面应用",
|
"subtitle": "在系统启动时自动启动 OpenList 桌面应用",
|
||||||
"description": "在系统启动时自动启动 OpenList 桌面应用"
|
"description": "在系统启动时自动启动 OpenList 桌面应用"
|
||||||
|
},
|
||||||
|
"showWindowOnStartup": {
|
||||||
|
"title": "启动时显示主窗口",
|
||||||
|
"description": "在 OpenList 桌面应用启动时显示主应用窗口"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -202,7 +262,9 @@
|
|||||||
"copyFailed": "复制日志到剪贴板失败",
|
"copyFailed": "复制日志到剪贴板失败",
|
||||||
"exportSuccess": "成功导出 {count} 条日志到文件",
|
"exportSuccess": "成功导出 {count} 条日志到文件",
|
||||||
"clearSuccess": "日志清理成功",
|
"clearSuccess": "日志清理成功",
|
||||||
"clearFailed": "清理日志失败"
|
"clearFailed": "清理日志失败",
|
||||||
|
"openDirectorySuccess": "日志目录打开成功",
|
||||||
|
"openDirectoryFailed": "打开日志目录失败"
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"pause": "暂停 (Space)",
|
"pause": "暂停 (Space)",
|
||||||
@@ -213,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)"
|
||||||
@@ -237,7 +300,8 @@
|
|||||||
"sources": {
|
"sources": {
|
||||||
"all": "所有来源",
|
"all": "所有来源",
|
||||||
"rclone": "Rclone",
|
"rclone": "Rclone",
|
||||||
"openlist": "OpenList"
|
"openlist": "OpenList",
|
||||||
|
"service": "服务"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"selectAll": "全选 (Ctrl+A)",
|
"selectAll": "全选 (Ctrl+A)",
|
||||||
@@ -257,7 +321,8 @@
|
|||||||
"stripAnsiColors": "去除 ANSI 颜色"
|
"stripAnsiColors": "去除 ANSI 颜色"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"confirmClear": "您确定要清除所有日志吗?"
|
"confirmClear": "您确定要清除所有日志吗?",
|
||||||
|
"confirmTitle": "清除日志"
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"timestamp": "时间",
|
"timestamp": "时间",
|
||||||
@@ -311,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",
|
||||||
@@ -322,15 +387,58 @@
|
|||||||
"password": "密码",
|
"password": "密码",
|
||||||
"passwordPlaceholder": "密码",
|
"passwordPlaceholder": "密码",
|
||||||
"mountPoint": "挂载点",
|
"mountPoint": "挂载点",
|
||||||
"mountPointPlaceholder": "例如:T: (Windows) 或 /mnt/remote (Linux)",
|
"mountPointPlaceholder": "例如:T: 或 /mnt/remote",
|
||||||
"volumeName": "远程路径",
|
"volumeName": "远程路径",
|
||||||
"volumeNamePlaceholder": "例如:/",
|
"volumeNamePlaceholder": "例如:/",
|
||||||
"autoMount": "开机自动挂载",
|
"autoMount": "开机自动挂载",
|
||||||
"advancedSettings": "高级设置",
|
"advancedSettings": "高级设置",
|
||||||
"extraFlags": "额外标志",
|
"extraFlags": "额外标志",
|
||||||
"flagPlaceholder": "例如:--vfs-cache-mode",
|
"flagPlaceholder": "例如:--vfs-cache-mode=full",
|
||||||
"addFlag": "添加标志",
|
"addFlag": "添加标志",
|
||||||
"removeFlag": "移除标志",
|
"removeFlag": "移除标志",
|
||||||
|
"quickFlags": "常用标志",
|
||||||
|
"quickFlagsTooltip": "快速选择常用 rclone 标志",
|
||||||
|
"selectCommonFlags": "选择常用标志",
|
||||||
|
"clickToToggleFlags": "点击即可立即添加或移除",
|
||||||
|
"flagCategories": {
|
||||||
|
"Performance": "性能",
|
||||||
|
"Caching": "缓存",
|
||||||
|
"Bandwidth": "宽带",
|
||||||
|
"Network": "网络",
|
||||||
|
"Security": "安全",
|
||||||
|
"WebDAV Specific": "WebDAV 专用",
|
||||||
|
"Debugging": "调试"
|
||||||
|
},
|
||||||
|
"flagDescriptions": {
|
||||||
|
"vfs-cache-mode-full": "缓存文件的完整内容",
|
||||||
|
"vfs-cache-mode-writes": "缓存本地写入,文件内容写入完成后上传",
|
||||||
|
"vfs-cache-mode-minimal": "只缓存文件的元信息",
|
||||||
|
"buffer-size-16M": "读取文件的缓冲区大小(默认 16M)",
|
||||||
|
"buffer-size-32M": "更大的缓冲区以获得更好的性能",
|
||||||
|
"vfs-read-chunk-size": "读取块大小(默认 128M)",
|
||||||
|
"transfers": "并行传输数量(默认 4)",
|
||||||
|
"checkers": "并行运行的检查器数量(默认 8)",
|
||||||
|
"vfs-cache-max-age": "缓存的最大生命周期(默认 24h)",
|
||||||
|
"vfs-cache-max-size": "缓存文件的最大大小(默认 10G)",
|
||||||
|
"dir-cache-time": "缓存目录列表的时间(默认 5m)",
|
||||||
|
"bwlimit-10M": "带宽限制(例如 10M)",
|
||||||
|
"bwlimit-10M:100M": "分别设置上传和下载宽带限制",
|
||||||
|
"bwlimit-schedule": "基于时间的带宽限制",
|
||||||
|
"timeout": "IO 空闲超时(默认 5m)",
|
||||||
|
"contimeout": "连接超时(默认 1m)",
|
||||||
|
"low-level-retries": "低级重试次数(默认 10)",
|
||||||
|
"retries": "重试操作次数(默认 3)",
|
||||||
|
"read-only": "以只读方式挂载",
|
||||||
|
"allow-other": "允许其他用户访问挂载点",
|
||||||
|
"allow-root": "允许 root 用户访问挂载点",
|
||||||
|
"umask": "覆盖文件权限(默认 022)",
|
||||||
|
"webdav-headers": "设置自定义 HTTP 标头",
|
||||||
|
"webdav-bearer-token": "自定义token",
|
||||||
|
"log-level": "日志级别:ERROR、NOTICE、INFO、DEBUG",
|
||||||
|
"verbose": "打印更多详细信息",
|
||||||
|
"use-json-log": "使用 JSON 格式记录日志",
|
||||||
|
"progress": "传输期间显示进度"
|
||||||
|
},
|
||||||
"types": {
|
"types": {
|
||||||
"webdav": "WebDAV"
|
"webdav": "WebDAV"
|
||||||
}
|
}
|
||||||
@@ -358,6 +466,8 @@
|
|||||||
"tip": {
|
"tip": {
|
||||||
"webdavTitle": "需要启用 WebDAV 管理功能",
|
"webdavTitle": "需要启用 WebDAV 管理功能",
|
||||||
"webdavMessage": "在挂载远程存储之前,请确保在 OpenList 核心中为用户启用了 WebDAV 管理功能",
|
"webdavMessage": "在挂载远程存储之前,请确保在 OpenList 核心中为用户启用了 WebDAV 管理功能",
|
||||||
|
"winfspTitle": "需要安装 WinFSP",
|
||||||
|
"winfspMessage": "在 Windows 系统上,您需要先安装 WinFSP 才能使用挂载功能。请从 GitHub 下载并安装:https://github.com/winfsp/winfsp/releases",
|
||||||
"dismissForever": "永久关闭"
|
"dismissForever": "永久关闭"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -365,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": "保持应用程序最新,获取最新功能和安全改进",
|
||||||
@@ -416,7 +496,7 @@
|
|||||||
"startingDownload": "开始下载...",
|
"startingDownload": "开始下载...",
|
||||||
"downloading": "下载中...",
|
"downloading": "下载中...",
|
||||||
"installingUpdate": "安装更新中...",
|
"installingUpdate": "安装更新中...",
|
||||||
"restartingApp": "重启应用中...",
|
"quitApp": "退出应用中...",
|
||||||
"noUpdatesFound": "没有可用更新",
|
"noUpdatesFound": "没有可用更新",
|
||||||
"aboutUpdates": "关于更新",
|
"aboutUpdates": "关于更新",
|
||||||
"autoCheckInfo": "自动更新检查让您的应用保持安全和最新状态",
|
"autoCheckInfo": "自动更新检查让您的应用保持安全和最新状态",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,82 +8,66 @@ export const useRcloneStore = defineStore('rclone', () => {
|
|||||||
const error = ref<string | undefined>()
|
const error = ref<string | undefined>()
|
||||||
const serviceRunning = ref(false)
|
const serviceRunning = ref(false)
|
||||||
|
|
||||||
function clearError() {
|
const setError = (msg?: string) => (error.value = msg)
|
||||||
error.value = undefined
|
|
||||||
|
const runWithLoading = async <T>(fn: () => Promise<T>): Promise<T> => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRcloneBackend() {
|
async function getRcloneProcessId(): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
const processList = await TauriAPI.process.list()
|
||||||
const isRunning = await TauriAPI.rclone.backend.isRunning()
|
return processList.find(p => p.config?.name === 'single_rclone_backend_process')?.id
|
||||||
if (isRunning) {
|
} catch (err) {
|
||||||
|
console.error('Failed to get Rclone process ID:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearError = () => setError()
|
||||||
|
|
||||||
|
const startRcloneBackend = () =>
|
||||||
|
runWithLoading(async () => {
|
||||||
|
if (await TauriAPI.rclone.backend.isRunning()) {
|
||||||
serviceRunning.value = true
|
serviceRunning.value = true
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
const result = await TauriAPI.rclone.backend.createAndStart()
|
const result = await TauriAPI.rclone.backend.createAndStart()
|
||||||
if (result) {
|
if (result) {
|
||||||
serviceRunning.value = true
|
serviceRunning.value = true
|
||||||
}
|
}
|
||||||
return result
|
}).catch(err => {
|
||||||
} catch (err: any) {
|
setError('Failed to start rclone service')
|
||||||
error.value = 'Failed to start rclone service'
|
|
||||||
throw err
|
throw err
|
||||||
} finally {
|
})
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRcloneProcessId() {
|
const stopRcloneBackend = () =>
|
||||||
try {
|
runWithLoading(async () => {
|
||||||
const processList = await TauriAPI.process.list()
|
|
||||||
const findRcloneBackend = processList.find(p => p.config?.name === 'single_rclone_backend_process')
|
|
||||||
if (findRcloneBackend) {
|
|
||||||
return findRcloneBackend.id
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to get Rclone process ID from database:', err)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopRcloneBackend() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
const id = await getRcloneProcessId()
|
const id = await getRcloneProcessId()
|
||||||
if (!id) {
|
if (!id) {
|
||||||
serviceRunning.value = false
|
serviceRunning.value = false
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
const result = await TauriAPI.process.stop(id)
|
const ok = await TauriAPI.process.stop(id)
|
||||||
if (result) {
|
if (ok) serviceRunning.value = false
|
||||||
serviceRunning.value = false
|
return ok
|
||||||
}
|
}).catch(err => {
|
||||||
return result
|
setError('Failed to stop rclone service')
|
||||||
} catch (err: any) {
|
|
||||||
error.value = 'Failed to stop rclone service'
|
|
||||||
throw err
|
throw err
|
||||||
} finally {
|
})
|
||||||
loading.value = false
|
|
||||||
}
|
const checkRcloneBackendStatus = async () => {
|
||||||
|
const running = await TauriAPI.rclone.backend.isRunning().catch(() => false)
|
||||||
|
serviceRunning.value = running
|
||||||
|
return running
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkRcloneBackendStatus() {
|
const init = () => console.log('Initializing Rclone store...')
|
||||||
try {
|
|
||||||
const isRunning = await TauriAPI.rclone.backend.isRunning()
|
|
||||||
serviceRunning.value = isRunning
|
|
||||||
return isRunning
|
|
||||||
} catch (err: any) {
|
|
||||||
serviceRunning.value = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
console.log('Initializing Rclone store...')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
serviceRunning,
|
serviceRunning,
|
||||||
|
|||||||
11
src/types/types.d.ts
vendored
11
src/types/types.d.ts
vendored
@@ -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,8 +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_api?: 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 {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import CoreMonitorCard from '../components/dashboard/CoreMonitorCard.vue'
|
||||||
|
import DocumentationCard from '../components/dashboard/DocumentationCard.vue'
|
||||||
|
import QuickActionsCard from '../components/dashboard/QuickActionsCard.vue'
|
||||||
|
import ServiceManagementCard from '../components/dashboard/ServiceManagementCard.vue'
|
||||||
|
import VersionManagerCard from '../components/dashboard/VersionManagerCard.vue'
|
||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
|
|
||||||
import QuickActionsCard from '../components/dashboard/QuickActionsCard.vue'
|
const appStore = useAppStore()
|
||||||
import CoreMonitorCard from '../components/dashboard/CoreMonitorCard.vue'
|
|
||||||
import VersionManagerCard from '../components/dashboard/VersionManagerCard.vue'
|
|
||||||
import DocumentationCard from '../components/dashboard/DocumentationCard.vue'
|
|
||||||
import ServiceManagementCard from '../components/dashboard/ServiceManagementCard.vue'
|
|
||||||
|
|
||||||
const store = useAppStore()
|
|
||||||
|
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ const layoutClass = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
serviceStatus.value.isRunning = store.isCoreRunning
|
serviceStatus.value.isRunning = appStore.isCoreRunning
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,38 +1,43 @@
|
|||||||
<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'
|
||||||
|
|
||||||
const store = useAppStore()
|
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 { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const logContainer = ref<HTMLElement>()
|
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
|
||||||
@@ -132,7 +155,7 @@ const parseLogEntry = (logText: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
const filteredLogs = computed(() => {
|
||||||
let logs = store.logs
|
let logs = appStore.logs
|
||||||
.slice(-maxLines.value)
|
.slice(-maxLines.value)
|
||||||
.filter((log: string | string[]) => !log.includes('/ping'))
|
.filter((log: string | string[]) => !log.includes('/ping'))
|
||||||
.map(parseLogEntry)
|
.map(parseLogEntry)
|
||||||
@@ -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 store.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 store.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 store.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 = store.logs.length
|
const oldLength = appStore.logs.length
|
||||||
await store.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 (store.logs.length > oldLength) {
|
if (appStore.logs.length > oldLength) {
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, (store.settings.app.monitor_interval || 5) * 1000)
|
}, 30 * 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -381,7 +406,7 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('keydown', handleKeydown)
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
})
|
})
|
||||||
|
|
||||||
const unwatchLogs = store.$subscribe(mutation => {
|
const unwatchLogs = appStore.$subscribe(mutation => {
|
||||||
if (mutation.storeId === 'app') {
|
if (mutation.storeId === 'app') {
|
||||||
const events = Array.isArray(mutation.events) ? mutation.events : [mutation.events]
|
const events = Array.isArray(mutation.events) ? mutation.events : [mutation.events]
|
||||||
if (events.some((event: any) => event.key === 'logs')) {
|
if (events.some((event: any) => event.key === 'logs')) {
|
||||||
@@ -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>
|
||||||
@@ -451,7 +476,7 @@ onUnmounted(() => {
|
|||||||
<div class="toolbar-section right">
|
<div class="toolbar-section right">
|
||||||
<div class="log-stats">
|
<div class="log-stats">
|
||||||
<span class="stat">{{
|
<span class="stat">{{
|
||||||
t('logs.stats.logsCount', { filtered: filteredLogs.length, total: store.logs.length })
|
t('logs.stats.logsCount', { filtered: filteredLogs.length, total: appStore.logs.length })
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-if="selectedEntries.size > 0" class="stat selected">
|
<span v-if="selectedEntries.size > 0" class="stat selected">
|
||||||
{{ t('logs.stats.selected', { count: selectedEntries.size }) }}
|
{{ t('logs.stats.selected', { count: selectedEntries.size }) }}
|
||||||
@@ -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>
|
||||||
@@ -618,7 +653,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<div class="status-right">
|
<div class="status-right">
|
||||||
<span class="status-item">
|
<span class="status-item">
|
||||||
{{ t('logs.status.showing', { filtered: filteredLogs.length, total: store.logs.length }) }}
|
{{ t('logs.status.showing', { filtered: filteredLogs.length, total: appStore.logs.length }) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
<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()
|
||||||
const store = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const showAddForm = ref(false)
|
const showAddForm = ref(false)
|
||||||
const editingConfig = ref<RcloneFormConfig | null>(null)
|
const editingConfig = ref<RcloneFormConfig | null>(null)
|
||||||
@@ -58,9 +60,77 @@ const configForm = ref({
|
|||||||
}
|
}
|
||||||
}) as Ref<RcloneFormConfig>
|
}) as Ref<RcloneFormConfig>
|
||||||
|
|
||||||
|
const commonFlags = ref([
|
||||||
|
{
|
||||||
|
category: 'Caching',
|
||||||
|
flags: [
|
||||||
|
{ flag: '--vfs-cache-mode', value: 'full', descriptionKey: 'vfs-cache-mode-full' },
|
||||||
|
{ flag: '--vfs-cache-mode', value: 'writes', descriptionKey: 'vfs-cache-mode-writes' },
|
||||||
|
{ flag: '--vfs-cache-mode', value: 'minimal', descriptionKey: 'vfs-cache-mode-minimal' },
|
||||||
|
{ flag: '--vfs-cache-max-age', value: '24h', descriptionKey: 'vfs-cache-max-age' },
|
||||||
|
{ flag: '--vfs-cache-max-size', value: '10G', descriptionKey: 'vfs-cache-max-size' },
|
||||||
|
{ flag: '--dir-cache-time', value: '5m', descriptionKey: 'dir-cache-time' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Performance',
|
||||||
|
flags: [
|
||||||
|
{ flag: '--buffer-size', value: '16M', descriptionKey: 'buffer-size-16M' },
|
||||||
|
{ flag: '--buffer-size', value: '32M', descriptionKey: 'buffer-size-32M' },
|
||||||
|
{ flag: '--vfs-read-chunk-size', value: '128M', descriptionKey: 'vfs-read-chunk-size' },
|
||||||
|
{ flag: '--transfers', value: '4', descriptionKey: 'transfers' },
|
||||||
|
{ flag: '--checkers', value: '8', descriptionKey: 'checkers' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Bandwidth',
|
||||||
|
flags: [
|
||||||
|
{ flag: '--bwlimit', value: '10M', descriptionKey: 'bwlimit-10M' },
|
||||||
|
{ flag: '--bwlimit', value: '10M:100M', descriptionKey: 'bwlimit-10M:100M' },
|
||||||
|
{ flag: '--bwlimit', value: '08:00,512k 18:00,10M 23:00,off', descriptionKey: 'bwlimit-schedule' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Network',
|
||||||
|
flags: [
|
||||||
|
{ flag: '--timeout', value: '5m', descriptionKey: 'timeout' },
|
||||||
|
{ flag: '--contimeout', value: '60s', descriptionKey: 'contimeout' },
|
||||||
|
{ flag: '--low-level-retries', value: '10', descriptionKey: 'low-level-retries' },
|
||||||
|
{ flag: '--retries', value: '3', descriptionKey: 'retries' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Security',
|
||||||
|
flags: [
|
||||||
|
{ flag: '--read-only', value: '', descriptionKey: 'read-only' },
|
||||||
|
{ flag: '--allow-other', value: '', descriptionKey: 'allow-other' },
|
||||||
|
{ flag: '--allow-root', value: '', descriptionKey: 'allow-root' },
|
||||||
|
{ flag: '--umask', value: '022', descriptionKey: 'umask' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'WebDAV Specific',
|
||||||
|
flags: [
|
||||||
|
{ flag: '--webdav-headers', value: 'User-Agent,rclone/1.0', descriptionKey: 'webdav-headers' },
|
||||||
|
{ flag: '--webdav-bearer-token', value: '', descriptionKey: 'webdav-bearer-token' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Debugging',
|
||||||
|
flags: [
|
||||||
|
{ flag: '--log-level', value: 'INFO', descriptionKey: 'log-level' },
|
||||||
|
{ flag: '--verbose', value: '', descriptionKey: 'verbose' },
|
||||||
|
{ flag: '--use-json-log', value: '', descriptionKey: 'use-json-log' },
|
||||||
|
{ flag: '--progress', value: '', descriptionKey: 'progress' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const showFlagSelector = ref(false)
|
||||||
|
|
||||||
const filteredConfigs: ComputedRef<RcloneFormConfig[]> = computed(() => {
|
const filteredConfigs: ComputedRef<RcloneFormConfig[]> = computed(() => {
|
||||||
let filtered: RcloneFormConfig[] = []
|
const filtered: RcloneFormConfig[] = []
|
||||||
const fullRemoteConfigs = store.fullRcloneConfigs
|
const fullRemoteConfigs = appStore.fullRcloneConfigs
|
||||||
|
|
||||||
for (const config of fullRemoteConfigs) {
|
for (const config of fullRemoteConfigs) {
|
||||||
if (!config) continue
|
if (!config) continue
|
||||||
@@ -71,7 +141,7 @@ const filteredConfigs: ComputedRef<RcloneFormConfig[]> = computed(() => {
|
|||||||
: true
|
: true
|
||||||
if (!matchesSearch) continue
|
if (!matchesSearch) continue
|
||||||
|
|
||||||
const mountInfo = store.mountInfos.find(mount => mount.name === config.name)
|
const mountInfo = appStore.mountInfos.find(mount => mount.name === config.name)
|
||||||
const status = mountInfo?.status || 'unmounted'
|
const status = mountInfo?.status || 'unmounted'
|
||||||
const matchesStatus = statusFilter.value === 'all' || status === statusFilter.value
|
const matchesStatus = statusFilter.value === 'all' || status === statusFilter.value
|
||||||
|
|
||||||
@@ -83,12 +153,12 @@ const filteredConfigs: ComputedRef<RcloneFormConfig[]> = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const configCounts = computed(() => {
|
const configCounts = computed(() => {
|
||||||
const fullConfigs = store.fullRcloneConfigs
|
const fullConfigs = appStore.fullRcloneConfigs
|
||||||
return {
|
return {
|
||||||
total: fullConfigs.length,
|
total: fullConfigs.length,
|
||||||
mounted: store.mountedConfigs.length,
|
mounted: appStore.mountedConfigs.length,
|
||||||
unmounted: fullConfigs.length - store.mountedConfigs.length,
|
unmounted: fullConfigs.length - appStore.mountedConfigs.length,
|
||||||
error: store.mountInfos.filter(m => m.status === 'error').length
|
error: appStore.mountInfos.filter(m => m.status === 'error').length
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,7 +192,7 @@ const saveConfig = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (editingConfig.value && editingConfig.value.name) {
|
if (editingConfig.value && editingConfig.value.name) {
|
||||||
await store.updateRemoteConfig(editingConfig.value.name, configForm.value.type, {
|
await appStore.updateRemoteConfig(editingConfig.value.name, configForm.value.type, {
|
||||||
name: configForm.value.name,
|
name: configForm.value.name,
|
||||||
type: configForm.value.type,
|
type: configForm.value.type,
|
||||||
url: configForm.value.url,
|
url: configForm.value.url,
|
||||||
@@ -135,7 +205,7 @@ const saveConfig = async () => {
|
|||||||
extraFlags: configForm.value.extraFlags
|
extraFlags: configForm.value.extraFlags
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await store.createRemoteConfig(configForm.value.name, configForm.value.type, {
|
await appStore.createRemoteConfig(configForm.value.name, configForm.value.type, {
|
||||||
name: configForm.value.name,
|
name: configForm.value.name,
|
||||||
type: configForm.value.type,
|
type: configForm.value.type,
|
||||||
url: configForm.value.url,
|
url: configForm.value.url,
|
||||||
@@ -178,7 +248,7 @@ const resetForm = () => {
|
|||||||
|
|
||||||
const mountConfig = async (config: RcloneFormConfig) => {
|
const mountConfig = async (config: RcloneFormConfig) => {
|
||||||
try {
|
try {
|
||||||
await store.mountRemote(config.name)
|
await appStore.mountRemote(config.name)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error.message || t('mount.messages.failedToMount'))
|
console.error(error.message || t('mount.messages.failedToMount'))
|
||||||
}
|
}
|
||||||
@@ -187,7 +257,7 @@ const mountConfig = async (config: RcloneFormConfig) => {
|
|||||||
const unmountConfig = async (config: RcloneFormConfig) => {
|
const unmountConfig = async (config: RcloneFormConfig) => {
|
||||||
if (!config.name) return
|
if (!config.name) return
|
||||||
try {
|
try {
|
||||||
await store.unmountRemote(config.name)
|
await appStore.unmountRemote(config.name)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error.message || t('mount.messages.failedToUnmount'))
|
console.error(error.message || t('mount.messages.failedToUnmount'))
|
||||||
}
|
}
|
||||||
@@ -209,7 +279,7 @@ const confirmDelete = async () => {
|
|||||||
if (!config || !config.name) return
|
if (!config || !config.name) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await store.deleteRemoteConfig(config.name)
|
await appStore.deleteRemoteConfig(config.name)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error.message || t('mount.messages.failedToDelete'))
|
console.error(error.message || t('mount.messages.failedToDelete'))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -228,8 +298,8 @@ const startBackend = async () => {
|
|||||||
await rcloneStore.startRcloneBackend()
|
await rcloneStore.startRcloneBackend()
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
await rcloneStore.checkRcloneBackendStatus()
|
await rcloneStore.checkRcloneBackendStatus()
|
||||||
await store.loadRemoteConfigs()
|
await appStore.loadRemoteConfigs()
|
||||||
await store.loadMountInfos()
|
await appStore.loadMountInfos()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error.message || t('mount.messages.failedToStartService'))
|
console.error(error.message || t('mount.messages.failedToStartService'))
|
||||||
}
|
}
|
||||||
@@ -237,14 +307,17 @@ const startBackend = async () => {
|
|||||||
|
|
||||||
const stopBackend = async () => {
|
const stopBackend = async () => {
|
||||||
try {
|
try {
|
||||||
await rcloneStore.stopRcloneBackend()
|
const stopped = await rcloneStore.stopRcloneBackend()
|
||||||
|
if (!stopped) {
|
||||||
|
throw new Error(t('mount.messages.failedToStopService'))
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error.message || t('mount.messages.failedToStopService'))
|
console.error(error.message || t('mount.messages.failedToStopService'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfigStatus = (config: RcloneFormConfig) => {
|
const getConfigStatus = (config: RcloneFormConfig) => {
|
||||||
const mountInfo = store.mountInfos.find(mount => mount.name === config.name)
|
const mountInfo = appStore.mountInfos.find(mount => mount.name === config.name)
|
||||||
return mountInfo?.status || 'unmounted'
|
return mountInfo?.status || 'unmounted'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +358,58 @@ const removeFlag = (index: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addFlagToConfig = (flag: { flag: string; value: string; descriptionKey: string }) => {
|
||||||
|
if (!configForm.value.extraFlags) {
|
||||||
|
configForm.value.extraFlags = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagKey = `${flag.flag}${flag.value ? `=${flag.value}` : ''}`
|
||||||
|
|
||||||
|
if (flag.flag === '--vfs-cache-mode' || flag.flag === '--buffer-size' || flag.flag === '--log-level') {
|
||||||
|
const existingIndex = configForm.value.extraFlags.findIndex(existingFlag => existingFlag.startsWith(flag.flag))
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
configForm.value.extraFlags.splice(existingIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configForm.value.extraFlags.includes(flagKey)) {
|
||||||
|
configForm.value.extraFlags.push(flagKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFlagFromConfig = (flag: { flag: string; value: string; descriptionKey: string }) => {
|
||||||
|
if (!configForm.value.extraFlags) return
|
||||||
|
|
||||||
|
const flagKey = `${flag.flag}${flag.value ? `=${flag.value}` : ''}`
|
||||||
|
const index = configForm.value.extraFlags.indexOf(flagKey)
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
configForm.value.extraFlags.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlagInConfig = (flag: { flag: string; value: string; descriptionKey: string }) => {
|
||||||
|
if (!configForm.value.extraFlags) return false
|
||||||
|
const flagKey = `${flag.flag}${flag.value ? `=${flag.value}` : ''}`
|
||||||
|
return configForm.value.extraFlags.includes(flagKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFlag = (flag: { flag: string; value: string; descriptionKey: string }) => {
|
||||||
|
if (isFlagInConfig(flag)) {
|
||||||
|
removeFlagFromConfig(flag)
|
||||||
|
} else {
|
||||||
|
addFlagToConfig(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeFlagSelector = () => {
|
||||||
|
showFlagSelector.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFlagDescription = (flag: { flag: string; value: string; descriptionKey: string }) => {
|
||||||
|
return t(`mount.config.flagDescriptions.${flag.descriptionKey}`)
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
const key = event.key
|
const key = event.key
|
||||||
const ctrl = event.ctrlKey
|
const ctrl = event.ctrlKey
|
||||||
@@ -294,8 +419,8 @@ const handleKeydown = (event: KeyboardEvent) => {
|
|||||||
addNewConfig()
|
addNewConfig()
|
||||||
} else if (ctrl && key === 'r') {
|
} else if (ctrl && key === 'r') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
store.loadRemoteConfigs()
|
appStore.loadRemoteConfigs()
|
||||||
store.loadMountInfos()
|
appStore.loadMountInfos()
|
||||||
} else if (key === 'Escape') {
|
} else if (key === 'Escape') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (showAddForm.value) {
|
if (showAddForm.value) {
|
||||||
@@ -311,7 +436,7 @@ const openInFileExplorer = async (path?: string) => {
|
|||||||
}
|
}
|
||||||
const normalizedPath = path.trim()
|
const normalizedPath = path.trim()
|
||||||
try {
|
try {
|
||||||
await store.openFolder(normalizedPath)
|
await appStore.openFolder(normalizedPath)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to open mount point in file explorer:', error)
|
console.error('Failed to open mount point in file explorer:', error)
|
||||||
const errorMessage = error.message || error.toString() || 'Unknown error'
|
const errorMessage = error.message || error.toString() || 'Unknown error'
|
||||||
@@ -330,16 +455,33 @@ const dismissWebdavTip = () => {
|
|||||||
localStorage.setItem('webdav_tip_dismissed', 'true')
|
localStorage.setItem('webdav_tip_dismissed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isWindows = computed(() => {
|
||||||
|
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
|
||||||
|
})
|
||||||
|
const showWinfspTip = ref(isWindows.value && !localStorage.getItem('winfsp_tip_dismissed'))
|
||||||
|
|
||||||
|
const dismissWinfspTip = () => {
|
||||||
|
showWinfspTip.value = false
|
||||||
|
localStorage.setItem('winfsp_tip_dismissed', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShowWebdavTip = computed(() => {
|
||||||
|
if (isWindows.value) {
|
||||||
|
return !showWinfspTip.value && showWebdavTip.value
|
||||||
|
}
|
||||||
|
return showWebdavTip.value
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
document.addEventListener('keydown', handleKeydown)
|
document.addEventListener('keydown', handleKeydown)
|
||||||
await rcloneStore.checkRcloneBackendStatus()
|
rcloneStore.checkRcloneBackendStatus()
|
||||||
await store.loadRemoteConfigs()
|
appStore.loadRemoteConfigs()
|
||||||
await store.loadMountInfos()
|
appStore.loadMountInfos()
|
||||||
mountRefreshInterval = setInterval(store.loadMountInfos, (store.settings.app.monitor_interval || 5) * 1000)
|
mountRefreshInterval = setInterval(appStore.loadMountInfos, 15 * 1000)
|
||||||
backendStatusCheckInterval = setInterval(() => {
|
backendStatusCheckInterval = setInterval(() => {
|
||||||
rcloneStore.checkRcloneBackendStatus()
|
rcloneStore.checkRcloneBackendStatus()
|
||||||
}, (store.settings.app.monitor_interval || 5) * 1000)
|
}, 15 * 1000)
|
||||||
await rcloneStore.init()
|
rcloneStore.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -393,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>
|
||||||
@@ -408,7 +550,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showWebdavTip" class="webdav-tip">
|
<div v-if="shouldShowWebdavTip" class="webdav-tip">
|
||||||
<div class="tip-content">
|
<div class="tip-content">
|
||||||
<div class="tip-icon">
|
<div class="tip-icon">
|
||||||
<Settings class="icon" />
|
<Settings class="icon" />
|
||||||
@@ -417,7 +559,22 @@ 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" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showWinfspTip" class="winfsp-tip">
|
||||||
|
<div class="tip-content">
|
||||||
|
<div class="tip-icon">
|
||||||
|
<HardDrive class="icon" />
|
||||||
|
</div>
|
||||||
|
<div class="tip-message">
|
||||||
|
<h4 class="tip-title">{{ t('mount.tip.winfspTitle') }}</h4>
|
||||||
|
<p class="tip-description">{{ t('mount.tip.winfspMessage') }}</p>
|
||||||
|
</div>
|
||||||
|
<button class="tip-close" :title="t('mount.tip.dismissForever')" @click="dismissWinfspTip">
|
||||||
<X class="close-icon" />
|
<X class="close-icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -441,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="store.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>
|
||||||
@@ -450,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>
|
||||||
@@ -462,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>
|
||||||
@@ -495,7 +652,7 @@ onUnmounted(() => {
|
|||||||
:is="getStatusIcon(getConfigStatus(config))"
|
:is="getStatusIcon(getConfigStatus(config))"
|
||||||
class="status-icon"
|
class="status-icon"
|
||||||
:class="{
|
:class="{
|
||||||
spinning: isConfigMounting(config) || store.loading,
|
spinning: isConfigMounting(config) || appStore.loading,
|
||||||
success: getConfigStatus(config) === 'mounted',
|
success: getConfigStatus(config) === 'mounted',
|
||||||
error: getConfigStatus(config) === 'error'
|
error: getConfigStatus(config) === 'error'
|
||||||
}"
|
}"
|
||||||
@@ -509,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 }}
|
||||||
@@ -524,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>
|
||||||
@@ -544,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>
|
||||||
@@ -569,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">
|
||||||
@@ -578,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>
|
||||||
@@ -689,6 +846,68 @@ onUnmounted(() => {
|
|||||||
<h3 class="section-title">{{ t('mount.config.advancedSettings') }}</h3>
|
<h3 class="section-title">{{ t('mount.config.advancedSettings') }}</h3>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="field-label">{{ t('mount.config.extraFlags') }}</label>
|
<label class="field-label">{{ t('mount.config.extraFlags') }}</label>
|
||||||
|
|
||||||
|
<div class="flags-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="quick-flags-btn"
|
||||||
|
:title="t('mount.config.quickFlagsTooltip')"
|
||||||
|
@click="showFlagSelector = !showFlagSelector"
|
||||||
|
>
|
||||||
|
<Settings class="btn-icon" />
|
||||||
|
<span>{{ t('mount.config.quickFlags') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showFlagSelector" class="flag-selector-backdrop" @click="closeFlagSelector">
|
||||||
|
<div class="flag-selector-popup" @click.stop>
|
||||||
|
<div class="flag-selector-header">
|
||||||
|
<h4>{{ t('mount.config.selectCommonFlags') }}</h4>
|
||||||
|
<button class="close-selector-btn" @click="closeFlagSelector">
|
||||||
|
<X class="btn-icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flag-selector-content">
|
||||||
|
<div class="flag-selector-help">
|
||||||
|
<p>{{ t('mount.config.clickToToggleFlags') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flag-categories">
|
||||||
|
<div v-for="category in commonFlags" :key="category.category" class="flag-category">
|
||||||
|
<div class="category-header">
|
||||||
|
<h5>{{ t(`mount.config.flagCategories.${category.category}`) }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="category-flags">
|
||||||
|
<div
|
||||||
|
v-for="flag in category.flags"
|
||||||
|
:key="`${flag.flag}-${flag.value}`"
|
||||||
|
class="flag-option"
|
||||||
|
:class="{
|
||||||
|
selected: isFlagInConfig(flag),
|
||||||
|
'in-config': isFlagInConfig(flag)
|
||||||
|
}"
|
||||||
|
:title="getFlagDescription(flag)"
|
||||||
|
@click="toggleFlag(flag)"
|
||||||
|
>
|
||||||
|
<div class="flag-checkbox">
|
||||||
|
<div class="custom-checkbox" :class="{ checked: isFlagInConfig(flag) }">
|
||||||
|
<CheckCircle v-if="isFlagInConfig(flag)" class="check-icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flag-content">
|
||||||
|
<code class="flag-code">{{ flag.flag }}{{ flag.value ? `=${flag.value}` : '' }}</code>
|
||||||
|
<span class="flag-description">{{ getFlagDescription(flag) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Flags Input -->
|
||||||
<div class="flags-container">
|
<div class="flags-container">
|
||||||
<div v-for="(_, index) in configForm.extraFlags || []" :key="index" class="flag-item">
|
<div v-for="(_, index) in configForm.extraFlags || []" :key="index" class="flag-item">
|
||||||
<input
|
<input
|
||||||
@@ -698,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>
|
||||||
@@ -717,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="store.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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
import { useTranslation } from '../composables/useI18n'
|
AlertCircle,
|
||||||
import { Settings, Server, HardDrive, Save, RotateCcw, AlertCircle, CheckCircle, Play } from 'lucide-vue-next'
|
CheckCircle,
|
||||||
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
|
ExternalLink,
|
||||||
|
FolderOpen,
|
||||||
|
HardDrive,
|
||||||
|
RotateCcw,
|
||||||
|
Save,
|
||||||
|
Server,
|
||||||
|
Settings
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const store = useAppStore()
|
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
|
||||||
|
import { useTranslation } from '../composables/useI18n'
|
||||||
|
import { useAppStore } from '../stores/app'
|
||||||
|
|
||||||
|
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({ ...store.settings.openlist })
|
const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
|
||||||
const rcloneSettings = reactive({ ...store.settings.rclone })
|
const rcloneSettings = reactive({ ...appStore.settings.rclone })
|
||||||
const appSettings = reactive({ ...store.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,33 +82,43 @@ 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_api === undefined) appSettings.gh_proxy_api = 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(() => {
|
||||||
let rcloneConfigChanged = false
|
let rcloneConfigChanged = false
|
||||||
try {
|
try {
|
||||||
const parsedConfig = JSON.parse(rcloneConfigJson.value)
|
const parsedConfig = JSON.parse(rcloneConfigJson.value)
|
||||||
rcloneConfigChanged = JSON.stringify(parsedConfig) !== JSON.stringify(store.settings.rclone.config)
|
rcloneConfigChanged = JSON.stringify(parsedConfig) !== JSON.stringify(appStore.settings.rclone.config)
|
||||||
} catch {
|
} catch {
|
||||||
rcloneConfigChanged = rcloneConfigJson.value !== JSON.stringify(store.settings.rclone.config, null, 2)
|
rcloneConfigChanged = rcloneConfigJson.value !== JSON.stringify(appStore.settings.rclone.config, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
JSON.stringify(openlistCoreSettings) !== JSON.stringify(store.settings.openlist) ||
|
JSON.stringify(openlistCoreSettings) !== JSON.stringify(appStore.settings.openlist) ||
|
||||||
JSON.stringify(rcloneSettings) !== JSON.stringify(store.settings.rclone) ||
|
JSON.stringify(rcloneSettings) !== JSON.stringify(appStore.settings.rclone) ||
|
||||||
JSON.stringify(appSettings) !== JSON.stringify(store.settings.app) ||
|
JSON.stringify(appSettings) !== JSON.stringify(appStore.settings.app) ||
|
||||||
rcloneConfigChanged
|
rcloneConfigChanged
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -104,16 +137,40 @@ const handleSave = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store.settings.openlist = { ...openlistCoreSettings }
|
appStore.settings.openlist = { ...openlistCoreSettings }
|
||||||
store.settings.rclone = { ...rcloneSettings }
|
appStore.settings.rclone = { ...rcloneSettings }
|
||||||
store.settings.app = { ...appSettings }
|
appStore.settings.app = { ...appSettings }
|
||||||
if (originalOpenlistPort !== openlistCoreSettings.port) {
|
|
||||||
await store.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 store.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'
|
||||||
@@ -127,29 +184,143 @@ const handleSave = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startTutorial() {
|
|
||||||
router.push({ name: 'Dashboard' })
|
|
||||||
store.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 store.resetSettings()
|
const selected = await open({
|
||||||
Object.assign(openlistCoreSettings, store.settings.openlist)
|
directory: true,
|
||||||
Object.assign(rcloneSettings, store.settings.rclone)
|
multiple: false,
|
||||||
Object.assign(appSettings, store.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>
|
||||||
@@ -165,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>
|
||||||
@@ -179,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>
|
||||||
@@ -215,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">
|
||||||
@@ -253,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>
|
||||||
@@ -283,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="store.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>
|
||||||
@@ -296,23 +544,51 @@ 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-group">
|
||||||
|
<div class="settings-section-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
:title="t('settings.app.config.openFile')"
|
||||||
|
@click="handleOpenSettingsFile"
|
||||||
|
>
|
||||||
|
<ExternalLink :size="16" />
|
||||||
|
{{ t('settings.app.config.openFile') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>{{ t('settings.app.ghProxy.title') }}</h2>
|
||||||
|
<p>{{ t('settings.app.ghProxy.subtitle') }}</p>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('settings.app.monitor.interval.label') }}</label>
|
<label>{{ t('settings.app.ghProxy.label') }}</label>
|
||||||
<input
|
<input
|
||||||
v-model.number="appSettings.monitor_interval"
|
v-model="appSettings.gh_proxy"
|
||||||
type="number"
|
type="text"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
:placeholder="t('settings.app.monitor.interval.placeholder')"
|
:placeholder="t('settings.app.ghProxy.placeholder')"
|
||||||
min="1"
|
|
||||||
max="60"
|
|
||||||
/>
|
/>
|
||||||
<small>{{ t('settings.app.monitor.interval.help') }}</small>
|
<small>{{ t('settings.app.ghProxy.help') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="switch-label">
|
||||||
|
<input v-model="appSettings.gh_proxy_api" type="checkbox" class="switch-input" />
|
||||||
|
<span class="switch-slider"></span>
|
||||||
|
<div class="switch-content">
|
||||||
|
<span class="switch-title">{{ t('settings.app.ghProxy.api.title') }}</span>
|
||||||
|
<span class="switch-description">{{ t('settings.app.ghProxy.api.description') }}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
@@ -331,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>
|
||||||
@@ -348,21 +639,33 @@ const handleReset = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h2>{{ t('settings.app.tutorial.title') }}</h2>
|
<h2>{{ t('settings.app.links.title') }}</h2>
|
||||||
<p>{{ t('settings.app.tutorial.subtitle') }}</p>
|
<p>{{ t('settings.app.links.subtitle') }}</p>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label class="switch-label">
|
||||||
<button @click="startTutorial" class="tutorial-btn" type="button">
|
<input v-model="appSettings.open_links_in_browser" type="checkbox" class="switch-input" />
|
||||||
<Play :size="16" />
|
<span class="switch-slider"></span>
|
||||||
{{ t('settings.app.tutorial.restart') }}
|
<div class="switch-content">
|
||||||
</button>
|
<span class="switch-title">{{ t('settings.app.links.openInBrowser.title') }}</span>
|
||||||
<small>{{ t('settings.app.tutorial.help') }}</small>
|
<span class="switch-description">{{ t('settings.app.links.openInBrowser.description') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
.mount-view {
|
.mount-view {
|
||||||
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%);
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.mount-view::before {
|
.mount-view::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -40,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,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 {
|
||||||
@@ -172,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 {
|
||||||
@@ -202,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,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;
|
||||||
@@ -257,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 {
|
||||||
@@ -284,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 {
|
||||||
@@ -303,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) {
|
||||||
@@ -320,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 {
|
||||||
@@ -371,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 {
|
||||||
@@ -440,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,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 {
|
||||||
@@ -564,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;
|
||||||
}
|
}
|
||||||
@@ -598,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);
|
||||||
@@ -657,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;
|
||||||
}
|
}
|
||||||
@@ -703,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) {
|
||||||
@@ -736,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;
|
||||||
@@ -754,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 {
|
||||||
@@ -795,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 {
|
||||||
@@ -856,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,
|
||||||
@@ -910,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 {
|
||||||
@@ -930,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) {
|
||||||
@@ -962,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 {
|
||||||
@@ -986,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 {
|
||||||
@@ -1012,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,91 +983,521 @@
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Quick Flag Selector Styles */
|
||||||
|
.flags-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-flags-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-flags-btn:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
color: var(--color-accent);
|
||||||
|
background: rgba(0, 122, 255, 0.05);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-flags-btn .btn-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-popup {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-selector-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-selector-btn:hover {
|
||||||
|
background: var(--color-background-tertiary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-selector-btn .btn-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-content {
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-content::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.flag-selector-help {
|
||||||
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.06), rgba(99, 102, 241, 0.04));
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-help::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-help p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-categories {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
max-height: 65vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-categories::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-categories::-webkit-scrollbar-track {
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-categories::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-categories::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-category {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-category:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header {
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-flags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border: none;
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-option:hover {
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-option.selected,
|
||||||
|
.flag-option.in-config {
|
||||||
|
background: rgba(34, 197, 94, 0.06);
|
||||||
|
border-left-color: #22c55e;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-option.selected:hover,
|
||||||
|
.flag-option.in-config:hover {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox:hover {
|
||||||
|
border-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox.checked {
|
||||||
|
background: #22c55e;
|
||||||
|
border-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: white;
|
||||||
|
stroke-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-checkbox input[type='checkbox'] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-help {
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.1);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-selector-help p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-code {
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--color-background-tertiary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--color-accent);
|
||||||
|
display: inline-block;
|
||||||
|
max-width: fit-content;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-description {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .flag-selector-popup,
|
||||||
|
:root.auto.dark .flag-selector-popup {
|
||||||
|
background: var(--color-background-tertiary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .flag-selector-header,
|
||||||
|
:root.auto.dark .flag-selector-header {
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .flag-category,
|
||||||
|
:root.auto.dark .flag-category {
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .flag-category:hover,
|
||||||
|
:root.auto.dark .flag-category:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .flag-option,
|
||||||
|
:root.auto.dark .flag-option {
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .flag-option:hover,
|
||||||
|
:root.auto.dark .flag-option:hover {
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .flag-code,
|
||||||
|
:root.auto.dark .flag-code {
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .custom-checkbox,
|
||||||
|
:root.auto.dark .custom-checkbox {
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .quick-flags-btn,
|
||||||
|
:root.auto.dark .quick-flags-btn {
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
.webdav-tip {
|
.webdav-tip {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin: 0 28px 20px;
|
margin: 0 28px 12px;
|
||||||
background: linear-gradient(135deg, #fef3cd 0%, #fff3cd 100%);
|
background: linear-gradient(135deg, #fef3cd 0%, #fff3cd 100%);
|
||||||
border: 1px solid #f9cc33;
|
border: 1px solid #f9cc33;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(249, 204, 51, 0.1);
|
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;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-content {
|
.tip-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
padding: 16px 20px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-icon {
|
.tip-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 40px;
|
width: 32px;
|
||||||
height: 40px;
|
height: 32px;
|
||||||
background: rgba(249, 204, 51, 0.1);
|
background: rgba(249, 204, 51, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.winfsp-tip .tip-icon {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.tip-icon .icon {
|
.tip-icon .icon {
|
||||||
width: 20px;
|
width: 16px;
|
||||||
height: 20px;
|
height: 16px;
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.winfsp-tip .tip-icon .icon {
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
.tip-message {
|
.tip-message {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-title {
|
.tip-title {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 4px 0;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #92400e;
|
color: #92400e;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.winfsp-tip .tip-title {
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
.tip-description {
|
.tip-description {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
color: #a16207;
|
color: #a16207;
|
||||||
line-height: 1.5;
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winfsp-tip .tip-description {
|
||||||
|
color: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-close {
|
.tip-close {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 32px;
|
width: 28px;
|
||||||
height: 32px;
|
height: 28px;
|
||||||
background: rgba(249, 204, 51, 0.1);
|
background: rgba(249, 204, 51, 0.1);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
}
|
||||||
|
|
||||||
|
.winfsp-tip .tip-close {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-close:hover {
|
.tip-close:hover {
|
||||||
background: rgba(249, 204, 51, 0.2);
|
background: rgba(249, 204, 51, 0.2);
|
||||||
transform: scale(1.05);
|
}
|
||||||
|
|
||||||
|
.winfsp-tip .tip-close:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-close .close-icon {
|
.tip-close .close-icon {
|
||||||
width: 16px;
|
width: 14px;
|
||||||
height: 16px;
|
height: 14px;
|
||||||
color: #a16207;
|
color: #a16207;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.winfsp-tip .tip-close .close-icon {
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
:root.dark .webdav-tip,
|
:root.dark .webdav-tip,
|
||||||
:root.auto.dark .webdav-tip {
|
:root.auto.dark .webdav-tip {
|
||||||
background: linear-gradient(135deg, #451a03 0%, #541c15 100%);
|
background: linear-gradient(135deg, #451a03 0%, #541c15 100%);
|
||||||
border-color: #a16207;
|
border-color: #a16207;
|
||||||
box-shadow: 0 2px 8px rgba(161, 98, 7, 0.1);
|
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.dark .tip-icon,
|
||||||
@@ -1119,36 +1505,71 @@
|
|||||||
background: rgba(161, 98, 7, 0.1);
|
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.dark .tip-icon .icon,
|
||||||
:root.auto.dark .tip-icon .icon {
|
:root.auto.dark .tip-icon .icon {
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.dark .winfsp-tip .tip-icon .icon,
|
||||||
|
:root.auto.dark .winfsp-tip .tip-icon .icon {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
:root.dark .tip-title,
|
:root.dark .tip-title,
|
||||||
:root.auto.dark .tip-title {
|
:root.auto.dark .tip-title {
|
||||||
color: #fbbf24;
|
color: #fbbf24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.dark .winfsp-tip .tip-title,
|
||||||
|
:root.auto.dark .winfsp-tip .tip-title {
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
:root.dark .tip-description,
|
:root.dark .tip-description,
|
||||||
:root.auto.dark .tip-description {
|
:root.auto.dark .tip-description {
|
||||||
color: #d97706;
|
color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.dark .winfsp-tip .tip-description,
|
||||||
|
:root.auto.dark .winfsp-tip .tip-description {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
:root.dark .tip-close,
|
:root.dark .tip-close,
|
||||||
:root.auto.dark .tip-close {
|
:root.auto.dark .tip-close {
|
||||||
background: rgba(161, 98, 7, 0.1);
|
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.dark .tip-close:hover,
|
||||||
:root.auto.dark .tip-close:hover {
|
:root.auto.dark .tip-close:hover {
|
||||||
background: rgba(161, 98, 7, 0.2);
|
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.dark .tip-close .close-icon,
|
||||||
:root.auto.dark .tip-close .close-icon {
|
:root.auto.dark .tip-close .close-icon {
|
||||||
color: #d97706;
|
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) {
|
@media (max-width: 1024px) {
|
||||||
.header-content {
|
.header-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1197,6 +1618,45 @@
|
|||||||
gap: 12px;
|
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 {
|
.config-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -1265,30 +1725,6 @@
|
|||||||
border-bottom-color: rgba(255, 59, 48, 0.3);
|
border-bottom-color: rgba(255, 59, 48, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation */
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-card {
|
|
||||||
animation: fadeIn 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-card:nth-child(even) {
|
|
||||||
animation-delay: 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-card:nth-child(3n) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Accessibility */
|
/* Accessibility */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.config-card,
|
.config-card,
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
/* Use CSS variables for theme support */
|
|
||||||
.settings-container {
|
.settings-container {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-background-secondary);
|
background: var(--color-background-secondary);
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
/* Hide scrollbar for webkit browsers */
|
scrollbar-width: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
-ms-overflow-style: none;
|
||||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-container::-webkit-scrollbar {
|
.settings-container::-webkit-scrollbar {
|
||||||
display: none; /* Safari and Chrome */
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override specific colors for settings */
|
|
||||||
:root.dark .settings-container,
|
:root.dark .settings-container,
|
||||||
:root.auto.dark .settings-container {
|
:root.auto.dark .settings-container {
|
||||||
background: var(--color-background-primary);
|
background: var(--color-background-primary);
|
||||||
@@ -120,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 {
|
||||||
@@ -211,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;
|
||||||
}
|
}
|
||||||
@@ -232,6 +227,7 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
min-height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.dark .settings-content,
|
:root.dark .settings-content,
|
||||||
@@ -242,6 +238,7 @@
|
|||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
min-height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-section {
|
.settings-section {
|
||||||
@@ -299,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);
|
||||||
}
|
}
|
||||||
@@ -315,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;
|
||||||
@@ -382,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 {
|
||||||
@@ -390,33 +384,50 @@
|
|||||||
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;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-label:hover {
|
.switch-label:hover {
|
||||||
background: rgb(243 244 246);
|
background: var(--color-background-secondary);
|
||||||
border-color: rgb(209 213 219);
|
border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
:root.dark .switch-label,
|
||||||
.switch-label {
|
:root.auto.dark .switch-label {
|
||||||
background: rgb(55 65 81);
|
background: var(--color-background-tertiary);
|
||||||
border-color: rgb(75 85 99);
|
border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-label:hover {
|
:root.dark .switch-label:hover,
|
||||||
background: rgb(75 85 99);
|
:root.auto.dark .switch-label:hover {
|
||||||
}
|
background: var(--color-background-secondary);
|
||||||
|
border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-input {
|
.switch-input {
|
||||||
@@ -429,11 +440,9 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
background: rgb(209 213 219);
|
background: var(--color-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition: all 0.2s ease;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-slider::before {
|
.switch-slider::before {
|
||||||
@@ -445,12 +454,11 @@
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-input:checked + .switch-slider {
|
.switch-input:checked + .switch-slider {
|
||||||
background: rgb(59 130 246);
|
background: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-input:checked + .switch-slider::before {
|
.switch-input:checked + .switch-slider::before {
|
||||||
@@ -461,108 +469,30 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-title {
|
.switch-title {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgb(55 65 81);
|
color: var(--color-text-primary);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-description {
|
.switch-description {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: rgb(107 114 128);
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
:root.dark .switch-title,
|
||||||
.switch-title {
|
:root.auto.dark .switch-title {
|
||||||
color: rgb(209 213 219);
|
color: var(--color-text-primary);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Flags */
|
:root.dark .switch-description,
|
||||||
.flags-container {
|
:root.auto.dark .switch-description {
|
||||||
display: flex;
|
color: var(--color-text-secondary);
|
||||||
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 */
|
||||||
@@ -598,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
14
src/vite-env.d.ts
vendored
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user