39 Commits

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

View File

@@ -66,7 +66,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@nightly
with:
targets: ${{ matrix.platform.target }}
components: rustfmt, clippy
@@ -150,7 +150,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@nightly
with:
targets: ${{ matrix.platform.target }}

View File

@@ -26,6 +26,7 @@ env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
PERSONAL_GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -317,8 +318,8 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -451,8 +452,8 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -624,7 +625,7 @@ jobs:
- name: Update WinGet package manifest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
run: |
$version = "${{ steps.version.outputs.version }}"
# URLs for both x64 and arm64 installers

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ PackageIdentifier: OpenListTeam.OpenListDesktop
PackageName: OpenList Desktop
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

View File

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

View File

@@ -138,11 +138,19 @@ yarn run tauri build
#### Windows
##### Installation via Installer
1. Download the `.exe` installer
2. Run the installer as Administrator
3. Follow the installation wizard
4. Launch from Start Menu or Desktop shortcut
##### Winget
```bash
winget install OpenListTeam.OpenListDesktop
```
#### macOS
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
2. **Service Installation**: Install the OpenList service when prompted
3. **Storage Configuration**: Configure your first cloud storage connection
4. **Tutorial**: Complete the interactive tutorial to learn key features
### Basic Operations
@@ -235,9 +242,10 @@ Add custom Rclone flags for optimal performance:
{
"openlist": {
"port": 5244,
"api_token": "your-secure-token",
"data_dir": "",
"auto_launch": true,
"ssl_enabled": false
"ssl_enabled": false,
"admin_password": ""
}
}
```
@@ -269,18 +277,13 @@ Add custom Rclone flags for optimal performance:
"app": {
"theme": "auto",
"auto_update_enabled": true,
"monitor_interval": 30000
"gh_proxy": "https://ghproxy.com/",
"gh_proxy_api": false,
"open_links_in_browser": 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 Environment Setup
@@ -288,7 +291,7 @@ Add custom Rclone flags for optimal performance:
#### Prerequisites
- **Node.js**: v22+ with yarn
- **Rust**: Latest stable version
- **Rust**: Latest nightly version
- **Git**: Version control
#### Setup Steps
@@ -301,35 +304,11 @@ cd openlist-desktop
# Install Node.js dependencies
yarn install
# Install Rust dependencies
cd src-tauri
cargo fetch
# Prepare development environment
cd ..
yarn run prebuild:dev
# Start development server
yarn run dev
```
#### Development Commands
```bash
# Start development server with hot reload
yarn run dev
# Start development without file watching
yarn run nowatch
# Run linting
yarn run lint
# Fix linting issues
yarn run lint:fix
# Type checking
yarn run build --dry-run
yarn tauri dev
```
## 🤝 Contributing

View File

