mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 19:27:33 +08:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a88b17c92f | ||
|
|
20aeb6a796 | ||
|
|
13efd8a629 | ||
|
|
9998563110 | ||
|
|
6f41bd708c | ||
|
|
4837ee592f | ||
|
|
8d25feefe0 | ||
|
|
6628c7936b | ||
|
|
9c53267589 | ||
|
|
e0d3250823 | ||
|
|
c9ccf6d1ce | ||
|
|
a19e74ce1f | ||
|
|
0231fa20d7 | ||
|
|
f69bfa6fd5 | ||
|
|
bb0f091849 | ||
|
|
9d95b6b46c | ||
|
|
249612344e | ||
|
|
3c5f64b1b4 | ||
|
|
2911922403 | ||
|
|
0093f15524 | ||
|
|
704d06ebe1 | ||
|
|
7038a1a255 | ||
|
|
7476e29e2b | ||
|
|
08a9eb38cc | ||
|
|
d0917ee550 | ||
|
|
ccb8b12f1e | ||
|
|
1d74daf8a5 | ||
|
|
ffdf996cd0 | ||
|
|
eb82a49270 | ||
|
|
0c09addb31 | ||
|
|
731ff435a5 | ||
|
|
3e1a5f121d | ||
|
|
9263ad9810 | ||
|
|
7b0565f210 | ||
|
|
90f935e6bb | ||
|
|
8aa4f93f6f | ||
|
|
4b08aeb4d3 | ||
|
|
97be081c47 | ||
|
|
c68102fa20 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Created using wingetcreate 1.9.14.0
|
||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json
|
||||
|
||||
PackageIdentifier: OpenListTeam.OpenListDesktop
|
||||
PackageVersion: 0.3.0
|
||||
InstallerType: nullsoft
|
||||
Scope: machine
|
||||
InstallModes:
|
||||
- interactive
|
||||
Installers:
|
||||
- Architecture: x64
|
||||
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.3.0/OpenList.Desktop_0.3.0_x64-setup.exe
|
||||
InstallerSha256: 43CC59B5E557F67A7D2F66ADBEF517FBB7CD4FD7E9032FA274FEF5373E38B885
|
||||
- Architecture: arm64
|
||||
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.3.0/OpenList.Desktop_0.3.0_arm64-setup.exe
|
||||
InstallerSha256: 3F99A8F566242EE749A3463E3DCB7D6D1CE75C51D3E31970AE1A39C46287035F
|
||||
ManifestType: installer
|
||||
ManifestVersion: 1.9.0
|
||||
@@ -0,0 +1,30 @@
|
||||
# Created using wingetcreate 1.9.14.0
|
||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json
|
||||
|
||||
PackageIdentifier: OpenListTeam.OpenListDesktop
|
||||
PackageVersion: 0.3.0
|
||||
PackageLocale: en-US
|
||||
Publisher: OpenList Team
|
||||
PublisherUrl: https://github.com/OpenListTeam
|
||||
PublisherSupportUrl: https://github.com/OpenListTeam/OpenList-Desktop/issues
|
||||
Author: Kuingsmile
|
||||
PackageName: OpenList Desktop
|
||||
PackageUrl: https://github.com/OpenListTeam/OpenList-Desktop
|
||||
License: GPL-3.0
|
||||
LicenseUrl: https://github.com/OpenListTeam/OpenList-Desktop/blob/main/LICENSE
|
||||
Copyright: Copyright (c) 2025 OpenList Team
|
||||
ShortDescription: A cross-platform desktop application for OpenList with local disk mounting
|
||||
Description: |
|
||||
OpenList Desktop is a modern desktop application that provides a seamless interface for managing OpenList.
|
||||
Features include local disk mounting, service management, real-time monitoring, and multi-language support.
|
||||
|
||||
Key Features:
|
||||
- Cross-platform support (Windows, macOS, Linux)
|
||||
- Local disk mounting
|
||||
- Service management and monitoring
|
||||
- Real-time log viewing
|
||||
- Multi-language support
|
||||
Tags:
|
||||
- openlist
|
||||
ManifestType: defaultLocale
|
||||
ManifestVersion: 1.9.0
|
||||
@@ -0,0 +1,8 @@
|
||||
# Created using wingetcreate 1.9.14.0
|
||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json
|
||||
|
||||
PackageIdentifier: OpenListTeam.OpenListDesktop
|
||||
PackageVersion: 0.3.0
|
||||
DefaultLocale: en-US
|
||||
ManifestType: version
|
||||
ManifestVersion: 1.9.0
|
||||
@@ -0,0 +1,18 @@
|
||||
# Created using wingetcreate 1.9.14.0
|
||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json
|
||||
|
||||
PackageIdentifier: OpenListTeam.OpenListDesktop
|
||||
PackageVersion: 0.4.0
|
||||
InstallerType: nullsoft
|
||||
Scope: machine
|
||||
InstallModes:
|
||||
- interactive
|
||||
Installers:
|
||||
- Architecture: x64
|
||||
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.4.0/OpenList.Desktop_0.4.0_x64-setup.exe
|
||||
InstallerSha256: 500DDBC34C73A663C1CA5A82F55DF56321AE4E2E8B727BE26D6EBF4F9F19F881
|
||||
- Architecture: arm64
|
||||
InstallerUrl: https://github.com/OpenListTeam/OpenList-Desktop/releases/download/v0.4.0/OpenList.Desktop_0.4.0_arm64-setup.exe
|
||||
InstallerSha256: 4D60544E1684AE3A90220DA6A044D57C144E9F566272D2D43A481DEC8ED573EA
|
||||
ManifestType: installer
|
||||
ManifestVersion: 1.9.0
|
||||
@@ -0,0 +1,30 @@
|
||||
# Created using wingetcreate 1.9.14.0
|
||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json
|
||||
|
||||
PackageIdentifier: OpenListTeam.OpenListDesktop
|
||||
PackageVersion: 0.4.0
|
||||
PackageLocale: en-US
|
||||
Publisher: OpenList Team
|
||||
PublisherUrl: https://github.com/OpenListTeam
|
||||
PublisherSupportUrl: https://github.com/OpenListTeam/OpenList-Desktop/issues
|
||||
Author: Kuingsmile
|
||||
PackageName: OpenList Desktop
|
||||
PackageUrl: https://github.com/OpenListTeam/OpenList-Desktop
|
||||
License: GPL-3.0
|
||||
LicenseUrl: https://github.com/OpenListTeam/OpenList-Desktop/blob/main/LICENSE
|
||||
Copyright: Copyright (c) 2025 OpenList Team
|
||||
ShortDescription: A cross-platform desktop application for OpenList with local disk mounting
|
||||
Description: |
|
||||
OpenList Desktop is a modern desktop application that provides a seamless interface for managing OpenList.
|
||||
Features include local disk mounting, service management, real-time monitoring, and multi-language support.
|
||||
|
||||
Key Features:
|
||||
- Cross-platform support (Windows, macOS, Linux)
|
||||
- Local disk mounting
|
||||
- Service management and monitoring
|
||||
- Real-time log viewing
|
||||
- Multi-language support
|
||||
Tags:
|
||||
- openlist
|
||||
ManifestType: defaultLocale
|
||||
ManifestVersion: 1.9.0
|
||||
@@ -0,0 +1,8 @@
|
||||
# Created using wingetcreate 1.9.14.0
|
||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json
|
||||
|
||||
PackageIdentifier: OpenListTeam.OpenListDesktop
|
||||
PackageVersion: 0.4.0
|
||||
DefaultLocale: en-US
|
||||
ManifestType: version
|
||||
ManifestVersion: 1.9.0
|
||||
@@ -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
|
||||
|
||||
53
README.md
53
README.md
@@ -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
|
||||
|
||||
53
README_en.md
53
README_en.md
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
2
src-tauri/Cargo.lock
generated
@@ -2898,7 +2898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openlist-desktop"
|
||||
version = "0.2.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openlist-desktop"
|
||||
version = "0.3.0"
|
||||
version = "0.5.1"
|
||||
description = "A Tauri App"
|
||||
authors = ["Kuingsmile"]
|
||||
edition = "2024"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
122
src-tauri/src/cmd/firewall.rs
Normal file
122
src-tauri/src/cmd/firewall.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::process::{Command, ExitStatus};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
#[cfg(target_os = "windows")]
|
||||
use runas::Command as RunasCommand;
|
||||
use tauri::State;
|
||||
|
||||
use crate::object::structs::AppState;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const RULE: &str = "OpenList Core";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn netsh_with_elevation(args: &[String]) -> Result<ExitStatus, String> {
|
||||
let token = Token::with_current_process().map_err(|e| format!("token: {e}"))?;
|
||||
let elevated = !matches!(
|
||||
token
|
||||
.privilege_level()
|
||||
.map_err(|e| format!("privilege: {e}"))?,
|
||||
PrivilegeLevel::NotPrivileged
|
||||
);
|
||||
|
||||
if elevated {
|
||||
Command::new("netsh")
|
||||
.args(args)
|
||||
.creation_flags(0x08000000)
|
||||
.status()
|
||||
} else {
|
||||
RunasCommand::new("netsh").args(args).show(false).status()
|
||||
}
|
||||
.map_err(|e| format!("netsh: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn firewall_rule(verb: &str, port: Option<u16>) -> Result<bool, String> {
|
||||
let mut args: Vec<String> = vec![
|
||||
"advfirewall".into(),
|
||||
"firewall".into(),
|
||||
verb.into(),
|
||||
"rule".into(),
|
||||
format!("name={RULE}"),
|
||||
];
|
||||
if let Some(p) = port {
|
||||
args.extend([
|
||||
"dir=in".into(),
|
||||
"action=allow".into(),
|
||||
"protocol=TCP".into(),
|
||||
format!("localport={p}"),
|
||||
"description=Allow OpenList Core web interface access".into(),
|
||||
]);
|
||||
}
|
||||
Ok(netsh_with_elevation(&args)?.success())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn rule_stdout() -> Result<Option<String>, String> {
|
||||
let out = Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall",
|
||||
"firewall",
|
||||
"show",
|
||||
"rule",
|
||||
&format!("name={RULE}"),
|
||||
])
|
||||
.creation_flags(0x08000000)
|
||||
.output()
|
||||
.map_err(|e| format!("netsh: {e}"))?;
|
||||
if out.status.success() {
|
||||
Ok(Some(String::from_utf8_lossy(&out.stdout).into()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn firewall_rule(_: &str, _: Option<u16>) -> Result<bool, String> {
|
||||
Ok(true)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn rule_stdout() -> Result<Option<String>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_firewall_rule(state: State<'_, AppState>) -> Result<bool, String> {
|
||||
let port = state
|
||||
.app_settings
|
||||
.read()
|
||||
.clone()
|
||||
.ok_or("read settings")?
|
||||
.openlist
|
||||
.port;
|
||||
|
||||
if let Some(out) = rule_stdout()? {
|
||||
Ok(out.contains(&port.to_string()))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_firewall_rule(state: State<'_, AppState>) -> Result<bool, String> {
|
||||
let port = state
|
||||
.app_settings
|
||||
.read()
|
||||
.clone()
|
||||
.ok_or("read settings")?
|
||||
.openlist
|
||||
.port;
|
||||
|
||||
let _ = firewall_rule("delete", None);
|
||||
firewall_rule("add", Some(port))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_firewall_rule(_state: State<'_, AppState>) -> Result<bool, String> {
|
||||
firewall_rule("delete", None)
|
||||
}
|
||||
@@ -1,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>)?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
88
src/App.vue
88
src/App.vue
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "自动更新检查让您的应用保持安全和最新状态",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
4
src/types/types.d.ts
vendored
4
src/types/types.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
42
yarn.lock
42
yarn.lock
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user