feat: align motrix-style download UI/actions and stabilize aria2 ops
This commit is contained in:
@@ -25,17 +25,24 @@
|
||||
- [x] Tauri + Vue 프로젝트 초기화
|
||||
- [x] Rust command: `engine_start`, `engine_stop`, `engine_status`
|
||||
- [x] Vue 대시보드에서 엔진 제어 UI 연결
|
||||
- [ ] aria2 번들 바이너리 경로 자동 탐지 (`resources/engine/*`)
|
||||
- [ ] 에러 메시지 분류(파일 없음, 권한 오류, 포트 충돌)
|
||||
- [x] aria2 바이너리 경로 자동 탐지 커맨드 추가 (`detect_aria2_binary`)
|
||||
- [x] 시작 실패 에러 분류(파일 없음, 권한 오류, 즉시 종료)
|
||||
- [x] aria2 번들 바이너리 리소스 구조 확정 (`src-tauri/resources/engine/*`)
|
||||
- [~] 실배포 번들 검증 (플랫폼별 smoke test 진행 중)
|
||||
|
||||
### Phase 2: 다운로드 RPC/큐
|
||||
- [ ] aria2 JSON-RPC client 계층 구현 (Rust 또는 Vue 중 1안 선택)
|
||||
- [ ] 작업 추가/중지/재시도/삭제
|
||||
- [ ] 작업 목록/속도/진행률 실시간 갱신
|
||||
- [ ] Magnet/Torrent 입력 파이프라인
|
||||
- [x] aria2 JSON-RPC client 계층 구현 (Rust command 래퍼)
|
||||
- [x] 작업 추가 (URI/Torrent)
|
||||
- [x] 작업 목록/속도/진행률 갱신
|
||||
- [x] 작업 제어(개별 pause/resume/remove, 전체 pause/resume)
|
||||
- [~] Magnet/Torrent 입력 파이프라인 (입력/추가는 완료, 메타데이터 UX 개선 필요)
|
||||
- [ ] 작업 상세 패널(파일/피어/트래커/활동) 구현
|
||||
- [ ] 선택 기반 배치 액션(다중 선택, 일괄 정지/재개/삭제)
|
||||
|
||||
### Phase 3: 설정/세션/마이그레이션
|
||||
- [ ] 설정 저장소 도입 (다운로드 폴더, 동시작업 수, 속도 제한 등)
|
||||
- [~] 설정 저장소 도입 (다운로드 폴더, 동시작업 수, 속도 제한 등)
|
||||
- [x] 기본 실행 설정(localStorage) 저장/복원
|
||||
- [ ] aria2 글로벌 옵션과 완전 동기화
|
||||
- [ ] 세션 파일 관리(종료 시 저장, 시작 시 복구)
|
||||
- [ ] Motrix 설정 키 매핑표 작성 및 자동 마이그레이션 도구
|
||||
|
||||
@@ -55,13 +62,32 @@
|
||||
- Electron API 차이: 기능별 대체표를 먼저 만들고 Tauri plugin으로 대응
|
||||
- 설정 호환성: 기존 키를 그대로 유지하지 않고 매핑 테이블로 이관
|
||||
|
||||
## 6. 현재 구현 상태 (2026-02-23)
|
||||
## 6. 현재 구현 상태 (2026-02-24)
|
||||
- 완료:
|
||||
- `scripts/version-bump.sh`: 빌드 전 patch 버전 자동 증가 훅 추가
|
||||
- `scripts/sync-aria2-from-motrix.sh`: Motrix extra 엔진 리소스 동기화
|
||||
- `src-tauri/resources/engine`: Motrix 기반 번들 aria2 바이너리 포함
|
||||
- `src-tauri/src/engine.rs`: RPC 포트 점유 시 기존 aria2 인스턴스 재사용
|
||||
- `@tauri-apps/plugin-dialog` 연동: 기본 저장 폴더 네이티브 선택 구현
|
||||
- `src/App.vue`: 엔진 제어를 수동 버튼 방식에서 자동 관리(Motrix 스타일)로 전환
|
||||
- `src/App.vue` + `src/style.css`: 메인 다운로드 화면의 엔진 하단 패널 제거(자동 엔진 관리 기반으로 UI 단순화)
|
||||
- `src/App.vue`: 저장 폴더 선택 버튼 상호작용 안정화(label 중첩 제거)
|
||||
- `src/style.css`: 설정 드롭다운(select) 플랫 스타일 적용
|
||||
- `src-tauri/capabilities/default.json`: dialog open 권한 추가
|
||||
- `src-tauri/tauri.conf.json`: 초기 창 크기 확대 및 `main` 라벨 명시
|
||||
- `src-tauri/src/engine.rs`: 삭제 동작 안정화(`aria2.remove` 실패 시 `aria2.removeDownloadResult` 폴백, 이미 삭제된 GID idempotent 처리)
|
||||
- `src/App.vue` + `src/style.css`: 다운로드 리스트를 테이블에서 Motrix형 카드 + 아이콘 액션 버튼 UI로 재구성
|
||||
- `src/App.vue` + `src/style.css`: 다운로드 화면 레이아웃을 스크린샷 기준(라이트 톤, 좌측 작업 패널, 상단 아이콘 툴바)으로 재정렬
|
||||
- `src-tauri/src/engine.rs` + `src/lib/engineApi.ts`: 파일관리자에서 경로 열기 커맨드(`open_path_in_file_manager`) 추가
|
||||
- `src-tauri/src/engine.rs`: task summary에 `uri` 노출 추가(링크 복사용)
|
||||
- `src/App.vue`: Motrix `TaskActions`/`TaskItemActions` 기능 매핑에 맞춘 상단/항목 아이콘 동작 연결
|
||||
- `src-tauri/src/engine.rs`: aria2 프로세스 시작/중지/상태 조회
|
||||
- `src-tauri/src/engine.rs`: 바이너리 자동 탐지 + 에러 분류 + 작업 제어 RPC 커맨드
|
||||
- `src-tauri/src/lib.rs`: Tauri invoke handler 연결
|
||||
- `src/lib/engineApi.ts`: 프런트 command 호출 래퍼
|
||||
- `src/App.vue`: 엔진 제어 UI
|
||||
- `src/lib/engineApi.ts`: 프런트 command 호출 래퍼(엔진 + 작업 제어)
|
||||
- `src/App.vue`: Motrix 스타일 사이드바/목록/추가 모달/액션 버튼
|
||||
- `src/style.css`: 작업 액션 UI 스타일 보강
|
||||
- 다음 우선순위:
|
||||
1. aria2 바이너리 경로 자동 탐지 + 리소스 번들 구조 확정
|
||||
2. JSON-RPC 기반 다운로드 목록 API 구현
|
||||
3. 설정 저장소 도입
|
||||
1. Motrix `Task Detail` 동등 기능(파일/피어/트래커/활동) 구현
|
||||
2. 설정 저장소 도입(local persist + aria2 global option 적용)
|
||||
3. 선택 기반 배치 작업 액션 및 리스트 인터랙션 개선
|
||||
|
||||
61
docs/TODO.md
Normal file
61
docs/TODO.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# gdown Porting TODO (Motrix 기준)
|
||||
|
||||
마지막 업데이트: 2026-02-24
|
||||
|
||||
## 규칙
|
||||
- 모든 기능은 `/Users/A/Work/Motrix` 기준으로 비교/포팅
|
||||
- 작업 후 반드시 이 파일과 `PORTING_PLAN.md` 동시 업데이트
|
||||
|
||||
## 현재 진행률
|
||||
- 전체: 약 30%
|
||||
- 엔진/RPC: 60%+
|
||||
- UI 동등성: 35~40%
|
||||
- 고급 기능: 10~20%
|
||||
|
||||
## In Progress
|
||||
- [ ] Task Detail 패널 1차 포팅
|
||||
- [ ] General
|
||||
- [ ] Activity
|
||||
- [ ] Files
|
||||
- [ ] Peers
|
||||
- [ ] Trackers
|
||||
|
||||
## Next
|
||||
- [~] 설정 저장소(local) 연결
|
||||
- [x] RPC Port / Secret / Binary Path 저장/복원
|
||||
- [x] Download Dir / Split / Concurrent 저장/복원
|
||||
- [ ] 테마/고급 설정 키 저장/복원
|
||||
- [ ] aria2 global option 반영 커맨드
|
||||
- [ ] `changeGlobalOption` 대응
|
||||
- [ ] 리스트 인터랙션 강화
|
||||
- [ ] 행 선택 상태
|
||||
- [ ] 다중 선택
|
||||
- [ ] 선택 항목 일괄 액션
|
||||
|
||||
## Done
|
||||
- [x] Motrix `TaskActions.vue` / `TaskItemActions.vue` 분석 기반 아이콘 기능 매핑 적용
|
||||
- [x] 항목 아이콘 기능 확장: 폴더 열기(네이티브 파일관리자), 링크 복사(URI 우선), 정보 표시
|
||||
- [x] 다운로드 화면을 스크린샷 기준으로 재정렬(라이트 톤, 좌측 작업 패널, 상단 아이콘 툴바)
|
||||
- [x] 다운로드 리스트를 Motrix 톤의 카드형 레이아웃 + 아이콘 액션 버튼(재개/일시정지/삭제)으로 리디자인
|
||||
- [x] 삭제 시 aria2 상태 경합 보완: `remove` 실패 시 `removeDownloadResult` 폴백 + 이미 삭제된 GID는 성공 처리
|
||||
- [x] dialog 권한(`dialog:allow-open`) 추가로 저장 폴더 선택 버튼 무반응 이슈 보완
|
||||
- [x] Tauri 초기 창 크기 확대(1280x860, 최소 1080x720)
|
||||
- [x] 저장 폴더 탐색 버튼 클릭 전달 구조 수정 (label 중첩 제거 + 클릭 이벤트 안정화)
|
||||
- [x] 설정 페이지 드롭다운(select) 플랫 스타일 적용
|
||||
- [x] 메인 다운로드 하단 엔진 패널 제거 (자동 엔진 관리 UX에 맞게 단일 리스트 집중형 레이아웃 적용)
|
||||
- [x] 기본 폴더 네이티브 선택 다이얼로그 구현 (`@tauri-apps/plugin-dialog`)
|
||||
- [x] 설정 페이지 여백/레이아웃을 Motrix 스크린샷 톤으로 재조정
|
||||
- [x] 설정 페이지 UI를 Motrix 스타일의 컴팩트 밀도 중심으로 재조정
|
||||
- [x] RPC 포트 점유 시 기존 aria2 엔진 재사용 로직 추가
|
||||
- [x] Motrix extra 기반 aria2 번들 리소스 도입 (`src-tauri/resources/engine`)
|
||||
- [x] Motrix -> gdown 엔진 리소스 동기화 스크립트 추가 (`npm run sync:aria2`)
|
||||
- [x] 엔진 수동 버튼 제거, Motrix 스타일 자동 엔진 관리로 전환
|
||||
- [x] 빌드 시 패치 버전 자동 증가 훅 추가 (`scripts/version-bump.sh`, `tauri:build`)
|
||||
- [x] 엔진 시작/중지/상태 조회
|
||||
- [x] URI/Torrent 추가
|
||||
- [x] 토렌트 드래그&드롭
|
||||
- [x] 작업 목록(Active/Waiting/Stopped) 표시
|
||||
- [x] 개별 작업 pause/resume/remove
|
||||
- [x] 전체 pause/resume
|
||||
- [x] aria2 바이너리 자동 탐지 + 시작 오류 분류
|
||||
- [x] 기본 실행 설정 localStorage 저장/복원
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"vue": "^3.5.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -19,6 +20,9 @@
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vue-tsc": "^3.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24 <25"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -1099,6 +1103,15 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz",
|
||||
"integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
"tauri:build": "bash scripts/version-bump.sh && tauri build",
|
||||
"version:bump": "bash scripts/version-bump.sh",
|
||||
"sync:aria2": "bash scripts/sync-aria2-from-motrix.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"vue": "^3.5.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
41
scripts/sync-aria2-from-motrix.sh
Executable file
41
scripts/sync-aria2-from-motrix.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MOTRIX_DIR="${1:-/Users/A/Work/Motrix}"
|
||||
SRC_BASE="$MOTRIX_DIR/extra"
|
||||
DST_BASE="$ROOT_DIR/src-tauri/resources/engine"
|
||||
|
||||
if [ ! -d "$SRC_BASE" ]; then
|
||||
echo "[sync-aria2] Motrix extra directory not found: $SRC_BASE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$DST_BASE"
|
||||
mkdir -p "$DST_BASE"
|
||||
|
||||
copy_engine_dir() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
if [ -d "$src" ]; then
|
||||
mkdir -p "$dst"
|
||||
cp "$src"/aria2c* "$dst"/ 2>/dev/null || true
|
||||
cp "$src"/aria2.conf "$dst"/ 2>/dev/null || true
|
||||
echo "[sync-aria2] copied $src -> $dst"
|
||||
fi
|
||||
}
|
||||
|
||||
# macOS
|
||||
copy_engine_dir "$SRC_BASE/darwin/arm64/engine" "$DST_BASE/darwin/arm64"
|
||||
copy_engine_dir "$SRC_BASE/darwin/x64/engine" "$DST_BASE/darwin/x64"
|
||||
|
||||
# Linux aliases
|
||||
copy_engine_dir "$SRC_BASE/linux/x64/engine" "$DST_BASE/linux/x64"
|
||||
copy_engine_dir "$SRC_BASE/linux/arm64/engine" "$DST_BASE/linux/arm64"
|
||||
copy_engine_dir "$SRC_BASE/linux/armv7l/engine" "$DST_BASE/linux/armv7l"
|
||||
|
||||
# Windows
|
||||
copy_engine_dir "$SRC_BASE/win32/x64/engine" "$DST_BASE/win32/x64"
|
||||
copy_engine_dir "$SRC_BASE/win32/ia32/engine" "$DST_BASE/win32/ia32"
|
||||
|
||||
echo "[sync-aria2] done"
|
||||
54
scripts/version-bump.sh
Executable file
54
scripts/version-bump.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
TAURI_CONF="src-tauri/tauri.conf.json"
|
||||
PKG_JSON="package.json"
|
||||
CARGO_TOML="src-tauri/Cargo.toml"
|
||||
|
||||
if [ ! -f "$TAURI_CONF" ] || [ ! -f "$PKG_JSON" ] || [ ! -f "$CARGO_TOML" ]; then
|
||||
echo "[version-bump] required files are missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_VERSION="$(node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync('$TAURI_CONF','utf8'));process.stdout.write(String(j.version||''));")"
|
||||
|
||||
if ! echo "$CURRENT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "[version-bump] invalid version format in $TAURI_CONF: $CURRENT_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||
|
||||
echo "[version-bump] $CURRENT_VERSION -> $NEW_VERSION"
|
||||
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const files = ['$PKG_JSON', '$TAURI_CONF'];
|
||||
for (const file of files) {
|
||||
const json = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
json.version = '$NEW_VERSION';
|
||||
fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n');
|
||||
}
|
||||
"
|
||||
|
||||
awk -v new_ver="$NEW_VERSION" '
|
||||
BEGIN { in_package=0; done=0 }
|
||||
/^\[package\]/ { in_package=1; print; next }
|
||||
/^\[/ {
|
||||
if ($0 != "[package]") in_package=0
|
||||
}
|
||||
{
|
||||
if (in_package && !done && $1 == "version") {
|
||||
sub(/"[^"]+"/, "\"" new_ver "\"")
|
||||
done=1
|
||||
}
|
||||
print
|
||||
}
|
||||
' "$CARGO_TOML" > "$CARGO_TOML.tmp"
|
||||
mv "$CARGO_TOML.tmp" "$CARGO_TOML"
|
||||
|
||||
echo "[version-bump] updated package.json / tauri.conf.json / Cargo.toml"
|
||||
67
src-tauri/Cargo.lock
generated
67
src-tauri/Cargo.lock
generated
@@ -86,6 +86,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-log",
|
||||
]
|
||||
|
||||
@@ -668,6 +669,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
@@ -2955,6 +2958,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3760,6 +3787,46 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.8.0"
|
||||
|
||||
@@ -23,5 +23,6 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.10.0", features = [] }
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
reqwest = { version = "0.12.24", default-features = false, features = ["json", "rustls-tls"] }
|
||||
base64 = "0.22"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
"core:default",
|
||||
"dialog:allow-open"
|
||||
]
|
||||
}
|
||||
|
||||
91
src-tauri/resources/engine/darwin/arm64/aria2.conf
Normal file
91
src-tauri/resources/engine/darwin/arm64/aria2.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
###############################
|
||||
# Motrix macOS Aria2 config file
|
||||
#
|
||||
# @see https://aria2.github.io/manual/en/html/aria2c.html
|
||||
#
|
||||
###############################
|
||||
|
||||
|
||||
################ RPC ################
|
||||
# Enable JSON-RPC/XML-RPC server.
|
||||
enable-rpc=true
|
||||
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
|
||||
rpc-allow-origin-all=true
|
||||
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
|
||||
rpc-listen-all=true
|
||||
|
||||
|
||||
################ File system ################
|
||||
# Save a control file(*.aria2) every SEC seconds.
|
||||
auto-save-interval=10
|
||||
# Enable disk cache.
|
||||
disk-cache=64M
|
||||
# Specify file allocation method.
|
||||
file-allocation=none
|
||||
# No file allocation is made for files whose size is smaller than SIZE
|
||||
no-file-allocation-limit=64M
|
||||
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
|
||||
save-session-interval=10
|
||||
|
||||
|
||||
################ Task ################
|
||||
# Exclude seed only downloads when counting concurrent active downloads
|
||||
bt-detach-seed-only=true
|
||||
# Verify the peer using certificates specified in --ca-certificate option.
|
||||
check-certificate=false
|
||||
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
|
||||
# without getting a single byte, then force the download to fail.
|
||||
max-file-not-found=10
|
||||
# Set number of tries.
|
||||
max-tries=0
|
||||
# Set the seconds to wait between retries. When SEC > 0, aria2 will retry downloads when the HTTP server returns a 503 response.
|
||||
retry-wait=10
|
||||
# Set the connect timeout in seconds to establish connection to HTTP/FTP/proxy server. After the connection is established, this option makes no effect and --timeout option is used instead.
|
||||
connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
timeout=10
|
||||
# aria2 does not split less than 2*SIZE byte range.
|
||||
min-split-size=1M
|
||||
# Send Accept: deflate, gzip request header.
|
||||
http-accept-gzip=true
|
||||
# Retrieve timestamp of the remote file from the remote HTTP/FTP server and if it is available, apply it to the local file.
|
||||
remote-time=true
|
||||
# Set interval in seconds to output download progress summary. Setting 0 suppresses the output.
|
||||
summary-interval=0
|
||||
# Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename*.
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=128
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
bt-seed-unverified=false
|
||||
# Set the connect timeout in seconds to establish connection to tracker. After the connection is established, this option makes no effect and --bt-tracker-timeout option is used instead.
|
||||
bt-tracker-connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
bt-tracker-timeout=10
|
||||
# Set host and port as an entry point to IPv4 DHT network.
|
||||
dht-entry-point=dht.transmissionbt.com:6881
|
||||
# Set host and port as an entry point to IPv6 DHT network.
|
||||
dht-entry-point6=dht.transmissionbt.com:6881
|
||||
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
|
||||
enable-dht=true
|
||||
# Enable IPv6 DHT functionality.
|
||||
enable-dht6=true
|
||||
# Enable Peer Exchange extension.
|
||||
enable-peer-exchange=true
|
||||
# Specify the string used during the bitorrent extended handshake for the peer's client version.
|
||||
peer-agent=Transmission/3.00
|
||||
# Specify the prefix of peer ID.
|
||||
peer-id-prefix=-TR3000-
|
||||
BIN
src-tauri/resources/engine/darwin/arm64/aria2c
Executable file
BIN
src-tauri/resources/engine/darwin/arm64/aria2c
Executable file
Binary file not shown.
91
src-tauri/resources/engine/darwin/x64/aria2.conf
Normal file
91
src-tauri/resources/engine/darwin/x64/aria2.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
###############################
|
||||
# Motrix macOS Aria2 config file
|
||||
#
|
||||
# @see https://aria2.github.io/manual/en/html/aria2c.html
|
||||
#
|
||||
###############################
|
||||
|
||||
|
||||
################ RPC ################
|
||||
# Enable JSON-RPC/XML-RPC server.
|
||||
enable-rpc=true
|
||||
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
|
||||
rpc-allow-origin-all=true
|
||||
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
|
||||
rpc-listen-all=true
|
||||
|
||||
|
||||
################ File system ################
|
||||
# Save a control file(*.aria2) every SEC seconds.
|
||||
auto-save-interval=10
|
||||
# Enable disk cache.
|
||||
disk-cache=64M
|
||||
# Specify file allocation method.
|
||||
file-allocation=none
|
||||
# No file allocation is made for files whose size is smaller than SIZE
|
||||
no-file-allocation-limit=64M
|
||||
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
|
||||
save-session-interval=10
|
||||
|
||||
|
||||
################ Task ################
|
||||
# Exclude seed only downloads when counting concurrent active downloads
|
||||
bt-detach-seed-only=true
|
||||
# Verify the peer using certificates specified in --ca-certificate option.
|
||||
check-certificate=false
|
||||
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
|
||||
# without getting a single byte, then force the download to fail.
|
||||
max-file-not-found=10
|
||||
# Set number of tries.
|
||||
max-tries=0
|
||||
# Set the seconds to wait between retries. When SEC > 0, aria2 will retry downloads when the HTTP server returns a 503 response.
|
||||
retry-wait=10
|
||||
# Set the connect timeout in seconds to establish connection to HTTP/FTP/proxy server. After the connection is established, this option makes no effect and --timeout option is used instead.
|
||||
connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
timeout=10
|
||||
# aria2 does not split less than 2*SIZE byte range.
|
||||
min-split-size=1M
|
||||
# Send Accept: deflate, gzip request header.
|
||||
http-accept-gzip=true
|
||||
# Retrieve timestamp of the remote file from the remote HTTP/FTP server and if it is available, apply it to the local file.
|
||||
remote-time=true
|
||||
# Set interval in seconds to output download progress summary. Setting 0 suppresses the output.
|
||||
summary-interval=0
|
||||
# Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename*.
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=128
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
bt-seed-unverified=false
|
||||
# Set the connect timeout in seconds to establish connection to tracker. After the connection is established, this option makes no effect and --bt-tracker-timeout option is used instead.
|
||||
bt-tracker-connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
bt-tracker-timeout=10
|
||||
# Set host and port as an entry point to IPv4 DHT network.
|
||||
dht-entry-point=dht.transmissionbt.com:6881
|
||||
# Set host and port as an entry point to IPv6 DHT network.
|
||||
dht-entry-point6=dht.transmissionbt.com:6881
|
||||
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
|
||||
enable-dht=true
|
||||
# Enable IPv6 DHT functionality.
|
||||
enable-dht6=true
|
||||
# Enable Peer Exchange extension.
|
||||
enable-peer-exchange=true
|
||||
# Specify the string used during the bitorrent extended handshake for the peer's client version.
|
||||
peer-agent=Transmission/3.00
|
||||
# Specify the prefix of peer ID.
|
||||
peer-id-prefix=-TR3000-
|
||||
BIN
src-tauri/resources/engine/darwin/x64/aria2c
Executable file
BIN
src-tauri/resources/engine/darwin/x64/aria2c
Executable file
Binary file not shown.
91
src-tauri/resources/engine/linux/arm64/aria2.conf
Normal file
91
src-tauri/resources/engine/linux/arm64/aria2.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
###############################
|
||||
# Motrix Linux Aria2 config file
|
||||
#
|
||||
# @see https://aria2.github.io/manual/en/html/aria2c.html
|
||||
#
|
||||
###############################
|
||||
|
||||
|
||||
################ RPC ################
|
||||
# Enable JSON-RPC/XML-RPC server.
|
||||
enable-rpc=true
|
||||
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
|
||||
rpc-allow-origin-all=true
|
||||
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
|
||||
rpc-listen-all=true
|
||||
|
||||
|
||||
################ File system ################
|
||||
# Save a control file(*.aria2) every SEC seconds.
|
||||
auto-save-interval=10
|
||||
# Enable disk cache.
|
||||
disk-cache=64M
|
||||
# Specify file allocation method.
|
||||
file-allocation=trunc
|
||||
# No file allocation is made for files whose size is smaller than SIZE
|
||||
no-file-allocation-limit=64M
|
||||
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
|
||||
save-session-interval=10
|
||||
|
||||
|
||||
################ Task ################
|
||||
# Exclude seed only downloads when counting concurrent active downloads
|
||||
bt-detach-seed-only=true
|
||||
# Verify the peer using certificates specified in --ca-certificate option.
|
||||
check-certificate=false
|
||||
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
|
||||
# without getting a single byte, then force the download to fail.
|
||||
max-file-not-found=10
|
||||
# Set number of tries.
|
||||
max-tries=0
|
||||
# Set the seconds to wait between retries. When SEC > 0, aria2 will retry downloads when the HTTP server returns a 503 response.
|
||||
retry-wait=10
|
||||
# Set the connect timeout in seconds to establish connection to HTTP/FTP/proxy server. After the connection is established, this option makes no effect and --timeout option is used instead.
|
||||
connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
timeout=10
|
||||
# aria2 does not split less than 2*SIZE byte range.
|
||||
min-split-size=1M
|
||||
# Send Accept: deflate, gzip request header.
|
||||
http-accept-gzip=true
|
||||
# Retrieve timestamp of the remote file from the remote HTTP/FTP server and if it is available, apply it to the local file.
|
||||
remote-time=true
|
||||
# Set interval in seconds to output download progress summary. Setting 0 suppresses the output.
|
||||
summary-interval=0
|
||||
# Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename*.
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=128
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
bt-seed-unverified=false
|
||||
# Set the connect timeout in seconds to establish connection to tracker. After the connection is established, this option makes no effect and --bt-tracker-timeout option is used instead.
|
||||
bt-tracker-connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
bt-tracker-timeout=10
|
||||
# Set host and port as an entry point to IPv4 DHT network.
|
||||
dht-entry-point=dht.transmissionbt.com:6881
|
||||
# Set host and port as an entry point to IPv6 DHT network.
|
||||
dht-entry-point6=dht.transmissionbt.com:6881
|
||||
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
|
||||
enable-dht=true
|
||||
# Enable IPv6 DHT functionality.
|
||||
enable-dht6=true
|
||||
# Enable Peer Exchange extension.
|
||||
enable-peer-exchange=true
|
||||
# Specify the string used during the bitorrent extended handshake for the peer's client version.
|
||||
peer-agent=Transmission/3.00
|
||||
# Specify the prefix of peer ID.
|
||||
peer-id-prefix=-TR3000-
|
||||
BIN
src-tauri/resources/engine/linux/arm64/aria2c
Executable file
BIN
src-tauri/resources/engine/linux/arm64/aria2c
Executable file
Binary file not shown.
91
src-tauri/resources/engine/linux/armv7l/aria2.conf
Normal file
91
src-tauri/resources/engine/linux/armv7l/aria2.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
###############################
|
||||
# Motrix Linux Aria2 config file
|
||||
#
|
||||
# @see https://aria2.github.io/manual/en/html/aria2c.html
|
||||
#
|
||||
###############################
|
||||
|
||||
|
||||
################ RPC ################
|
||||
# Enable JSON-RPC/XML-RPC server.
|
||||
enable-rpc=true
|
||||
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
|
||||
rpc-allow-origin-all=true
|
||||
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
|
||||
rpc-listen-all=true
|
||||
|
||||
|
||||
################ File system ################
|
||||
# Save a control file(*.aria2) every SEC seconds.
|
||||
auto-save-interval=10
|
||||
# Enable disk cache.
|
||||
disk-cache=64M
|
||||
# Specify file allocation method.
|
||||
file-allocation=trunc
|
||||
# No file allocation is made for files whose size is smaller than SIZE
|
||||
no-file-allocation-limit=64M
|
||||
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
|
||||
save-session-interval=10
|
||||
|
||||
|
||||
################ Task ################
|
||||
# Exclude seed only downloads when counting concurrent active downloads
|
||||
bt-detach-seed-only=true
|
||||
# Verify the peer using certificates specified in --ca-certificate option.
|
||||
check-certificate=false
|
||||
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
|
||||
# without getting a single byte, then force the download to fail.
|
||||
max-file-not-found=10
|
||||
# Set number of tries.
|
||||
max-tries=0
|
||||
# Set the seconds to wait between retries. When SEC > 0, aria2 will retry downloads when the HTTP server returns a 503 response.
|
||||
retry-wait=10
|
||||
# Set the connect timeout in seconds to establish connection to HTTP/FTP/proxy server. After the connection is established, this option makes no effect and --timeout option is used instead.
|
||||
connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
timeout=10
|
||||
# aria2 does not split less than 2*SIZE byte range.
|
||||
min-split-size=1M
|
||||
# Send Accept: deflate, gzip request header.
|
||||
http-accept-gzip=true
|
||||
# Retrieve timestamp of the remote file from the remote HTTP/FTP server and if it is available, apply it to the local file.
|
||||
remote-time=true
|
||||
# Set interval in seconds to output download progress summary. Setting 0 suppresses the output.
|
||||
summary-interval=0
|
||||
# Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename*.
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=128
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
bt-seed-unverified=false
|
||||
# Set the connect timeout in seconds to establish connection to tracker. After the connection is established, this option makes no effect and --bt-tracker-timeout option is used instead.
|
||||
bt-tracker-connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
bt-tracker-timeout=10
|
||||
# Set host and port as an entry point to IPv4 DHT network.
|
||||
dht-entry-point=dht.transmissionbt.com:6881
|
||||
# Set host and port as an entry point to IPv6 DHT network.
|
||||
dht-entry-point6=dht.transmissionbt.com:6881
|
||||
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
|
||||
enable-dht=true
|
||||
# Enable IPv6 DHT functionality.
|
||||
enable-dht6=true
|
||||
# Enable Peer Exchange extension.
|
||||
enable-peer-exchange=true
|
||||
# Specify the string used during the bitorrent extended handshake for the peer's client version.
|
||||
peer-agent=Transmission/3.00
|
||||
# Specify the prefix of peer ID.
|
||||
peer-id-prefix=-TR3000-
|
||||
BIN
src-tauri/resources/engine/linux/armv7l/aria2c
Executable file
BIN
src-tauri/resources/engine/linux/armv7l/aria2c
Executable file
Binary file not shown.
91
src-tauri/resources/engine/linux/x64/aria2.conf
Normal file
91
src-tauri/resources/engine/linux/x64/aria2.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
###############################
|
||||
# Motrix Linux Aria2 config file
|
||||
#
|
||||
# @see https://aria2.github.io/manual/en/html/aria2c.html
|
||||
#
|
||||
###############################
|
||||
|
||||
|
||||
################ RPC ################
|
||||
# Enable JSON-RPC/XML-RPC server.
|
||||
enable-rpc=true
|
||||
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
|
||||
rpc-allow-origin-all=true
|
||||
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
|
||||
rpc-listen-all=true
|
||||
|
||||
|
||||
################ File system ################
|
||||
# Save a control file(*.aria2) every SEC seconds.
|
||||
auto-save-interval=10
|
||||
# Enable disk cache.
|
||||
disk-cache=64M
|
||||
# Specify file allocation method.
|
||||
file-allocation=trunc
|
||||
# No file allocation is made for files whose size is smaller than SIZE
|
||||
no-file-allocation-limit=64M
|
||||
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
|
||||
save-session-interval=10
|
||||
|
||||
|
||||
################ Task ################
|
||||
# Exclude seed only downloads when counting concurrent active downloads
|
||||
bt-detach-seed-only=true
|
||||
# Verify the peer using certificates specified in --ca-certificate option.
|
||||
check-certificate=false
|
||||
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
|
||||
# without getting a single byte, then force the download to fail.
|
||||
max-file-not-found=10
|
||||
# Set number of tries.
|
||||
max-tries=0
|
||||
# Set the seconds to wait between retries. When SEC > 0, aria2 will retry downloads when the HTTP server returns a 503 response.
|
||||
retry-wait=10
|
||||
# Set the connect timeout in seconds to establish connection to HTTP/FTP/proxy server. After the connection is established, this option makes no effect and --timeout option is used instead.
|
||||
connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
timeout=10
|
||||
# aria2 does not split less than 2*SIZE byte range.
|
||||
min-split-size=1M
|
||||
# Send Accept: deflate, gzip request header.
|
||||
http-accept-gzip=true
|
||||
# Retrieve timestamp of the remote file from the remote HTTP/FTP server and if it is available, apply it to the local file.
|
||||
remote-time=true
|
||||
# Set interval in seconds to output download progress summary. Setting 0 suppresses the output.
|
||||
summary-interval=0
|
||||
# Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename*.
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=128
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
bt-seed-unverified=false
|
||||
# Set the connect timeout in seconds to establish connection to tracker. After the connection is established, this option makes no effect and --bt-tracker-timeout option is used instead.
|
||||
bt-tracker-connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
bt-tracker-timeout=10
|
||||
# Set host and port as an entry point to IPv4 DHT network.
|
||||
dht-entry-point=dht.transmissionbt.com:6881
|
||||
# Set host and port as an entry point to IPv6 DHT network.
|
||||
dht-entry-point6=dht.transmissionbt.com:6881
|
||||
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
|
||||
enable-dht=true
|
||||
# Enable IPv6 DHT functionality.
|
||||
enable-dht6=true
|
||||
# Enable Peer Exchange extension.
|
||||
enable-peer-exchange=true
|
||||
# Specify the string used during the bitorrent extended handshake for the peer's client version.
|
||||
peer-agent=Transmission/3.00
|
||||
# Specify the prefix of peer ID.
|
||||
peer-id-prefix=-TR3000-
|
||||
BIN
src-tauri/resources/engine/linux/x64/aria2c
Executable file
BIN
src-tauri/resources/engine/linux/x64/aria2c
Executable file
Binary file not shown.
91
src-tauri/resources/engine/win32/ia32/aria2.conf
Normal file
91
src-tauri/resources/engine/win32/ia32/aria2.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
###############################
|
||||
# Motrix Windows Aria2 config file
|
||||
#
|
||||
# @see https://aria2.github.io/manual/en/html/aria2c.html
|
||||
#
|
||||
###############################
|
||||
|
||||
|
||||
################ RPC ################
|
||||
# Enable JSON-RPC/XML-RPC server.
|
||||
enable-rpc=true
|
||||
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
|
||||
rpc-allow-origin-all=true
|
||||
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
|
||||
rpc-listen-all=true
|
||||
|
||||
|
||||
################ File system ################
|
||||
# Save a control file(*.aria2) every SEC seconds.
|
||||
auto-save-interval=10
|
||||
# Enable disk cache.
|
||||
disk-cache=64M
|
||||
# Specify file allocation method.
|
||||
file-allocation=none
|
||||
# No file allocation is made for files whose size is smaller than SIZE
|
||||
no-file-allocation-limit=64M
|
||||
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
|
||||
save-session-interval=10
|
||||
|
||||
|
||||
################ Task ################
|
||||
# Exclude seed only downloads when counting concurrent active downloads
|
||||
bt-detach-seed-only=true
|
||||
# Verify the peer using certificates specified in --ca-certificate option.
|
||||
check-certificate=false
|
||||
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
|
||||
# without getting a single byte, then force the download to fail.
|
||||
max-file-not-found=10
|
||||
# Set number of tries.
|
||||
max-tries=0
|
||||
# Set the seconds to wait between retries. When SEC > 0, aria2 will retry downloads when the HTTP server returns a 503 response.
|
||||
retry-wait=10
|
||||
# Set the connect timeout in seconds to establish connection to HTTP/FTP/proxy server. After the connection is established, this option makes no effect and --timeout option is used instead.
|
||||
connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
timeout=10
|
||||
# aria2 does not split less than 2*SIZE byte range.
|
||||
min-split-size=1M
|
||||
# Send Accept: deflate, gzip request header.
|
||||
http-accept-gzip=true
|
||||
# Retrieve timestamp of the remote file from the remote HTTP/FTP server and if it is available, apply it to the local file.
|
||||
remote-time=true
|
||||
# Set interval in seconds to output download progress summary. Setting 0 suppresses the output.
|
||||
summary-interval=0
|
||||
# Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename*.
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=128
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
bt-seed-unverified=false
|
||||
# Set the connect timeout in seconds to establish connection to tracker. After the connection is established, this option makes no effect and --bt-tracker-timeout option is used instead.
|
||||
bt-tracker-connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
bt-tracker-timeout=10
|
||||
# Set host and port as an entry point to IPv4 DHT network.
|
||||
dht-entry-point=dht.transmissionbt.com:6881
|
||||
# Set host and port as an entry point to IPv6 DHT network.
|
||||
dht-entry-point6=dht.transmissionbt.com:6881
|
||||
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
|
||||
enable-dht=true
|
||||
# Enable IPv6 DHT functionality.
|
||||
enable-dht6=true
|
||||
# Enable Peer Exchange extension.
|
||||
enable-peer-exchange=true
|
||||
# Specify the string used during the bitorrent extended handshake for the peer's client version.
|
||||
peer-agent=Transmission/3.00
|
||||
# Specify the prefix of peer ID.
|
||||
peer-id-prefix=-TR3000-
|
||||
BIN
src-tauri/resources/engine/win32/ia32/aria2c.exe
Executable file
BIN
src-tauri/resources/engine/win32/ia32/aria2c.exe
Executable file
Binary file not shown.
91
src-tauri/resources/engine/win32/x64/aria2.conf
Normal file
91
src-tauri/resources/engine/win32/x64/aria2.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
###############################
|
||||
# Motrix Windows Aria2 config file
|
||||
#
|
||||
# @see https://aria2.github.io/manual/en/html/aria2c.html
|
||||
#
|
||||
###############################
|
||||
|
||||
|
||||
################ RPC ################
|
||||
# Enable JSON-RPC/XML-RPC server.
|
||||
enable-rpc=true
|
||||
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
|
||||
rpc-allow-origin-all=true
|
||||
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
|
||||
rpc-listen-all=true
|
||||
|
||||
|
||||
################ File system ################
|
||||
# Save a control file(*.aria2) every SEC seconds.
|
||||
auto-save-interval=10
|
||||
# Enable disk cache.
|
||||
disk-cache=64M
|
||||
# Specify file allocation method.
|
||||
file-allocation=falloc
|
||||
# No file allocation is made for files whose size is smaller than SIZE
|
||||
no-file-allocation-limit=64M
|
||||
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
|
||||
save-session-interval=10
|
||||
|
||||
|
||||
################ Task ################
|
||||
# Exclude seed only downloads when counting concurrent active downloads
|
||||
bt-detach-seed-only=true
|
||||
# Verify the peer using certificates specified in --ca-certificate option.
|
||||
check-certificate=false
|
||||
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
|
||||
# without getting a single byte, then force the download to fail.
|
||||
max-file-not-found=10
|
||||
# Set number of tries.
|
||||
max-tries=0
|
||||
# Set the seconds to wait between retries. When SEC > 0, aria2 will retry downloads when the HTTP server returns a 503 response.
|
||||
retry-wait=10
|
||||
# Set the connect timeout in seconds to establish connection to HTTP/FTP/proxy server. After the connection is established, this option makes no effect and --timeout option is used instead.
|
||||
connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
timeout=10
|
||||
# aria2 does not split less than 2*SIZE byte range.
|
||||
min-split-size=1M
|
||||
# Send Accept: deflate, gzip request header.
|
||||
http-accept-gzip=true
|
||||
# Retrieve timestamp of the remote file from the remote HTTP/FTP server and if it is available, apply it to the local file.
|
||||
remote-time=true
|
||||
# Set interval in seconds to output download progress summary. Setting 0 suppresses the output.
|
||||
summary-interval=0
|
||||
# Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename*.
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=128
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
bt-seed-unverified=false
|
||||
# Set the connect timeout in seconds to establish connection to tracker. After the connection is established, this option makes no effect and --bt-tracker-timeout option is used instead.
|
||||
bt-tracker-connect-timeout=10
|
||||
# Set timeout in seconds.
|
||||
bt-tracker-timeout=10
|
||||
# Set host and port as an entry point to IPv4 DHT network.
|
||||
dht-entry-point=dht.transmissionbt.com:6881
|
||||
# Set host and port as an entry point to IPv6 DHT network.
|
||||
dht-entry-point6=dht.transmissionbt.com:6881
|
||||
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
|
||||
enable-dht=true
|
||||
# Enable IPv6 DHT functionality.
|
||||
enable-dht6=true
|
||||
# Enable Peer Exchange extension.
|
||||
enable-peer-exchange=true
|
||||
# Specify the string used during the bitorrent extended handshake for the peer's client version.
|
||||
peer-agent=Transmission/3.00
|
||||
# Specify the prefix of peer ID.
|
||||
peer-id-prefix=-TR3000-
|
||||
BIN
src-tauri/resources/engine/win32/x64/aria2c.exe
Executable file
BIN
src-tauri/resources/engine/win32/x64/aria2c.exe
Executable file
Binary file not shown.
@@ -2,11 +2,13 @@ use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::env;
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::path::Path;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::State;
|
||||
use tauri::{Manager, State};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -46,6 +48,13 @@ pub struct Aria2AddTorrentRequest {
|
||||
pub split: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Aria2TaskCommandRequest {
|
||||
pub rpc: Aria2RpcConfig,
|
||||
pub gid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EngineStatusResponse {
|
||||
@@ -66,6 +75,7 @@ pub struct Aria2TaskSummary {
|
||||
pub download_speed: String,
|
||||
pub dir: String,
|
||||
pub file_name: String,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -84,9 +94,20 @@ pub struct TorrentFilePayload {
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Aria2BinaryProbeResponse {
|
||||
pub found: bool,
|
||||
pub binary_path: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub candidates: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EngineRuntime {
|
||||
child: Option<Child>,
|
||||
external_reuse: bool,
|
||||
rpc_port: Option<u16>,
|
||||
binary_path: Option<String>,
|
||||
args: Vec<String>,
|
||||
started_at: Option<u64>,
|
||||
@@ -96,6 +117,8 @@ impl Default for EngineRuntime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
child: None,
|
||||
external_reuse: false,
|
||||
rpc_port: None,
|
||||
binary_path: None,
|
||||
args: vec![],
|
||||
started_at: None,
|
||||
@@ -111,7 +134,7 @@ pub struct EngineState {
|
||||
impl EngineState {
|
||||
fn status(runtime: &EngineRuntime) -> EngineStatusResponse {
|
||||
EngineStatusResponse {
|
||||
running: runtime.child.is_some(),
|
||||
running: runtime.child.is_some() || runtime.external_reuse,
|
||||
pid: runtime.child.as_ref().map(std::process::Child::id),
|
||||
binary_path: runtime.binary_path.clone(),
|
||||
args: runtime.args.clone(),
|
||||
@@ -153,6 +176,133 @@ fn default_aria2_binary() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_binary_hint(binary_hint: Option<&str>) -> String {
|
||||
let value = binary_hint.unwrap_or("").trim();
|
||||
if value.is_empty() {
|
||||
default_aria2_binary()
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate_push(path: &Path, out: &mut Vec<String>) {
|
||||
if let Some(s) = path.to_str() {
|
||||
let v = s.trim();
|
||||
if !v.is_empty() {
|
||||
out.push(v.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn platform_aliases() -> Vec<&'static str> {
|
||||
match env::consts::OS {
|
||||
"macos" => vec!["macos", "darwin"],
|
||||
"windows" => vec!["windows", "win32"],
|
||||
"linux" => vec!["linux"],
|
||||
_ => vec![env::consts::OS],
|
||||
}
|
||||
}
|
||||
|
||||
fn arch_aliases() -> Vec<&'static str> {
|
||||
match env::consts::ARCH {
|
||||
"aarch64" => vec!["aarch64", "arm64"],
|
||||
"x86_64" => vec!["x86_64", "x64"],
|
||||
"x86" => vec!["x86", "ia32"],
|
||||
"arm" => vec!["arm", "armv7l"],
|
||||
other => vec![other],
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_binary_candidates(app: &tauri::AppHandle, binary_hint: Option<&str>) -> Vec<String> {
|
||||
let binary_name = normalize_binary_hint(binary_hint);
|
||||
let mut candidates: Vec<String> = vec![];
|
||||
|
||||
if let Some(raw) = binary_hint.map(str::trim).filter(|v| !v.is_empty()) {
|
||||
candidates.push(raw.to_string());
|
||||
}
|
||||
|
||||
if let Ok(env_path) = env::var("ARIA2C_BIN") {
|
||||
let trimmed = env_path.trim();
|
||||
if !trimmed.is_empty() {
|
||||
candidates.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
if let Ok(env_path) = env::var("ARIA2C_PATH") {
|
||||
let trimmed = env_path.trim();
|
||||
if !trimmed.is_empty() {
|
||||
candidates.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path_var) = env::var_os("PATH") {
|
||||
for dir in env::split_paths(&path_var) {
|
||||
candidate_push(&dir.join(&binary_name), &mut candidates);
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let lower = binary_name.to_ascii_lowercase();
|
||||
if !lower.ends_with(".exe") {
|
||||
candidate_push(&dir.join(format!("{binary_name}.exe")), &mut candidates);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let source_engine_base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources").join("engine");
|
||||
let mut engine_bases = vec![source_engine_base];
|
||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||
engine_bases.push(resource_dir.join("engine"));
|
||||
}
|
||||
|
||||
let platforms = platform_aliases();
|
||||
let arches = arch_aliases();
|
||||
for base in engine_bases {
|
||||
for platform in &platforms {
|
||||
for arch in &arches {
|
||||
candidate_push(&base.join(platform).join(arch).join(&binary_name), &mut candidates);
|
||||
}
|
||||
candidate_push(&base.join(platform).join(&binary_name), &mut candidates);
|
||||
}
|
||||
candidate_push(&base.join(&binary_name), &mut candidates);
|
||||
}
|
||||
|
||||
let mut deduped = Vec::with_capacity(candidates.len());
|
||||
for path in candidates {
|
||||
if !deduped.contains(&path) {
|
||||
deduped.push(path);
|
||||
}
|
||||
}
|
||||
deduped
|
||||
}
|
||||
|
||||
fn resolve_binary_from_candidates(candidates: &[String]) -> Option<(String, String)> {
|
||||
for (idx, candidate) in candidates.iter().enumerate() {
|
||||
let path = Path::new(candidate);
|
||||
if path.is_file() {
|
||||
let source = if idx == 0 { "user_or_default" } else { "auto_detected" };
|
||||
return Some((candidate.clone(), source.to_string()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn classify_engine_spawn_error(binary: &str, err: &std::io::Error) -> String {
|
||||
use std::io::ErrorKind;
|
||||
match err.kind() {
|
||||
ErrorKind::NotFound => format!(
|
||||
"aria2 binary not found: '{binary}'. Binary Path를 지정하거나 PATH/resources/engine 경로를 확인하세요."
|
||||
),
|
||||
ErrorKind::PermissionDenied => {
|
||||
format!("aria2 binary is not executable: '{binary}'. 실행 권한(chmod +x)을 확인하세요.")
|
||||
}
|
||||
_ => format!("failed to start aria2 engine with '{binary}': {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_local_port_open(port: u16) -> bool {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(220)).is_ok()
|
||||
}
|
||||
|
||||
fn rpc_endpoint(config: &Aria2RpcConfig) -> String {
|
||||
format!(
|
||||
"http://127.0.0.1:{}/jsonrpc",
|
||||
@@ -249,6 +399,17 @@ fn map_task(task: &Value) -> Aria2TaskSummary {
|
||||
download_speed: value_to_string(task.get("downloadSpeed")),
|
||||
dir: value_to_string(task.get("dir")),
|
||||
file_name: pick_file_name(file_path),
|
||||
uri: task
|
||||
.get("files")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|files| files.first())
|
||||
.and_then(|file| file.get("uris"))
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|uris| uris.first())
|
||||
.and_then(|uri| uri.get("uri"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,16 +438,33 @@ fn build_rpc_options(out: Option<&String>, dir: Option<&String>, split: Option<u
|
||||
Value::Object(options)
|
||||
}
|
||||
|
||||
fn is_gid_not_found_error(message: &str) -> bool {
|
||||
let lower = message.to_ascii_lowercase();
|
||||
lower.contains("not found for gid#")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn engine_start(
|
||||
app: tauri::AppHandle,
|
||||
state: State<'_, EngineState>,
|
||||
request: EngineStartRequest,
|
||||
) -> Result<EngineStatusResponse, String> {
|
||||
let rpc_port = request.rpc_listen_port.unwrap_or(6800);
|
||||
let mut runtime = state
|
||||
.runtime
|
||||
.lock()
|
||||
.map_err(|err| format!("failed to lock engine state: {err}"))?;
|
||||
|
||||
if runtime.external_reuse {
|
||||
if let Some(port) = runtime.rpc_port {
|
||||
if is_local_port_open(port) {
|
||||
return Ok(EngineState::status(&runtime));
|
||||
}
|
||||
}
|
||||
runtime.external_reuse = false;
|
||||
runtime.rpc_port = None;
|
||||
}
|
||||
|
||||
if let Some(child) = runtime.child.as_mut() {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => {
|
||||
@@ -302,7 +480,31 @@ pub fn engine_start(
|
||||
}
|
||||
|
||||
let args = build_engine_args(&request);
|
||||
let binary = request.binary_path.unwrap_or_else(default_aria2_binary);
|
||||
|
||||
// Reuse existing engine when the target RPC port is already occupied.
|
||||
// This mirrors Motrix-style behavior where an already-running aria2 instance is reused.
|
||||
if is_local_port_open(rpc_port) {
|
||||
runtime.child = None;
|
||||
runtime.external_reuse = true;
|
||||
runtime.rpc_port = Some(rpc_port);
|
||||
runtime.binary_path = Some(format!("external://127.0.0.1:{rpc_port}"));
|
||||
runtime.args = args;
|
||||
runtime.started_at = Some(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|err| format!("failed to get system time: {err}"))?
|
||||
.as_secs(),
|
||||
);
|
||||
return Ok(EngineState::status(&runtime));
|
||||
}
|
||||
|
||||
let candidates = collect_binary_candidates(&app, request.binary_path.as_deref());
|
||||
let (binary, _) = resolve_binary_from_candidates(&candidates).ok_or_else(|| {
|
||||
let sample = candidates.into_iter().take(6).collect::<Vec<String>>().join(", ");
|
||||
format!(
|
||||
"aria2 binary를 찾지 못했습니다. Binary Path를 직접 지정하세요. (검색 후보: {sample})"
|
||||
)
|
||||
})?;
|
||||
|
||||
let child = Command::new(&binary)
|
||||
.args(&args)
|
||||
@@ -310,9 +512,25 @@ pub fn engine_start(
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|err| format!("failed to start aria2 engine with '{binary}': {err}"))?;
|
||||
.map_err(|err| classify_engine_spawn_error(&binary, &err))?;
|
||||
|
||||
let mut child = child;
|
||||
std::thread::sleep(std::time::Duration::from_millis(220));
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
return Err(format!(
|
||||
"aria2 engine exited immediately (status={status}). 포트 충돌 또는 잘못된 옵션일 수 있습니다."
|
||||
));
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
return Err(format!("failed to inspect aria2 engine process: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
runtime.child = Some(child);
|
||||
runtime.external_reuse = false;
|
||||
runtime.rpc_port = Some(rpc_port);
|
||||
runtime.binary_path = Some(binary);
|
||||
runtime.args = args;
|
||||
runtime.started_at = Some(
|
||||
@@ -325,6 +543,21 @@ pub fn engine_start(
|
||||
Ok(EngineState::status(&runtime))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn detect_aria2_binary(
|
||||
app: tauri::AppHandle,
|
||||
binary_path: Option<String>,
|
||||
) -> Aria2BinaryProbeResponse {
|
||||
let candidates = collect_binary_candidates(&app, binary_path.as_deref());
|
||||
let resolved = resolve_binary_from_candidates(&candidates);
|
||||
Aria2BinaryProbeResponse {
|
||||
found: resolved.is_some(),
|
||||
binary_path: resolved.as_ref().map(|v| v.0.clone()),
|
||||
source: resolved.map(|v| v.1),
|
||||
candidates,
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn engine_stop(state: State<'_, EngineState>) -> Result<EngineStatusResponse, String> {
|
||||
let mut runtime = state
|
||||
@@ -339,6 +572,8 @@ pub fn engine_stop(state: State<'_, EngineState>) -> Result<EngineStatusResponse
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
runtime.external_reuse = false;
|
||||
runtime.rpc_port = None;
|
||||
runtime.started_at = None;
|
||||
Ok(EngineState::status(&runtime))
|
||||
}
|
||||
@@ -363,6 +598,15 @@ pub fn engine_status(state: State<'_, EngineState>) -> Result<EngineStatusRespon
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.external_reuse {
|
||||
let alive = runtime.rpc_port.map(is_local_port_open).unwrap_or(false);
|
||||
if !alive {
|
||||
runtime.external_reuse = false;
|
||||
runtime.rpc_port = None;
|
||||
runtime.started_at = None;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(EngineState::status(&runtime))
|
||||
}
|
||||
|
||||
@@ -426,6 +670,105 @@ pub async fn aria2_list_tasks(config: Aria2RpcConfig) -> Result<Aria2TaskSnapsho
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn aria2_pause_task(request: Aria2TaskCommandRequest) -> Result<String, String> {
|
||||
let gid = request.gid.trim();
|
||||
if gid.is_empty() {
|
||||
return Err("gid is required".to_string());
|
||||
}
|
||||
let client = Client::new();
|
||||
let result = call_aria2_rpc(&client, &request.rpc, "aria2.pause", vec![json!(gid)]).await?;
|
||||
result
|
||||
.as_str()
|
||||
.map(|value| value.to_string())
|
||||
.ok_or_else(|| format!("aria2.pause returned unexpected result: {result}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn aria2_resume_task(request: Aria2TaskCommandRequest) -> Result<String, String> {
|
||||
let gid = request.gid.trim();
|
||||
if gid.is_empty() {
|
||||
return Err("gid is required".to_string());
|
||||
}
|
||||
let client = Client::new();
|
||||
let result = call_aria2_rpc(&client, &request.rpc, "aria2.unpause", vec![json!(gid)]).await?;
|
||||
result
|
||||
.as_str()
|
||||
.map(|value| value.to_string())
|
||||
.ok_or_else(|| format!("aria2.unpause returned unexpected result: {result}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn aria2_remove_task(request: Aria2TaskCommandRequest) -> Result<String, String> {
|
||||
let gid = request.gid.trim();
|
||||
if gid.is_empty() {
|
||||
return Err("gid is required".to_string());
|
||||
}
|
||||
let client = Client::new();
|
||||
match call_aria2_rpc(&client, &request.rpc, "aria2.remove", vec![json!(gid)]).await {
|
||||
Ok(result) => result
|
||||
.as_str()
|
||||
.map(|value| value.to_string())
|
||||
.ok_or_else(|| format!("aria2.remove returned unexpected result: {result}")),
|
||||
Err(err) if is_gid_not_found_error(&err) => {
|
||||
match call_aria2_rpc(
|
||||
&client,
|
||||
&request.rpc,
|
||||
"aria2.removeDownloadResult",
|
||||
vec![json!(gid)],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result
|
||||
.as_str()
|
||||
.map(|value| value.to_string())
|
||||
.ok_or_else(|| format!("aria2.removeDownloadResult returned unexpected result: {result}")),
|
||||
Err(fallback_err) if is_gid_not_found_error(&fallback_err) => Ok(gid.to_string()),
|
||||
Err(fallback_err) => Err(fallback_err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn aria2_remove_task_record(request: Aria2TaskCommandRequest) -> Result<String, String> {
|
||||
let gid = request.gid.trim();
|
||||
if gid.is_empty() {
|
||||
return Err("gid is required".to_string());
|
||||
}
|
||||
let client = Client::new();
|
||||
match call_aria2_rpc(&client, &request.rpc, "aria2.removeDownloadResult", vec![json!(gid)]).await
|
||||
{
|
||||
Ok(result) => result
|
||||
.as_str()
|
||||
.map(|value| value.to_string())
|
||||
.ok_or_else(|| format!("aria2.removeDownloadResult returned unexpected result: {result}")),
|
||||
Err(err) if is_gid_not_found_error(&err) => Ok(gid.to_string()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn aria2_pause_all(config: Aria2RpcConfig) -> Result<String, String> {
|
||||
let client = Client::new();
|
||||
let result = call_aria2_rpc(&client, &config, "aria2.pauseAll", vec![]).await?;
|
||||
result
|
||||
.as_str()
|
||||
.map(|value| value.to_string())
|
||||
.ok_or_else(|| format!("aria2.pauseAll returned unexpected result: {result}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn aria2_resume_all(config: Aria2RpcConfig) -> Result<String, String> {
|
||||
let client = Client::new();
|
||||
let result = call_aria2_rpc(&client, &config, "aria2.unpauseAll", vec![]).await?;
|
||||
result
|
||||
.as_str()
|
||||
.map(|value| value.to_string())
|
||||
.ok_or_else(|| format!("aria2.unpauseAll returned unexpected result: {result}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_torrent_file(path: String) -> Result<TorrentFilePayload, String> {
|
||||
if !path.to_ascii_lowercase().ends_with(".torrent") {
|
||||
@@ -445,3 +788,34 @@ pub fn load_torrent_file(path: String) -> Result<TorrentFilePayload, String> {
|
||||
size: bytes.len() as u64,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_path_in_file_manager(path: String) -> Result<(), String> {
|
||||
let target = path.trim();
|
||||
if target.is_empty() {
|
||||
return Err("path is required".to_string());
|
||||
}
|
||||
|
||||
let mut command = if cfg!(target_os = "macos") {
|
||||
let mut cmd = Command::new("open");
|
||||
cmd.arg(target);
|
||||
cmd
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let mut cmd = Command::new("explorer");
|
||||
cmd.arg(target);
|
||||
cmd
|
||||
} else {
|
||||
let mut cmd = Command::new("xdg-open");
|
||||
cmd.arg(target);
|
||||
cmd
|
||||
};
|
||||
|
||||
let status = command
|
||||
.status()
|
||||
.map_err(|err| format!("failed to open path in file manager: {err}"))?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(format!("file manager command exited with status: {status}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
mod engine;
|
||||
|
||||
use engine::{
|
||||
aria2_add_torrent, aria2_add_uri, aria2_list_tasks, engine_start, engine_status, engine_stop,
|
||||
load_torrent_file, EngineState,
|
||||
aria2_add_torrent, aria2_add_uri, aria2_list_tasks, aria2_pause_all, aria2_pause_task,
|
||||
aria2_remove_task, aria2_remove_task_record, aria2_resume_all, aria2_resume_task,
|
||||
detect_aria2_binary, engine_start, engine_status, engine_stop, load_torrent_file,
|
||||
open_path_in_file_manager, EngineState,
|
||||
};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.manage(EngineState::default())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
@@ -23,10 +26,18 @@ pub fn run() {
|
||||
engine_start,
|
||||
engine_stop,
|
||||
engine_status,
|
||||
detect_aria2_binary,
|
||||
aria2_add_torrent,
|
||||
aria2_add_uri,
|
||||
aria2_list_tasks,
|
||||
load_torrent_file
|
||||
aria2_pause_task,
|
||||
aria2_resume_task,
|
||||
aria2_remove_task,
|
||||
aria2_remove_task_record,
|
||||
aria2_pause_all,
|
||||
aria2_resume_all,
|
||||
load_torrent_file,
|
||||
open_path_in_file_manager
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -12,9 +12,12 @@
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "gdown",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"width": 1280,
|
||||
"height": 860,
|
||||
"minWidth": 1080,
|
||||
"minHeight": 720,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
@@ -26,6 +29,9 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"resources": [
|
||||
"resources/engine/**/*"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
505
src/App.vue
505
src/App.vue
@@ -2,14 +2,22 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||
import {
|
||||
addAria2Torrent,
|
||||
addAria2Uri,
|
||||
detectAria2Binary,
|
||||
getEngineStatus,
|
||||
listAria2Tasks,
|
||||
loadTorrentFile,
|
||||
pauseAllAria2,
|
||||
pauseAria2Task,
|
||||
openPathInFileManager,
|
||||
removeAria2Task,
|
||||
removeAria2TaskRecord,
|
||||
resumeAllAria2,
|
||||
resumeAria2Task,
|
||||
startEngine,
|
||||
stopEngine,
|
||||
type Aria2Task,
|
||||
type Aria2TaskSnapshot,
|
||||
type EngineStatus,
|
||||
@@ -19,6 +27,17 @@ type TaskFilter = 'all' | 'active' | 'waiting' | 'stopped'
|
||||
type AddTab = 'url' | 'torrent'
|
||||
type AppPage = 'downloads' | 'settings'
|
||||
type SettingsTab = 'basic' | 'advanced' | 'lab'
|
||||
type PersistedSettings = {
|
||||
binaryPath?: string
|
||||
rpcPort?: number
|
||||
rpcSecret?: string
|
||||
downloadDir?: string
|
||||
split?: number
|
||||
maxConcurrentDownloads?: number
|
||||
autoRefresh?: boolean
|
||||
}
|
||||
|
||||
const SETTINGS_STORAGE_KEY = 'gdown.settings.v1'
|
||||
|
||||
const binaryPath = ref('aria2c')
|
||||
const rpcPort = ref(6800)
|
||||
@@ -30,10 +49,11 @@ const maxConcurrentDownloads = ref(5)
|
||||
const loadingEngine = ref(false)
|
||||
const loadingTasks = ref(false)
|
||||
const loadingAddTask = ref(false)
|
||||
const loadingTaskAction = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const successMessage = ref('')
|
||||
const autoRefresh = ref(true)
|
||||
const filter = ref<TaskFilter>('all')
|
||||
const filter = ref<TaskFilter>('active')
|
||||
const page = ref<AppPage>('downloads')
|
||||
const settingsTab = ref<SettingsTab>('basic')
|
||||
|
||||
@@ -83,16 +103,45 @@ const tasks = ref<Aria2TaskSnapshot>({
|
||||
let refreshTimer: number | null = null
|
||||
let unlistenDragDrop: UnlistenFn | null = null
|
||||
|
||||
const totalCount = computed(() => tasks.value.active.length + tasks.value.waiting.length + tasks.value.stopped.length)
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
if (filter.value === 'active') return tasks.value.active
|
||||
if (filter.value === 'waiting') return tasks.value.waiting
|
||||
if (filter.value === 'stopped') return tasks.value.stopped
|
||||
return [...tasks.value.active, ...tasks.value.waiting, ...tasks.value.stopped]
|
||||
})
|
||||
const filteredTaskCount = computed(() => filteredTasks.value.length)
|
||||
|
||||
const runtimeLabel = computed(() => (status.value.running ? 'Running' : 'Stopped'))
|
||||
function loadSettingsFromStorage() {
|
||||
try {
|
||||
const raw = localStorage.getItem(SETTINGS_STORAGE_KEY)
|
||||
if (!raw) return
|
||||
const parsed = JSON.parse(raw) as PersistedSettings
|
||||
if (typeof parsed.binaryPath === 'string') binaryPath.value = parsed.binaryPath
|
||||
if (typeof parsed.rpcPort === 'number' && Number.isFinite(parsed.rpcPort)) rpcPort.value = parsed.rpcPort
|
||||
if (typeof parsed.rpcSecret === 'string') rpcSecret.value = parsed.rpcSecret
|
||||
if (typeof parsed.downloadDir === 'string') downloadDir.value = parsed.downloadDir
|
||||
if (typeof parsed.split === 'number' && Number.isFinite(parsed.split)) split.value = parsed.split
|
||||
if (typeof parsed.maxConcurrentDownloads === 'number' && Number.isFinite(parsed.maxConcurrentDownloads)) {
|
||||
maxConcurrentDownloads.value = parsed.maxConcurrentDownloads
|
||||
}
|
||||
if (typeof parsed.autoRefresh === 'boolean') autoRefresh.value = parsed.autoRefresh
|
||||
} catch {
|
||||
// ignore malformed settings
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettingsToStorage() {
|
||||
const payload: PersistedSettings = {
|
||||
binaryPath: binaryPath.value,
|
||||
rpcPort: rpcPort.value,
|
||||
rpcSecret: rpcSecret.value,
|
||||
downloadDir: downloadDir.value,
|
||||
split: split.value,
|
||||
maxConcurrentDownloads: maxConcurrentDownloads.value,
|
||||
autoRefresh: autoRefresh.value,
|
||||
}
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(payload))
|
||||
}
|
||||
|
||||
function pushError(message: string) {
|
||||
successMessage.value = ''
|
||||
@@ -111,11 +160,6 @@ function rpcConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(epochSec: number | null): string {
|
||||
if (!epochSec) return '-'
|
||||
return new Date(epochSec * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
function formatBytes(value: string): string {
|
||||
const n = Number(value)
|
||||
if (!Number.isFinite(n)) return '-'
|
||||
@@ -187,39 +231,164 @@ function updateRefreshTimer() {
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
async function onStartEngine() {
|
||||
async function autoStartEngine(silent = true) {
|
||||
loadingEngine.value = true
|
||||
try {
|
||||
let resolvedBinaryPath = binaryPath.value.trim() || undefined
|
||||
const probe = await detectAria2Binary(resolvedBinaryPath).catch(() => null)
|
||||
if (probe?.found && probe.binaryPath) {
|
||||
binaryPath.value = probe.binaryPath
|
||||
resolvedBinaryPath = probe.binaryPath
|
||||
}
|
||||
|
||||
status.value = await startEngine({
|
||||
binaryPath: binaryPath.value.trim() || undefined,
|
||||
binaryPath: resolvedBinaryPath,
|
||||
rpcListenPort: rpcPort.value,
|
||||
rpcSecret: rpcSecret.value.trim() || undefined,
|
||||
downloadDir: downloadDir.value.trim() || undefined,
|
||||
maxConcurrentDownloads: maxConcurrentDownloads.value,
|
||||
split: split.value,
|
||||
})
|
||||
pushSuccess('aria2 engine started.')
|
||||
saveSettingsToStorage()
|
||||
if (!silent) pushSuccess('aria2 engine started.')
|
||||
await refreshTasks()
|
||||
return true
|
||||
} catch (error) {
|
||||
pushError(String(error))
|
||||
return false
|
||||
} finally {
|
||||
loadingEngine.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onStopEngine() {
|
||||
loadingEngine.value = true
|
||||
async function ensureEngineRunning() {
|
||||
if (status.value.running) return true
|
||||
await refreshEngineStatus()
|
||||
if (status.value.running) return true
|
||||
return autoStartEngine(false)
|
||||
}
|
||||
|
||||
async function onPauseAllTasks() {
|
||||
const ready = await ensureEngineRunning()
|
||||
if (!ready) return
|
||||
loadingTaskAction.value = true
|
||||
try {
|
||||
status.value = await stopEngine()
|
||||
tasks.value = { active: [], waiting: [], stopped: [] }
|
||||
pushSuccess('aria2 engine stopped.')
|
||||
await pauseAllAria2(rpcConfig())
|
||||
pushSuccess('모든 작업을 일시정지했습니다.')
|
||||
await refreshTasks()
|
||||
} catch (error) {
|
||||
pushError(String(error))
|
||||
} finally {
|
||||
loadingEngine.value = false
|
||||
loadingTaskAction.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onResumeAllTasks() {
|
||||
const ready = await ensureEngineRunning()
|
||||
if (!ready) return
|
||||
loadingTaskAction.value = true
|
||||
try {
|
||||
await resumeAllAria2(rpcConfig())
|
||||
pushSuccess('모든 작업을 재개했습니다.')
|
||||
await refreshTasks()
|
||||
} catch (error) {
|
||||
pushError(String(error))
|
||||
} finally {
|
||||
loadingTaskAction.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onTaskAction(task: Aria2Task, action: 'pause' | 'resume' | 'remove' | 'purge') {
|
||||
const ready = await ensureEngineRunning()
|
||||
if (!ready) return
|
||||
loadingTaskAction.value = true
|
||||
try {
|
||||
const payload = { rpc: rpcConfig(), gid: task.gid }
|
||||
if (action === 'pause') {
|
||||
await pauseAria2Task(payload)
|
||||
} else if (action === 'resume') {
|
||||
await resumeAria2Task(payload)
|
||||
} else if (action === 'remove') {
|
||||
await removeAria2Task(payload)
|
||||
} else {
|
||||
await removeAria2TaskRecord(payload)
|
||||
}
|
||||
await refreshTasks()
|
||||
} catch (error) {
|
||||
pushError(String(error))
|
||||
} finally {
|
||||
loadingTaskAction.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemoveFilteredTasks() {
|
||||
const ready = await ensureEngineRunning()
|
||||
if (!ready) return
|
||||
const targetTasks = [...filteredTasks.value]
|
||||
if (targetTasks.length === 0) return
|
||||
|
||||
loadingTaskAction.value = true
|
||||
try {
|
||||
for (const task of targetTasks) {
|
||||
const payload = { rpc: rpcConfig(), gid: task.gid }
|
||||
if (task.status === 'stopped') {
|
||||
await removeAria2TaskRecord(payload)
|
||||
} else {
|
||||
await removeAria2Task(payload)
|
||||
}
|
||||
}
|
||||
if (filter.value === 'stopped') {
|
||||
pushSuccess(`${targetTasks.length}개 기록을 정리했습니다.`)
|
||||
} else {
|
||||
pushSuccess(`${targetTasks.length}개 작업을 삭제했습니다.`)
|
||||
}
|
||||
await refreshTasks()
|
||||
} catch (error) {
|
||||
pushError(String(error))
|
||||
} finally {
|
||||
loadingTaskAction.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function taskPrimaryAction(task: Aria2Task): 'pause' | 'resume' {
|
||||
return task.status === 'active' ? 'pause' : 'resume'
|
||||
}
|
||||
|
||||
function taskPrimaryIcon(task: Aria2Task): string {
|
||||
return task.status === 'active' ? '□' : '▶'
|
||||
}
|
||||
|
||||
function taskPrimaryTitle(task: Aria2Task): string {
|
||||
return task.status === 'active' ? '정지' : '재개'
|
||||
}
|
||||
|
||||
async function onOpenTaskFolder(task: Aria2Task) {
|
||||
const dir = task.dir?.trim()
|
||||
if (!dir) {
|
||||
pushError('작업 폴더 경로가 없습니다.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await openPathInFileManager(dir)
|
||||
} catch (error) {
|
||||
pushError(String(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function onCopyTaskLink(task: Aria2Task) {
|
||||
const text = task.uri?.trim() || task.gid
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
pushSuccess('작업 링크를 클립보드에 복사했습니다.')
|
||||
} catch (error) {
|
||||
pushError(`클립보드 복사 실패: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
function onShowTaskInfo(task: Aria2Task) {
|
||||
pushSuccess(`gid=${task.gid} / status=${task.status} / dir=${task.dir || '-'}`)
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
showAddModal.value = true
|
||||
addTab.value = 'url'
|
||||
@@ -234,9 +403,28 @@ function openSettingsPage() {
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
saveSettingsToStorage()
|
||||
pushSuccess('설정이 저장되었습니다.')
|
||||
}
|
||||
|
||||
async function pickDownloadFolder() {
|
||||
try {
|
||||
const selected = await openDialog({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: downloadDir.value || undefined,
|
||||
title: '기본 다운로드 폴더 선택',
|
||||
})
|
||||
if (typeof selected === 'string' && selected.trim()) {
|
||||
downloadDir.value = selected
|
||||
saveSettingsToStorage()
|
||||
pushSuccess('기본 폴더를 변경했습니다.')
|
||||
}
|
||||
} catch (error) {
|
||||
pushError(String(error))
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddModal() {
|
||||
showAddModal.value = false
|
||||
modalDropActive.value = false
|
||||
@@ -388,26 +576,32 @@ async function onWindowDrop(event: DragEvent) {
|
||||
}
|
||||
|
||||
async function onSubmitAddTask() {
|
||||
if (!status.value.running) {
|
||||
pushError('먼저 엔진을 시작하세요.')
|
||||
return
|
||||
}
|
||||
const ready = await ensureEngineRunning()
|
||||
if (!ready) return
|
||||
|
||||
loadingAddTask.value = true
|
||||
try {
|
||||
if (addTab.value === 'url') {
|
||||
if (!addUrl.value.trim()) {
|
||||
const uris = addUrl.value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
if (uris.length === 0) {
|
||||
pushError('URL을 입력하세요.')
|
||||
return
|
||||
}
|
||||
const gid = await addAria2Uri({
|
||||
rpc: rpcConfig(),
|
||||
uri: addUrl.value.trim(),
|
||||
out: addOut.value.trim() || undefined,
|
||||
dir: downloadDir.value.trim() || undefined,
|
||||
split: addSplit.value,
|
||||
})
|
||||
pushSuccess(`작업이 추가되었습니다. gid=${gid}`)
|
||||
const gids: string[] = []
|
||||
for (const uri of uris) {
|
||||
const gid = await addAria2Uri({
|
||||
rpc: rpcConfig(),
|
||||
uri,
|
||||
out: uris.length === 1 ? (addOut.value.trim() || undefined) : undefined,
|
||||
dir: downloadDir.value.trim() || undefined,
|
||||
split: addSplit.value,
|
||||
})
|
||||
gids.push(gid)
|
||||
}
|
||||
pushSuccess(`${gids.length}개 작업이 추가되었습니다.`)
|
||||
} else {
|
||||
if (!torrentBase64.value.trim()) {
|
||||
pushError('.torrent 파일을 선택하세요.')
|
||||
@@ -439,7 +633,11 @@ async function onSubmitAddTask() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadSettingsFromStorage()
|
||||
await refreshEngineStatus()
|
||||
if (!status.value.running) {
|
||||
await autoStartEngine(true)
|
||||
}
|
||||
await refreshTasks()
|
||||
updateRefreshTimer()
|
||||
|
||||
@@ -510,116 +708,85 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section v-if="page === 'downloads'" class="content">
|
||||
<header class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<h1>다운로드 중</h1>
|
||||
<span class="count">{{ totalCount }} tasks</span>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<label class="switcher">
|
||||
<input v-model="autoRefresh" type="checkbox" @change="updateRefreshTimer" />
|
||||
<span>Auto refresh</span>
|
||||
</label>
|
||||
<button class="ghost" :disabled="loadingTasks" @click="refreshTasks()">새로고침</button>
|
||||
</div>
|
||||
</header>
|
||||
<section v-if="page === 'downloads'" class="content downloads-view">
|
||||
<aside class="downloads-filter card">
|
||||
<h2>작업</h2>
|
||||
<button :class="{ active: filter === 'active' }" @click="filter = 'active'">▶ 다운로드 중</button>
|
||||
<button :class="{ active: filter === 'waiting' }" @click="filter = 'waiting'">⏸ 대기 중</button>
|
||||
<button :class="{ active: filter === 'stopped' }" @click="filter = 'stopped'">■ 중단됨</button>
|
||||
</aside>
|
||||
|
||||
<section v-if="errorMessage" class="notice error">{{ errorMessage }}</section>
|
||||
<section v-if="successMessage" class="notice success">{{ successMessage }}</section>
|
||||
<section class="downloads-main">
|
||||
<header class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<h1>{{ filter === 'active' ? '다운로드 중' : filter === 'waiting' ? '대기 중' : '중단됨' }}</h1>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button
|
||||
class="icon-tool ghost"
|
||||
:disabled="loadingTaskAction || filteredTaskCount === 0"
|
||||
title="목록 삭제"
|
||||
@click="onRemoveFilteredTasks"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<button class="icon-tool ghost" :disabled="loadingTasks" title="새로고침" @click="refreshTasks()">↻</button>
|
||||
<button class="icon-tool ghost" :disabled="loadingTaskAction || !status.running" title="전체 재개" @click="onResumeAllTasks">▶</button>
|
||||
<button class="icon-tool ghost" :disabled="loadingTaskAction || !status.running" title="전체 일시정지" @click="onPauseAllTasks">⏸</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section v-if="errorMessage" class="notice error">{{ errorMessage }}</section>
|
||||
<section v-if="successMessage" class="notice success">{{ successMessage }}</section>
|
||||
|
||||
<section class="main-grid">
|
||||
<article class="task-pane card">
|
||||
<div class="tabs">
|
||||
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">전체</button>
|
||||
<button :class="{ active: filter === 'active' }" @click="filter = 'active'">
|
||||
다운로드 중 ({{ tasks.active.length }})
|
||||
</button>
|
||||
<button :class="{ active: filter === 'waiting' }" @click="filter = 'waiting'">
|
||||
대기 중 ({{ tasks.waiting.length }})
|
||||
</button>
|
||||
<button :class="{ active: filter === 'stopped' }" @click="filter = 'stopped'">
|
||||
중단됨 ({{ tasks.stopped.length }})
|
||||
</button>
|
||||
<div class="task-card-list">
|
||||
<article v-for="task in filteredTasks" :key="task.gid" class="task-card">
|
||||
<div class="task-card-head">
|
||||
<div class="file-cell">
|
||||
<strong>{{ task.fileName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="task-actions-pill">
|
||||
<button
|
||||
class="icon-pill ghost"
|
||||
:disabled="loadingTaskAction || task.status === 'stopped'"
|
||||
:title="taskPrimaryTitle(task)"
|
||||
@click="onTaskAction(task, taskPrimaryAction(task))"
|
||||
>
|
||||
{{ taskPrimaryIcon(task) }}
|
||||
</button>
|
||||
<button
|
||||
class="icon-pill ghost"
|
||||
:disabled="loadingTaskAction"
|
||||
title="삭제"
|
||||
@click="onTaskAction(task, task.status === 'stopped' ? 'purge' : 'remove')"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<button class="icon-pill ghost" title="폴더 열기" @click="onOpenTaskFolder(task)">📁</button>
|
||||
<button class="icon-pill ghost" :title="task.uri ? '링크 복사' : 'GID 복사'" @click="onCopyTaskLink(task)">
|
||||
🔗
|
||||
</button>
|
||||
<button class="icon-pill ghost" title="정보" @click="onShowTaskInfo(task)">ⓘ</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-row">
|
||||
<div class="bar">
|
||||
<span :style="{ width: `${progress(task).toFixed(1)}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-meta-row">
|
||||
<span>{{ formatBytes(task.completedLength) }} / {{ formatBytes(task.totalLength) }}</span>
|
||||
<span class="task-meta-right">
|
||||
<span>{{ formatSpeed(task.downloadSpeed) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div v-if="filteredTasks.length === 0" class="empty">현재 다운로드 없음</div>
|
||||
</div>
|
||||
|
||||
<div class="task-table-wrap">
|
||||
<table class="task-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>작업</th>
|
||||
<th>상태</th>
|
||||
<th>진행률</th>
|
||||
<th>속도</th>
|
||||
<th>전체 크기</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="task in filteredTasks" :key="task.gid">
|
||||
<td>
|
||||
<div class="file-cell">
|
||||
<strong>{{ task.fileName || '-' }}</strong>
|
||||
<small>{{ task.gid }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="task.status">{{ task.status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="progress-row">
|
||||
<div class="bar">
|
||||
<span :style="{ width: `${progress(task).toFixed(1)}%` }" />
|
||||
</div>
|
||||
<small>{{ progress(task).toFixed(1) }}%</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ formatSpeed(task.downloadSpeed) }}</td>
|
||||
<td>{{ formatBytes(task.totalLength) }}</td>
|
||||
</tr>
|
||||
<tr v-if="filteredTasks.length === 0">
|
||||
<td colspan="5" class="empty">현재 다운로드 없음</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="right-pane card">
|
||||
<h2>Engine</h2>
|
||||
<p class="runtime" :class="status.running ? 'ok' : 'off'">{{ runtimeLabel }}</p>
|
||||
<p><strong>PID:</strong> {{ status.pid ?? '-' }}</p>
|
||||
<p><strong>Started:</strong> {{ formatDateTime(status.startedAt) }}</p>
|
||||
|
||||
<div class="engine-actions">
|
||||
<button :disabled="loadingEngine || status.running" @click="onStartEngine">Start</button>
|
||||
<button class="ghost" :disabled="loadingEngine || !status.running" @click="onStopEngine">Stop</button>
|
||||
</div>
|
||||
|
||||
<h3>연결 설정</h3>
|
||||
<label>
|
||||
<span>Binary Path</span>
|
||||
<input v-model="binaryPath" type="text" placeholder="aria2c" />
|
||||
</label>
|
||||
<label>
|
||||
<span>RPC Port</span>
|
||||
<input v-model.number="rpcPort" type="number" min="1" max="65535" />
|
||||
</label>
|
||||
<label>
|
||||
<span>RPC Secret</span>
|
||||
<input v-model="rpcSecret" type="text" placeholder="optional" />
|
||||
</label>
|
||||
<label>
|
||||
<span>폴더</span>
|
||||
<input v-model="downloadDir" type="text" placeholder="/path/to/downloads" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Split</span>
|
||||
<input v-model.number="split" type="number" min="1" max="64" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Max Concurrent</span>
|
||||
<input v-model.number="maxConcurrentDownloads" type="number" min="1" max="20" />
|
||||
</label>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
@@ -627,18 +794,32 @@ onUnmounted(() => {
|
||||
<section v-else class="content settings-content">
|
||||
<section class="settings-layout">
|
||||
<aside class="settings-nav card">
|
||||
<h2>설정</h2>
|
||||
<button :class="{ active: settingsTab === 'basic' }" @click="settingsTab = 'basic'">기본</button>
|
||||
<button :class="{ active: settingsTab === 'advanced' }" @click="settingsTab = 'advanced'">고급</button>
|
||||
<button :class="{ active: settingsTab === 'lab' }" @click="settingsTab = 'lab'">실험실</button>
|
||||
<h2>Settings</h2>
|
||||
<p class="settings-nav-sub">다운로드 엔진과 앱 동작을 조정합니다.</p>
|
||||
<button :class="{ active: settingsTab === 'basic' }" @click="settingsTab = 'basic'">
|
||||
<span>기본</span>
|
||||
<small>일반 사용 옵션</small>
|
||||
</button>
|
||||
<button :class="{ active: settingsTab === 'advanced' }" @click="settingsTab = 'advanced'">
|
||||
<span>고급</span>
|
||||
<small>네트워크/RPC</small>
|
||||
</button>
|
||||
<button :class="{ active: settingsTab === 'lab' }" @click="settingsTab = 'lab'">
|
||||
<span>실험실</span>
|
||||
<small>미리보기 기능</small>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<article class="settings-panel card">
|
||||
<h1>기본</h1>
|
||||
<header class="settings-header">
|
||||
<h1>환경 설정</h1>
|
||||
<p>Motrix 스타일에 맞춘 엔진/다운로드 환경 구성</p>
|
||||
</header>
|
||||
|
||||
<div v-if="settingsTab === 'basic'" class="settings-form">
|
||||
<section class="settings-group">
|
||||
<label>테마</label>
|
||||
<section class="settings-group settings-card-section">
|
||||
<div class="group-title">UI & 표시</div>
|
||||
<label class="field-label">테마</label>
|
||||
<div class="theme-row">
|
||||
<button class="theme-tile" :class="{ active: settingTheme === 'auto' }" @click="settingTheme = 'auto'">자동</button>
|
||||
<button class="theme-tile" :class="{ active: settingTheme === 'light' }" @click="settingTheme = 'light'">밝게</button>
|
||||
@@ -646,13 +827,15 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<section class="settings-group settings-card-section">
|
||||
<div class="group-title">시작/표시 동작</div>
|
||||
<label class="check-row"><input v-model="settingHideWindowOnStartup" type="checkbox" /> 창 자동으로 숨기기</label>
|
||||
<label class="check-row"><input v-model="settingTraySpeed" type="checkbox" /> 메뉴 막대 트레이에 실시간 속도 표시</label>
|
||||
<label class="check-row"><input v-model="settingShowDockProgress" type="checkbox" /> 다운로드 진행률 막대 보기</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-group two-col">
|
||||
<section class="settings-group settings-card-section two-col">
|
||||
<div class="group-title full-row">기본 파라미터</div>
|
||||
<label>언어
|
||||
<select v-model="settingLanguage">
|
||||
<option>한국어</option>
|
||||
@@ -664,19 +847,26 @@ onUnmounted(() => {
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<section class="settings-group settings-card-section">
|
||||
<div class="group-title">세션</div>
|
||||
<label class="check-row"><input v-model="settingRunOnLogin" type="checkbox" /> 로그인 시 실행</label>
|
||||
<label class="check-row"><input v-model="settingRememberWindow" type="checkbox" /> 창 크기 및 위치 기억</label>
|
||||
<label class="check-row"><input v-model="settingAutoResume" type="checkbox" /> 완료되지 않은 작업 자동 재개</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<label>기본 폴더
|
||||
<input v-model="downloadDir" type="text" />
|
||||
</label>
|
||||
<section class="settings-group settings-card-section">
|
||||
<div class="group-title">다운로드 디렉터리</div>
|
||||
<label class="field-label">기본 폴더</label>
|
||||
<div class="folder-picker-row">
|
||||
<input v-model="downloadDir" type="text" placeholder="/Users/.../Downloads" />
|
||||
<button type="button" class="ghost folder-browse-btn" @click.stop.prevent="pickDownloadFolder">
|
||||
폴더 선택
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group two-col">
|
||||
<section class="settings-group settings-card-section two-col">
|
||||
<div class="group-title full-row">대역폭 제한</div>
|
||||
<label>업로드 제한 (KB/s)
|
||||
<input v-model.number="settingUploadLimit" type="number" min="0" />
|
||||
</label>
|
||||
@@ -685,8 +875,8 @@ onUnmounted(() => {
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h3>BitTorrent</h3>
|
||||
<section class="settings-group settings-card-section">
|
||||
<div class="group-title">BitTorrent</div>
|
||||
<label class="check-row"><input v-model="settingMagnetAsTorrent" type="checkbox" /> 마그넷 링크를 토렌트 파일로 저장</label>
|
||||
<label class="check-row"><input v-model="settingAutoDownloadTorrentMeta" type="checkbox" /> 마그넷 및 토렌트 내용 자동 다운로드</label>
|
||||
<label class="check-row"><input v-model="settingBtForceEncryption" type="checkbox" /> BT 강제 암호화</label>
|
||||
@@ -702,8 +892,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="settings-placeholder">
|
||||
<p>{{ settingsTab === 'advanced' ? '고급 설정 화면(포팅 진행 중)' : '실험실 설정 화면(포팅 진행 중)' }}</p>
|
||||
<div v-else class="settings-placeholder card">
|
||||
<h3>{{ settingsTab === 'advanced' ? '고급 설정' : '실험실 설정' }}</h3>
|
||||
<p>{{ settingsTab === 'advanced' ? 'RPC, 프록시, 트래커 등 고급 항목을 Motrix 구조에 맞춰 포팅 중입니다.' : '실험 기능과 실험적 네트워크 옵션을 단계적으로 이식합니다.' }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -8,6 +8,13 @@ export interface EngineStatus {
|
||||
startedAt: number | null
|
||||
}
|
||||
|
||||
export interface BinaryProbeResult {
|
||||
found: boolean
|
||||
binaryPath: string | null
|
||||
source: string | null
|
||||
candidates: string[]
|
||||
}
|
||||
|
||||
export interface EngineStartPayload {
|
||||
binaryPath?: string
|
||||
rpcListenPort?: number
|
||||
@@ -30,6 +37,7 @@ export interface Aria2Task {
|
||||
downloadSpeed: string
|
||||
dir: string
|
||||
fileName: string
|
||||
uri: string
|
||||
}
|
||||
|
||||
export interface Aria2TaskSnapshot {
|
||||
@@ -54,6 +62,11 @@ export interface AddTorrentPayload {
|
||||
split?: number
|
||||
}
|
||||
|
||||
export interface TaskCommandPayload {
|
||||
rpc: Aria2RpcConfig
|
||||
gid: string
|
||||
}
|
||||
|
||||
export interface TorrentFilePayload {
|
||||
name: string
|
||||
base64: string
|
||||
@@ -72,6 +85,10 @@ export async function stopEngine(): Promise<EngineStatus> {
|
||||
return invoke<EngineStatus>('engine_stop')
|
||||
}
|
||||
|
||||
export async function detectAria2Binary(binaryPath?: string): Promise<BinaryProbeResult> {
|
||||
return invoke<BinaryProbeResult>('detect_aria2_binary', { binaryPath })
|
||||
}
|
||||
|
||||
export async function listAria2Tasks(config: Aria2RpcConfig): Promise<Aria2TaskSnapshot> {
|
||||
return invoke<Aria2TaskSnapshot>('aria2_list_tasks', { config })
|
||||
}
|
||||
@@ -87,3 +104,31 @@ export async function addAria2Torrent(payload: AddTorrentPayload): Promise<strin
|
||||
export async function loadTorrentFile(path: string): Promise<TorrentFilePayload> {
|
||||
return invoke<TorrentFilePayload>('load_torrent_file', { path })
|
||||
}
|
||||
|
||||
export async function pauseAria2Task(payload: TaskCommandPayload): Promise<string> {
|
||||
return invoke<string>('aria2_pause_task', { request: payload })
|
||||
}
|
||||
|
||||
export async function resumeAria2Task(payload: TaskCommandPayload): Promise<string> {
|
||||
return invoke<string>('aria2_resume_task', { request: payload })
|
||||
}
|
||||
|
||||
export async function removeAria2Task(payload: TaskCommandPayload): Promise<string> {
|
||||
return invoke<string>('aria2_remove_task', { request: payload })
|
||||
}
|
||||
|
||||
export async function removeAria2TaskRecord(payload: TaskCommandPayload): Promise<string> {
|
||||
return invoke<string>('aria2_remove_task_record', { request: payload })
|
||||
}
|
||||
|
||||
export async function pauseAllAria2(config: Aria2RpcConfig): Promise<string> {
|
||||
return invoke<string>('aria2_pause_all', { config })
|
||||
}
|
||||
|
||||
export async function resumeAllAria2(config: Aria2RpcConfig): Promise<string> {
|
||||
return invoke<string>('aria2_resume_all', { config })
|
||||
}
|
||||
|
||||
export async function openPathInFileManager(path: string): Promise<void> {
|
||||
return invoke<void>('open_path_in_file_manager', { path })
|
||||
}
|
||||
|
||||
497
src/style.css
497
src/style.css
@@ -120,6 +120,121 @@ body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.downloads-view {
|
||||
background: #f3f3f5;
|
||||
display: grid;
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
gap: 22px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.downloads-view .card {
|
||||
background: #f3f3f5;
|
||||
border: 1px solid #dfdfe4;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.downloads-filter {
|
||||
padding: 16px;
|
||||
align-self: start;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.downloads-filter h2 {
|
||||
margin: 2px 0 14px;
|
||||
color: #2a2a2e;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.downloads-filter button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: #3f424a;
|
||||
font-size: 0.98rem;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.downloads-filter button.active {
|
||||
background: #e9e9ee;
|
||||
color: #5b5fe9;
|
||||
}
|
||||
|
||||
.downloads-main .toolbar {
|
||||
border-bottom: 1px solid #d8d8de;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.downloads-main h1 {
|
||||
color: #2f3136;
|
||||
font-size: 2.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.downloads-main .icon-tool.ghost {
|
||||
background: transparent;
|
||||
color: #5f626b;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.downloads-main .icon-tool.ghost:hover {
|
||||
background: #ebecf1;
|
||||
color: #383b42;
|
||||
}
|
||||
|
||||
.downloads-main .task-pane {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.downloads-main .task-card {
|
||||
border: 1px solid #ceced6;
|
||||
border-radius: 10px;
|
||||
background: #f9f9fb;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.downloads-main .file-cell strong {
|
||||
color: #37393f;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.downloads-main .task-actions-pill {
|
||||
background: #f5f5f8;
|
||||
border: 1px solid #dfdfe6;
|
||||
}
|
||||
|
||||
.downloads-main .icon-pill {
|
||||
color: #727782;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.downloads-main .icon-pill:hover {
|
||||
background: #e7e8ee;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.downloads-main .bar {
|
||||
height: 8px;
|
||||
background: #e0e1ea;
|
||||
}
|
||||
|
||||
.downloads-main .bar span {
|
||||
background: #6268ee;
|
||||
}
|
||||
|
||||
.downloads-main .task-meta-row {
|
||||
color: #757b87;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -135,6 +250,17 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
||||
|
||||
.toolbar-right { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
.icon-tool {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
.switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -170,7 +296,7 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
||||
|
||||
.main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -193,27 +319,24 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
||||
color: #e0e3ff;
|
||||
}
|
||||
|
||||
.task-table-wrap { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
||||
|
||||
.task-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.84rem;
|
||||
.task-card-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-table th,
|
||||
.task-table td {
|
||||
text-align: left;
|
||||
padding: 9px;
|
||||
border-bottom: 1px solid #2b2f37;
|
||||
.task-card {
|
||||
border: 1px solid #363b45;
|
||||
border-radius: 10px;
|
||||
background: #20242c;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.task-table th {
|
||||
font-size: 0.72rem;
|
||||
color: #8f9ab0;
|
||||
background: #1a1c22;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
.task-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-cell { display: flex; flex-direction: column; gap: 2px; }
|
||||
@@ -240,8 +363,8 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
||||
|
||||
.progress-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -259,35 +382,45 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
||||
background: linear-gradient(90deg, #5b86ff, #5860f0);
|
||||
}
|
||||
|
||||
.progress-row small { color: #a8b2c4; font-size: 0.72rem; }
|
||||
|
||||
.empty { text-align: center; color: var(--text-sub); }
|
||||
|
||||
.right-pane {
|
||||
padding: 10px;
|
||||
.task-actions-pill {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
gap: 6px;
|
||||
justify-content: flex-start;
|
||||
background: #262b33;
|
||||
border: 1px solid #3a404c;
|
||||
border-radius: 999px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.right-pane h2 { margin: 0; font-size: 0.98rem; }
|
||||
.right-pane h3 { margin: 6px 0 2px; font-size: 0.86rem; }
|
||||
|
||||
.runtime { margin: 0; font-weight: 700; }
|
||||
.runtime.ok { color: var(--success); }
|
||||
.runtime.off { color: #949db0; }
|
||||
|
||||
.right-pane p { margin: 0; color: #9aa3b8; font-size: 0.8rem; }
|
||||
|
||||
.right-pane label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
font-size: 0.75rem;
|
||||
color: #a3acbe;
|
||||
.icon-pill {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.task-meta-row {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: #a5afc1;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.task-meta-right {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.right-pane input,
|
||||
.add-modal input,
|
||||
.add-modal textarea {
|
||||
height: 33px;
|
||||
@@ -305,7 +438,6 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
.right-pane input:focus,
|
||||
.add-modal input:focus,
|
||||
.add-modal textarea:focus {
|
||||
outline: none;
|
||||
@@ -338,11 +470,19 @@ button.ghost:hover {
|
||||
}
|
||||
|
||||
button.small { padding: 4px 8px; font-size: 0.74rem; }
|
||||
button.tiny { padding: 3px 7px; font-size: 0.72rem; }
|
||||
button.ghost.danger {
|
||||
color: #ffbec8;
|
||||
border-color: #66414a;
|
||||
background: #4b2e35;
|
||||
}
|
||||
button.ghost.danger:hover {
|
||||
background: #5a3640;
|
||||
border-color: #7a4a55;
|
||||
}
|
||||
|
||||
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
|
||||
.engine-actions { display: flex; gap: 7px; margin: 4px 0 6px; }
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -487,11 +627,278 @@ button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 208px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
padding: 12px;
|
||||
position: sticky;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.settings-nav h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.settings-nav-sub {
|
||||
margin: 8px 0 14px;
|
||||
color: #97a2b9;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.settings-nav button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
margin-bottom: 8px;
|
||||
padding: 9px 10px;
|
||||
background: #23262d;
|
||||
color: #c9d1e3;
|
||||
}
|
||||
|
||||
.settings-nav button span {
|
||||
display: block;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settings-nav button small {
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
color: #919db4;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.settings-nav button.active {
|
||||
border-color: #575ced;
|
||||
background: #2f345e;
|
||||
color: #eef1ff;
|
||||
}
|
||||
|
||||
.settings-nav button.active small {
|
||||
color: #c9d2ff;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
padding: 18px 20px;
|
||||
min-height: 640px;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.settings-header p {
|
||||
margin: 6px 0 0;
|
||||
color: #98a2b8;
|
||||
font-size: 0.81rem;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-card-section {
|
||||
border: 1px solid #3a404c;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
background: #252a33;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
color: #e5e9f7;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.group-title.full-row {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
color: #9aa6bf;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.theme-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.theme-tile {
|
||||
background: #2d313a;
|
||||
border: 1px solid #464d5b;
|
||||
color: #cdd4e6;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.theme-tile.active {
|
||||
background: #3a3f73;
|
||||
border-color: #646dff;
|
||||
color: #f0f3ff;
|
||||
}
|
||||
|
||||
.settings-group label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
color: #b1b9cb;
|
||||
font-size: 0.79rem;
|
||||
}
|
||||
|
||||
.settings-group input,
|
||||
.settings-group select {
|
||||
height: 33px;
|
||||
border: 1px solid #434955;
|
||||
border-radius: 4px;
|
||||
padding: 7px 10px;
|
||||
font-size: 0.81rem;
|
||||
color: var(--text-main);
|
||||
background: #1b1f27;
|
||||
}
|
||||
|
||||
.settings-group input:focus,
|
||||
.settings-group select:focus {
|
||||
outline: none;
|
||||
border-color: #656cf5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.settings-group select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #8e97aa 50%),
|
||||
linear-gradient(135deg, #8e97aa 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 14px) calc(50% - 2px),
|
||||
calc(100% - 9px) calc(50% - 2px);
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.settings-group.two-col {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.check-row {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: 6px !important;
|
||||
color: #c1c8d9 !important;
|
||||
}
|
||||
|
||||
.check-row input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #373d49;
|
||||
}
|
||||
|
||||
.folder-picker-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.folder-browse-btn {
|
||||
white-space: nowrap;
|
||||
padding: 7px 12px;
|
||||
min-height: 33px;
|
||||
}
|
||||
|
||||
.settings-placeholder {
|
||||
padding: 20px;
|
||||
background: #242831;
|
||||
border: 1px solid #3a404c;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.settings-placeholder h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
.settings-placeholder p {
|
||||
margin: 0;
|
||||
color: #9aa6bf;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.main-grid { grid-template-columns: 1fr; }
|
||||
.settings-layout { grid-template-columns: 1fr; }
|
||||
.settings-nav {
|
||||
position: static;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.settings-nav h2,
|
||||
.settings-nav-sub {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.settings-nav button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.downloads-view {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: auto;
|
||||
}
|
||||
.downloads-filter {
|
||||
min-height: auto;
|
||||
}
|
||||
.app-shell { grid-template-columns: 1fr; }
|
||||
.sidebar { display: none; }
|
||||
.settings-group.two-col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.theme-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user