mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 19:27:33 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
954ee010c1 | ||
|
|
c219afa54e | ||
|
|
06e54d1b01 | ||
|
|
386147d5ff | ||
|
|
2eeba5f428 | ||
|
|
dc1cb41e61 | ||
|
|
99c426c15c | ||
|
|
77f9f81dea | ||
|
|
5cc2c1640c | ||
|
|
24b45446cc | ||
|
|
f3cc4a021b | ||
|
|
b6cfda7648 | ||
|
|
3b9910da0a | ||
|
|
a88b17c92f | ||
|
|
20aeb6a796 | ||
|
|
13efd8a629 | ||
|
|
9998563110 | ||
|
|
6f41bd708c | ||
|
|
4837ee592f | ||
|
|
8d25feefe0 | ||
|
|
6628c7936b | ||
|
|
9c53267589 | ||
|
|
e0d3250823 | ||
|
|
c9ccf6d1ce | ||
|
|
a19e74ce1f | ||
|
|
0231fa20d7 | ||
|
|
f69bfa6fd5 | ||
|
|
bb0f091849 | ||
|
|
9d95b6b46c | ||
|
|
249612344e | ||
|
|
3c5f64b1b4 | ||
|
|
2911922403 | ||
|
|
0093f15524 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.platform.target }}
|
targets: ${{ matrix.platform.target }}
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.platform.target }}
|
targets: ${{ matrix.platform.target }}
|
||||||
|
|
||||||
|
|||||||
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_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
PERSONAL_GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
@@ -317,8 +318,8 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust Stable
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: rustup target add ${{ matrix.target }}
|
run: rustup target add ${{ matrix.target }}
|
||||||
@@ -451,8 +452,8 @@ jobs:
|
|||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust Stable
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: rustup target add ${{ matrix.target }}
|
run: rustup target add ${{ matrix.target }}
|
||||||
@@ -624,7 +625,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update WinGet package manifest
|
- name: Update WinGet package manifest
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
$version = "${{ steps.version.outputs.version }}"
|
$version = "${{ steps.version.outputs.version }}"
|
||||||
# URLs for both x64 and arm64 installers
|
# URLs for both x64 and arm64 installers
|
||||||
|
|||||||
@@ -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
|
||||||
40
README.md
40
README.md
@@ -242,9 +242,10 @@ winget install OpenListTeam.OpenListDesktop
|
|||||||
{
|
{
|
||||||
"openlist": {
|
"openlist": {
|
||||||
"port": 5244,
|
"port": 5244,
|
||||||
"api_token": "your-secure-token",
|
"data_dir": "",
|
||||||
"auto_launch": true,
|
"auto_launch": true,
|
||||||
"ssl_enabled": false
|
"ssl_enabled": false,
|
||||||
|
"admin_password": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -283,13 +284,6 @@ winget install OpenListTeam.OpenListDesktop
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
- `OPENLIST_API_TOKEN`:覆盖默认 API 令牌
|
|
||||||
- `OPENLIST_PORT`:覆盖默认端口(5244)
|
|
||||||
- `RCLONE_CONFIG_DIR`:自定义 Rclone 配置目录
|
|
||||||
- `LOG_LEVEL`:设置日志级别(debug、info、warn、error)
|
|
||||||
|
|
||||||
## 🔧 开发
|
## 🔧 开发
|
||||||
|
|
||||||
### 开发环境设置
|
### 开发环境设置
|
||||||
@@ -297,7 +291,7 @@ winget install OpenListTeam.OpenListDesktop
|
|||||||
#### 先决条件
|
#### 先决条件
|
||||||
|
|
||||||
- **Node.js**:v22+ 和 yarn
|
- **Node.js**:v22+ 和 yarn
|
||||||
- **Rust**:最新稳定版本
|
- **Rust**:最新nightly版本
|
||||||
- **Git**:版本控制
|
- **Git**:版本控制
|
||||||
|
|
||||||
#### 设置步骤
|
#### 设置步骤
|
||||||
@@ -310,35 +304,11 @@ cd openlist-desktop
|
|||||||
# 安装 Node.js 依赖
|
# 安装 Node.js 依赖
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# 安装 Rust 依赖
|
|
||||||
cd src-tauri
|
|
||||||
cargo fetch
|
|
||||||
|
|
||||||
# 准备开发环境
|
# 准备开发环境
|
||||||
cd ..
|
|
||||||
yarn run prebuild:dev
|
yarn run prebuild:dev
|
||||||
|
|
||||||
# 启动开发服务器
|
# 启动开发服务器
|
||||||
yarn run dev
|
yarn tauri dev
|
||||||
```
|
|
||||||
|
|
||||||
#### 开发命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动带热重载的开发服务器
|
|
||||||
yarn run dev
|
|
||||||
|
|
||||||
# 启动不带文件监视的开发
|
|
||||||
yarn run nowatch
|
|
||||||
|
|
||||||
# 运行代码检查
|
|
||||||
yarn run lint
|
|
||||||
|
|
||||||
# 修复代码检查问题
|
|
||||||
yarn run lint:fix
|
|
||||||
|
|
||||||
# 类型检查
|
|
||||||
yarn run build --dry-run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 提交PR
|
#### 提交PR
|
||||||
|
|||||||
40
README_en.md
40
README_en.md
@@ -242,9 +242,10 @@ Add custom Rclone flags for optimal performance:
|
|||||||
{
|
{
|
||||||
"openlist": {
|
"openlist": {
|
||||||
"port": 5244,
|
"port": 5244,
|
||||||
"api_token": "your-secure-token",
|
"data_dir": "",
|
||||||
"auto_launch": true,
|
"auto_launch": true,
|
||||||
"ssl_enabled": false
|
"ssl_enabled": false,
|
||||||
|
"admin_password": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -283,13 +284,6 @@ Add custom Rclone flags for optimal performance:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
- `OPENLIST_API_TOKEN`: Override default API token
|
|
||||||
- `OPENLIST_PORT`: Override default port (5244)
|
|
||||||
- `RCLONE_CONFIG_DIR`: Custom Rclone configuration directory
|
|
||||||
- `LOG_LEVEL`: Set logging level (debug, info, warn, error)
|
|
||||||
|
|
||||||
## 🔧 Development
|
## 🔧 Development
|
||||||
|
|
||||||
### Development Environment Setup
|
### Development Environment Setup
|
||||||
@@ -297,7 +291,7 @@ Add custom Rclone flags for optimal performance:
|
|||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
|
|
||||||
- **Node.js**: v22+ with yarn
|
- **Node.js**: v22+ with yarn
|
||||||
- **Rust**: Latest stable version
|
- **Rust**: Latest nightly version
|
||||||
- **Git**: Version control
|
- **Git**: Version control
|
||||||
|
|
||||||
#### Setup Steps
|
#### Setup Steps
|
||||||
@@ -310,35 +304,11 @@ cd openlist-desktop
|
|||||||
# Install Node.js dependencies
|
# Install Node.js dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Install Rust dependencies
|
|
||||||
cd src-tauri
|
|
||||||
cargo fetch
|
|
||||||
|
|
||||||
# Prepare development environment
|
# Prepare development environment
|
||||||
cd ..
|
|
||||||
yarn run prebuild:dev
|
yarn run prebuild:dev
|
||||||
|
|
||||||
# Start development server
|
# Start development server
|
||||||
yarn run dev
|
yarn tauri dev
|
||||||
```
|
|
||||||
|
|
||||||
#### Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start development server with hot reload
|
|
||||||
yarn run dev
|
|
||||||
|
|
||||||
# Start development without file watching
|
|
||||||
yarn run nowatch
|
|
||||||
|
|
||||||
# Run linting
|
|
||||||
yarn run lint
|
|
||||||
|
|
||||||
# Fix linting issues
|
|
||||||
yarn run lint:fix
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
yarn run build --dry-run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"tauri"
|
"tauri"
|
||||||
],
|
],
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "OpenList Team",
|
"name": "OpenList Team",
|
||||||
"email": "96409857+Kuingsmile@users.noreply.github.com"
|
"email": "96409857+Kuingsmile@users.noreply.github.com"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"check:all": "yarn check:frontend && yarn check:rust:all",
|
"check:all": "yarn check:frontend && yarn check:rust:all",
|
||||||
"fix:rust": "cd src-tauri && cargo fmt --all && cargo clippy --all-targets --all-features --fix --allow-dirty",
|
"fix:rust": "cd src-tauri && cargo fmt --all && cargo clippy --all-targets --all-features --fix --allow-dirty",
|
||||||
"fix:frontend": "yarn lint:fix",
|
"fix:frontend": "yarn lint:fix",
|
||||||
"fix:all": "yarn fix:frontend && yarn fix:rust"
|
"fix:all": "yarn fix:frontend && yarn fix:rust && yarn i18n:check"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"lucide-vue-next": "^0.525.0",
|
"lucide-vue-next": "^0.525.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-i18n": "11.1.9",
|
"vue-i18n": "11.1.10",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { execSync } from 'node:child_process'
|
import { execSync } from 'node:child_process'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
import fsp from 'node:fs/promises'
|
import fsp from 'node:fs/promises'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ if (!getOpenlistArchMap[platformArch]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rclone version management
|
// Rclone version management
|
||||||
let rcloneVersion = 'v1.70.1'
|
let rcloneVersion = 'v1.70.3'
|
||||||
const rcloneVersionUrl = 'https://github.com/rclone/rclone/releases/latest/download/version.txt'
|
const rcloneVersionUrl = 'https://github.com/rclone/rclone/releases/latest/download/version.txt'
|
||||||
|
|
||||||
async function getLatestRcloneVersion() {
|
async function getLatestRcloneVersion() {
|
||||||
@@ -42,7 +43,7 @@ async function getLatestRcloneVersion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// openlist version management
|
// openlist version management
|
||||||
let openlistVersion = 'v4.0.3'
|
let openlistVersion = 'v4.0.8'
|
||||||
|
|
||||||
async function getLatestOpenlistVersion() {
|
async function getLatestOpenlistVersion() {
|
||||||
try {
|
try {
|
||||||
@@ -51,7 +52,7 @@ async function getLatestOpenlistVersion() {
|
|||||||
getFetchOptions()
|
getFetchOptions()
|
||||||
)
|
)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
openlistVersion = data.tag_name || 'v4.0.3'
|
openlistVersion = data.tag_name || 'v4.0.8'
|
||||||
console.log(`Latest OpenList version: ${openlistVersion}`)
|
console.log(`Latest OpenList version: ${openlistVersion}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error fetching latest OpenList version:', error.message)
|
console.log('Error fetching latest OpenList version:', error.message)
|
||||||
@@ -112,6 +113,16 @@ const resolveSimpleServicePlugin = async pluginDir => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calculateSha256 = async filePath => {
|
||||||
|
const hash = crypto.createHash('sha256')
|
||||||
|
const fileStream = fs.createReadStream(filePath)
|
||||||
|
fileStream.on('data', chunk => hash.update(chunk))
|
||||||
|
fileStream.on('end', () => {
|
||||||
|
const digest = hash.digest('hex')
|
||||||
|
console.log(`SHA-256 hash of ${filePath}: ${digest}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const resolveAccessControlPlugin = async pluginDir => {
|
const resolveAccessControlPlugin = async pluginDir => {
|
||||||
const url = 'https://nsis.sourceforge.io/mediawiki/images/4/4a/AccessControl.zip'
|
const url = 'https://nsis.sourceforge.io/mediawiki/images/4/4a/AccessControl.zip'
|
||||||
const TEMP_DIR = path.join(cwd, 'temp')
|
const TEMP_DIR = path.join(cwd, 'temp')
|
||||||
@@ -184,6 +195,7 @@ async function resolveSidecar(binInfo) {
|
|||||||
}
|
}
|
||||||
await fs.remove(zipPath)
|
await fs.remove(zipPath)
|
||||||
await fs.chmod(binaryPath, 0o755)
|
await fs.chmod(binaryPath, 0o755)
|
||||||
|
await calculateSha256(binaryPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error preparing "${name}":`, err.message)
|
console.error(`Error preparing "${name}":`, err.message)
|
||||||
await fs.rm(binaryPath, { recursive: true, force: true })
|
await fs.rm(binaryPath, { recursive: true, force: true })
|
||||||
@@ -240,12 +252,7 @@ async function main() {
|
|||||||
await retryTask('rclone', async () => {
|
await retryTask('rclone', async () => {
|
||||||
await getLatestRcloneVersion()
|
await getLatestRcloneVersion()
|
||||||
await resolveSidecar(
|
await resolveSidecar(
|
||||||
createBinaryInfo(
|
createBinaryInfo('rclone', getRcloneArchMap(rcloneVersion), `https://downloads.rclone.org`, rcloneVersion)
|
||||||
'rclone',
|
|
||||||
getRcloneArchMap(rcloneVersion),
|
|
||||||
'https://github.com/rclone/rclone/releases/download',
|
|
||||||
rcloneVersion
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if (isWin) {
|
if (isWin) {
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -2898,7 +2898,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openlist-desktop"
|
name = "openlist-desktop"
|
||||||
version = "0.3.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "openlist-desktop"
|
name = "openlist-desktop"
|
||||||
version = "0.4.0"
|
version = "0.6.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["Kuingsmile"]
|
authors = ["Kuingsmile"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ use std::path::PathBuf;
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
use crate::cmd::http_api::{get_process_list, start_process, stop_process};
|
use crate::cmd::http_api::{delete_process, get_process_list, start_process, stop_process};
|
||||||
|
use crate::cmd::openlist_core::create_openlist_core_process;
|
||||||
use crate::conf::config::MergedSettings;
|
use crate::conf::config::MergedSettings;
|
||||||
use crate::object::structs::AppState;
|
use crate::object::structs::AppState;
|
||||||
use crate::utils::path::app_config_file_path;
|
use crate::utils::path::{app_config_file_path, get_default_openlist_data_dir};
|
||||||
|
|
||||||
fn write_json_to_file<T: serde::Serialize>(path: PathBuf, value: &T) -> Result<(), String> {
|
fn write_json_to_file<T: serde::Serialize>(path: PathBuf, value: &T) -> Result<(), String> {
|
||||||
let json = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
|
let json = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
|
||||||
@@ -19,13 +20,13 @@ fn persist_app_settings(settings: &MergedSettings) -> Result<(), String> {
|
|||||||
write_json_to_file(path, settings)
|
write_json_to_file(path, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_data_config_port(port: u16) -> Result<(), String> {
|
fn update_data_config(port: u16, data_dir: Option<&str>) -> Result<(), String> {
|
||||||
let exe_dir = std::env::current_exe()
|
let data_config_path = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||||
.map_err(|e| e.to_string())?
|
PathBuf::from(dir).join("config.json")
|
||||||
.parent()
|
} else {
|
||||||
.ok_or("Failed to get exe parent dir")?
|
get_default_openlist_data_dir()?.join("config.json")
|
||||||
.to_path_buf();
|
};
|
||||||
let data_config_path = exe_dir.join("data").join("config.json");
|
|
||||||
if let Some(parent) = data_config_path.parent() {
|
if let Some(parent) = data_config_path.parent() {
|
||||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,30 @@ async fn restart_openlist_core(state: State<'_, AppState>) -> Result<(), String>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn recreate_openlist_core_process(state: State<'_, AppState>) -> Result<(), String> {
|
||||||
|
let procs = get_process_list(state.clone()).await?;
|
||||||
|
if let Some(proc) = procs
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.config.name == "single_openlist_core_process")
|
||||||
|
{
|
||||||
|
let id = proc.config.id.clone();
|
||||||
|
let _ = stop_process(id.clone(), state.clone()).await;
|
||||||
|
sleep(Duration::from_millis(1000)).await;
|
||||||
|
let _ = delete_process(id, state.clone()).await;
|
||||||
|
sleep(Duration::from_millis(1000)).await;
|
||||||
|
|
||||||
|
let auto_launch = state
|
||||||
|
.app_settings
|
||||||
|
.read()
|
||||||
|
.clone()
|
||||||
|
.map(|settings| settings.openlist.auto_launch)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
create_openlist_core_process(auto_launch, state.clone()).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn load_settings(state: State<'_, AppState>) -> Result<Option<MergedSettings>, String> {
|
pub async fn load_settings(state: State<'_, AppState>) -> Result<Option<MergedSettings>, String> {
|
||||||
state.load_settings()?;
|
state.load_settings()?;
|
||||||
@@ -85,14 +110,38 @@ pub async fn save_settings_with_update_port(
|
|||||||
settings: MergedSettings,
|
settings: MergedSettings,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
|
let old_settings = state.get_settings();
|
||||||
|
let needs_process_recreation = if let Some(old) = old_settings {
|
||||||
|
old.openlist.data_dir != settings.openlist.data_dir
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
state.update_settings(settings.clone());
|
state.update_settings(settings.clone());
|
||||||
persist_app_settings(&settings)?;
|
persist_app_settings(&settings)?;
|
||||||
update_data_config_port(settings.openlist.port)?;
|
let data_dir = if settings.openlist.data_dir.is_empty() {
|
||||||
if let Err(e) = restart_openlist_core(state.clone()).await {
|
None
|
||||||
log::error!("{e}");
|
} else {
|
||||||
return Err(e);
|
Some(settings.openlist.data_dir.as_str())
|
||||||
|
};
|
||||||
|
update_data_config(settings.openlist.port, data_dir)?;
|
||||||
|
|
||||||
|
if needs_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)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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,33 +1,137 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use tauri::State;
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
static ADMIN_PWD_REGEX: Lazy<Regex> = Lazy::new(|| {
|
use crate::object::structs::AppState;
|
||||||
Regex::new(r"Successfully created the admin user and the initial password is: (\w+)")
|
use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir, get_service_log_path};
|
||||||
.expect("Invalid regex pattern")
|
|
||||||
});
|
|
||||||
|
|
||||||
fn resolve_log_paths(source: Option<&str>) -> Result<Vec<PathBuf>, String> {
|
fn generate_random_password() -> String {
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
|
||||||
|
if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
|
duration.as_nanos().hash(&mut hasher);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::id().hash(&mut hasher);
|
||||||
|
|
||||||
|
let dummy = [1, 2, 3];
|
||||||
|
(dummy.as_ptr() as usize).hash(&mut hasher);
|
||||||
|
|
||||||
|
let hash = hasher.finish();
|
||||||
|
|
||||||
|
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let mut password = String::new();
|
||||||
|
let mut current_hash = hash;
|
||||||
|
|
||||||
|
for _ in 0..16 {
|
||||||
|
let index = (current_hash % chars.len() as u64) as usize;
|
||||||
|
password.push(chars.chars().nth(index).unwrap());
|
||||||
|
current_hash = current_hash.wrapping_mul(1103515245).wrapping_add(12345);
|
||||||
|
}
|
||||||
|
|
||||||
|
password
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_openlist_admin_set(
|
||||||
|
password: &str,
|
||||||
|
state: &State<'_, AppState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
let exe_path =
|
let exe_path =
|
||||||
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?;
|
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?;
|
||||||
let app_dir = exe_path
|
let app_dir = exe_path
|
||||||
.parent()
|
.parent()
|
||||||
.ok_or("Executable has no parent directory")?
|
.ok_or("Executable has no parent directory")?;
|
||||||
.to_path_buf();
|
|
||||||
|
let possible_names = ["openlist", "openlist.exe"];
|
||||||
|
|
||||||
|
let mut openlist_exe = None;
|
||||||
|
for name in &possible_names {
|
||||||
|
let exe_path = app_dir.join(name);
|
||||||
|
if exe_path.exists() {
|
||||||
|
openlist_exe = Some(exe_path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let openlist_exe = openlist_exe.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"OpenList executable not found. Searched for: {:?} in {}",
|
||||||
|
possible_names,
|
||||||
|
app_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Setting new admin password using: {}",
|
||||||
|
openlist_exe.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&openlist_exe);
|
||||||
|
cmd.args(["admin", "set", password]);
|
||||||
|
cmd.current_dir(app_dir);
|
||||||
|
|
||||||
|
let effective_data_dir = if let Some(settings) = state.get_settings()
|
||||||
|
&& !settings.openlist.data_dir.is_empty()
|
||||||
|
{
|
||||||
|
settings.openlist.data_dir
|
||||||
|
} else {
|
||||||
|
get_default_openlist_data_dir()
|
||||||
|
.map_err(|e| format!("Failed to get default data directory: {e}"))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
cmd.arg("--data");
|
||||||
|
cmd.arg(&effective_data_dir);
|
||||||
|
log::info!("Using data directory: {effective_data_dir}");
|
||||||
|
log::info!("Executing command: {cmd:?}");
|
||||||
|
let output = cmd
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute openlist command: {e}"))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
log::error!("OpenList admin set command failed. stdout: {stdout}, stderr: {stderr}");
|
||||||
|
return Err(format!("OpenList admin set command failed: {stderr}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
log::info!("Successfully set admin password. Output: {stdout}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
|
||||||
|
let logs_dir = get_app_logs_dir()?;
|
||||||
|
let service_path = get_service_log_path()?;
|
||||||
|
|
||||||
|
let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||||
|
PathBuf::from(dir)
|
||||||
|
} else {
|
||||||
|
get_default_openlist_data_dir()
|
||||||
|
.map_err(|e| format!("Failed to get default data directory: {e}"))?
|
||||||
|
};
|
||||||
|
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
match source {
|
match source {
|
||||||
Some("openlist") => paths.push(app_dir.join("data/log/log.log")),
|
Some("openlist") => paths.push(openlist_log_base.join("log/log.log")),
|
||||||
Some("app") => paths.push(app_dir.join("logs/app.log")),
|
Some("app") => paths.push(logs_dir.join("app.log")),
|
||||||
Some("rclone") => paths.push(app_dir.join("logs/process_rclone.log")),
|
Some("rclone") => paths.push(logs_dir.join("process_rclone.log")),
|
||||||
Some("openlist_core") => paths.push(app_dir.join("logs/process_openlist_core.log")),
|
Some("openlist_core") => paths.push(logs_dir.join("process_openlist_core.log")),
|
||||||
None => {
|
Some("service") => paths.push(service_path),
|
||||||
paths.push(app_dir.join("data/log/log.log"));
|
Some("all") => {
|
||||||
paths.push(app_dir.join("logs/app.log"));
|
paths.push(openlist_log_base.join("log/log.log"));
|
||||||
paths.push(app_dir.join("logs/process_rclone.log"));
|
paths.push(logs_dir.join("app.log"));
|
||||||
paths.push(app_dir.join("logs/process_openlist_core.log"));
|
paths.push(logs_dir.join("process_rclone.log"));
|
||||||
|
paths.push(logs_dir.join("process_openlist_core.log"));
|
||||||
|
paths.push(service_path);
|
||||||
}
|
}
|
||||||
_ => return Err("Invalid log source".into()),
|
_ => return Err("Invalid log source".into()),
|
||||||
}
|
}
|
||||||
@@ -35,34 +139,117 @@ fn resolve_log_paths(source: Option<&str>) -> Result<Vec<PathBuf>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_admin_password() -> Result<String, String> {
|
pub async fn get_admin_password(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
let paths = resolve_log_paths(Some("openlist_core"))?;
|
if let Some(settings) = state.get_settings()
|
||||||
let content =
|
&& let Some(ref stored_password) = settings.app.admin_password
|
||||||
std::fs::read_to_string(&paths[0]).map_err(|e| format!("Failed to read log file: {e}"))?;
|
&& !stored_password.is_empty()
|
||||||
|
{
|
||||||
|
log::info!("Found admin password in local settings");
|
||||||
|
return Ok(stored_password.clone());
|
||||||
|
}
|
||||||
|
|
||||||
ADMIN_PWD_REGEX
|
let new_password = generate_random_password();
|
||||||
.captures_iter(&content)
|
|
||||||
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
|
if let Err(e) = execute_openlist_admin_set(&new_password, &state).await {
|
||||||
.last()
|
return Err(format!("Failed to set new admin password: {e}"));
|
||||||
.ok_or_else(|| "No admin password found in logs".into())
|
}
|
||||||
|
|
||||||
|
log::info!("Successfully generated and set new admin password");
|
||||||
|
|
||||||
|
if let Some(mut settings) = state.get_settings() {
|
||||||
|
settings.app.admin_password = Some(new_password.clone());
|
||||||
|
state.update_settings(settings.clone());
|
||||||
|
|
||||||
|
if let Err(e) = settings.save() {
|
||||||
|
log::warn!("Failed to save new admin password to settings: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(new_password)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_logs(source: Option<String>) -> Result<Vec<String>, String> {
|
pub async fn reset_admin_password(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
let paths = resolve_log_paths(source.as_deref())?;
|
log::info!("Forcing admin password reset");
|
||||||
|
let new_password = generate_random_password();
|
||||||
|
if let Err(e) = execute_openlist_admin_set(&new_password, &state).await {
|
||||||
|
return Err(format!("Failed to set new admin password: {e}"));
|
||||||
|
}
|
||||||
|
log::info!("Successfully generated and set new admin password via force reset");
|
||||||
|
|
||||||
|
if let Some(mut settings) = state.get_settings() {
|
||||||
|
settings.app.admin_password = Some(new_password.clone());
|
||||||
|
state.update_settings(settings.clone());
|
||||||
|
|
||||||
|
if let Err(e) = settings.save() {
|
||||||
|
log::warn!("Failed to save new admin password to settings: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(new_password)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_admin_password(
|
||||||
|
password: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
log::info!("Setting custom admin password");
|
||||||
|
|
||||||
|
if let Err(e) = execute_openlist_admin_set(&password, &state).await {
|
||||||
|
return Err(format!("Failed to set admin password: {e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Successfully set custom admin password");
|
||||||
|
|
||||||
|
if let Some(mut settings) = state.get_settings() {
|
||||||
|
settings.app.admin_password = Some(password.clone());
|
||||||
|
state.update_settings(settings.clone());
|
||||||
|
|
||||||
|
if let Err(e) = settings.save() {
|
||||||
|
log::warn!("Failed to save admin password to settings: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_logs(
|
||||||
|
source: Option<String>,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let data_dir = state
|
||||||
|
.get_settings()
|
||||||
|
.map(|s| s.openlist.data_dir)
|
||||||
|
.filter(|d| !d.is_empty());
|
||||||
|
|
||||||
|
let paths = resolve_log_paths(source.as_deref(), data_dir.as_deref())?;
|
||||||
let mut logs = Vec::new();
|
let mut logs = Vec::new();
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let content =
|
if path.exists() {
|
||||||
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {path:?}: {e}"))?;
|
let content = std::fs::read_to_string(&path)
|
||||||
logs.extend(content.lines().map(str::to_string));
|
.map_err(|e| format!("Failed to read {path:?}: {e}"))?;
|
||||||
|
logs.extend(content.lines().map(str::to_string));
|
||||||
|
} else {
|
||||||
|
log::info!("Log file does not exist: {path:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(logs)
|
Ok(logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn clear_logs(source: Option<String>) -> Result<bool, String> {
|
pub async fn clear_logs(
|
||||||
let paths = resolve_log_paths(source.as_deref())?;
|
source: Option<String>,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let data_dir = state
|
||||||
|
.get_settings()
|
||||||
|
.map(|s| s.openlist.data_dir)
|
||||||
|
.filter(|d| !d.is_empty());
|
||||||
|
|
||||||
|
let paths = resolve_log_paths(source.as_deref(), data_dir.as_deref())?;
|
||||||
let mut cleared_count = 0;
|
let mut cleared_count = 0;
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod binary;
|
pub mod binary;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod custom_updater;
|
pub mod custom_updater;
|
||||||
|
pub mod firewall;
|
||||||
pub mod http_api;
|
pub mod http_api;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod openlist_core;
|
pub mod openlist_core;
|
||||||
|
|||||||
@@ -4,13 +4,22 @@ use url::Url;
|
|||||||
|
|
||||||
use crate::object::structs::{AppState, ServiceStatus};
|
use crate::object::structs::{AppState, ServiceStatus};
|
||||||
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
||||||
use crate::utils::path::{get_app_logs_dir, get_openlist_binary_path};
|
use crate::utils::path::{
|
||||||
|
get_app_logs_dir, get_default_openlist_data_dir, get_openlist_binary_path,
|
||||||
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_openlist_core_process(
|
pub async fn create_openlist_core_process(
|
||||||
auto_start: bool,
|
auto_start: bool,
|
||||||
_state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<ProcessConfig, String> {
|
) -> Result<ProcessConfig, String> {
|
||||||
|
let data_dir = state
|
||||||
|
.app_settings
|
||||||
|
.read()
|
||||||
|
.clone()
|
||||||
|
.ok_or("Failed to read app settings")?
|
||||||
|
.openlist
|
||||||
|
.data_dir;
|
||||||
let binary_path = get_openlist_binary_path()
|
let binary_path = get_openlist_binary_path()
|
||||||
.map_err(|e| format!("Failed to get OpenList binary path: {e}"))?;
|
.map_err(|e| format!("Failed to get OpenList binary path: {e}"))?;
|
||||||
let log_file_path =
|
let log_file_path =
|
||||||
@@ -19,12 +28,25 @@ pub async fn create_openlist_core_process(
|
|||||||
|
|
||||||
let api_key = get_api_key();
|
let api_key = get_api_key();
|
||||||
let port = get_server_port();
|
let port = get_server_port();
|
||||||
|
let mut args = vec!["server".into()];
|
||||||
|
|
||||||
|
// Use custom data dir if set, otherwise use platform-specific default
|
||||||
|
let effective_data_dir = if !data_dir.is_empty() {
|
||||||
|
data_dir
|
||||||
|
} else {
|
||||||
|
get_default_openlist_data_dir()
|
||||||
|
.map_err(|e| format!("Failed to get default data directory: {e}"))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
args.push("--data".into());
|
||||||
|
args.push(effective_data_dir);
|
||||||
let config = ProcessConfig {
|
let config = ProcessConfig {
|
||||||
id: "openlist_core".into(),
|
id: "openlist_core".into(),
|
||||||
name: "single_openlist_core_process".into(),
|
name: "single_openlist_core_process".into(),
|
||||||
bin_path: binary_path.to_string_lossy().into_owned(),
|
bin_path: binary_path.to_string_lossy().into_owned(),
|
||||||
args: vec!["server".into()],
|
args,
|
||||||
log_file: log_file_path.to_string_lossy().into_owned(),
|
log_file: log_file_path.to_string_lossy().into_owned(),
|
||||||
working_dir: binary_path
|
working_dir: binary_path
|
||||||
.parent()
|
.parent()
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use tauri::{AppHandle, State};
|
|||||||
use crate::cmd::http_api::{get_process_list, start_process, stop_process};
|
use crate::cmd::http_api::{get_process_list, start_process, stop_process};
|
||||||
use crate::object::structs::{AppState, FileItem};
|
use crate::object::structs::{AppState, FileItem};
|
||||||
use crate::utils::github_proxy::apply_github_proxy;
|
use crate::utils::github_proxy::apply_github_proxy;
|
||||||
use crate::utils::path::{get_openlist_binary_path, get_rclone_binary_path};
|
use crate::utils::path::{
|
||||||
|
app_config_file_path, get_app_logs_dir, get_default_openlist_data_dir,
|
||||||
|
get_openlist_binary_path, get_rclone_binary_path, get_rclone_config_path,
|
||||||
|
};
|
||||||
|
|
||||||
fn normalize_path(path: &str) -> String {
|
fn normalize_path(path: &str) -> String {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -615,3 +618,45 @@ fn extract_tar_gz(
|
|||||||
executable_path
|
executable_path
|
||||||
.ok_or_else(|| format!("Executable '{executable_name}' not found in tar.gz archive"))
|
.ok_or_else(|| format!("Executable '{executable_name}' not found in tar.gz archive"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_logs_directory() -> Result<bool, String> {
|
||||||
|
let logs_dir = get_app_logs_dir()?;
|
||||||
|
if !logs_dir.exists() {
|
||||||
|
fs::create_dir_all(&logs_dir)
|
||||||
|
.map_err(|e| format!("Failed to create logs directory: {e}"))?;
|
||||||
|
}
|
||||||
|
open::that(logs_dir.as_os_str()).map_err(|e| e.to_string())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_openlist_data_dir() -> Result<bool, String> {
|
||||||
|
let config_path = get_default_openlist_data_dir()?;
|
||||||
|
if !config_path.exists() {
|
||||||
|
fs::create_dir_all(&config_path)
|
||||||
|
.map_err(|e| format!("Failed to create config directory: {e}"))?;
|
||||||
|
}
|
||||||
|
open::that(config_path.as_os_str()).map_err(|e| e.to_string())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_rclone_config_file() -> Result<bool, String> {
|
||||||
|
let config_path = get_rclone_config_path()?;
|
||||||
|
if !config_path.exists() {
|
||||||
|
fs::File::create(&config_path).map_err(|e| format!("Failed to create config file: {e}"))?;
|
||||||
|
}
|
||||||
|
open::that_detached(config_path.as_os_str()).map_err(|e| e.to_string())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_settings_file() -> Result<bool, String> {
|
||||||
|
let settings_path = app_config_file_path()?;
|
||||||
|
if !settings_path.exists() {
|
||||||
|
return Err("Settings file does not exist".to_string());
|
||||||
|
}
|
||||||
|
open::that_detached(settings_path.as_os_str()).map_err(|e| e.to_string())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use tauri::State;
|
|||||||
use crate::cmd::http_api::{get_process_list, start_process};
|
use crate::cmd::http_api::{get_process_list, start_process};
|
||||||
use crate::object::structs::AppState;
|
use crate::object::structs::AppState;
|
||||||
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
||||||
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path};
|
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path, get_rclone_config_path};
|
||||||
|
|
||||||
// use 45572 due to the reserved port on Windows
|
// use 45572 due to the reserved port on Windows
|
||||||
pub const RCLONE_API_BASE: &str = "http://127.0.0.1:45572";
|
pub const RCLONE_API_BASE: &str = "http://127.0.0.1:45572";
|
||||||
@@ -40,10 +40,8 @@ pub async fn create_rclone_backend_process(
|
|||||||
get_rclone_binary_path().map_err(|e| format!("Failed to get rclone binary path: {e}"))?;
|
get_rclone_binary_path().map_err(|e| format!("Failed to get rclone binary path: {e}"))?;
|
||||||
let log_file_path =
|
let log_file_path =
|
||||||
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?;
|
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?;
|
||||||
let rclone_conf_path = binary_path
|
let rclone_conf_path =
|
||||||
.parent()
|
get_rclone_config_path().map_err(|e| format!("Failed to get rclone config path: {e}"))?;
|
||||||
.map(|p| p.join("rclone.conf"))
|
|
||||||
.ok_or_else(|| "Failed to determine rclone.conf path".to_string())?;
|
|
||||||
let log_file_path = log_file_path.join("process_rclone.log");
|
let log_file_path = log_file_path.join("process_rclone.log");
|
||||||
let api_key = get_api_key();
|
let api_key = get_api_key();
|
||||||
let port = get_server_port();
|
let port = get_server_port();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::object::structs::{
|
|||||||
};
|
};
|
||||||
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
use crate::utils::api::{CreateProcessResponse, ProcessConfig, get_api_key, get_server_port};
|
||||||
use crate::utils::args::split_args_vec;
|
use crate::utils::args::split_args_vec;
|
||||||
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path};
|
use crate::utils::path::{get_app_logs_dir, get_rclone_binary_path, get_rclone_config_path};
|
||||||
|
|
||||||
struct RcloneApi {
|
struct RcloneApi {
|
||||||
client: Client,
|
client: Client,
|
||||||
@@ -189,10 +189,8 @@ pub async fn create_rclone_mount_remote_process(
|
|||||||
let log_file_path =
|
let log_file_path =
|
||||||
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?;
|
get_app_logs_dir().map_err(|e| format!("Failed to get app logs directory: {e}"))?;
|
||||||
let log_file_path = log_file_path.join("process_rclone.log");
|
let log_file_path = log_file_path.join("process_rclone.log");
|
||||||
let rclone_conf_path = binary_path
|
let rclone_conf_path =
|
||||||
.parent()
|
get_rclone_config_path().map_err(|e| format!("Failed to get rclone config path: {e}"))?;
|
||||||
.map(|p| p.join("rclone.conf"))
|
|
||||||
.ok_or_else(|| "Failed to determine rclone.conf path".to_string())?;
|
|
||||||
|
|
||||||
let api_key = get_api_key();
|
let api_key = get_api_key();
|
||||||
let port = get_server_port();
|
let port = get_server_port();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub struct AppConfig {
|
|||||||
pub gh_proxy: Option<String>,
|
pub gh_proxy: Option<String>,
|
||||||
pub gh_proxy_api: Option<bool>,
|
pub gh_proxy_api: Option<bool>,
|
||||||
pub open_links_in_browser: Option<bool>,
|
pub open_links_in_browser: Option<bool>,
|
||||||
|
pub admin_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
@@ -17,6 +18,7 @@ impl AppConfig {
|
|||||||
gh_proxy: None,
|
gh_proxy: None,
|
||||||
gh_proxy_api: Some(false),
|
gh_proxy_api: Some(false),
|
||||||
open_links_in_browser: Some(false),
|
open_links_in_browser: Some(false),
|
||||||
|
admin_password: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use super::app::AppConfig;
|
use super::app::AppConfig;
|
||||||
use crate::conf::core::OpenListCoreConfig;
|
use crate::conf::core::OpenListCoreConfig;
|
||||||
use crate::conf::rclone::RcloneConfig;
|
use crate::conf::rclone::RcloneConfig;
|
||||||
use crate::utils::path::app_config_file_path;
|
use crate::utils::path::{app_config_file_path, get_default_openlist_data_dir};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct MergedSettings {
|
pub struct MergedSettings {
|
||||||
@@ -29,23 +29,22 @@ impl MergedSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_data_config_path() -> Result<PathBuf, String> {
|
pub fn get_data_config_path_for_dir(data_dir: Option<&str>) -> Result<PathBuf, String> {
|
||||||
let exe =
|
if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||||
std::env::current_exe().map_err(|e| format!("Failed to get current exe path: {e}"))?;
|
Ok(PathBuf::from(dir).join("config.json"))
|
||||||
let dir = exe
|
} else {
|
||||||
.parent()
|
Ok(get_default_openlist_data_dir()?.join("config.json"))
|
||||||
.ok_or_else(|| "Failed to get executable parent directory".to_string())?;
|
}
|
||||||
Ok(dir.join("data").join("config.json"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_data_config() -> Result<serde_json::Value, String> {
|
pub fn read_data_config_for_dir(data_dir: Option<&str>) -> Result<serde_json::Value, String> {
|
||||||
let path = Self::get_data_config_path()?;
|
let path = Self::get_data_config_path_for_dir(data_dir)?;
|
||||||
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
|
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||||
serde_json::from_str(&content).map_err(|e| e.to_string())
|
serde_json::from_str(&content).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_port_from_data_config() -> Result<Option<u16>, String> {
|
fn get_port_from_data_config_for_dir(data_dir: Option<&str>) -> Result<Option<u16>, String> {
|
||||||
let config = Self::read_data_config()?;
|
let config = Self::read_data_config_for_dir(data_dir)?;
|
||||||
Ok(config
|
Ok(config
|
||||||
.get("scheme")
|
.get("scheme")
|
||||||
.and_then(|s| s.get("http_port"))
|
.and_then(|s| s.get("http_port"))
|
||||||
@@ -77,7 +76,13 @@ impl MergedSettings {
|
|||||||
default
|
default
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(Some(port)) = Self::get_port_from_data_config()
|
let data_dir = if settings.openlist.data_dir.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(settings.openlist.data_dir.as_str())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(Some(port)) = Self::get_port_from_data_config_for_dir(data_dir)
|
||||||
&& settings.openlist.port != port
|
&& settings.openlist.port != port
|
||||||
{
|
{
|
||||||
settings.openlist.port = port;
|
settings.openlist.port = port;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct OpenListCoreConfig {
|
pub struct OpenListCoreConfig {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub api_token: String,
|
pub data_dir: String,
|
||||||
pub auto_launch: bool,
|
pub auto_launch: bool,
|
||||||
pub ssl_enabled: bool,
|
pub ssl_enabled: bool,
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ impl OpenListCoreConfig {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
port: 5244,
|
port: 5244,
|
||||||
api_token: "".to_string(),
|
data_dir: "".to_string(),
|
||||||
auto_launch: false,
|
auto_launch: false,
|
||||||
ssl_enabled: false,
|
ssl_enabled: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ pub struct RcloneCreateRemoteRequest {
|
|||||||
pub parameters: RcloneWebdavConfig,
|
pub parameters: RcloneWebdavConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct RcloneRemoteParameters {
|
|
||||||
pub url: String,
|
|
||||||
pub vendor: Option<String>,
|
|
||||||
pub user: String,
|
|
||||||
pub pass: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct RcloneMountRequest {
|
pub struct RcloneMountRequest {
|
||||||
pub fs: String,
|
pub fs: String,
|
||||||
@@ -49,13 +41,6 @@ pub struct RcloneMountOptions {
|
|||||||
pub volume_name: Option<String>,
|
pub volume_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RcloneApiResponse<T> {
|
|
||||||
pub success: bool,
|
|
||||||
pub data: Option<T>,
|
|
||||||
pub error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RcloneConfig {
|
impl RcloneConfig {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -732,28 +732,27 @@ pub async fn start_service() -> Result<bool, Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
|
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
|
||||||
log::info!("Extracted PID value: {pid_value}");
|
log::info!("Extracted PID value: {pid_value}");
|
||||||
if let Ok(pid) = pid_value.parse::<i32>() {
|
if let Ok(pid) = pid_value.parse::<i32>()
|
||||||
if pid > 0 {
|
&& pid > 0
|
||||||
log::info!("Service is running with PID: {pid}");
|
{
|
||||||
return Ok(true);
|
log::info!("Service is running with PID: {pid}");
|
||||||
}
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus") {
|
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus")
|
||||||
if let Ok(status) = exit_status.parse::<i32>() {
|
&& let Ok(status) = exit_status.parse::<i32>()
|
||||||
if status == 0 {
|
{
|
||||||
log::info!(
|
if status == 0 {
|
||||||
"Service is loaded but not running (clean exit), attempting to \
|
log::info!(
|
||||||
start"
|
"Service is loaded but not running (clean exit), attempting to start"
|
||||||
);
|
);
|
||||||
return start_macos_service(SERVICE_IDENTIFIER).await;
|
return start_macos_service(SERVICE_IDENTIFIER).await;
|
||||||
} else {
|
} else {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Service has non-zero exit status: {status}, attempting to restart"
|
"Service has non-zero exit status: {status}, attempting to restart"
|
||||||
);
|
);
|
||||||
return start_macos_service(SERVICE_IDENTIFIER).await;
|
return start_macos_service(SERVICE_IDENTIFIER).await;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,23 +794,23 @@ pub async fn check_service_status() -> Result<String, Box<dyn std::error::Error>
|
|||||||
|
|
||||||
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
|
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
|
||||||
log::info!("Extracted PID value: {pid_value}");
|
log::info!("Extracted PID value: {pid_value}");
|
||||||
if let Ok(pid) = pid_value.parse::<i32>() {
|
if let Ok(pid) = pid_value.parse::<i32>()
|
||||||
if pid > 0 {
|
&& pid > 0
|
||||||
log::info!("Service is running with PID: {pid}");
|
{
|
||||||
return Ok("running".to_string());
|
log::info!("Service is running with PID: {pid}");
|
||||||
}
|
return Ok("running".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus") {
|
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus")
|
||||||
if let Ok(status) = exit_status.parse::<i32>() {
|
&& let Ok(status) = exit_status.parse::<i32>()
|
||||||
if status == 0 {
|
{
|
||||||
log::info!("Service is loaded but not running (clean exit)");
|
if status == 0 {
|
||||||
return Ok("stopped".to_string());
|
log::info!("Service is loaded but not running (clean exit)");
|
||||||
} else {
|
return Ok("stopped".to_string());
|
||||||
log::warn!("Service has non-zero exit status: {status}");
|
} else {
|
||||||
return Ok("stopped".to_string());
|
log::warn!("Service has non-zero exit status: {status}");
|
||||||
}
|
return Ok("stopped".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,15 +854,15 @@ async fn start_macos_service(service_identifier: &str) -> Result<bool, Box<dyn s
|
|||||||
let output_str = String::from_utf8_lossy(&verify_output.stdout);
|
let output_str = String::from_utf8_lossy(&verify_output.stdout);
|
||||||
log::info!("Verification output: {output_str}");
|
log::info!("Verification output: {output_str}");
|
||||||
|
|
||||||
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
|
if let Some(pid_value) = extract_plist_value(&output_str, "PID")
|
||||||
if let Ok(pid) = pid_value.parse::<i32>() {
|
&& let Ok(pid) = pid_value.parse::<i32>()
|
||||||
if pid > 0 {
|
{
|
||||||
log::info!("Service verified as running with PID: {pid}");
|
if pid > 0 {
|
||||||
return Ok(true);
|
log::info!("Service verified as running with PID: {pid}");
|
||||||
} else {
|
return Ok(true);
|
||||||
log::warn!("Service has invalid PID: {pid}");
|
} else {
|
||||||
return Ok(false);
|
log::warn!("Service has invalid PID: {pid}");
|
||||||
}
|
return Ok(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,19 +886,19 @@ fn extract_plist_value(plist_output: &str, key: &str) -> Option<String> {
|
|||||||
|
|
||||||
for line in plist_output.lines() {
|
for line in plist_output.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if trimmed.starts_with(&pattern) {
|
if trimmed.starts_with(&pattern)
|
||||||
if let Some(equals_pos) = trimmed.find('=') {
|
&& let Some(equals_pos) = trimmed.find('=')
|
||||||
let value_part = &trimmed[equals_pos + 1..];
|
{
|
||||||
let value_trimmed = value_part.trim();
|
let value_part = &trimmed[equals_pos + 1..];
|
||||||
|
let value_trimmed = value_part.trim();
|
||||||
|
|
||||||
let value_clean = if let Some(stripped) = value_trimmed.strip_suffix(';') {
|
let value_clean = if let Some(stripped) = value_trimmed.strip_suffix(';') {
|
||||||
stripped
|
stripped
|
||||||
} else {
|
} else {
|
||||||
value_trimmed
|
value_trimmed
|
||||||
};
|
};
|
||||||
|
|
||||||
return Some(value_clean.trim().to_string());
|
return Some(value_clean.trim().to_string());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,18 @@ use cmd::custom_updater::{
|
|||||||
check_for_updates, download_update, get_current_version, install_update_and_restart,
|
check_for_updates, download_update, get_current_version, install_update_and_restart,
|
||||||
is_auto_check_enabled, set_auto_check_enabled,
|
is_auto_check_enabled, set_auto_check_enabled,
|
||||||
};
|
};
|
||||||
|
use cmd::firewall::{add_firewall_rule, check_firewall_rule, remove_firewall_rule};
|
||||||
use cmd::http_api::{
|
use cmd::http_api::{
|
||||||
delete_process, get_process_list, restart_process, start_process, stop_process, update_process,
|
delete_process, get_process_list, restart_process, start_process, stop_process, update_process,
|
||||||
};
|
};
|
||||||
use cmd::logs::{clear_logs, get_admin_password, get_logs};
|
use cmd::logs::{
|
||||||
|
clear_logs, get_admin_password, get_logs, reset_admin_password, set_admin_password,
|
||||||
|
};
|
||||||
use cmd::openlist_core::{create_openlist_core_process, get_openlist_core_status};
|
use cmd::openlist_core::{create_openlist_core_process, get_openlist_core_status};
|
||||||
use cmd::os_operate::{
|
use cmd::os_operate::{
|
||||||
get_available_versions, list_files, open_file, open_folder, open_url, open_url_in_browser,
|
get_available_versions, list_files, open_file, open_folder, open_logs_directory,
|
||||||
select_directory, update_tool_version,
|
open_openlist_data_dir, open_rclone_config_file, open_settings_file, open_url,
|
||||||
|
open_url_in_browser, select_directory, update_tool_version,
|
||||||
};
|
};
|
||||||
use cmd::rclone_core::{
|
use cmd::rclone_core::{
|
||||||
create_and_start_rclone_backend, create_rclone_backend_process, get_rclone_backend_status,
|
create_and_start_rclone_backend, create_rclone_backend_process, get_rclone_backend_status,
|
||||||
@@ -138,6 +142,10 @@ pub fn run() {
|
|||||||
list_files,
|
list_files,
|
||||||
open_file,
|
open_file,
|
||||||
open_folder,
|
open_folder,
|
||||||
|
open_logs_directory,
|
||||||
|
open_openlist_data_dir,
|
||||||
|
open_rclone_config_file,
|
||||||
|
open_settings_file,
|
||||||
open_url,
|
open_url,
|
||||||
open_url_in_browser,
|
open_url_in_browser,
|
||||||
save_settings,
|
save_settings,
|
||||||
@@ -147,6 +155,8 @@ pub fn run() {
|
|||||||
get_logs,
|
get_logs,
|
||||||
clear_logs,
|
clear_logs,
|
||||||
get_admin_password,
|
get_admin_password,
|
||||||
|
reset_admin_password,
|
||||||
|
set_admin_password,
|
||||||
get_binary_version,
|
get_binary_version,
|
||||||
select_directory,
|
select_directory,
|
||||||
get_available_versions,
|
get_available_versions,
|
||||||
@@ -159,6 +169,9 @@ pub fn run() {
|
|||||||
check_service_status,
|
check_service_status,
|
||||||
stop_service,
|
stop_service,
|
||||||
start_service,
|
start_service,
|
||||||
|
check_firewall_rule,
|
||||||
|
add_firewall_rule,
|
||||||
|
remove_firewall_rule,
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
download_update,
|
download_update,
|
||||||
install_update_and_restart,
|
install_update_and_restart,
|
||||||
|
|||||||
@@ -31,13 +31,6 @@ pub struct RcloneMountInfo {
|
|||||||
pub status: String,
|
pub status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct TransferStats {
|
|
||||||
pub read: u64,
|
|
||||||
pub write: u64,
|
|
||||||
pub errors: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct RcloneRemoteListResponse {
|
pub struct RcloneRemoteListResponse {
|
||||||
pub remotes: Vec<String>,
|
pub remotes: Vec<String>,
|
||||||
|
|||||||
@@ -16,6 +16,46 @@ fn get_app_dir() -> Result<PathBuf, String> {
|
|||||||
Ok(app_dir)
|
Ok(app_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_user_data_dir() -> Result<PathBuf, String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||||
|
let data_dir = PathBuf::from(home)
|
||||||
|
.join("Library")
|
||||||
|
.join("Application Support")
|
||||||
|
.join("OpenList Desktop");
|
||||||
|
fs::create_dir_all(&data_dir)
|
||||||
|
.map_err(|e| format!("Failed to create data directory: {e}"))?;
|
||||||
|
Ok(data_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
get_app_dir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_user_logs_dir() -> Result<PathBuf, String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||||
|
let logs_dir = PathBuf::from(home)
|
||||||
|
.join("Library")
|
||||||
|
.join("Logs")
|
||||||
|
.join("OpenList Desktop");
|
||||||
|
fs::create_dir_all(&logs_dir)
|
||||||
|
.map_err(|e| format!("Failed to create logs directory: {e}"))?;
|
||||||
|
Ok(logs_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
let logs = get_app_dir()?.join("logs");
|
||||||
|
fs::create_dir_all(&logs).map_err(|e| e.to_string())?;
|
||||||
|
Ok(logs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_binary_path(binary: &str, service_name: &str) -> Result<PathBuf, String> {
|
fn get_binary_path(binary: &str, service_name: &str) -> Result<PathBuf, String> {
|
||||||
let mut name = binary.to_string();
|
let mut name = binary.to_string();
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
@@ -40,7 +80,7 @@ pub fn get_rclone_binary_path() -> Result<PathBuf, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_app_config_dir() -> Result<PathBuf, String> {
|
pub fn get_app_config_dir() -> Result<PathBuf, String> {
|
||||||
get_app_dir()
|
get_user_data_dir()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_config_file_path() -> Result<PathBuf, String> {
|
pub fn app_config_file_path() -> Result<PathBuf, String> {
|
||||||
@@ -48,7 +88,41 @@ pub fn app_config_file_path() -> Result<PathBuf, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_app_logs_dir() -> Result<PathBuf, String> {
|
pub fn get_app_logs_dir() -> Result<PathBuf, String> {
|
||||||
let logs = get_app_dir()?.join("logs");
|
get_user_logs_dir()
|
||||||
fs::create_dir_all(&logs).map_err(|e| e.to_string())?;
|
}
|
||||||
Ok(logs)
|
|
||||||
|
pub fn get_rclone_config_path() -> Result<PathBuf, String> {
|
||||||
|
Ok(get_user_data_dir()?.join("rclone.conf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_openlist_data_dir() -> Result<PathBuf, String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Ok(get_user_data_dir()?.join("data"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
Ok(get_app_dir()?.join("data"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_service_log_path() -> Result<PathBuf, String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||||
|
let logs = PathBuf::from(home)
|
||||||
|
.join("Library")
|
||||||
|
.join("Application Support")
|
||||||
|
.join("io.github.openlistteam.openlist.service.bundle")
|
||||||
|
.join("Contents")
|
||||||
|
.join("MacOS")
|
||||||
|
.join("openlist-desktop-service.log");
|
||||||
|
Ok(logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
Ok(get_app_dir()?.join("openlist-desktop-service.log"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "OpenList Desktop",
|
"productName": "OpenList Desktop",
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"identifier": "io.github.openlistteam.openlist.desktop",
|
"identifier": "io.github.openlistteam.openlist.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "yarn run dev",
|
"beforeDevCommand": "yarn run dev",
|
||||||
|
|||||||
@@ -60,8 +60,12 @@ export class TauriAPI {
|
|||||||
list: (path: string): Promise<FileItem[]> => call('list_files', { path }),
|
list: (path: string): Promise<FileItem[]> => call('list_files', { path }),
|
||||||
open: (path: string): Promise<boolean> => call('open_file', { path }),
|
open: (path: string): Promise<boolean> => call('open_file', { path }),
|
||||||
folder: (path: string): Promise<boolean> => call('open_folder', { path }),
|
folder: (path: string): Promise<boolean> => call('open_folder', { path }),
|
||||||
url: (path: string): Promise<boolean> => call('open_url', { path }),
|
url: (url: string): Promise<boolean> => call('open_url', { url }),
|
||||||
urlInBrowser: (url: string): Promise<boolean> => call('open_url_in_browser', { url })
|
urlInBrowser: (url: string): Promise<boolean> => call('open_url_in_browser', { url }),
|
||||||
|
openOpenListDataDir: (): Promise<boolean> => call('open_openlist_data_dir'),
|
||||||
|
openLogsDirectory: (): Promise<boolean> => call('open_logs_directory'),
|
||||||
|
openRcloneConfigFile: (): Promise<boolean> => call('open_rclone_config_file'),
|
||||||
|
openSettingsFile: (): Promise<boolean> => call('open_settings_file')
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Settings management ---
|
// --- Settings management ---
|
||||||
@@ -75,11 +79,13 @@ export class TauriAPI {
|
|||||||
|
|
||||||
// --- Logs management ---
|
// --- Logs management ---
|
||||||
static logs = {
|
static logs = {
|
||||||
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<string[]> =>
|
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<string[]> =>
|
||||||
call('get_logs', { source: src }),
|
call('get_logs', { source: src }),
|
||||||
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<boolean> =>
|
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<boolean> =>
|
||||||
call('clear_logs', { source: src }),
|
call('clear_logs', { source: src }),
|
||||||
adminPassword: (): Promise<string> => call('get_admin_password')
|
adminPassword: (): Promise<string> => call('get_admin_password'),
|
||||||
|
resetAdminPassword: (): Promise<string> => call('reset_admin_password'),
|
||||||
|
setAdminPassword: (password: string): Promise<string> => call('set_admin_password', { password })
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Binary management ---
|
// --- Binary management ---
|
||||||
@@ -104,6 +110,13 @@ export class TauriAPI {
|
|||||||
listen: (cb: (action: string) => void) => listen('tray-core-action', e => cb(e.payload as string))
|
listen: (cb: (action: string) => void) => listen('tray-core-action', e => cb(e.payload as string))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Firewall management ---
|
||||||
|
static firewall = {
|
||||||
|
check: (): Promise<boolean> => call('check_firewall_rule'),
|
||||||
|
add: (): Promise<boolean> => call('add_firewall_rule'),
|
||||||
|
remove: (): Promise<boolean> => call('remove_firewall_rule')
|
||||||
|
}
|
||||||
|
|
||||||
// --- Update management ---
|
// --- Update management ---
|
||||||
static updater = {
|
static updater = {
|
||||||
check: (): Promise<UpdateCheck> => call('check_for_updates'),
|
check: (): Promise<UpdateCheck> => call('check_for_updates'),
|
||||||
|
|||||||
@@ -27,11 +27,16 @@ const navigationItems = computed(() => [
|
|||||||
|
|
||||||
const openLink = async (url: string) => {
|
const openLink = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url)
|
if (appStore.settings.app.open_links_in_browser) {
|
||||||
|
await TauriAPI.files.urlInBrowser(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open link:', error)
|
console.error('Failed to open link:', error)
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -223,12 +223,13 @@ const updateChartSize = () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updateChartSize()
|
updateChartSize()
|
||||||
|
await appStore.refreshOpenListCoreStatus()
|
||||||
|
|
||||||
if (isCoreRunning.value) {
|
if (isCoreRunning.value) {
|
||||||
startTime.value = Date.now()
|
startTime.value = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
monitoringInterval.value = window.setInterval(checkCoreHealth, 30 * 1000)
|
monitoringInterval.value = window.setInterval(checkCoreHealth, 15 * 1000)
|
||||||
window.addEventListener('resize', updateChartSize)
|
window.addEventListener('resize', updateChartSize)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -102,11 +102,16 @@ const openRcloneGitHub = () => {
|
|||||||
|
|
||||||
const openLink = async (url: string) => {
|
const openLink = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url)
|
if (appStore.settings.app.open_links_in_browser) {
|
||||||
|
await TauriAPI.files.urlInBrowser(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open link:', error)
|
console.error('Failed to open link:', error)
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,34 @@
|
|||||||
<div class="action-section">
|
<div class="action-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h4>{{ t('dashboard.quickActions.openlistService') }}</h4>
|
<h4>{{ t('dashboard.quickActions.openlistService') }}</h4>
|
||||||
|
<div v-if="isCoreLoading" class="section-loading-indicator">
|
||||||
|
<Loader :size="12" class="loading-icon" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button @click="toggleCore" :class="['action-btn', 'service-btn', { running: isCoreRunning }]">
|
<button
|
||||||
<component :is="serviceButtonIcon" :size="20" />
|
@click="toggleCore"
|
||||||
<span>{{ serviceButtonText }}</span>
|
:disabled="isCoreLoading"
|
||||||
|
:class="['action-btn', 'service-btn', { running: isCoreRunning, loading: isCoreLoading }]"
|
||||||
|
>
|
||||||
|
<component v-if="!isCoreLoading" :is="serviceButtonIcon" :size="20" />
|
||||||
|
<Loader v-else :size="20" class="loading-icon" />
|
||||||
|
<span>{{ isCoreLoading ? t('dashboard.quickActions.processing') : serviceButtonText }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="restartCore" :disabled="!isCoreRunning" class="action-btn restart-btn">
|
<button
|
||||||
<RotateCcw :size="18" />
|
@click="restartCore"
|
||||||
|
:disabled="!isCoreRunning || isCoreLoading"
|
||||||
|
:class="['action-btn', 'restart-btn', { loading: isCoreLoading }]"
|
||||||
|
>
|
||||||
|
<RotateCcw v-if="!isCoreLoading" :size="18" />
|
||||||
|
<Loader v-else :size="18" class="loading-icon" />
|
||||||
<span>{{ t('dashboard.quickActions.restart') }}</span>
|
<span>{{ t('dashboard.quickActions.restart') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="openWebUI"
|
@click="openWebUI"
|
||||||
:disabled="!isCoreRunning"
|
:disabled="!isCoreRunning || isCoreLoading"
|
||||||
class="action-btn web-btn"
|
class="action-btn web-btn"
|
||||||
:title="appStore.openListCoreUrl"
|
:title="appStore.openListCoreUrl"
|
||||||
>
|
>
|
||||||
@@ -27,27 +40,46 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="showAdminPassword"
|
@click="copyAdminPassword"
|
||||||
class="action-btn password-btn icon-only-btn"
|
class="action-btn password-btn icon-only-btn"
|
||||||
:title="t('dashboard.quickActions.showAdminPassword')"
|
:title="t('dashboard.quickActions.copyAdminPassword')"
|
||||||
>
|
>
|
||||||
<Key :size="16" />
|
<Key :size="16" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="resetAdminPassword"
|
||||||
|
class="action-btn reset-password-btn icon-only-btn"
|
||||||
|
:title="t('dashboard.quickActions.resetAdminPassword')"
|
||||||
|
>
|
||||||
|
<RotateCcw :size="16" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-section">
|
<div class="action-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h4>{{ t('dashboard.quickActions.rclone') }}</h4>
|
<h4>{{ t('dashboard.quickActions.rclone') }}</h4>
|
||||||
|
<div v-if="isRcloneLoading" class="section-loading-indicator">
|
||||||
|
<Loader :size="12" class="loading-icon" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button
|
<button
|
||||||
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
|
@click="rcloneStore.serviceRunning ? stopBackend() : startBackend()"
|
||||||
:class="['action-btn', 'service-indicator-btn', { active: rcloneStore.serviceRunning }]"
|
:disabled="isRcloneLoading"
|
||||||
|
:class="[
|
||||||
|
'action-btn',
|
||||||
|
'service-indicator-btn',
|
||||||
|
{ active: rcloneStore.serviceRunning, loading: isRcloneLoading }
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<component :is="rcloneStore.serviceRunning ? Square : Play" :size="18" />
|
<component v-if="!isRcloneLoading" :is="rcloneStore.serviceRunning ? Square : Play" :size="18" />
|
||||||
|
<Loader v-else :size="18" class="loading-icon" />
|
||||||
<span>{{
|
<span>{{
|
||||||
rcloneStore.serviceRunning
|
isRcloneLoading
|
||||||
|
? t('dashboard.quickActions.processing')
|
||||||
|
: rcloneStore.serviceRunning
|
||||||
? t('dashboard.quickActions.stopRclone')
|
? t('dashboard.quickActions.stopRclone')
|
||||||
: t('dashboard.quickActions.startRclone')
|
: t('dashboard.quickActions.startRclone')
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -75,6 +107,28 @@
|
|||||||
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" />
|
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" />
|
||||||
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
|
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- Windows Firewall Management-->
|
||||||
|
<button
|
||||||
|
v-if="isWindows"
|
||||||
|
@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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,13 +136,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '../../stores/app'
|
import { useAppStore } from '../../stores/app'
|
||||||
import { useRcloneStore } from '../../stores/rclone'
|
import { useRcloneStore } from '../../stores/rclone'
|
||||||
import { useTranslation } from '../../composables/useI18n'
|
import { useTranslation } from '../../composables/useI18n'
|
||||||
import Card from '../ui/Card.vue'
|
import Card from '../ui/Card.vue'
|
||||||
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key } from 'lucide-vue-next'
|
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key, Shield, Loader } from 'lucide-vue-next'
|
||||||
import { TauriAPI } from '@/api/tauri'
|
import { TauriAPI } from '@/api/tauri'
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -97,9 +151,17 @@ const appStore = useAppStore()
|
|||||||
const rcloneStore = useRcloneStore()
|
const rcloneStore = useRcloneStore()
|
||||||
|
|
||||||
const isCoreRunning = computed(() => appStore.isCoreRunning)
|
const isCoreRunning = computed(() => appStore.isCoreRunning)
|
||||||
|
const isCoreLoading = computed(() => appStore.loading)
|
||||||
|
const isRcloneLoading = computed(() => rcloneStore.loading)
|
||||||
const settings = computed(() => appStore.settings)
|
const settings = computed(() => appStore.settings)
|
||||||
let statusCheckInterval: number | null = null
|
let statusCheckInterval: number | null = null
|
||||||
|
|
||||||
|
const firewallEnabled = ref(false)
|
||||||
|
const firewallLoading = ref(false)
|
||||||
|
const isWindows = computed(() => {
|
||||||
|
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
|
||||||
|
})
|
||||||
|
|
||||||
const serviceButtonIcon = computed(() => {
|
const serviceButtonIcon = computed(() => {
|
||||||
return isCoreRunning.value ? Square : Play
|
return isCoreRunning.value ? Square : Play
|
||||||
})
|
})
|
||||||
@@ -136,7 +198,7 @@ const viewMounts = () => {
|
|||||||
router.push({ name: 'Mount' })
|
router.push({ name: 'Mount' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAdminPassword = async () => {
|
const copyAdminPassword = async () => {
|
||||||
try {
|
try {
|
||||||
const password = await appStore.getAdminPassword()
|
const password = await appStore.getAdminPassword()
|
||||||
if (password) {
|
if (password) {
|
||||||
@@ -209,38 +271,54 @@ const showAdminPassword = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get admin password:', error)
|
console.error('Failed to get admin password:', error)
|
||||||
|
showNotification('error', 'Failed to get admin password. Please check the logs.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const notification = document.createElement('div')
|
const resetAdminPassword = async () => {
|
||||||
notification.innerHTML = `
|
try {
|
||||||
<div style="
|
const newPassword = await appStore.resetAdminPassword()
|
||||||
position: fixed;
|
if (newPassword) {
|
||||||
top: 20px;
|
await navigator.clipboard.writeText(newPassword)
|
||||||
right: 20px;
|
|
||||||
background: linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38));
|
const notification = document.createElement('div')
|
||||||
color: white;
|
notification.innerHTML = `
|
||||||
padding: 12px 20px;
|
<div style="
|
||||||
border-radius: 8px;
|
position: fixed;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
top: 20px;
|
||||||
z-index: 10000;
|
right: 20px;
|
||||||
font-weight: 500;
|
background: linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105));
|
||||||
max-width: 300px;
|
color: white;
|
||||||
">
|
padding: 12px 20px;
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
border-radius: 8px;
|
||||||
<div style="font-size: 18px;">✗</div>
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
<div>
|
z-index: 10000;
|
||||||
<div style="font-size: 14px; margin-bottom: 4px;">Failed to get admin password</div>
|
font-weight: 500;
|
||||||
<div style="font-size: 12px; opacity: 0.9;">Please check the logs.</div>
|
max-width: 300px;
|
||||||
|
word-break: break-all;
|
||||||
|
">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<div style="font-size: 18px;">✓</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 14px; margin-bottom: 4px;">Admin password reset and copied!</div>
|
||||||
|
<div style="font-size: 12px; opacity: 0.9; font-family: monospace;">${newPassword}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`
|
||||||
`
|
document.body.appendChild(notification)
|
||||||
document.body.appendChild(notification)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (notification.parentNode) {
|
if (notification.parentNode) {
|
||||||
notification.parentNode.removeChild(notification)
|
notification.parentNode.removeChild(notification)
|
||||||
}
|
}
|
||||||
}, 4000)
|
}, 4000)
|
||||||
|
} else {
|
||||||
|
showNotification('error', 'Failed to reset admin password. Please check the logs.')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reset admin password:', error)
|
||||||
|
showNotification('error', 'Failed to reset admin password. Please check the logs.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,18 +352,99 @@ 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) => {
|
const openLink = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
await (appStore.settings.app.open_links_in_browser ? TauriAPI.files.urlInBrowser : TauriAPI.files.url)(url)
|
if (appStore.settings.app.open_links_in_browser) {
|
||||||
|
await TauriAPI.files.urlInBrowser(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open link:', error)
|
console.error('Failed to open link:', error)
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await rcloneStore.checkRcloneBackendStatus()
|
await rcloneStore.checkRcloneBackendStatus()
|
||||||
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 30 * 1000)
|
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 15 * 1000)
|
||||||
|
|
||||||
|
await checkFirewallStatus()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -321,6 +480,16 @@ onUnmounted(() => {
|
|||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-loading-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
opacity: 0.7;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.icon-only-btn {
|
.icon-only-btn {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
@@ -378,6 +547,15 @@ onUnmounted(() => {
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
.action-btn.loading {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: wait !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.loading .loading-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.service-btn.running {
|
.service-btn.running {
|
||||||
background: rgb(239, 68, 68);
|
background: rgb(239, 68, 68);
|
||||||
@@ -435,6 +613,12 @@ onUnmounted(() => {
|
|||||||
border-color: rgba(147, 51, 234, 0.3);
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reset-password-btn:hover:not(:disabled) {
|
||||||
|
background: rgb(239, 68, 68);
|
||||||
|
color: white;
|
||||||
|
border-color: rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.service-indicator-btn {
|
.service-indicator-btn {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border-color: var(--color-border-secondary);
|
border-color: var(--color-border-secondary);
|
||||||
@@ -516,6 +700,41 @@ onUnmounted(() => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--color-border-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn:hover:not(:disabled) {
|
||||||
|
background: var(--color-surface-elevated);
|
||||||
|
border-color: rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn.active {
|
||||||
|
background: rgb(16, 185, 129);
|
||||||
|
color: white;
|
||||||
|
border-color: rgba(5, 150, 105, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn.active:hover:not(:disabled) {
|
||||||
|
background: rgb(5, 150, 105);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.actions-grid {
|
.actions-grid {
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|||||||
@@ -225,7 +225,17 @@ const stopService = async () => {
|
|||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('Service stop failed')
|
throw new Error('Service stop failed')
|
||||||
}
|
}
|
||||||
await checkServiceStatus()
|
let attempts = 0
|
||||||
|
const maxAttempts = 5
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const status = await checkServiceStatus()
|
||||||
|
if (status === 'stopped' || status === 'not-installed' || status === 'error') {
|
||||||
|
serviceStatus.value = status
|
||||||
|
break
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop service:', error)
|
console.error('Failed to stop service:', error)
|
||||||
serviceStatus.value = 'error'
|
serviceStatus.value = 'error'
|
||||||
@@ -265,7 +275,6 @@ const cancelUninstall = () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkServiceStatus()
|
await checkServiceStatus()
|
||||||
statusCheckInterval = window.setInterval(checkServiceStatus, 30 * 1000)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -9,7 +9,11 @@
|
|||||||
<span class="current-version">{{ currentVersions.openlist }}</span>
|
<span class="current-version">{{ currentVersions.openlist }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn">
|
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn">
|
||||||
<component :is="refreshing ? LoaderIcon : RefreshCw" :size="16" />
|
<component
|
||||||
|
:is="refreshing ? Loader : RefreshCw"
|
||||||
|
:size="16"
|
||||||
|
:class="{ 'rotate-animation': refreshing && !loading.openlist }"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-controls">
|
<div class="version-controls">
|
||||||
@@ -26,8 +30,10 @@
|
|||||||
"
|
"
|
||||||
class="update-btn"
|
class="update-btn"
|
||||||
>
|
>
|
||||||
<component :is="loading.openlist ? LoaderIcon : Download" :size="14" />
|
<component :is="loading.openlist ? Loader : Download" :size="14" />
|
||||||
<span>{{ loading.openlist ? t('common.loading') : t('dashboard.versionManager.update') }}</span>
|
<span>{{
|
||||||
|
loading.openlist ? t('dashboard.versionManager.updating') : t('dashboard.versionManager.update')
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +44,11 @@
|
|||||||
<span class="current-version">{{ currentVersions.rclone }}</span>
|
<span class="current-version">{{ currentVersions.rclone }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn">
|
<button @click="refreshVersions" :disabled="refreshing" class="refresh-icon-btn">
|
||||||
<component :is="refreshing ? LoaderIcon : RefreshCw" :size="16" />
|
<component
|
||||||
|
:is="refreshing ? Loader : RefreshCw"
|
||||||
|
:size="16"
|
||||||
|
:class="{ 'rotate-animation': refreshing && !loading.rclone }"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-controls">
|
<div class="version-controls">
|
||||||
@@ -55,8 +65,10 @@
|
|||||||
"
|
"
|
||||||
class="update-btn"
|
class="update-btn"
|
||||||
>
|
>
|
||||||
<component :is="loading.rclone ? LoaderIcon : Download" :size="14" />
|
<component :is="loading.rclone ? Loader : Download" :size="14" />
|
||||||
<span>{{ loading.rclone ? t('common.loading') : t('dashboard.versionManager.update') }}</span>
|
<span>{{
|
||||||
|
loading.rclone ? t('dashboard.versionManager.updating') : t('dashboard.versionManager.update')
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +80,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useTranslation } from '../../composables/useI18n'
|
import { useTranslation } from '../../composables/useI18n'
|
||||||
import { Download, RefreshCw, Loader2 as LoaderIcon } from 'lucide-vue-next'
|
import { Download, RefreshCw, Loader } from 'lucide-vue-next'
|
||||||
import Card from '../ui/Card.vue'
|
import Card from '../ui/Card.vue'
|
||||||
import { TauriAPI } from '../../api/tauri'
|
import { TauriAPI } from '../../api/tauri'
|
||||||
|
|
||||||
@@ -144,20 +156,95 @@ const refreshVersions = async () => {
|
|||||||
|
|
||||||
const updateVersion = async (type: 'openlist' | 'rclone') => {
|
const updateVersion = async (type: 'openlist' | 'rclone') => {
|
||||||
loading.value[type] = true
|
loading.value[type] = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await TauriAPI.bin.updateVersion(type, selectedVersions.value[type])
|
const result = await TauriAPI.bin.updateVersion(type, selectedVersions.value[type])
|
||||||
|
|
||||||
currentVersions.value[type] = selectedVersions.value[type]
|
currentVersions.value[type] = selectedVersions.value[type]
|
||||||
selectedVersions.value[type] = ''
|
selectedVersions.value[type] = ''
|
||||||
|
|
||||||
|
showNotification(
|
||||||
|
'success',
|
||||||
|
t('dashboard.versionManager.updateSuccess', { type: type.charAt(0).toUpperCase() + type.slice(1) })
|
||||||
|
)
|
||||||
|
|
||||||
console.log(`Updated ${type}:`, result)
|
console.log(`Updated ${type}:`, result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to update ${type}:`, error)
|
console.error(`Failed to update ${type}:`, error)
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
showNotification(
|
||||||
|
'error',
|
||||||
|
t('dashboard.versionManager.updateError', {
|
||||||
|
type: type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
|
error: errorMessage
|
||||||
|
})
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value[type] = false
|
loading.value[type] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showNotification = (type: 'success' | 'error', message: string) => {
|
||||||
|
const notification = document.createElement('div')
|
||||||
|
const bgColor =
|
||||||
|
type === 'success'
|
||||||
|
? 'linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105))'
|
||||||
|
: 'linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38))'
|
||||||
|
const icon = type === 'success' ? '✓' : '⚠'
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div style="
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: ${bgColor};
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 10000;
|
||||||
|
font-weight: 500;
|
||||||
|
max-width: 350px;
|
||||||
|
word-break: break-word;
|
||||||
|
animation: slideInRight 0.3s ease-out;
|
||||||
|
">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<div style="font-size: 18px;">${icon}</div>
|
||||||
|
<div style="font-size: 14px;">${message}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
if (!document.querySelector('#notification-styles')) {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.id = 'notification-styles'
|
||||||
|
style.innerHTML = `
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(notification)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.style.animation = 'slideInRight 0.3s ease-in reverse'
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.parentNode?.removeChild(notification)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshVersions()
|
refreshVersions()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"minimize": "Minimize",
|
"minimize": "Minimize",
|
||||||
@@ -23,16 +24,26 @@
|
|||||||
"openlistService": "OpenList Core",
|
"openlistService": "OpenList Core",
|
||||||
"rclone": "RClone",
|
"rclone": "RClone",
|
||||||
"quickSettings": "Quick Settings",
|
"quickSettings": "Quick Settings",
|
||||||
"startOpenListCore": "Start Core",
|
"startOpenListCore": "Start",
|
||||||
"stopOpenListCore": "Stop Core",
|
"stopOpenListCore": "Stop",
|
||||||
|
"processing": "Processing...",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"openWeb": "Web UI",
|
"openWeb": "Web",
|
||||||
"configRclone": "Configure RClone",
|
"configRclone": "Configure RClone",
|
||||||
"startRclone": "Start RClone",
|
"startRclone": "Start RClone",
|
||||||
"stopRclone": "Stop RClone",
|
"stopRclone": "Stop RClone",
|
||||||
"manageMounts": "Manage Mounts",
|
"manageMounts": "Manage Mounts",
|
||||||
"autoLaunch": "Auto Launch Core(not app)",
|
"autoLaunch": "Auto Launch Core(not app)",
|
||||||
"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": {
|
"coreMonitor": {
|
||||||
"title": "Core Monitor",
|
"title": "Core Monitor",
|
||||||
@@ -47,7 +58,10 @@
|
|||||||
"openlist": "OpenList",
|
"openlist": "OpenList",
|
||||||
"rclone": "Rclone",
|
"rclone": "Rclone",
|
||||||
"selectVersion": "Select Version",
|
"selectVersion": "Select Version",
|
||||||
"update": "Update"
|
"update": "Update",
|
||||||
|
"updating": "Updating...",
|
||||||
|
"updateSuccess": "{type} updated successfully!",
|
||||||
|
"updateError": "Failed to update {type}: {error}"
|
||||||
},
|
},
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"title": "Documentation",
|
"title": "Documentation",
|
||||||
@@ -89,7 +103,10 @@
|
|||||||
"subtitle": "Configure your OpenList Desktop application",
|
"subtitle": "Configure your OpenList Desktop application",
|
||||||
"saveChanges": "Save Changes",
|
"saveChanges": "Save Changes",
|
||||||
"resetToDefaults": "Reset to defaults",
|
"resetToDefaults": "Reset to defaults",
|
||||||
"confirmReset": "Are you sure you want to reset all settings to defaults? This action cannot be undone.",
|
"confirmReset": {
|
||||||
|
"title": "Reset Settings",
|
||||||
|
"message": "Are you sure you want to reset all settings to defaults? This action cannot be undone."
|
||||||
|
},
|
||||||
"saved": "Settings saved successfully!",
|
"saved": "Settings saved successfully!",
|
||||||
"saveFailed": "Failed to save settings. Please try again.",
|
"saveFailed": "Failed to save settings. Please try again.",
|
||||||
"resetSuccess": "Settings reset to defaults successfully!",
|
"resetSuccess": "Settings reset to defaults successfully!",
|
||||||
@@ -125,10 +142,15 @@
|
|||||||
"placeholder": "5244",
|
"placeholder": "5244",
|
||||||
"help": "Port number for the web interface (1-65535)"
|
"help": "Port number for the web interface (1-65535)"
|
||||||
},
|
},
|
||||||
"apiToken": {
|
"dataDir": {
|
||||||
"label": "API Token",
|
"label": "Data Directory",
|
||||||
"placeholder": "Optional. Secure API access with authentication",
|
"placeholder": "Optional. Custom data directory path",
|
||||||
"help": "Optional. Secure API access with authentication"
|
"help": "Optional. Specify a custom directory for OpenList data storage",
|
||||||
|
"selectTitle": "Select Data Directory",
|
||||||
|
"selectError": "Failed to select directory. Please try again or enter path manually.",
|
||||||
|
"openTitle": "Open Data Directory",
|
||||||
|
"openSuccess": "Data directory opened successfully",
|
||||||
|
"openError": "Failed to open data directory"
|
||||||
},
|
},
|
||||||
"ssl": {
|
"ssl": {
|
||||||
"title": "Enable SSL/HTTPS",
|
"title": "Enable SSL/HTTPS",
|
||||||
@@ -140,6 +162,18 @@
|
|||||||
"title": "Auto-launch on startup",
|
"title": "Auto-launch on startup",
|
||||||
"description": "Automatically start OpenList service when the application launches"
|
"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": {
|
"rclone": {
|
||||||
@@ -148,8 +182,11 @@
|
|||||||
"title": "Remote Storage",
|
"title": "Remote Storage",
|
||||||
"subtitle": "Configure rclone for remote storage access",
|
"subtitle": "Configure rclone for remote storage access",
|
||||||
"label": "Rclone Configuration (JSON)",
|
"label": "Rclone Configuration (JSON)",
|
||||||
"tips": "Enter your rclone configuration as JSON. This will be used to configure rclone remotes.",
|
"tips": "View your rclone configuration as JSON. This will be used to configure rclone remotes.",
|
||||||
"invalidJson": "Invalid JSON configuration. Please check your syntax."
|
"invalidJson": "Invalid JSON configuration. Please check your syntax.",
|
||||||
|
"openFile": "Open rclone.conf",
|
||||||
|
"openSuccess": "Rclone config file opened successfully",
|
||||||
|
"openError": "Failed to open rclone config file"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
@@ -162,6 +199,13 @@
|
|||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"autoDesc": "Follow system"
|
"autoDesc": "Follow system"
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"title": "Configuration Files",
|
||||||
|
"subtitle": "Access application configuration files",
|
||||||
|
"openFile": "Open settings.json",
|
||||||
|
"openSuccess": "Settings file opened successfully",
|
||||||
|
"openError": "Failed to open settings file"
|
||||||
|
},
|
||||||
"ghProxy": {
|
"ghProxy": {
|
||||||
"title": "GitHub Proxy",
|
"title": "GitHub Proxy",
|
||||||
"subtitle": "Accelerate GitHub with proxy service",
|
"subtitle": "Accelerate GitHub with proxy service",
|
||||||
@@ -205,7 +249,9 @@
|
|||||||
"copyFailed": "Failed to copy logs to clipboard",
|
"copyFailed": "Failed to copy logs to clipboard",
|
||||||
"exportSuccess": "Successfully exported {count} logs entries to file",
|
"exportSuccess": "Successfully exported {count} logs entries to file",
|
||||||
"clearSuccess": "Logs cleared successfully",
|
"clearSuccess": "Logs cleared successfully",
|
||||||
"clearFailed": "Failed to clear logs"
|
"clearFailed": "Failed to clear logs",
|
||||||
|
"openDirectorySuccess": "Logs directory opened successfully",
|
||||||
|
"openDirectoryFailed": "Failed to open logs directory"
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"pause": "Pause (Space)",
|
"pause": "Pause (Space)",
|
||||||
@@ -216,6 +262,7 @@
|
|||||||
"copyToClipboard": "Copy to Clipboard (Ctrl+C)",
|
"copyToClipboard": "Copy to Clipboard (Ctrl+C)",
|
||||||
"exportLogs": "Export Logs",
|
"exportLogs": "Export Logs",
|
||||||
"clearLogs": "Clear Logs (Ctrl+Delete)",
|
"clearLogs": "Clear Logs (Ctrl+Delete)",
|
||||||
|
"openLogsDirectory": "Open Logs Directory",
|
||||||
"toggleFullscreen": "Toggle Fullscreen (F11)",
|
"toggleFullscreen": "Toggle Fullscreen (F11)",
|
||||||
"scrollToTop": "Scroll to Top (Home)",
|
"scrollToTop": "Scroll to Top (Home)",
|
||||||
"scrollToBottom": "Scroll to Bottom (End)"
|
"scrollToBottom": "Scroll to Bottom (End)"
|
||||||
@@ -240,7 +287,8 @@
|
|||||||
"sources": {
|
"sources": {
|
||||||
"all": "All Sources",
|
"all": "All Sources",
|
||||||
"rclone": "Rclone",
|
"rclone": "Rclone",
|
||||||
"openlist": "OpenList"
|
"openlist": "OpenList",
|
||||||
|
"service": "Service"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"selectAll": "Select All (Ctrl+A)",
|
"selectAll": "Select All (Ctrl+A)",
|
||||||
@@ -260,7 +308,8 @@
|
|||||||
"stripAnsiColors": "Strip ANSI Colors"
|
"stripAnsiColors": "Strip ANSI Colors"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"confirmClear": "Are you sure you want to clear all logs?"
|
"confirmClear": "Are you sure you want to clear all logs?",
|
||||||
|
"confirmTitle": "Clear Logs"
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"timestamp": "Timestamp",
|
"timestamp": "Timestamp",
|
||||||
@@ -314,7 +363,7 @@
|
|||||||
"authentication": "Authentication",
|
"authentication": "Authentication",
|
||||||
"mountSettings": "Mount Settings",
|
"mountSettings": "Mount Settings",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"namePlaceholder": "e.g., my-webdav-remote",
|
"namePlaceholder": "e.g.mount1",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"urlPlaceholder": "e.g., http://localhost:5264/dav/189",
|
"urlPlaceholder": "e.g., http://localhost:5264/dav/189",
|
||||||
@@ -325,7 +374,7 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
"passwordPlaceholder": "Password",
|
"passwordPlaceholder": "Password",
|
||||||
"mountPoint": "Mount Path",
|
"mountPoint": "Mount Path",
|
||||||
"mountPointPlaceholder": "e.g., T: (Windows) or /mnt/remote (Linux)",
|
"mountPointPlaceholder": "e.g., T: or /mnt/remote",
|
||||||
"volumeName": "Remote Path",
|
"volumeName": "Remote Path",
|
||||||
"volumeNamePlaceholder": "e.g., /",
|
"volumeNamePlaceholder": "e.g., /",
|
||||||
"autoMount": "Auto-mount on startup",
|
"autoMount": "Auto-mount on startup",
|
||||||
@@ -358,7 +407,7 @@
|
|||||||
"checkers": "Number of checkers to run in parallel (default 8)",
|
"checkers": "Number of checkers to run in parallel (default 8)",
|
||||||
"vfs-cache-max-age": "Max age of objects in cache (default 24h)",
|
"vfs-cache-max-age": "Max age of objects in cache (default 24h)",
|
||||||
"vfs-cache-max-size": "Max total size of cache (default 10G)",
|
"vfs-cache-max-size": "Max total size of cache (default 10G)",
|
||||||
"vfs-dir-cache-time": "How long to cache directory listings (default 5m)",
|
"dir-cache-time": "How long to cache directory listings (default 5m)",
|
||||||
"bwlimit-10M": "Bandwidth limit (e.g. 10M)",
|
"bwlimit-10M": "Bandwidth limit (e.g. 10M)",
|
||||||
"bwlimit-10M:100M": "Set separate upload and download bandwidth limits",
|
"bwlimit-10M:100M": "Set separate upload and download bandwidth limits",
|
||||||
"bwlimit-schedule": "Time-based bandwidth limits",
|
"bwlimit-schedule": "Time-based bandwidth limits",
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
|
"confirm": "确认",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"minimize": "最小化",
|
"minimize": "最小化",
|
||||||
"maximize": "最大化",
|
"maximize": "最大化",
|
||||||
"loading": "加载中...",
|
"loading": "处理中...",
|
||||||
"saving": "保存中...",
|
"saving": "保存中...",
|
||||||
"add": "添加"
|
"add": "添加"
|
||||||
},
|
},
|
||||||
@@ -23,16 +24,26 @@
|
|||||||
"openlistService": "OpenList 核心",
|
"openlistService": "OpenList 核心",
|
||||||
"rclone": "RClone",
|
"rclone": "RClone",
|
||||||
"quickSettings": "快速设置",
|
"quickSettings": "快速设置",
|
||||||
"startOpenListCore": "启动核心",
|
"startOpenListCore": "启动",
|
||||||
"stopOpenListCore": "停止核心",
|
"stopOpenListCore": "停止",
|
||||||
|
"processing": "处理中...",
|
||||||
"restart": "重启",
|
"restart": "重启",
|
||||||
"openWeb": "网页界面",
|
"openWeb": "网页",
|
||||||
"configRclone": "配置 RClone",
|
"configRclone": "配置 RClone",
|
||||||
"startRclone": "启动 RClone",
|
"startRclone": "启动 RClone",
|
||||||
"stopRclone": "停止 RClone",
|
"stopRclone": "停止 RClone",
|
||||||
"manageMounts": "管理挂载",
|
"manageMounts": "管理挂载",
|
||||||
"autoLaunch": "自动启动核心(非桌面app)",
|
"autoLaunch": "自动启动核心(非桌面app)",
|
||||||
"showAdminPassword": "显示/复制日志中的管理员密码"
|
"copyAdminPassword": "复制管理员密码",
|
||||||
|
"resetAdminPassword": "重置管理员密码",
|
||||||
|
"firewall": {
|
||||||
|
"enable": "放行端口",
|
||||||
|
"disable": "移除端口放行",
|
||||||
|
"added": "端口放行成功",
|
||||||
|
"removed": "端口移除成功",
|
||||||
|
"failedToAdd": "添加端口放行失败",
|
||||||
|
"failedToRemove": "移除端口放行失败"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"coreMonitor": {
|
"coreMonitor": {
|
||||||
"title": "核心监控",
|
"title": "核心监控",
|
||||||
@@ -47,7 +58,10 @@
|
|||||||
"openlist": "OpenList",
|
"openlist": "OpenList",
|
||||||
"rclone": "Rclone",
|
"rclone": "Rclone",
|
||||||
"selectVersion": "选择版本",
|
"selectVersion": "选择版本",
|
||||||
"update": "更新"
|
"update": "更新",
|
||||||
|
"updating": "更新中...",
|
||||||
|
"updateSuccess": "{type} 更新成功!",
|
||||||
|
"updateError": "更新 {type} 失败:{error}"
|
||||||
},
|
},
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"title": "文档",
|
"title": "文档",
|
||||||
@@ -89,7 +103,10 @@
|
|||||||
"subtitle": "配置您的 OpenList 桌面应用程序",
|
"subtitle": "配置您的 OpenList 桌面应用程序",
|
||||||
"saveChanges": "保存更改",
|
"saveChanges": "保存更改",
|
||||||
"resetToDefaults": "重置为默认值",
|
"resetToDefaults": "重置为默认值",
|
||||||
"confirmReset": "您确定要将所有设置重置为默认值吗?此操作无法撤消。",
|
"confirmReset": {
|
||||||
|
"title": "重置设置",
|
||||||
|
"message": "您确定要将所有设置重置为默认值吗?此操作无法撤消。"
|
||||||
|
},
|
||||||
"saved": "设置保存成功!",
|
"saved": "设置保存成功!",
|
||||||
"saveFailed": "保存设置失败,请重试。",
|
"saveFailed": "保存设置失败,请重试。",
|
||||||
"resetSuccess": "设置已重置为默认值!",
|
"resetSuccess": "设置已重置为默认值!",
|
||||||
@@ -125,10 +142,15 @@
|
|||||||
"placeholder": "5244",
|
"placeholder": "5244",
|
||||||
"help": "Web 界面的端口号 (1-65535)"
|
"help": "Web 界面的端口号 (1-65535)"
|
||||||
},
|
},
|
||||||
"apiToken": {
|
"dataDir": {
|
||||||
"label": "API 令牌",
|
"label": "数据目录",
|
||||||
"placeholder": "可选。用于 API 访问的身份验证",
|
"placeholder": "可选。自定义数据目录路径",
|
||||||
"help": "可选。用于 API 访问的身份验证"
|
"help": "可选。为 OpenList 数据存储指定自定义目录",
|
||||||
|
"selectTitle": "选择数据目录",
|
||||||
|
"selectError": "选择目录失败。请重试或手动输入路径。",
|
||||||
|
"openTitle": "打开数据目录",
|
||||||
|
"openSuccess": "数据目录打开成功",
|
||||||
|
"openError": "打开数据目录失败"
|
||||||
},
|
},
|
||||||
"ssl": {
|
"ssl": {
|
||||||
"title": "启用 SSL/HTTPS",
|
"title": "启用 SSL/HTTPS",
|
||||||
@@ -140,6 +162,18 @@
|
|||||||
"title": "开机自启",
|
"title": "开机自启",
|
||||||
"description": "应用程序启动时自动启动 OpenList 服务"
|
"description": "应用程序启动时自动启动 OpenList 服务"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "管理员密码",
|
||||||
|
"subtitle": "管理 OpenList 核心网页界面的管理员密码",
|
||||||
|
"currentPassword": "管理员密码",
|
||||||
|
"passwordPlaceholder": "输入管理员密码或点击重置生成",
|
||||||
|
"resetTitle": "重置管理员密码为新的随机值",
|
||||||
|
"resetSuccess": "管理员密码重置成功!已生成新密码并保存。",
|
||||||
|
"resetFailed": "重置管理员密码失败。请查看日志了解详细信息。",
|
||||||
|
"passwordUpdated": "管理员密码更新成功!",
|
||||||
|
"passwordUpdateFailed": "更新管理员密码失败。请查看日志了解详细信息。",
|
||||||
|
"help": "输入自定义管理员密码或点击重置按钮生成新的随机密码。点击'保存更改'将密码应用到 OpenList 核心。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rclone": {
|
"rclone": {
|
||||||
@@ -149,7 +183,10 @@
|
|||||||
"subtitle": "配置 rclone 远程存储访问",
|
"subtitle": "配置 rclone 远程存储访问",
|
||||||
"label": "Rclone 配置 (JSON)",
|
"label": "Rclone 配置 (JSON)",
|
||||||
"invalidJson": "无效的 JSON 配置。请检查您的语法。",
|
"invalidJson": "无效的 JSON 配置。请检查您的语法。",
|
||||||
"tips": "输入你的JSON配置"
|
"tips": "查看你的JSON配置",
|
||||||
|
"openFile": "打开 rclone.conf",
|
||||||
|
"openSuccess": "Rclone 配置文件打开成功",
|
||||||
|
"openError": "打开 Rclone 配置文件失败"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
@@ -162,6 +199,13 @@
|
|||||||
"auto": "自动",
|
"auto": "自动",
|
||||||
"autoDesc": "跟随系统"
|
"autoDesc": "跟随系统"
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"title": "配置文件",
|
||||||
|
"subtitle": "访问应用程序配置文件",
|
||||||
|
"openFile": "打开 settings.json",
|
||||||
|
"openSuccess": "设置文件打开成功",
|
||||||
|
"openError": "打开设置文件失败"
|
||||||
|
},
|
||||||
"ghProxy": {
|
"ghProxy": {
|
||||||
"title": "GitHub 代理",
|
"title": "GitHub 代理",
|
||||||
"subtitle": "使用代理服务加速 GitHub",
|
"subtitle": "使用代理服务加速 GitHub",
|
||||||
@@ -205,7 +249,9 @@
|
|||||||
"copyFailed": "复制日志到剪贴板失败",
|
"copyFailed": "复制日志到剪贴板失败",
|
||||||
"exportSuccess": "成功导出 {count} 条日志到文件",
|
"exportSuccess": "成功导出 {count} 条日志到文件",
|
||||||
"clearSuccess": "日志清理成功",
|
"clearSuccess": "日志清理成功",
|
||||||
"clearFailed": "清理日志失败"
|
"clearFailed": "清理日志失败",
|
||||||
|
"openDirectorySuccess": "日志目录打开成功",
|
||||||
|
"openDirectoryFailed": "打开日志目录失败"
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"pause": "暂停 (Space)",
|
"pause": "暂停 (Space)",
|
||||||
@@ -216,6 +262,7 @@
|
|||||||
"copyToClipboard": "复制到剪贴板 (Ctrl+C)",
|
"copyToClipboard": "复制到剪贴板 (Ctrl+C)",
|
||||||
"exportLogs": "导出日志",
|
"exportLogs": "导出日志",
|
||||||
"clearLogs": "清除日志 (Ctrl+Delete)",
|
"clearLogs": "清除日志 (Ctrl+Delete)",
|
||||||
|
"openLogsDirectory": "打开日志目录",
|
||||||
"toggleFullscreen": "切换全屏 (F11)",
|
"toggleFullscreen": "切换全屏 (F11)",
|
||||||
"scrollToTop": "滚动到顶部 (Home)",
|
"scrollToTop": "滚动到顶部 (Home)",
|
||||||
"scrollToBottom": "滚动到底部 (End)"
|
"scrollToBottom": "滚动到底部 (End)"
|
||||||
@@ -240,7 +287,8 @@
|
|||||||
"sources": {
|
"sources": {
|
||||||
"all": "所有来源",
|
"all": "所有来源",
|
||||||
"rclone": "Rclone",
|
"rclone": "Rclone",
|
||||||
"openlist": "OpenList"
|
"openlist": "OpenList",
|
||||||
|
"service": "服务"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"selectAll": "全选 (Ctrl+A)",
|
"selectAll": "全选 (Ctrl+A)",
|
||||||
@@ -260,7 +308,8 @@
|
|||||||
"stripAnsiColors": "去除 ANSI 颜色"
|
"stripAnsiColors": "去除 ANSI 颜色"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"confirmClear": "您确定要清除所有日志吗?"
|
"confirmClear": "您确定要清除所有日志吗?",
|
||||||
|
"confirmTitle": "清除日志"
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"timestamp": "时间",
|
"timestamp": "时间",
|
||||||
@@ -314,7 +363,7 @@
|
|||||||
"authentication": "身份认证",
|
"authentication": "身份认证",
|
||||||
"mountSettings": "挂载设置",
|
"mountSettings": "挂载设置",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
"namePlaceholder": "例如:我的webdav远程",
|
"namePlaceholder": "例如:mount1",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"urlPlaceholder": "例如:http://localhost:5264/dav/189",
|
"urlPlaceholder": "例如:http://localhost:5264/dav/189",
|
||||||
@@ -325,7 +374,7 @@
|
|||||||
"password": "密码",
|
"password": "密码",
|
||||||
"passwordPlaceholder": "密码",
|
"passwordPlaceholder": "密码",
|
||||||
"mountPoint": "挂载点",
|
"mountPoint": "挂载点",
|
||||||
"mountPointPlaceholder": "例如:T: (Windows) 或 /mnt/remote (Linux)",
|
"mountPointPlaceholder": "例如:T: 或 /mnt/remote",
|
||||||
"volumeName": "远程路径",
|
"volumeName": "远程路径",
|
||||||
"volumeNamePlaceholder": "例如:/",
|
"volumeNamePlaceholder": "例如:/",
|
||||||
"autoMount": "开机自动挂载",
|
"autoMount": "开机自动挂载",
|
||||||
@@ -358,7 +407,7 @@
|
|||||||
"checkers": "并行运行的检查器数量(默认 8)",
|
"checkers": "并行运行的检查器数量(默认 8)",
|
||||||
"vfs-cache-max-age": "缓存的最大生命周期(默认 24h)",
|
"vfs-cache-max-age": "缓存的最大生命周期(默认 24h)",
|
||||||
"vfs-cache-max-size": "缓存文件的最大大小(默认 10G)",
|
"vfs-cache-max-size": "缓存文件的最大大小(默认 10G)",
|
||||||
"vfs-dir-cache-time": "缓存目录列表的时间(默认 5m)",
|
"dir-cache-time": "缓存目录列表的时间(默认 5m)",
|
||||||
"bwlimit-10M": "带宽限制(例如 10M)",
|
"bwlimit-10M": "带宽限制(例如 10M)",
|
||||||
"bwlimit-10M:100M": "分别设置上传和下载宽带限制",
|
"bwlimit-10M:100M": "分别设置上传和下载宽带限制",
|
||||||
"bwlimit-schedule": "基于时间的带宽限制",
|
"bwlimit-schedule": "基于时间的带宽限制",
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ type ActionFn<T = any> = () => Promise<T>
|
|||||||
|
|
||||||
export const useAppStore = defineStore('app', () => {
|
export const useAppStore = defineStore('app', () => {
|
||||||
const settings = ref<MergedSettings>({
|
const settings = ref<MergedSettings>({
|
||||||
openlist: { port: 5244, api_token: '', auto_launch: false, ssl_enabled: false },
|
openlist: { port: 5244, data_dir: '', auto_launch: false, ssl_enabled: false },
|
||||||
rclone: { config: {} },
|
rclone: { config: {} },
|
||||||
app: { theme: 'light', auto_update_enabled: true, gh_proxy: '', gh_proxy_api: false, open_links_in_browser: false }
|
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 openlistCoreStatus = ref<OpenListCoreStatus>({ running: false })
|
||||||
const remoteConfigs = ref<IRemoteConfig>({})
|
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')
|
const saveSettings = () => withLoading(() => TauriAPI.settings.save(settings.value), 'Failed to save settings')
|
||||||
|
|
||||||
async function saveSettingsWithUpdatePort(): Promise<boolean> {
|
async function saveSettingsWithCoreUpdate(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await TauriAPI.settings.saveWithUpdatePort(settings.value)
|
await TauriAPI.settings.saveWithUpdatePort(settings.value)
|
||||||
return true
|
return true
|
||||||
@@ -546,7 +553,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLogs(source?: 'openlist' | 'rclone' | 'app') {
|
async function loadLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
|
||||||
try {
|
try {
|
||||||
source = source || 'openlist'
|
source = source || 'openlist'
|
||||||
const logEntries = await TauriAPI.logs.get(source)
|
const logEntries = await TauriAPI.logs.get(source)
|
||||||
@@ -556,9 +563,10 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearLogs(source?: 'openlist' | 'rclone' | 'app') {
|
async function clearLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
source = source || 'openlist'
|
||||||
const result = await TauriAPI.logs.clear(source)
|
const result = await TauriAPI.logs.clear(source)
|
||||||
if (result) {
|
if (result) {
|
||||||
logs.value = []
|
logs.value = []
|
||||||
@@ -597,6 +605,46 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openLogsDirectory() {
|
||||||
|
try {
|
||||||
|
await TauriAPI.files.openLogsDirectory()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to open logs directory'
|
||||||
|
console.error('Failed to open logs directory:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openOpenListDataDir() {
|
||||||
|
try {
|
||||||
|
await TauriAPI.files.openOpenListDataDir()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to open openlist data directory'
|
||||||
|
console.error('Failed to open openlist data directory:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openRcloneConfigFile() {
|
||||||
|
try {
|
||||||
|
await TauriAPI.files.openRcloneConfigFile()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to open rclone config file'
|
||||||
|
console.error('Failed to open rclone config file:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSettingsFile() {
|
||||||
|
try {
|
||||||
|
await TauriAPI.files.openSettingsFile()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to open settings file'
|
||||||
|
console.error('Failed to open settings file:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function selectDirectory(title: string): Promise<string | null> {
|
async function selectDirectory(title: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
return await TauriAPI.util.selectDirectory(title)
|
return await TauriAPI.util.selectDirectory(title)
|
||||||
@@ -698,6 +746,36 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Update management functions
|
||||||
function setUpdateAvailable(available: boolean, updateInfo?: UpdateCheck) {
|
function setUpdateAvailable(available: boolean, updateInfo?: UpdateCheck) {
|
||||||
updateAvailable.value = available
|
updateAvailable.value = available
|
||||||
@@ -739,7 +817,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
|
|
||||||
loadSettings,
|
loadSettings,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
saveSettingsWithUpdatePort,
|
saveSettingsWithCoreUpdate,
|
||||||
resetSettings,
|
resetSettings,
|
||||||
|
|
||||||
startOpenListCore,
|
startOpenListCore,
|
||||||
@@ -752,10 +830,16 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
listFiles,
|
listFiles,
|
||||||
openFile,
|
openFile,
|
||||||
openFolder,
|
openFolder,
|
||||||
|
openLogsDirectory,
|
||||||
|
openOpenListDataDir,
|
||||||
|
openRcloneConfigFile,
|
||||||
|
openSettingsFile,
|
||||||
selectDirectory,
|
selectDirectory,
|
||||||
clearError,
|
clearError,
|
||||||
init,
|
init,
|
||||||
getAdminPassword,
|
getAdminPassword,
|
||||||
|
resetAdminPassword,
|
||||||
|
setAdminPassword,
|
||||||
|
|
||||||
setTheme,
|
setTheme,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
|
|||||||
3
src/types/types.d.ts
vendored
3
src/types/types.d.ts
vendored
@@ -6,7 +6,7 @@ interface IRemoteConfig {
|
|||||||
|
|
||||||
interface OpenListCoreConfig {
|
interface OpenListCoreConfig {
|
||||||
port: number
|
port: number
|
||||||
api_token: string
|
data_dir: string
|
||||||
auto_launch: boolean
|
auto_launch: boolean
|
||||||
ssl_enabled: boolean
|
ssl_enabled: boolean
|
||||||
}
|
}
|
||||||
@@ -49,6 +49,7 @@ interface AppConfig {
|
|||||||
gh_proxy?: string
|
gh_proxy?: string
|
||||||
gh_proxy_api?: boolean
|
gh_proxy_api?: boolean
|
||||||
open_links_in_browser?: boolean
|
open_links_in_browser?: boolean
|
||||||
|
admin_password?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MergedSettings {
|
interface MergedSettings {
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ import {
|
|||||||
Minimize2,
|
Minimize2,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Info,
|
Info,
|
||||||
AlertTriangle
|
AlertTriangle,
|
||||||
|
FolderOpen
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import * as chrono from 'chrono-node'
|
import * as chrono from 'chrono-node'
|
||||||
|
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
|
||||||
|
|
||||||
|
type filterSourceType = 'openlist' | 'rclone' | 'app' | 'service' | 'all'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -29,10 +33,10 @@ const searchInputRef = ref<HTMLInputElement>()
|
|||||||
const autoScroll = ref(true)
|
const autoScroll = ref(true)
|
||||||
const isPaused = ref(false)
|
const isPaused = ref(false)
|
||||||
const filterLevel = ref<string>('all')
|
const filterLevel = ref<string>('all')
|
||||||
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'all')
|
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'openlist')
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedEntries = ref<Set<number>>(new Set())
|
const selectedEntries = ref<Set<number>>(new Set())
|
||||||
const showFilters = ref(false)
|
const showFilters = ref(true)
|
||||||
const showSettings = ref(false)
|
const showSettings = ref(false)
|
||||||
const fontSize = ref(13)
|
const fontSize = ref(13)
|
||||||
const maxLines = ref(1000)
|
const maxLines = ref(1000)
|
||||||
@@ -42,14 +46,19 @@ const stripAnsiColors = ref(true)
|
|||||||
const showNotification = ref(false)
|
const showNotification = ref(false)
|
||||||
const notificationMessage = ref('')
|
const notificationMessage = ref('')
|
||||||
const notificationType = ref<'success' | 'info' | 'warning' | 'error'>('success')
|
const notificationType = ref<'success' | 'info' | 'warning' | 'error'>('success')
|
||||||
|
const showConfirmDialog = ref(false)
|
||||||
|
const confirmDialogConfig = ref({
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
onConfirm: () => {},
|
||||||
|
onCancel: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(filterSource, async newValue => {
|
||||||
filterSource,
|
localStorage.setItem('logFilterSource', newValue)
|
||||||
newValue => {
|
await appStore.loadLogs((newValue !== 'gin' ? newValue : 'openlist') as filterSourceType)
|
||||||
localStorage.setItem('logFilterSource', newValue)
|
await scrollToBottom()
|
||||||
},
|
})
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
let logRefreshInterval: NodeJS.Timeout | null = null
|
let logRefreshInterval: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
@@ -63,6 +72,16 @@ const showNotificationMessage = (message: string, type: 'success' | 'info' | 'wa
|
|||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openLogsDirectory = async () => {
|
||||||
|
try {
|
||||||
|
await appStore.openLogsDirectory()
|
||||||
|
showNotificationMessage(t('logs.notifications.openDirectorySuccess'), 'success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open logs directory:', error)
|
||||||
|
showNotificationMessage(t('logs.notifications.openDirectoryFailed'), 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const stripAnsiCodes = (text: string): string => {
|
const stripAnsiCodes = (text: string): string => {
|
||||||
return text.replace(/\u001b\[[0-9;]*[mGKHF]/g, '')
|
return text.replace(/\u001b\[[0-9;]*[mGKHF]/g, '')
|
||||||
}
|
}
|
||||||
@@ -104,11 +123,8 @@ const parseLogEntry = (logText: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (cleanText.includes('openlist_desktop') || cleanText.includes('tao::')) {
|
} else {
|
||||||
source = 'app'
|
source = filterSource.value
|
||||||
level = 'info'
|
|
||||||
} else if (cleanText.toLowerCase().includes('rclone')) {
|
|
||||||
source = 'rclone'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message
|
message = message
|
||||||
@@ -187,21 +203,30 @@ const scrollToTop = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clearLogs = async () => {
|
const clearLogs = async () => {
|
||||||
if (confirm(t('logs.messages.confirmClear'))) {
|
confirmDialogConfig.value = {
|
||||||
try {
|
title: t('logs.messages.confirmTitle') || t('common.confirm'),
|
||||||
await appStore.clearLogs(
|
message: t('logs.messages.confirmClear'),
|
||||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
onConfirm: async () => {
|
||||||
| 'openlist'
|
showConfirmDialog.value = false
|
||||||
| 'rclone'
|
try {
|
||||||
| 'app'
|
await appStore.clearLogs(
|
||||||
)
|
(filterSource.value !== 'all' && filterSource.value !== 'gin'
|
||||||
selectedEntries.value.clear()
|
? filterSource.value
|
||||||
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
|
: 'openlist') as filterSourceType
|
||||||
} catch (error) {
|
)
|
||||||
console.error('Failed to clear logs:', error)
|
selectedEntries.value.clear()
|
||||||
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
|
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear logs:', error)
|
||||||
|
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
showConfirmDialog.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConfirmDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyLogsToClipboard = async () => {
|
const copyLogsToClipboard = async () => {
|
||||||
@@ -273,7 +298,9 @@ const togglePause = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refreshLogs = async () => {
|
const refreshLogs = async () => {
|
||||||
await appStore.loadLogs()
|
await appStore.loadLogs(
|
||||||
|
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType
|
||||||
|
)
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
if (isPaused.value) {
|
if (isPaused.value) {
|
||||||
isPaused.value = false
|
isPaused.value = false
|
||||||
@@ -347,28 +374,16 @@ const handleKeydown = (event: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
appStore
|
appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType).then(() => {
|
||||||
.loadLogs(
|
scrollToBottom()
|
||||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
})
|
||||||
| 'openlist'
|
|
||||||
| 'rclone'
|
|
||||||
| 'app'
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
})
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeydown)
|
document.addEventListener('keydown', handleKeydown)
|
||||||
|
|
||||||
logRefreshInterval = setInterval(async () => {
|
logRefreshInterval = setInterval(async () => {
|
||||||
if (!isPaused.value) {
|
if (!isPaused.value) {
|
||||||
const oldLength = appStore.logs.length
|
const oldLength = appStore.logs.length
|
||||||
await appStore.loadLogs(
|
await appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType)
|
||||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
|
||||||
| 'openlist'
|
|
||||||
| 'rclone'
|
|
||||||
| 'app'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (appStore.logs.length > oldLength) {
|
if (appStore.logs.length > oldLength) {
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
@@ -485,6 +500,10 @@ onUnmounted(() => {
|
|||||||
<Trash2 :size="16" />
|
<Trash2 :size="16" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="toolbar-btn" @click="openLogsDirectory" :title="t('logs.toolbar.openLogsDirectory')">
|
||||||
|
<FolderOpen :size="16" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="toolbar-separator"></div>
|
<div class="toolbar-separator"></div>
|
||||||
|
|
||||||
<button class="toolbar-btn" @click="toggleFullscreen" :title="t('logs.toolbar.toggleFullscreen')">
|
<button class="toolbar-btn" @click="toggleFullscreen" :title="t('logs.toolbar.toggleFullscreen')">
|
||||||
@@ -512,6 +531,7 @@ onUnmounted(() => {
|
|||||||
<option value="openlist">{{ t('logs.filters.sources.openlist') }}</option>
|
<option value="openlist">{{ t('logs.filters.sources.openlist') }}</option>
|
||||||
<option value="gin">GIN Server</option>
|
<option value="gin">GIN Server</option>
|
||||||
<option value="rclone">{{ t('logs.filters.sources.rclone') }}</option>
|
<option value="rclone">{{ t('logs.filters.sources.rclone') }}</option>
|
||||||
|
<option value="service">{{ t('logs.filters.sources.service') }}</option>
|
||||||
<option value="app">{{ t('logs.filters.app') }}</option>
|
<option value="app">{{ t('logs.filters.app') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -639,6 +659,17 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
:is-open="showConfirmDialog"
|
||||||
|
:title="confirmDialogConfig.title"
|
||||||
|
:message="confirmDialogConfig.message"
|
||||||
|
:confirm-text="t('common.confirm')"
|
||||||
|
:cancel-text="t('common.cancel')"
|
||||||
|
variant="danger"
|
||||||
|
@confirm="confirmDialogConfig.onConfirm"
|
||||||
|
@cancel="confirmDialogConfig.onCancel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const commonFlags = ref([
|
|||||||
{ flag: '--vfs-cache-mode', value: 'minimal', descriptionKey: 'vfs-cache-mode-minimal' },
|
{ flag: '--vfs-cache-mode', value: 'minimal', descriptionKey: 'vfs-cache-mode-minimal' },
|
||||||
{ flag: '--vfs-cache-max-age', value: '24h', descriptionKey: 'vfs-cache-max-age' },
|
{ flag: '--vfs-cache-max-age', value: '24h', descriptionKey: 'vfs-cache-max-age' },
|
||||||
{ flag: '--vfs-cache-max-size', value: '10G', descriptionKey: 'vfs-cache-max-size' },
|
{ flag: '--vfs-cache-max-size', value: '10G', descriptionKey: 'vfs-cache-max-size' },
|
||||||
{ flag: '--vfs-dir-cache-time', value: '5m', descriptionKey: 'vfs-dir-cache-time' }
|
{ flag: 'dir-cache-time', value: '5m', descriptionKey: 'dir-cache-time' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -453,7 +453,9 @@ const dismissWebdavTip = () => {
|
|||||||
localStorage.setItem('webdav_tip_dismissed', 'true')
|
localStorage.setItem('webdav_tip_dismissed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWindows = /win/i.test(navigator.platform) || /win/i.test(navigator.userAgent)
|
const isWindows = computed(() => {
|
||||||
|
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
|
||||||
|
})
|
||||||
const showWinfspTip = ref(isWindows && !localStorage.getItem('winfsp_tip_dismissed'))
|
const showWinfspTip = ref(isWindows && !localStorage.getItem('winfsp_tip_dismissed'))
|
||||||
|
|
||||||
const dismissWinfspTip = () => {
|
const dismissWinfspTip = () => {
|
||||||
@@ -473,10 +475,10 @@ onMounted(async () => {
|
|||||||
rcloneStore.checkRcloneBackendStatus()
|
rcloneStore.checkRcloneBackendStatus()
|
||||||
appStore.loadRemoteConfigs()
|
appStore.loadRemoteConfigs()
|
||||||
appStore.loadMountInfos()
|
appStore.loadMountInfos()
|
||||||
mountRefreshInterval = setInterval(appStore.loadMountInfos, 30 * 1000)
|
mountRefreshInterval = setInterval(appStore.loadMountInfos, 15 * 1000)
|
||||||
backendStatusCheckInterval = setInterval(() => {
|
backendStatusCheckInterval = setInterval(() => {
|
||||||
rcloneStore.checkRcloneBackendStatus()
|
rcloneStore.checkRcloneBackendStatus()
|
||||||
}, 30 * 1000)
|
}, 15 * 1000)
|
||||||
rcloneStore.init()
|
rcloneStore.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -722,7 +724,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Configuration Modal -->
|
<!-- Configuration Modal -->
|
||||||
<div v-if="showAddForm" class="modal-backdrop" @click="cancelForm">
|
<div v-if="showAddForm" class="modal-backdrop">
|
||||||
<div class="config-modal" @click.stop>
|
<div class="config-modal" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title-section">
|
<div class="modal-title-section">
|
||||||
|
|||||||
@@ -3,8 +3,20 @@ import { ref, reactive, computed, onMounted, watch } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
import { useTranslation } from '../composables/useI18n'
|
import { useTranslation } from '../composables/useI18n'
|
||||||
import { Settings, Server, HardDrive, Save, RotateCcw, AlertCircle, CheckCircle } from 'lucide-vue-next'
|
import {
|
||||||
|
Settings,
|
||||||
|
Server,
|
||||||
|
HardDrive,
|
||||||
|
Save,
|
||||||
|
RotateCcw,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
FolderOpen,
|
||||||
|
ExternalLink
|
||||||
|
} from 'lucide-vue-next'
|
||||||
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
|
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -15,11 +27,21 @@ const messageType = ref<'success' | 'error' | 'info'>('info')
|
|||||||
const activeTab = ref('openlist')
|
const activeTab = ref('openlist')
|
||||||
const rcloneConfigJson = ref('')
|
const rcloneConfigJson = ref('')
|
||||||
const autoStartApp = ref(false)
|
const autoStartApp = ref(false)
|
||||||
|
const isResettingPassword = ref(false)
|
||||||
|
const showConfirmDialog = ref(false)
|
||||||
|
const confirmDialogConfig = ref({
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
onConfirm: () => {},
|
||||||
|
onCancel: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
|
const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
|
||||||
const rcloneSettings = reactive({ ...appStore.settings.rclone })
|
const rcloneSettings = reactive({ ...appStore.settings.rclone })
|
||||||
const appSettings = reactive({ ...appStore.settings.app })
|
const appSettings = reactive({ ...appStore.settings.app })
|
||||||
let originalOpenlistPort = openlistCoreSettings.port || 5244
|
let originalOpenlistPort = openlistCoreSettings.port || 5244
|
||||||
|
let originalDataDir = openlistCoreSettings.data_dir
|
||||||
|
let originalAdminPassword = appStore.settings.app.admin_password || ''
|
||||||
|
|
||||||
watch(autoStartApp, async newValue => {
|
watch(autoStartApp, async newValue => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
@@ -58,7 +80,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!openlistCoreSettings.port) openlistCoreSettings.port = 5244
|
if (!openlistCoreSettings.port) openlistCoreSettings.port = 5244
|
||||||
if (!openlistCoreSettings.api_token) openlistCoreSettings.api_token = ''
|
if (!openlistCoreSettings.data_dir) openlistCoreSettings.data_dir = ''
|
||||||
if (openlistCoreSettings.auto_launch === undefined) openlistCoreSettings.auto_launch = false
|
if (openlistCoreSettings.auto_launch === undefined) openlistCoreSettings.auto_launch = false
|
||||||
if (openlistCoreSettings.ssl_enabled === undefined) openlistCoreSettings.ssl_enabled = false
|
if (openlistCoreSettings.ssl_enabled === undefined) openlistCoreSettings.ssl_enabled = false
|
||||||
|
|
||||||
@@ -71,7 +93,12 @@ onMounted(async () => {
|
|||||||
if (!appSettings.gh_proxy) appSettings.gh_proxy = ''
|
if (!appSettings.gh_proxy) appSettings.gh_proxy = ''
|
||||||
if (appSettings.gh_proxy_api === undefined) appSettings.gh_proxy_api = false
|
if (appSettings.gh_proxy_api === undefined) appSettings.gh_proxy_api = false
|
||||||
if (appSettings.open_links_in_browser === undefined) appSettings.open_links_in_browser = false
|
if (appSettings.open_links_in_browser === undefined) appSettings.open_links_in_browser = false
|
||||||
|
if (!appSettings.admin_password) appSettings.admin_password = ''
|
||||||
originalOpenlistPort = openlistCoreSettings.port || 5244
|
originalOpenlistPort = openlistCoreSettings.port || 5244
|
||||||
|
originalDataDir = openlistCoreSettings.data_dir
|
||||||
|
|
||||||
|
// Load current admin password
|
||||||
|
await loadCurrentAdminPassword()
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasUnsavedChanges = computed(() => {
|
const hasUnsavedChanges = computed(() => {
|
||||||
@@ -108,13 +135,32 @@ const handleSave = async () => {
|
|||||||
appStore.settings.openlist = { ...openlistCoreSettings }
|
appStore.settings.openlist = { ...openlistCoreSettings }
|
||||||
appStore.settings.rclone = { ...rcloneSettings }
|
appStore.settings.rclone = { ...rcloneSettings }
|
||||||
appStore.settings.app = { ...appSettings }
|
appStore.settings.app = { ...appSettings }
|
||||||
if (originalOpenlistPort !== openlistCoreSettings.port) {
|
|
||||||
await appStore.saveSettingsWithUpdatePort()
|
const needsPasswordUpdate = originalAdminPassword !== appSettings.admin_password && appSettings.admin_password
|
||||||
|
|
||||||
|
if (originalOpenlistPort !== openlistCoreSettings.port || originalDataDir !== openlistCoreSettings.data_dir) {
|
||||||
|
await appStore.saveSettingsWithCoreUpdate()
|
||||||
} else {
|
} else {
|
||||||
await appStore.saveSettings()
|
await appStore.saveSettings()
|
||||||
}
|
}
|
||||||
message.value = t('settings.saved')
|
|
||||||
messageType.value = 'success'
|
if (needsPasswordUpdate) {
|
||||||
|
try {
|
||||||
|
await appStore.setAdminPassword(appSettings.admin_password!)
|
||||||
|
message.value = t('settings.service.admin.passwordUpdated')
|
||||||
|
messageType.value = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update admin password:', error)
|
||||||
|
message.value = t('settings.service.admin.passwordUpdateFailed')
|
||||||
|
messageType.value = 'error'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.value = t('settings.saved')
|
||||||
|
messageType.value = 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
originalOpenlistPort = openlistCoreSettings.port || 5244
|
||||||
|
originalDataDir = openlistCoreSettings.data_dir
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.value = t('settings.saveFailed')
|
message.value = t('settings.saveFailed')
|
||||||
messageType.value = 'error'
|
messageType.value = 'error'
|
||||||
@@ -129,23 +175,143 @@ const handleSave = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
if (!confirm(t('settings.confirmReset'))) {
|
confirmDialogConfig.value = {
|
||||||
return
|
title: t('settings.confirmReset.title'),
|
||||||
|
message: t('settings.confirmReset.message'),
|
||||||
|
onConfirm: async () => {
|
||||||
|
showConfirmDialog.value = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appStore.resetSettings()
|
||||||
|
Object.assign(openlistCoreSettings, appStore.settings.openlist)
|
||||||
|
Object.assign(rcloneSettings, appStore.settings.rclone)
|
||||||
|
Object.assign(appSettings, appStore.settings.app)
|
||||||
|
|
||||||
|
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
|
||||||
|
|
||||||
|
message.value = t('settings.resetSuccess')
|
||||||
|
messageType.value = 'info'
|
||||||
|
} catch (error) {
|
||||||
|
message.value = t('settings.resetFailed')
|
||||||
|
messageType.value = 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
showConfirmDialog.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConfirmDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectDataDir = async () => {
|
||||||
try {
|
try {
|
||||||
await appStore.resetSettings()
|
const selected = await open({
|
||||||
Object.assign(openlistCoreSettings, appStore.settings.openlist)
|
directory: true,
|
||||||
Object.assign(rcloneSettings, appStore.settings.rclone)
|
multiple: false,
|
||||||
Object.assign(appSettings, appStore.settings.app)
|
title: t('settings.service.network.dataDir.selectTitle'),
|
||||||
|
defaultPath: openlistCoreSettings.data_dir || undefined
|
||||||
|
})
|
||||||
|
|
||||||
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
|
if (selected && typeof selected === 'string') {
|
||||||
|
openlistCoreSettings.data_dir = selected
|
||||||
message.value = t('settings.resetSuccess')
|
}
|
||||||
messageType.value = 'info'
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.value = t('settings.resetFailed')
|
console.error('Failed to select directory:', error)
|
||||||
|
message.value = t('settings.service.network.dataDir.selectError')
|
||||||
messageType.value = 'error'
|
messageType.value = 'error'
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = ''
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenDataDir = async () => {
|
||||||
|
try {
|
||||||
|
if (openlistCoreSettings.data_dir) {
|
||||||
|
await appStore.openFolder(openlistCoreSettings.data_dir)
|
||||||
|
} else {
|
||||||
|
await appStore.openOpenListDataDir()
|
||||||
|
}
|
||||||
|
message.value = t('settings.service.network.dataDir.openSuccess')
|
||||||
|
messageType.value = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open data directory:', error)
|
||||||
|
message.value = t('settings.service.network.dataDir.openError')
|
||||||
|
messageType.value = 'error'
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = ''
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetAdminPassword = async () => {
|
||||||
|
isResettingPassword.value = true
|
||||||
|
try {
|
||||||
|
const newPassword = await appStore.resetAdminPassword()
|
||||||
|
if (newPassword) {
|
||||||
|
appSettings.admin_password = newPassword
|
||||||
|
message.value = t('settings.service.admin.resetSuccess')
|
||||||
|
messageType.value = 'success'
|
||||||
|
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 handleOpenRcloneConfig = async () => {
|
||||||
|
try {
|
||||||
|
await appStore.openRcloneConfigFile()
|
||||||
|
message.value = t('settings.rclone.config.openSuccess')
|
||||||
|
messageType.value = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open rclone config file:', error)
|
||||||
|
message.value = t('settings.rclone.config.openError')
|
||||||
|
messageType.value = 'error'
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = ''
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenSettingsFile = async () => {
|
||||||
|
try {
|
||||||
|
await appStore.openSettingsFile()
|
||||||
|
message.value = t('settings.app.config.openSuccess')
|
||||||
|
messageType.value = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open settings file:', error)
|
||||||
|
message.value = t('settings.app.config.openError')
|
||||||
|
messageType.value = 'error'
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = ''
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCurrentAdminPassword = async () => {
|
||||||
|
try {
|
||||||
|
const password = await appStore.getAdminPassword()
|
||||||
|
if (password) {
|
||||||
|
appSettings.admin_password = password
|
||||||
|
originalAdminPassword = password
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load admin password:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -211,14 +377,32 @@ const handleReset = async () => {
|
|||||||
<small>{{ t('settings.service.network.port.help') }}</small>
|
<small>{{ t('settings.service.network.port.help') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('settings.service.network.apiToken.label') }}</label>
|
<label>{{ t('settings.service.network.dataDir.label') }}</label>
|
||||||
<input
|
<div class="input-group">
|
||||||
v-model="openlistCoreSettings.api_token"
|
<input
|
||||||
type="password"
|
v-model="openlistCoreSettings.data_dir"
|
||||||
class="form-input"
|
type="text"
|
||||||
:placeholder="t('settings.service.network.apiToken.placeholder')"
|
class="form-input"
|
||||||
/>
|
:placeholder="t('settings.service.network.dataDir.placeholder')"
|
||||||
<small>{{ t('settings.service.network.apiToken.help') }}</small>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleSelectDataDir"
|
||||||
|
class="input-addon-btn"
|
||||||
|
:title="t('settings.service.network.dataDir.selectTitle')"
|
||||||
|
>
|
||||||
|
<FolderOpen :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleOpenDataDir"
|
||||||
|
class="input-addon-btn"
|
||||||
|
:title="t('settings.service.network.dataDir.openTitle')"
|
||||||
|
>
|
||||||
|
<ExternalLink :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small>{{ t('settings.service.network.dataDir.help') }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -249,6 +433,33 @@ const handleReset = async () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>{{ t('settings.service.admin.title') }}</h2>
|
||||||
|
<p>{{ t('settings.service.admin.subtitle') }}</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ t('settings.service.admin.currentPassword') }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
v-model="appSettings.admin_password"
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
:placeholder="t('settings.service.admin.passwordPlaceholder')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@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>
|
||||||
|
|
||||||
<div v-if="activeTab === 'rclone'" class="tab-content">
|
<div v-if="activeTab === 'rclone'" class="tab-content">
|
||||||
@@ -258,11 +469,23 @@ const handleReset = async () => {
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('settings.rclone.config.label') }}</label>
|
<label>{{ t('settings.rclone.config.label') }}</label>
|
||||||
|
<div class="settings-section-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleOpenRcloneConfig"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
:title="t('settings.rclone.config.openFile')"
|
||||||
|
>
|
||||||
|
<ExternalLink :size="16" />
|
||||||
|
{{ t('settings.rclone.config.openFile') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="rcloneConfigJson"
|
v-model="rcloneConfigJson"
|
||||||
class="form-textarea"
|
class="form-textarea"
|
||||||
placeholder='{ "remote1": { "type": "s3", "provider": "AWS" } }'
|
placeholder='{ "remote1": { "type": "s3", "provider": "AWS" } }'
|
||||||
rows="10"
|
rows="10"
|
||||||
|
readonly
|
||||||
></textarea>
|
></textarea>
|
||||||
<small>{{ t('settings.rclone.config.tips') }}</small>
|
<small>{{ t('settings.rclone.config.tips') }}</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,6 +514,25 @@ const handleReset = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>{{ t('settings.app.config.title') }}</h2>
|
||||||
|
<p>{{ t('settings.app.config.subtitle') }}</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="settings-section-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleOpenSettingsFile"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
:title="t('settings.app.config.openFile')"
|
||||||
|
>
|
||||||
|
<ExternalLink :size="16" />
|
||||||
|
{{ t('settings.app.config.openFile') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h2>{{ t('settings.app.ghProxy.title') }}</h2>
|
<h2>{{ t('settings.app.ghProxy.title') }}</h2>
|
||||||
<p>{{ t('settings.app.ghProxy.subtitle') }}</p>
|
<p>{{ t('settings.app.ghProxy.subtitle') }}</p>
|
||||||
@@ -369,6 +611,17 @@ const handleReset = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
:is-open="showConfirmDialog"
|
||||||
|
:title="confirmDialogConfig.title"
|
||||||
|
:message="confirmDialogConfig.message"
|
||||||
|
:confirm-text="t('common.confirm')"
|
||||||
|
:cancel-text="t('common.cancel')"
|
||||||
|
variant="danger"
|
||||||
|
@confirm="confirmDialogConfig.onConfirm"
|
||||||
|
@cancel="confirmDialogConfig.onCancel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -384,6 +384,22 @@
|
|||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-addon-btn.reset-password-btn {
|
||||||
|
background: var(--color-error-background, #fef2f2);
|
||||||
|
color: var(--color-error, #dc2626);
|
||||||
|
border-color: var(--color-error-border, #fecaca);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-addon-btn.reset-password-btn:hover:not(:disabled) {
|
||||||
|
background: var(--color-error, #dc2626);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-addon-btn.reset-password-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Switch */
|
/* Switch */
|
||||||
.switch-label {
|
.switch-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -511,3 +527,33 @@
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings Section Actions */
|
||||||
|
.settings-section-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin: 0;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-actions .btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .input-addon-btn:last-child:not(:only-child) {
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .input-addon-btn:not(:first-child):not(:last-child) {
|
||||||
|
border-radius: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .input-addon-btn:first-child:not(:only-child) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|||||||
42
yarn.lock
42
yarn.lock
@@ -572,26 +572,26 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.7.tgz#b46bcf377b3172dbc768fdbd053e6492ad801a09"
|
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.7.tgz#b46bcf377b3172dbc768fdbd053e6492ad801a09"
|
||||||
integrity sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==
|
integrity sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==
|
||||||
|
|
||||||
"@intlify/core-base@11.1.9":
|
"@intlify/core-base@11.1.10":
|
||||||
version "11.1.9"
|
version "11.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.9.tgz#54201e7985d52240627b9c327a4d57c08a96cd39"
|
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.10.tgz#4731748992bc6d8e723ca6c2cc5aa5a4c90cf7a5"
|
||||||
integrity sha512-Lrdi4wp3XnGhWmB/mMD/XtfGUw1Jt+PGpZI/M63X1ZqhTDjNHRVCs/i8vv8U1cwaj1A9fb0bkCQHLSL0SK+pIQ==
|
integrity sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@intlify/message-compiler" "11.1.9"
|
"@intlify/message-compiler" "11.1.10"
|
||||||
"@intlify/shared" "11.1.9"
|
"@intlify/shared" "11.1.10"
|
||||||
|
|
||||||
"@intlify/message-compiler@11.1.9":
|
"@intlify/message-compiler@11.1.10":
|
||||||
version "11.1.9"
|
version "11.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.9.tgz#c84a3a2777b0d95342348d1bf95669329d71e10c"
|
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.10.tgz#ff5c92c311cd72144126f5c128912adb4e911207"
|
||||||
integrity sha512-84SNs3Ikjg0rD1bOuchzb3iK1vR2/8nxrkyccIl5DjFTeMzE/Fxv6X+A7RN5ZXjEWelc1p5D4kHA6HEOhlKL5Q==
|
integrity sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@intlify/shared" "11.1.9"
|
"@intlify/shared" "11.1.10"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
"@intlify/shared@11.1.9":
|
"@intlify/shared@11.1.10":
|
||||||
version "11.1.9"
|
version "11.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.9.tgz#20244e53322ba01233df7ddb6dc677561b3c7e0b"
|
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.10.tgz#d869aa8fbc1aa307f26a58848fea6df3c9785b6f"
|
||||||
integrity sha512-H/83xgU1l8ox+qG305p6ucmoy93qyjIPnvxGWRA7YdOoHe1tIiW9IlEu4lTdsOR7cfP1ecrwyflQSqXdXBacXA==
|
integrity sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==
|
||||||
|
|
||||||
"@isaacs/balanced-match@^4.0.1":
|
"@isaacs/balanced-match@^4.0.1":
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
|
||||||
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
|
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
|
||||||
|
|
||||||
vue-i18n@11.1.9:
|
vue-i18n@11.1.10:
|
||||||
version "11.1.9"
|
version "11.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.9.tgz#214816d3a5461a3169ee1eb507cac045a03a15d8"
|
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.10.tgz#04578ea4213f96c37939a08f516a648a6d0b84a1"
|
||||||
integrity sha512-N9ZTsXdRmX38AwS9F6Rh93RtPkvZTkSy/zNv63FTIwZCUbLwwrpqlKz9YQuzFLdlvRdZTnWAUE5jMxr8exdl7g==
|
integrity sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@intlify/core-base" "11.1.9"
|
"@intlify/core-base" "11.1.10"
|
||||||
"@intlify/shared" "11.1.9"
|
"@intlify/shared" "11.1.10"
|
||||||
"@vue/devtools-api" "^6.5.0"
|
"@vue/devtools-api" "^6.5.0"
|
||||||
|
|
||||||
vue-router@^4.5.1:
|
vue-router@^4.5.1:
|
||||||
|
|||||||
Reference in New Issue
Block a user