@@ -9,7 +9,7 @@
"tauri"
],
"private": true,
"version": "0.3.0",
"version": "0.5.1",
"author": {
"name": "OpenList Team",
"email": "96409857+Kuingsmile@users.noreply.github.com"
@@ -41,7 +41,7 @@
"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:frontend": "yarn lint:fix",
"fix:all": "yarn fix:frontend && yarn fix:rust"
"fix:all": "yarn fix:frontend && yarn fix:rust && yarn i18n:check"
},
"config": {
"commitizen": {
@@ -70,7 +70,7 @@
"lucide-vue-next": "^0.525.0",
"pinia": "^3.0.3",
"vue": "^3.5.17",
"vue-i18n": "11.1.9",
"vue-i18n": "11.1.10",
"vue-router": "^4.5.1"
},
"devDependencies": {

View File

@@ -28,7 +28,7 @@ if (!getOpenlistArchMap[platformArch]) {
}
// 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'
async function getLatestRcloneVersion() {
@@ -42,7 +42,7 @@ async function getLatestRcloneVersion() {
}
// openlist version management
let openlistVersion = 'v4.0.3'
let openlistVersion = 'v4.0.8'
async function getLatestOpenlistVersion() {
try {
@@ -51,7 +51,7 @@ async function getLatestOpenlistVersion() {
getFetchOptions()
)
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}`)
} catch (error) {
console.log('Error fetching latest OpenList version:', error.message)
@@ -240,12 +240,7 @@ async function main() {
await retryTask('rclone', async () => {
await getLatestRcloneVersion()
await resolveSidecar(
createBinaryInfo(
'rclone',
getRcloneArchMap(rcloneVersion),
'https://github.com/rclone/rclone/releases/download',
rcloneVersion
)
createBinaryInfo('rclone', getRcloneArchMap(rcloneVersion), `https://downloads.rclone.org`, rcloneVersion)
)
})
if (isWin) {

2
src-tauri/Cargo.lock generated
View File

@@ -2898,7 +2898,7 @@ dependencies = [
[[package]]
name = "openlist-desktop"
version = "0.2.0"
version = "0.5.1"
dependencies = [
"anyhow",
"base64 0.22.1",

View File

@@ -1,6 +1,6 @@
[package]
name = "openlist-desktop"
version = "0.3.0"
version = "0.5.1"
description = "A Tauri App"
authors = ["Kuingsmile"]
edition = "2024"

View File

@@ -467,6 +467,22 @@ FunctionEnd
nsis_tauri_utils::KillProcess "openlist.exe"
!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
!macro StartOpenListDesktopService
@@ -967,6 +983,13 @@ Section Uninstall
SetShellVarContext current
RmDir /r "$APPDATA\${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}
SetShellVarContext current

View File

@@ -4,7 +4,8 @@ use std::path::PathBuf;
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::conf::config::MergedSettings;
use crate::object::structs::AppState;
use crate::utils::path::app_config_file_path;
@@ -19,13 +20,19 @@ fn persist_app_settings(settings: &MergedSettings) -> Result<(), String> {
write_json_to_file(path, settings)
}
fn update_data_config_port(port: u16) -> Result<(), String> {
fn update_data_config(port: u16, data_dir: Option<&str>) -> Result<(), String> {
let exe_dir = std::env::current_exe()
.map_err(|e| e.to_string())?
.parent()
.ok_or("Failed to get exe parent dir")?
.to_path_buf();
let data_config_path = exe_dir.join("data").join("config.json");
let data_config_path = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
PathBuf::from(dir).join("config.json")
} else {
exe_dir.join("data").join("config.json")
};
if let Some(parent) = data_config_path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
@@ -63,6 +70,30 @@ async fn restart_openlist_core(state: State<'_, AppState>) -> Result<(), String>
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(())
}
#[tauri::command]
pub async fn load_settings(state: State<'_, AppState>) -> Result<Option<MergedSettings>, String> {
state.load_settings()?;
@@ -85,14 +116,38 @@ pub async fn save_settings_with_update_port(
settings: MergedSettings,
state: State<'_, AppState>,
) -> Result<bool, String> {
let old_settings = state.get_settings();
let needs_process_recreation = if let Some(old) = old_settings {
old.openlist.data_dir != settings.openlist.data_dir
} else {
false
};
state.update_settings(settings.clone());
persist_app_settings(&settings)?;
update_data_config_port(settings.openlist.port)?;
if let Err(e) = restart_openlist_core(state.clone()).await {
log::error!("{e}");
return Err(e);
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_process_recreation {
if let Err(e) = recreate_openlist_core_process(state.clone()).await {
log::error!("{e}");
return Err(e);
}
log::info!(
"Settings saved and OpenList core recreated with new data directory successfully"
);
} else {
if let Err(e) = restart_openlist_core(state.clone()).await {
log::error!("{e}");
return Err(e);
}
log::info!("Settings saved and OpenList core restarted with new port successfully");
}
log::info!("Settings saved and OpenList core restarted with new port successfully");
Ok(true)
}

View File

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

View File

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

View File

@@ -1,15 +1,105 @@
use std::env;
use std::path::PathBuf;
use std::process::Command;
use once_cell::sync::Lazy;
use regex::Regex;
use tauri::State;
static ADMIN_PWD_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"Successfully created the admin user and the initial password is: (\w+)")
.expect("Invalid regex pattern")
});
use crate::object::structs::AppState;
fn resolve_log_paths(source: Option<&str>) -> Result<Vec<PathBuf>, String> {
fn generate_random_password() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};
let mut hasher = DefaultHasher::new();
if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
duration.as_nanos().hash(&mut hasher);
}
std::process::id().hash(&mut hasher);
let dummy = [1, 2, 3];
(dummy.as_ptr() as usize).hash(&mut hasher);
let hash = hasher.finish();
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut password = String::new();
let mut current_hash = hash;
for _ in 0..16 {
let index = (current_hash % chars.len() as u64) as usize;
password.push(chars.chars().nth(index).unwrap());
current_hash = current_hash.wrapping_mul(1103515245).wrapping_add(12345);
}
password
}
async fn execute_openlist_admin_set(
password: &str,
state: &State<'_, AppState>,
) -> Result<(), String> {
let exe_path =
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;
}
}
let openlist_exe = openlist_exe.ok_or_else(|| {
format!(
"OpenList executable not found. Searched for: {:?} in {}",
possible_names,
app_dir.display()
)
})?;
log::info!(
"Setting new admin password using: {}",
openlist_exe.display()
);
let mut cmd = Command::new(&openlist_exe);
cmd.args(["admin", "set", password]);
if let Some(settings) = state.get_settings()
&& !settings.openlist.data_dir.is_empty()
{
cmd.arg("--data");
cmd.arg(&settings.openlist.data_dir);
log::info!("Using data directory: {}", settings.openlist.data_dir);
}
log::info!("Executing command: {cmd:?}");
let output = cmd
.output()
.map_err(|e| format!("Failed to execute openlist command: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
log::error!("OpenList admin set command failed. stdout: {stdout}, stderr: {stderr}");
return Err(format!("OpenList admin set command failed: {stderr}"));
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::info!("Successfully set admin password. Output: {stdout}");
Ok(())
}
fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
let exe_path =
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?;
let app_dir = exe_path
@@ -17,14 +107,20 @@ fn resolve_log_paths(source: Option<&str>) -> Result<Vec<PathBuf>, String> {
.ok_or("Executable has no parent directory")?
.to_path_buf();
let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
PathBuf::from(dir)
} else {
app_dir.join("data")
};
let mut paths = Vec::new();
match source {
Some("openlist") => paths.push(app_dir.join("data/log/log.log")),
Some("openlist") => paths.push(openlist_log_base.join("log/log.log")),
Some("app") => paths.push(app_dir.join("logs/app.log")),
Some("rclone") => paths.push(app_dir.join("logs/process_rclone.log")),
Some("openlist_core") => paths.push(app_dir.join("logs/process_openlist_core.log")),
None => {
paths.push(app_dir.join("data/log/log.log"));
paths.push(openlist_log_base.join("log/log.log"));
paths.push(app_dir.join("logs/app.log"));
paths.push(app_dir.join("logs/process_rclone.log"));
paths.push(app_dir.join("logs/process_openlist_core.log"));
@@ -35,21 +131,92 @@ fn resolve_log_paths(source: Option<&str>) -> Result<Vec<PathBuf>, String> {
}
#[tauri::command]
pub async fn get_admin_password() -> Result<String, String> {
let paths = resolve_log_paths(Some("openlist_core"))?;
let content =
std::fs::read_to_string(&paths[0]).map_err(|e| format!("Failed to read log file: {e}"))?;
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());
}
ADMIN_PWD_REGEX
.captures_iter(&content)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.last()
.ok_or_else(|| "No admin password found in logs".into())
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}");
}
}
Ok(new_password)
}
#[tauri::command]
pub async fn get_logs(source: Option<String>) -> Result<Vec<String>, String> {
let paths = resolve_log_paths(source.as_deref())?;
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 {
@@ -61,8 +228,16 @@ pub async fn get_logs(source: Option<String>) -> Result<Vec<String>, String> {
}
#[tauri::command]
pub async fn clear_logs(source: Option<String>) -> Result<bool, String> {
let paths = resolve_log_paths(source.as_deref())?;
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 {

View File

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

View File

@@ -9,8 +9,15 @@ use crate::utils::path::{get_app_logs_dir, get_openlist_binary_path};
#[tauri::command]
pub async fn create_openlist_core_process(
auto_start: bool,
_state: State<'_, AppState>,
state: State<'_, AppState>,
) -> 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()
.map_err(|e| format!("Failed to get OpenList binary path: {e}"))?;
let log_file_path =
@@ -19,12 +26,16 @@ pub async fn create_openlist_core_process(
let api_key = get_api_key();
let port = get_server_port();
let mut args = vec!["server".into()];
if !data_dir.is_empty() {
args.push("--data".into());
args.push(data_dir);
}
let config = ProcessConfig {
id: "openlist_core".into(),
name: "single_openlist_core_process".into(),
bin_path: binary_path.to_string_lossy().into_owned(),
args: vec!["server".into()],
args,
log_file: log_file_path.to_string_lossy().into_owned(),
working_dir: binary_path
.parent()

View File

@@ -3,22 +3,22 @@ use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig {
pub theme: Option<String>,
pub monitor_interval: Option<u64>,
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>,
}
impl AppConfig {
pub fn new() -> Self {
Self {
theme: Some("light".to_string()),
monitor_interval: Some(5),
auto_update_enabled: Some(true),
gh_proxy: None,
gh_proxy_api: Some(false),
open_links_in_browser: Some(false),
admin_password: None,
}
}
}

View File

@@ -29,23 +29,27 @@ impl MergedSettings {
}
}
pub fn get_data_config_path() -> Result<PathBuf, String> {
let exe =
std::env::current_exe().map_err(|e| format!("Failed to get current exe path: {e}"))?;
let dir = exe
.parent()
.ok_or_else(|| "Failed to get executable parent directory".to_string())?;
Ok(dir.join("data").join("config.json"))
pub fn get_data_config_path_for_dir(data_dir: Option<&str>) -> Result<PathBuf, String> {
if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
Ok(PathBuf::from(dir).join("config.json"))
} else {
let exe = std::env::current_exe()
.map_err(|e| format!("Failed to get current exe path: {e}"))?;
let dir = exe
.parent()
.ok_or_else(|| "Failed to get executable parent directory".to_string())?;
Ok(dir.join("data").join("config.json"))
}
}
pub fn read_data_config() -> Result<serde_json::Value, String> {
let path = Self::get_data_config_path()?;
pub fn read_data_config_for_dir(data_dir: Option<&str>) -> Result<serde_json::Value, String> {
let path = Self::get_data_config_path_for_dir(data_dir)?;
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
serde_json::from_str(&content).map_err(|e| e.to_string())
}
fn get_port_from_data_config() -> Result<Option<u16>, String> {
let config = Self::read_data_config()?;
fn get_port_from_data_config_for_dir(data_dir: Option<&str>) -> Result<Option<u16>, String> {
let config = Self::read_data_config_for_dir(data_dir)?;
Ok(config
.get("scheme")
.and_then(|s| s.get("http_port"))
@@ -77,7 +81,13 @@ impl MergedSettings {
default
};
if let Ok(Some(port)) = Self::get_port_from_data_config()
let data_dir = if settings.openlist.data_dir.is_empty() {
None
} else {
Some(settings.openlist.data_dir.as_str())
};
if let Ok(Some(port)) = Self::get_port_from_data_config_for_dir(data_dir)
&& settings.openlist.port != port
{
settings.openlist.port = port;

View File

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

View File

@@ -22,14 +22,6 @@ pub struct RcloneCreateRemoteRequest {
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)]
pub struct RcloneMountRequest {
pub fs: String,
@@ -49,13 +41,6 @@ pub struct RcloneMountOptions {
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 {
pub fn new() -> Self {
Self {

View File

@@ -732,28 +732,27 @@ pub async fn start_service() -> Result<bool, Box<dyn std::error::Error>> {
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
log::info!("Extracted PID value: {pid_value}");
if let Ok(pid) = pid_value.parse::<i32>() {
if pid > 0 {
log::info!("Service is running with PID: {pid}");
return Ok(true);
}
if let Ok(pid) = pid_value.parse::<i32>()
&& pid > 0
{
log::info!("Service is running with PID: {pid}");
return Ok(true);
}
}
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus") {
if let Ok(status) = exit_status.parse::<i32>() {
if status == 0 {
log::info!(
"Service is loaded but not running (clean exit), attempting to \
start"
);
return start_macos_service(SERVICE_IDENTIFIER).await;
} else {
log::warn!(
"Service has non-zero exit status: {status}, attempting to restart"
);
return start_macos_service(SERVICE_IDENTIFIER).await;
}
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus")
&& let Ok(status) = exit_status.parse::<i32>()
{
if status == 0 {
log::info!(
"Service is loaded but not running (clean exit), attempting to start"
);
return start_macos_service(SERVICE_IDENTIFIER).await;
} else {
log::warn!(
"Service has non-zero exit status: {status}, attempting to restart"
);
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") {
log::info!("Extracted PID value: {pid_value}");
if let Ok(pid) = pid_value.parse::<i32>() {
if pid > 0 {
log::info!("Service is running with PID: {pid}");
return Ok("running".to_string());
}
if let Ok(pid) = pid_value.parse::<i32>()
&& pid > 0
{
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 Ok(status) = exit_status.parse::<i32>() {
if status == 0 {
log::info!("Service is loaded but not running (clean exit)");
return Ok("stopped".to_string());
} else {
log::warn!("Service has non-zero exit status: {status}");
return Ok("stopped".to_string());
}
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus")
&& let Ok(status) = exit_status.parse::<i32>()
{
if status == 0 {
log::info!("Service is loaded but not running (clean exit)");
return Ok("stopped".to_string());
} else {
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);
log::info!("Verification output: {output_str}");
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
if let Ok(pid) = pid_value.parse::<i32>() {
if pid > 0 {
log::info!("Service verified as running with PID: {pid}");
return Ok(true);
} else {
log::warn!("Service has invalid PID: {pid}");
return Ok(false);
}
if let Some(pid_value) = extract_plist_value(&output_str, "PID")
&& let Ok(pid) = pid_value.parse::<i32>()
{
if pid > 0 {
log::info!("Service verified as running with PID: {pid}");
return Ok(true);
} else {
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() {
let trimmed = line.trim();
if trimmed.starts_with(&pattern) {
if let Some(equals_pos) = trimmed.find('=') {
let value_part = &trimmed[equals_pos + 1..];
let value_trimmed = value_part.trim();
if trimmed.starts_with(&pattern)
&& let Some(equals_pos) = trimmed.find('=')
{
let value_part = &trimmed[equals_pos + 1..];
let value_trimmed = value_part.trim();
let value_clean = if let Some(stripped) = value_trimmed.strip_suffix(';') {
stripped
} else {
value_trimmed
};
let value_clean = if let Some(stripped) = value_trimmed.strip_suffix(';') {
stripped
} else {
value_trimmed
};
return Some(value_clean.trim().to_string());
}
return Some(value_clean.trim().to_string());
}
}

View File

@@ -11,12 +11,15 @@ use cmd::binary::get_binary_version;
use cmd::config::{load_settings, reset_settings, save_settings, save_settings_with_update_port};
use cmd::custom_updater::{
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::{
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::os_operate::{
get_available_versions, list_files, open_file, open_folder, open_url, open_url_in_browser,
@@ -65,7 +68,7 @@ async fn force_update_tray_menu(
fn setup_background_update_checker(app_handle: &tauri::AppHandle) {
let app_handle_initial = app_handle.clone();
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>();
match is_auto_check_enabled(app_state).await {
@@ -147,6 +150,8 @@ pub fn run() {
get_logs,
clear_logs,
get_admin_password,
reset_admin_password,
set_admin_password,
get_binary_version,
select_directory,
get_available_versions,
@@ -159,13 +164,15 @@ pub fn run() {
check_service_status,
stop_service,
start_service,
check_firewall_rule,
add_firewall_rule,
remove_firewall_rule,
check_for_updates,
download_update,
install_update_and_restart,
get_current_version,
set_auto_check_enabled,
is_auto_check_enabled,
restart_app,
is_auto_check_enabled
])
.setup(|app| {
let app_handle = app.app_handle();

View File

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

View File

@@ -6,7 +6,7 @@ use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}
use tauri::{AppHandle, Emitter, Manager};
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<()> {
let quit_i = MenuItem::with_id(app_handle, "quit", "退出", true, None::<&str>)?;

View File

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

View File

@@ -2,6 +2,24 @@
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.openlistteam.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"
}
],
"security": {
"csp": null
}
},
"bundle": {
"targets": ["app", "dmg"],
"macOS": {

View File

@@ -8,7 +8,6 @@ import { useTray } from './composables/useTray'
import { TauriAPI } from './api/tauri'
import Navigation from './components/Navigation.vue'
import TitleBar from './components/ui/TitleBar.vue'
import TutorialOverlay from './components/ui/TutorialOverlay.vue'
const appStore = useAppStore()
const { t } = useTranslation()
@@ -58,16 +57,12 @@ onMounted(async () => {
console.log('Global update listener: Update available', updateInfo)
appStore.setUpdateAvailable(true, updateInfo)
})
console.log('Global update listener set up successfully')
} catch (err) {
console.warn('Failed to set up global update listener:', err)
}
document.addEventListener('keydown', handleKeydown)
} finally {
setTimeout(() => {
isLoading.value = false
}, 1000)
isLoading.value = false
}
})
@@ -147,8 +142,6 @@ onUnmounted(() => {
</router-view>
</div>
</main>
<TutorialOverlay />
</div>
</template>
@@ -374,7 +367,6 @@ body {
inset: 0;
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%);
animation: float 20s ease-in-out infinite;
}
.loading-content {
@@ -402,7 +394,6 @@ body {
z-index: 2;
color: white;
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.2));
animation: logoFloat 3s ease-in-out infinite;
}
.logo-shimmer {
@@ -410,7 +401,6 @@ body {
inset: -20px;
background: conic-gradient(from 0deg, transparent, rgba(255, 255, 255, 0.2), transparent);
border-radius: 50%;
animation: shimmer 2s linear infinite;
}
.loading-title {
@@ -462,57 +452,6 @@ body {
.progress-fill {
height: 100%;
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 {
@@ -539,7 +478,6 @@ body {
height: 80%;
background: radial-gradient(circle, rgba(0, 122, 255, 0.05) 0%, transparent 70%);
border-radius: 50%;
animation: gradientFloat 20s ease-in-out infinite;
}
.bg-gradient-secondary {
@@ -550,22 +488,6 @@ body {
height: 60%;
background: radial-gradient(circle, rgba(175, 82, 222, 0.03) 0%, transparent 70%);
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 {
@@ -590,14 +512,6 @@ body {
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 {
opacity: 0;
transform: translateY(24px) scale(0.98);

View File

@@ -60,7 +60,7 @@ export class TauriAPI {
list: (path: string): Promise<FileItem[]> => call('list_files', { path }),
open: (path: string): Promise<boolean> => call('open_file', { 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 })
}
@@ -79,7 +79,9 @@ export class TauriAPI {
call('get_logs', { source: src }),
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<boolean> =>
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 ---
@@ -104,6 +106,13 @@ export class TauriAPI {
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 ---
static updater = {
check: (): Promise<UpdateCheck> => call('check_for_updates'),
@@ -119,6 +128,6 @@ export class TauriAPI {
listen('download-progress', e => cb(e.payload as DownloadProgress)),
onInstallStarted: (cb: () => void) => listen('update-install-started', () => cb()),
onInstallError: (cb: (err: string) => void) => listen('update-install-error', e => cb(e.payload as string)),
onAppRestarting: (cb: () => void) => listen('app-restarting', () => cb())
onAppQuit: (cb: () => void) => listen('quit-app', () => cb())
}
}

View File

@@ -27,11 +27,14 @@ const navigationItems = computed(() => [
const openLink = async (url: string) => {
try {
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url)
if (appStore.settings.app.open_links_in_browser) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
window.open(url, '_blank')
}
window.open(url, '_blank')
}
</script>
@@ -189,7 +192,6 @@ const openLink = async (url: string) => {
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
:root.dark .nav-item,
@@ -237,7 +239,6 @@ const openLink = async (url: string) => {
background: rgb(220 38 38);
border-radius: 50%;
border: 2px solid var(--color-background-secondary);
animation: pulse 2s infinite;
}
.nav-item.has-notification .nav-icon-container {
@@ -272,7 +273,6 @@ const openLink = async (url: string) => {
color: rgb(75 85 99);
text-decoration: none;
border-radius: 0.5rem;
transition: all 0.2s ease-in-out;
}
:root.dark .github-link,
@@ -283,7 +283,6 @@ const openLink = async (url: string) => {
.github-link:hover {
background: rgb(243 244 246);
color: rgb(17 24 39);
transform: scale(1.1);
}
:root.dark .github-link:hover,
@@ -291,14 +290,4 @@ const openLink = async (url: string) => {
background: rgb(55 65 81);
color: rgb(243 244 246);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style>

View File

@@ -223,12 +223,13 @@ const updateChartSize = () => {
onMounted(async () => {
await nextTick()
updateChartSize()
await appStore.refreshOpenListCoreStatus()
if (isCoreRunning.value) {
startTime.value = Date.now()
}
monitoringInterval.value = window.setInterval(checkCoreHealth, (appStore.settings.app.monitor_interval || 5) * 1000)
monitoringInterval.value = window.setInterval(checkCoreHealth, 15 * 1000)
window.addEventListener('resize', updateChartSize)
})
@@ -256,9 +257,8 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
padding: 0.5rem 0.875rem;
border-radius: 20px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
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) {
@@ -287,20 +287,6 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
background: currentColor;
}
.status-indicator.online .pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.heartbeat-section {
margin-bottom: 2rem;
}
@@ -348,9 +334,7 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
padding: 0.5rem 0.875rem;
border-radius: 20px;
font-weight: 600;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
}
.metric:hover {
@@ -422,7 +406,6 @@ watch(isCoreRunning, (newValue: boolean, oldValue: boolean) => {
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(226, 232, 240, 0.6);
backdrop-filter: blur(10px);
}
@media (prefers-color-scheme: dark) {

View File

@@ -102,11 +102,14 @@ const openRcloneGitHub = () => {
const openLink = async (url: string) => {
try {
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url)
if (appStore.settings.app.open_links_in_browser) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
window.open(url, '_blank')
}
window.open(url, '_blank')
}
</script>
@@ -128,12 +131,11 @@ const openLink = async (url: string) => {
border-radius: 0.75rem;
padding: 1rem;
background: rgb(249 250 251);
transition: all 0.2s;
}
.doc-section:hover {
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,
@@ -145,7 +147,7 @@ const openLink = async (url: string) => {
:root.dark .doc-section:hover,
:root.auto.dark .doc-section:hover {
border-color: rgb(75 85 99);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
background: rgb(55 65 81);
}
.doc-header {
@@ -220,7 +222,6 @@ const openLink = async (url: string) => {
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
flex: 1;
justify-content: center;
@@ -298,7 +299,6 @@ const openLink = async (url: string) => {
border: 1px solid rgb(209 213 219);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}

View File

@@ -6,16 +6,12 @@
<h4>{{ t('dashboard.quickActions.openlistService') }}</h4>
</div>
<div class="action-buttons">
<button
@click="toggleCore"
:class="['action-btn', 'service-btn', { running: isCoreRunning }]"
:disabled="loading"
>
<button @click="toggleCore" :class="['action-btn', 'service-btn', { running: isCoreRunning }]">
<component :is="serviceButtonIcon" :size="20" />
<span>{{ serviceButtonText }}</span>
</button>
<button @click="restartCore" :disabled="!isCoreRunning || loading" class="action-btn restart-btn">
<button @click="restartCore" :disabled="!isCoreRunning" class="action-btn restart-btn">
<RotateCcw :size="18" />
<span>{{ t('dashboard.quickActions.restart') }}</span>
</button>
@@ -31,12 +27,20 @@
</button>
<button
@click="showAdminPassword"
@click="copyAdminPassword"
class="action-btn password-btn icon-only-btn"
:title="t('dashboard.quickActions.showAdminPassword')"
:title="t('dashboard.quickActions.copyAdminPassword')"
>
<Key :size="16" />
</button>
<button
@click="resetAdminPassword"
class="action-btn reset-password-btn icon-only-btn"
:title="t('dashboard.quickActions.resetAdminPassword')"
>
<RotateCcw :size="16" />
</button>
</div>
</div>
@@ -47,7 +51,6 @@
<div class="action-buttons">
<button
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
:disabled="loading || rcloneStore.loading"
:class="['action-btn', 'service-indicator-btn', { active: rcloneStore.serviceRunning }]"
>
<component :is="rcloneStore.serviceRunning ? Square : Play" :size="18" />
@@ -80,6 +83,28 @@
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" />
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
</label>
<!-- Windows Firewall Management-->
<button
v-if="isWindows"
@click="toggleFirewallRule"
:class="['firewall-toggle-btn', { active: firewallEnabled }]"
:disabled="firewallLoading"
:title="
firewallEnabled
? t('dashboard.quickActions.firewall.disable')
: t('dashboard.quickActions.firewall.enable')
"
>
<Shield :size="18" />
<span>
{{
firewallEnabled
? t('dashboard.quickActions.firewall.disable')
: t('dashboard.quickActions.firewall.enable')
}}
</span>
</button>
</div>
</div>
</div>
@@ -87,13 +112,13 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAppStore } from '../../stores/app'
import { useRcloneStore } from '../../stores/rclone'
import { useTranslation } from '../../composables/useI18n'
import Card from '../ui/Card.vue'
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Loader, Key } from 'lucide-vue-next'
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key, Shield } from 'lucide-vue-next'
import { TauriAPI } from '@/api/tauri'
const { t } = useTranslation()
@@ -102,17 +127,20 @@ const appStore = useAppStore()
const rcloneStore = useRcloneStore()
const isCoreRunning = computed(() => appStore.isCoreRunning)
const loading = computed(() => appStore.loading)
const settings = computed(() => appStore.settings)
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(() => {
if (loading.value) return Loader
return isCoreRunning.value ? Square : Play
})
const serviceButtonText = computed(() => {
if (loading.value) return t('dashboard.quickActions.processing')
return isCoreRunning.value
? t('dashboard.quickActions.stopOpenListCore')
: t('dashboard.quickActions.startOpenListCore')
@@ -144,7 +172,7 @@ const viewMounts = () => {
router.push({ name: 'Mount' })
}
const showAdminPassword = async () => {
const copyAdminPassword = async () => {
try {
const password = await appStore.getAdminPassword()
if (password) {
@@ -217,38 +245,54 @@ const showAdminPassword = async () => {
}
} catch (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')
notification.innerHTML = `
<div style="
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38));
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;
">
<div style="display: flex; align-items: center; gap: 8px;">
<div style="font-size: 18px;">✗</div>
<div>
<div style="font-size: 14px; margin-bottom: 4px;">Failed to get admin password</div>
<div style="font-size: 12px; opacity: 0.9;">Please check the logs.</div>
const resetAdminPassword = async () => {
try {
const newPassword = await appStore.resetAdminPassword()
if (newPassword) {
await navigator.clipboard.writeText(newPassword)
const notification = document.createElement('div')
notification.innerHTML = `
<div style="
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105));
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-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>
`
document.body.appendChild(notification)
`
document.body.appendChild(notification)
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification)
}
}, 4000)
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification)
}
}, 4000)
} else {
showNotification('error', 'Failed to reset admin password. Please check the logs.')
}
} catch (error) {
console.error('Failed to reset admin password:', error)
showNotification('error', 'Failed to reset admin password. Please check the logs.')
}
}
@@ -282,21 +326,97 @@ const stopBackend = async () => {
}
}
const checkFirewallStatus = async () => {
if (!isWindows.value) return
try {
firewallEnabled.value = await TauriAPI.firewall.check()
} catch (error) {
console.error('Failed to check firewall status:', error)
}
}
const toggleFirewallRule = async () => {
if (!isWindows.value) return
try {
firewallLoading.value = true
if (firewallEnabled.value) {
await TauriAPI.firewall.remove()
firewallEnabled.value = false
showNotification('success', t('dashboard.quickActions.firewall.removed'))
} else {
await TauriAPI.firewall.add()
firewallEnabled.value = true
showNotification('success', t('dashboard.quickActions.firewall.added'))
}
} catch (error: any) {
console.error('Failed to toggle firewall rule:', error)
const message = firewallEnabled.value
? t('dashboard.quickActions.firewall.failedToRemove')
: t('dashboard.quickActions.firewall.failedToAdd')
showNotification('error', message + ': ' + (error.message || error))
} finally {
firewallLoading.value = false
}
}
const showNotification = (type: 'success' | 'error', message: string) => {
const notification = document.createElement('div')
const bgColor =
type === 'success'
? 'linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105))'
: 'linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38))'
const icon = type === 'success' ? '✓' : '⚠'
notification.innerHTML = `
<div style="
position: fixed;
top: 20px;
right: 20px;
background: ${bgColor};
color: white;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
font-weight: 500;
max-width: 300px;
word-break: break-word;
">
<div style="display: flex; align-items: center; gap: 8px;">
<div style="font-size: 18px;">${icon}</div>
<div style="font-size: 14px;">${message}</div>
</div>
</div>
`
document.body.appendChild(notification)
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification)
}
}, 4000)
}
const openLink = async (url: string) => {
try {
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url)
if (appStore.settings.app.open_links_in_browser) {
await TauriAPI.files.urlInBrowser(url)
return
}
} catch (error) {
console.error('Failed to open link:', error)
window.open(url, '_blank')
}
window.open(url, '_blank')
}
onMounted(async () => {
await rcloneStore.checkRcloneBackendStatus()
statusCheckInterval = window.setInterval(
rcloneStore.checkRcloneBackendStatus,
(appStore.settings.app.monitor_interval || 5) * 1000
)
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 15 * 1000)
await checkFirewallStatus()
})
onUnmounted(() => {
@@ -360,13 +480,10 @@ onUnmounted(() => {
border: 1px solid var(--color-border-secondary);
border-radius: 10px;
background: var(--color-surface);
backdrop-filter: blur(10px);
color: var(--color-text-primary);
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-sm);
flex: 1;
min-width: 0;
text-align: center;
@@ -380,122 +497,117 @@ onUnmounted(() => {
}
.action-btn:hover:not(:disabled) {
transform: translateY(-1px);
background: var(--color-surface-elevated);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: var(--shadow-md);
}
.action-btn:active {
transform: translateY(0);
opacity: 0.8;
}
.action-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.service-btn.running {
background: linear-gradient(135deg, rgb(239, 68, 68) 0%, rgb(220, 38, 38) 100%);
background: rgb(239, 68, 68);
color: white;
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) {
background: linear-gradient(135deg, rgb(220, 38, 38) 0%, rgb(185, 28, 28) 100%);
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
background: rgb(220, 38, 38);
}
.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;
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) {
background: linear-gradient(135deg, rgb(5, 150, 105) 0%, rgb(4, 120, 87) 100%);
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
background: rgb(5, 150, 105);
}
.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;
border-color: rgba(245, 158, 11, 0.3);
}
.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;
border-color: rgba(37, 99, 235, 0.3);
}
.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;
border-color: rgba(124, 58, 237, 0.3);
}
.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;
border-color: rgba(8, 145, 178, 0.3);
}
.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;
border-color: rgba(234, 88, 12, 0.3);
}
.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;
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 {
background: var(--color-surface);
border-color: var(--color-border-secondary);
}
.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;
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);
border-color: rgba(220, 38, 38, 0.3);
}
.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);
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) {
background: linear-gradient(135deg, rgb(16, 185, 129) 0%, rgb(5, 150, 105) 100%);
background: rgb(16, 185, 129);
color: white;
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) {
background: linear-gradient(135deg, rgb(5, 150, 105) 0%, rgb(4, 120, 87) 100%);
background: rgb(5, 150, 105);
color: white;
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) {
background: linear-gradient(135deg, rgb(100, 116, 139) 0%, rgb(71, 85, 105) 100%);
background: rgb(100, 116, 139);
color: white;
border-color: rgba(71, 85, 105, 0.3);
}
.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;
border-color: rgba(124, 58, 237, 0.3);
}
@@ -518,7 +630,6 @@ onUnmounted(() => {
cursor: pointer;
padding: 0.375rem;
border-radius: 8px;
transition: background-color 0.2s ease;
flex: 1;
white-space: nowrap;
}
@@ -542,6 +653,41 @@ onUnmounted(() => {
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) {
.actions-grid {
gap: 1.5rem;

View File

@@ -104,9 +104,7 @@ import Card from '../ui/Card.vue'
import ConfirmDialog from '../ui/ConfirmDialog.vue'
import { TauriAPI } from '../../api/tauri'
import { useRcloneStore } from '@/stores/rclone'
import { useAppStore } from '../../stores/app'
const appStore = useAppStore()
const rcloneStore = useRcloneStore()
const { t } = useTranslation()
@@ -267,7 +265,7 @@ const cancelUninstall = () => {
onMounted(async () => {
await checkServiceStatus()
statusCheckInterval = window.setInterval(checkServiceStatus, (appStore.settings.app.monitor_interval || 5) * 1000)
statusCheckInterval = window.setInterval(checkServiceStatus, 30 * 1000)
})
onUnmounted(() => {
@@ -438,7 +436,6 @@ onUnmounted(() => {
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
flex: 1;
justify-content: center;
min-width: 7rem;
@@ -494,21 +491,6 @@ onUnmounted(() => {
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 */
@media (max-width: 768px) {
.action-buttons {

View File

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

View File

@@ -184,7 +184,6 @@ onMounted(() => {
border-radius: 0.75rem;
padding: 0.875rem;
background: var(--color-background-tertiary, rgb(249 250 251));
transition: all 0.2s ease;
display: flex;
flex-direction: column;
min-height: 0;
@@ -192,7 +191,7 @@ onMounted(() => {
.version-item:hover {
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,
@@ -204,7 +203,7 @@ onMounted(() => {
:root.dark .version-item:hover,
:root.auto.dark .version-item:hover {
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 {
@@ -234,7 +233,6 @@ onMounted(() => {
border: 1px solid var(--color-border-secondary, rgb(209 213 219));
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
@@ -308,7 +306,6 @@ onMounted(() => {
font-size: 0.875rem;
color: var(--color-text-primary, rgb(17 24 39));
width: 100%;
transition: border-color 0.2s ease;
}
:root.dark .version-select,
@@ -337,7 +334,6 @@ onMounted(() => {
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
width: 100%;
}
@@ -365,18 +361,4 @@ onMounted(() => {
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>

View File

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

View File

@@ -74,7 +74,6 @@ export default {
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.dialog-container {
@@ -138,7 +137,6 @@ export default {
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s;
min-width: 4rem;
}

View File

@@ -37,7 +37,7 @@ onMounted(() => {
<div ref="dropdownRef" class="language-switcher relative">
<button @click="toggleDropdown" class="language-button">
<span class="language-label">{{ currentLanguage?.name }}</span>
<ChevronDown :size="12" :class="{ 'rotate-180': isOpen }" class="transition-transform" />
<ChevronDown :size="12" :class="{ flipped: isOpen }" />
</button>
<div v-if="isOpen" class="language-dropdown">
@@ -72,7 +72,6 @@ onMounted(() => {
color: var(--color-text-primary);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
min-width: 120px;
}
@@ -86,6 +85,10 @@ onMounted(() => {
text-align: center;
}
.flipped {
opacity: 0.7;
}
.language-dropdown {
position: absolute;
top: 100%;
@@ -107,7 +110,6 @@ onMounted(() => {
gap: 0.75rem;
padding: 0.75rem;
cursor: pointer;
transition: background-color 0.2s ease;
font-size: 0.875rem;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,8 +32,16 @@
"stopRclone": "Stop RClone",
"manageMounts": "Manage Mounts",
"autoLaunch": "Auto Launch Core(not app)",
"processing": "Processing...",
"showAdminPassword": "Show/Copy admin password from logs"
"copyAdminPassword": "Copy admin password",
"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": {
"title": "Core Monitor",
@@ -126,10 +134,12 @@
"placeholder": "5244",
"help": "Port number for the web interface (1-65535)"
},
"apiToken": {
"label": "API Token",
"placeholder": "Optional. Secure API access with authentication",
"help": "Optional. Secure API access with authentication"
"dataDir": {
"label": "Data Directory",
"placeholder": "Optional. Custom data directory path",
"help": "Optional. Specify a custom directory for OpenList data storage",
"selectTitle": "Select Data Directory",
"selectError": "Failed to select directory. Please try again or enter path manually."
},
"ssl": {
"title": "Enable SSL/HTTPS",
@@ -141,6 +151,18 @@
"title": "Auto-launch on startup",
"description": "Automatically start OpenList service when the application launches"
}
},
"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": {
@@ -149,7 +171,7 @@
"title": "Remote Storage",
"subtitle": "Configure rclone for remote storage access",
"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."
}
},
@@ -163,15 +185,6 @@
"auto": "Auto",
"autoDesc": "Follow system"
},
"monitor": {
"title": "Monitoring",
"subtitle": "System monitoring and refresh settings",
"interval": {
"label": "Monitor Interval (seconds)",
"placeholder": "5",
"help": "How often to refresh system metrics and status"
}
},
"ghProxy": {
"title": "GitHub Proxy",
"subtitle": "Accelerate GitHub with proxy service",
@@ -191,12 +204,6 @@
"description": "Use system default browser instead of built-in window"
}
},
"tutorial": {
"title": "Tutorial",
"subtitle": "Learn how to use OpenList Desktop",
"restart": "Start Tutorial",
"help": "Restart the tutorial to learn about app features and navigation"
},
"updates": {
"title": "Updates",
"subtitle": "Manage automatic updates and notifications",
@@ -330,7 +337,7 @@
"authentication": "Authentication",
"mountSettings": "Mount Settings",
"name": "Name",
"namePlaceholder": "e.g., my-webdav-remote",
"namePlaceholder": "e.g.mount1",
"type": "Type",
"url": "URL",
"urlPlaceholder": "e.g., http://localhost:5264/dav/189",
@@ -341,7 +348,7 @@
"password": "Password",
"passwordPlaceholder": "Password",
"mountPoint": "Mount Path",
"mountPointPlaceholder": "e.g., T: (Windows) or /mnt/remote (Linux)",
"mountPointPlaceholder": "e.g., T: or /mnt/remote",
"volumeName": "Remote Path",
"volumeNamePlaceholder": "e.g., /",
"autoMount": "Auto-mount on startup",
@@ -429,36 +436,6 @@
"title": "OpenList",
"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": {
"title": "App Updates",
"subtitle": "Keep your application up to date with the latest features and security improvements",
@@ -480,7 +457,7 @@
"startingDownload": "Starting download...",
"downloading": "Downloading",
"installingUpdate": "Installing update...",
"restartingApp": "Restarting application...",
"quitApp": "Quitting application...",
"noUpdatesFound": "No updates available",
"aboutUpdates": "About Updates",
"autoCheckInfo": "Automatic update checks keep your app secure and up-to-date",

View File

@@ -32,8 +32,16 @@
"stopRclone": "停止 RClone",
"manageMounts": "管理挂载",
"autoLaunch": "自动启动核心(非桌面app)",
"processing": "处理中...",
"showAdminPassword": "显示/复制日志中的管理员密码"
"copyAdminPassword": "复制管理员密码",
"resetAdminPassword": "重置管理员密码",
"firewall": {
"enable": "放行端口",
"disable": "移除端口放行",
"added": "端口放行成功",
"removed": "端口移除成功",
"failedToAdd": "添加端口放行失败",
"failedToRemove": "移除端口放行失败"
}
},
"coreMonitor": {
"title": "核心监控",
@@ -126,10 +134,12 @@
"placeholder": "5244",
"help": "Web 界面的端口号 (1-65535)"
},
"apiToken": {
"label": "API 令牌",
"placeholder": "可选。用于 API 访问的身份验证",
"help": "可选。用于 API 访问的身份验证"
"dataDir": {
"label": "数据目录",
"placeholder": "可选。自定义数据目录路径",
"help": "可选。为 OpenList 数据存储指定自定义目录",
"selectTitle": "选择数据目录",
"selectError": "选择目录失败。请重试或手动输入路径。"
},
"ssl": {
"title": "启用 SSL/HTTPS",
@@ -141,6 +151,18 @@
"title": "开机自启",
"description": "应用程序启动时自动启动 OpenList 服务"
}
},
"admin": {
"title": "管理员密码",
"subtitle": "管理 OpenList 核心网页界面的管理员密码",
"currentPassword": "管理员密码",
"passwordPlaceholder": "输入管理员密码或点击重置生成",
"resetTitle": "重置管理员密码为新的随机值",
"resetSuccess": "管理员密码重置成功!已生成新密码并保存。",
"resetFailed": "重置管理员密码失败。请查看日志了解详细信息。",
"passwordUpdated": "管理员密码更新成功!",
"passwordUpdateFailed": "更新管理员密码失败。请查看日志了解详细信息。",
"help": "输入自定义管理员密码或点击重置按钮生成新的随机密码。点击'保存更改'将密码应用到 OpenList 核心。"
}
},
"rclone": {
@@ -150,7 +172,7 @@
"subtitle": "配置 rclone 远程存储访问",
"label": "Rclone 配置 (JSON)",
"invalidJson": "无效的 JSON 配置。请检查您的语法。",
"tips": "输入你的JSON配置"
"tips": "查看你的JSON配置"
}
},
"app": {
@@ -163,15 +185,6 @@
"auto": "自动",
"autoDesc": "跟随系统"
},
"monitor": {
"title": "监控",
"subtitle": "系统监控和刷新设置",
"interval": {
"label": "监控间隔(秒)",
"placeholder": "5",
"help": "刷新系统指标和状态的频率"
}
},
"ghProxy": {
"title": "GitHub 代理",
"subtitle": "使用代理服务加速 GitHub",
@@ -191,12 +204,6 @@
"description": "使用系统默认浏览器而不是内置窗口"
}
},
"tutorial": {
"title": "教程",
"subtitle": "学习如何使用 OpenList 桌面版",
"restart": "开始教程",
"help": "重新启动教程以了解应用功能和导航"
},
"updates": {
"title": "更新",
"subtitle": "管理自动更新和通知",
@@ -330,7 +337,7 @@
"authentication": "身份认证",
"mountSettings": "挂载设置",
"name": "名称",
"namePlaceholder": "例如:我的webdav远程",
"namePlaceholder": "例如:mount1",
"type": "类型",
"url": "URL",
"urlPlaceholder": "例如http://localhost:5264/dav/189",
@@ -341,7 +348,7 @@
"password": "密码",
"passwordPlaceholder": "密码",
"mountPoint": "挂载点",
"mountPointPlaceholder": "例如T: (Windows) 或 /mnt/remote (Linux)",
"mountPointPlaceholder": "例如T: 或 /mnt/remote",
"volumeName": "远程路径",
"volumeNamePlaceholder": "例如:/",
"autoMount": "开机自动挂载",
@@ -429,36 +436,6 @@
"title": "OpenList",
"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": {
"title": "应用更新",
"subtitle": "保持应用程序最新,获取最新功能和安全改进",
@@ -480,7 +457,7 @@
"startingDownload": "开始下载...",
"downloading": "下载中...",
"installingUpdate": "安装更新中...",
"restartingApp": "重启应用中...",
"quitApp": "退出应用中...",
"noUpdatesFound": "没有可用更新",
"aboutUpdates": "关于更新",
"autoCheckInfo": "自动更新检查让您的应用保持安全和最新状态",

View File

@@ -7,9 +7,16 @@ type ActionFn<T = any> = () => Promise<T>
export const useAppStore = defineStore('app', () => {
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: {} },
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
}
})
const openlistCoreStatus = ref<OpenListCoreStatus>({ running: false })
const remoteConfigs = ref<IRemoteConfig>({})
@@ -97,7 +104,7 @@ export const useAppStore = defineStore('app', () => {
const saveSettings = () => withLoading(() => TauriAPI.settings.save(settings.value), 'Failed to save settings')
async function saveSettingsWithUpdatePort(): Promise<boolean> {
async function saveSettingsWithCoreUpdate(): Promise<boolean> {
try {
await TauriAPI.settings.saveWithUpdatePort(settings.value)
return true
@@ -273,13 +280,10 @@ export const useAppStore = defineStore('app', () => {
async function loadRemoteConfigs() {
try {
loading.value = true
remoteConfigs.value = await TauriAPI.rclone.remotes.listConfig('webdav')
} catch (err: any) {
error.value = 'Failed to load remote configurations'
console.error('Failed to load remote configs:', err)
} finally {
loading.value = false
}
}
@@ -375,10 +379,6 @@ export const useAppStore = defineStore('app', () => {
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> {
try {
const processList = await TauriAPI.process.list()
@@ -683,7 +683,6 @@ export const useAppStore = defineStore('app', () => {
async function init() {
try {
initTutorial()
await loadSettings()
await refreshOpenListCoreStatus()
await TauriAPI.tray.updateDelayed(openlistCoreStatus.value.running)
@@ -697,43 +696,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> {
try {
return await TauriAPI.logs.adminPassword()
@@ -743,8 +705,34 @@ export const useAppStore = defineStore('app', () => {
}
}
function closeTutorial() {
showTutorial.value = false
async function resetAdminPassword(): Promise<string | null> {
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
@@ -783,16 +771,12 @@ export const useAppStore = defineStore('app', () => {
updateAvailable,
updateCheck,
showTutorial,
tutorialStep,
tutorialSkipped,
isCoreRunning,
openListCoreUrl,
loadSettings,
saveSettings,
saveSettingsWithUpdatePort,
saveSettingsWithCoreUpdate,
resetSettings,
startOpenListCore,
@@ -809,18 +793,13 @@ export const useAppStore = defineStore('app', () => {
clearError,
init,
getAdminPassword,
resetAdminPassword,
setAdminPassword,
setTheme,
toggleTheme,
applyTheme,
initTutorial,
startTutorial,
nextTutorialStep,
prevTutorialStep,
skipTutorial,
completeTutorial,
closeTutorial,
setUpdateAvailable,
clearUpdateStatus
}

View File

@@ -6,7 +6,7 @@ interface IRemoteConfig {
interface OpenListCoreConfig {
port: number
api_token: string
data_dir: string
auto_launch: boolean
ssl_enabled: boolean
}
@@ -45,11 +45,11 @@ interface RcloneMountInfo {
interface AppConfig {
theme?: 'light' | 'dark' | 'auto'
monitor_interval?: number
auto_update_enabled?: boolean
gh_proxy?: string
gh_proxy_api?: boolean
open_links_in_browser?: boolean
admin_password?: string
}
interface MergedSettings {

View File

@@ -347,13 +347,16 @@ const handleKeydown = (event: KeyboardEvent) => {
}
onMounted(async () => {
await appStore.loadLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
| 'openlist'
| 'rclone'
| 'app'
)
await scrollToBottom()
appStore
.loadLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
| 'openlist'
| 'rclone'
| 'app'
)
.then(() => {
scrollToBottom()
})
document.addEventListener('keydown', handleKeydown)
@@ -371,7 +374,7 @@ onMounted(async () => {
await scrollToBottom()
}
}
}, (appStore.settings.app.monitor_interval || 5) * 1000)
}, 30 * 1000)
})
onUnmounted(() => {

View File

@@ -453,7 +453,9 @@ const dismissWebdavTip = () => {
localStorage.setItem('webdav_tip_dismissed', 'true')
}
const isWindows = /win/i.test(navigator.platform) || /win/i.test(navigator.userAgent)
const isWindows = computed(() => {
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
})
const showWinfspTip = ref(isWindows && !localStorage.getItem('winfsp_tip_dismissed'))
const dismissWinfspTip = () => {
@@ -470,14 +472,14 @@ const shouldShowWebdavTip = computed(() => {
onMounted(async () => {
document.addEventListener('keydown', handleKeydown)
await rcloneStore.checkRcloneBackendStatus()
await appStore.loadRemoteConfigs()
await appStore.loadMountInfos()
mountRefreshInterval = setInterval(appStore.loadMountInfos, (appStore.settings.app.monitor_interval || 5) * 1000)
rcloneStore.checkRcloneBackendStatus()
appStore.loadRemoteConfigs()
appStore.loadMountInfos()
mountRefreshInterval = setInterval(appStore.loadMountInfos, 15 * 1000)
backendStatusCheckInterval = setInterval(() => {
rcloneStore.checkRcloneBackendStatus()
}, (appStore.settings.app.monitor_interval || 5) * 1000)
await rcloneStore.init()
}, 15 * 1000)
rcloneStore.init()
})
onUnmounted(() => {
@@ -722,7 +724,7 @@ onUnmounted(() => {
</div>
</div>
<!-- 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="modal-header">
<div class="modal-title-section">

View File

@@ -1,14 +1,14 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import { useAppStore } from '../stores/app'
import { useTranslation } from '../composables/useI18n'
import { Settings, Server, HardDrive, Save, RotateCcw, AlertCircle, CheckCircle, Play } from 'lucide-vue-next'
import { Settings, Server, HardDrive, Save, RotateCcw, AlertCircle, CheckCircle, FolderOpen } from 'lucide-vue-next'
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
import { open } from '@tauri-apps/plugin-dialog'
const appStore = useAppStore()
const route = useRoute()
const router = useRouter()
const { t } = useTranslation()
const isSaving = ref(false)
const message = ref('')
@@ -16,11 +16,13 @@ const messageType = ref<'success' | 'error' | 'info'>('info')
const activeTab = ref('openlist')
const rcloneConfigJson = ref('')
const autoStartApp = ref(false)
const isResettingPassword = ref(false)
const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
const rcloneSettings = reactive({ ...appStore.settings.rclone })
const appSettings = reactive({ ...appStore.settings.app })
let originalOpenlistPort = openlistCoreSettings.port || 5244
let originalDataDir = openlistCoreSettings.data_dir
watch(autoStartApp, async newValue => {
if (newValue) {
@@ -59,7 +61,7 @@ onMounted(async () => {
}
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.ssl_enabled === undefined) openlistCoreSettings.ssl_enabled = false
@@ -68,12 +70,16 @@ onMounted(async () => {
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
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.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.admin_password) appSettings.admin_password = ''
originalOpenlistPort = openlistCoreSettings.port || 5244
originalDataDir = openlistCoreSettings.data_dir
// Load current admin password
await loadCurrentAdminPassword()
})
const hasUnsavedChanges = computed(() => {
@@ -110,13 +116,33 @@ const handleSave = async () => {
appStore.settings.openlist = { ...openlistCoreSettings }
appStore.settings.rclone = { ...rcloneSettings }
appStore.settings.app = { ...appSettings }
if (originalOpenlistPort !== openlistCoreSettings.port) {
await appStore.saveSettingsWithUpdatePort()
const originalAdminPassword = appStore.settings.app.admin_password
const needsPasswordUpdate = originalAdminPassword !== appSettings.admin_password && appSettings.admin_password
if (originalOpenlistPort !== openlistCoreSettings.port || originalDataDir !== openlistCoreSettings.data_dir) {
await appStore.saveSettingsWithCoreUpdate()
} else {
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
originalDataDir = openlistCoreSettings.data_dir
} catch (error) {
message.value = t('settings.saveFailed')
messageType.value = 'error'
@@ -130,11 +156,6 @@ const handleSave = async () => {
}
}
async function startTutorial() {
router.push({ name: 'Dashboard' })
appStore.startTutorial()
}
const handleReset = async () => {
if (!confirm(t('settings.confirmReset'))) {
return
@@ -155,6 +176,64 @@ const handleReset = async () => {
messageType.value = 'error'
}
}
const handleSelectDataDir = async () => {
try {
const selected = await open({
directory: true,
multiple: false,
title: t('settings.service.network.dataDir.selectTitle'),
defaultPath: openlistCoreSettings.data_dir || undefined
})
if (selected && typeof selected === 'string') {
openlistCoreSettings.data_dir = selected
}
} catch (error) {
console.error('Failed to select directory:', error)
message.value = t('settings.service.network.dataDir.selectError')
messageType.value = 'error'
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'
await navigator.clipboard.writeText(newPassword)
} 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 loadCurrentAdminPassword = async () => {
try {
const password = await appStore.getAdminPassword()
if (password) {
appSettings.admin_password = password
}
} catch (error) {
console.error('Failed to load admin password:', error)
}
}
</script>
<template>
@@ -218,14 +297,24 @@ const handleReset = async () => {
<small>{{ t('settings.service.network.port.help') }}</small>
</div>
<div class="form-group">
<label>{{ t('settings.service.network.apiToken.label') }}</label>
<input
v-model="openlistCoreSettings.api_token"
type="password"
class="form-input"
:placeholder="t('settings.service.network.apiToken.placeholder')"
/>
<small>{{ t('settings.service.network.apiToken.help') }}</small>
<label>{{ t('settings.service.network.dataDir.label') }}</label>
<div class="input-group">
<input
v-model="openlistCoreSettings.data_dir"
type="text"
class="form-input"
:placeholder="t('settings.service.network.dataDir.placeholder')"
/>
<button
type="button"
@click="handleSelectDataDir"
class="input-addon-btn"
:title="t('settings.service.network.dataDir.selectTitle')"
>
<FolderOpen :size="16" />
</button>
</div>
<small>{{ t('settings.service.network.dataDir.help') }}</small>
</div>
<div class="form-group">
@@ -256,6 +345,33 @@ const handleReset = async () => {
</label>
</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"
@click="handleResetAdminPassword"
:disabled="isResettingPassword"
class="input-addon-btn reset-password-btn"
:title="t('settings.service.admin.resetTitle')"
>
<RotateCcw :size="16" />
</button>
</div>
<small>{{ t('settings.service.admin.help') }}</small>
</div>
</div>
</div>
<div v-if="activeTab === 'rclone'" class="tab-content">
@@ -270,6 +386,7 @@ const handleReset = async () => {
class="form-textarea"
placeholder='{ "remote1": { "type": "s3", "provider": "AWS" } }'
rows="10"
readonly
></textarea>
<small>{{ t('settings.rclone.config.tips') }}</small>
</div>
@@ -298,26 +415,6 @@ const handleReset = async () => {
</div>
</div>
<div class="settings-section">
<h2>{{ t('settings.app.monitor.title') }}</h2>
<p>{{ t('settings.app.monitor.subtitle') }}</p>
<div class="form-grid">
<div class="form-group">
<label>{{ t('settings.app.monitor.interval.label') }}</label>
<input
v-model.number="appSettings.monitor_interval"
type="number"
class="form-input"
:placeholder="t('settings.app.monitor.interval.placeholder')"
min="1"
max="60"
/>
<small>{{ t('settings.app.monitor.interval.help') }}</small>
</div>
</div>
</div>
<div class="settings-section">
<h2>{{ t('settings.app.ghProxy.title') }}</h2>
<p>{{ t('settings.app.ghProxy.subtitle') }}</p>
@@ -394,21 +491,6 @@ const handleReset = async () => {
</label>
</div>
</div>
<div class="settings-section">
<h2>{{ t('settings.app.tutorial.title') }}</h2>
<p>{{ t('settings.app.tutorial.subtitle') }}</p>
<div class="form-grid">
<div class="form-group">
<button @click="startTutorial" class="tutorial-btn" type="button">
<Play :size="16" />
{{ t('settings.app.tutorial.restart') }}
</button>
<small>{{ t('settings.app.tutorial.help') }}</small>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -100,7 +100,6 @@ const goToSettings = () => {
text-decoration: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
}
@@ -128,7 +127,6 @@ const goToSettings = () => {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
transition: all 0.2s ease;
}
.info-card:hover {

View File

@@ -56,20 +56,9 @@
rgba(139, 92, 246, 0.3) 80%,
transparent 100%
);
animation: shimmer 8s ease-in-out infinite;
z-index: 0;
}
@keyframes shimmer {
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 0.8;
}
}
.metrics-overview {
position: relative;
z-index: 1;
@@ -196,22 +185,17 @@
flex: 1;
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
transform-origin: center;
position: relative;
border-radius: 16px;
background: transparent;
will-change: transform;
}
.dashboard-card-wrapper:hover {
transform: translateY(-2px) scale(1.01);
z-index: 10;
background: rgba(255, 255, 255, 0.5);
}
.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),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
border-color: rgba(59, 130, 246, 0.3);
}
:root.dark .dashboard-card-wrapper:hover > *,
@@ -232,7 +216,6 @@
.dashboard-grid:focus-within > *:not(:focus-within) {
opacity: 0.7;
transform: scale(0.98);
}
.dashboard-grid > *:focus-within {
@@ -241,49 +224,6 @@
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) {
.dashboard-grid {
grid-template-columns: repeat(2, 1fr);
@@ -370,10 +310,6 @@
.dashboard-subtitle {
font-size: 0.875rem;
}
.dashboard-grid > *:hover {
transform: translateY(-2px) scale(1.001);
}
}
@media (max-width: 480px) {
@@ -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 {
text-align: center;
margin-bottom: 1rem;

View File

@@ -22,7 +22,6 @@
padding: 12px 20px;
background: var(--color-surface-elevated);
border-bottom: 1px solid var(--color-border);
backdrop-filter: blur(20px) saturate(180%);
gap: 20px;
flex-shrink: 0;
min-height: 56px;
@@ -58,7 +57,6 @@
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.toolbar-btn:hover:not(:disabled) {
@@ -111,7 +109,6 @@
background: var(--color-background-secondary);
font-size: 13px;
color: var(--color-text-primary);
transition: all var(--transition-medium);
}
.search-input:focus {
@@ -154,19 +151,6 @@
border-bottom: 1px solid var(--color-border);
gap: 20px;
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,
@@ -220,7 +204,6 @@
color: var(--color-text-primary);
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
}
.filter-btn:hover:not(:disabled) {
@@ -318,7 +301,6 @@
background: transparent;
color: var(--color-text-tertiary);
cursor: pointer;
transition: all var(--transition-fast);
}
.scroll-btn:hover {
@@ -370,7 +352,6 @@
padding: 8px 16px;
border-bottom: 1px solid var(--color-border-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.log-entry:hover {
@@ -684,7 +665,6 @@
padding: 16px 20px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 14px;
font-weight: 500;
@@ -747,21 +727,11 @@
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 {
transform: translateX(100%) scale(0.8);
opacity: 0;
}
.notification-leave-to {
transform: translateX(100%) scale(0.8);
opacity: 0;
}

View File

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

View File

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

View File

@@ -572,26 +572,26 @@
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.7.tgz#b46bcf377b3172dbc768fdbd053e6492ad801a09"
integrity sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==
"@intlify/core-base@11.1.9":
version "11.1.9"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.9.tgz#54201e7985d52240627b9c327a4d57c08a96cd39"
integrity sha512-Lrdi4wp3XnGhWmB/mMD/XtfGUw1Jt+PGpZI/M63X1ZqhTDjNHRVCs/i8vv8U1cwaj1A9fb0bkCQHLSL0SK+pIQ==
"@intlify/core-base@11.1.10":
version "11.1.10"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.10.tgz#4731748992bc6d8e723ca6c2cc5aa5a4c90cf7a5"
integrity sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==
dependencies:
"@intlify/message-compiler" "11.1.9"
"@intlify/shared" "11.1.9"
"@intlify/message-compiler" "11.1.10"
"@intlify/shared" "11.1.10"
"@intlify/message-compiler@11.1.9":
version "11.1.9"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.9.tgz#c84a3a2777b0d95342348d1bf95669329d71e10c"
integrity sha512-84SNs3Ikjg0rD1bOuchzb3iK1vR2/8nxrkyccIl5DjFTeMzE/Fxv6X+A7RN5ZXjEWelc1p5D4kHA6HEOhlKL5Q==
"@intlify/message-compiler@11.1.10":
version "11.1.10"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.10.tgz#ff5c92c311cd72144126f5c128912adb4e911207"
integrity sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==
dependencies:
"@intlify/shared" "11.1.9"
"@intlify/shared" "11.1.10"
source-map-js "^1.0.2"
"@intlify/shared@11.1.9":
version "11.1.9"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.9.tgz#20244e53322ba01233df7ddb6dc677561b3c7e0b"
integrity sha512-H/83xgU1l8ox+qG305p6ucmoy93qyjIPnvxGWRA7YdOoHe1tIiW9IlEu4lTdsOR7cfP1ecrwyflQSqXdXBacXA==
"@intlify/shared@11.1.10":
version "11.1.10"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.10.tgz#d869aa8fbc1aa307f26a58848fea6df3c9785b6f"
integrity sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==
"@isaacs/balanced-match@^4.0.1":
version "4.0.1"
@@ -3926,13 +3926,13 @@ vscode-uri@^3.0.8:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
vue-i18n@11.1.9:
version "11.1.9"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.9.tgz#214816d3a5461a3169ee1eb507cac045a03a15d8"
integrity sha512-N9ZTsXdRmX38AwS9F6Rh93RtPkvZTkSy/zNv63FTIwZCUbLwwrpqlKz9YQuzFLdlvRdZTnWAUE5jMxr8exdl7g==
vue-i18n@11.1.10:
version "11.1.10"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.10.tgz#04578ea4213f96c37939a08f516a648a6d0b84a1"
integrity sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==
dependencies:
"@intlify/core-base" "11.1.9"
"@intlify/shared" "11.1.9"
"@intlify/core-base" "11.1.10"
"@intlify/shared" "11.1.10"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.5.1: