feat: align motrix-style download UI/actions and stabilize aria2 ops
This commit is contained in:
@@ -25,17 +25,24 @@
|
|||||||
- [x] Tauri + Vue 프로젝트 초기화
|
- [x] Tauri + Vue 프로젝트 초기화
|
||||||
- [x] Rust command: `engine_start`, `engine_stop`, `engine_status`
|
- [x] Rust command: `engine_start`, `engine_stop`, `engine_status`
|
||||||
- [x] Vue 대시보드에서 엔진 제어 UI 연결
|
- [x] Vue 대시보드에서 엔진 제어 UI 연결
|
||||||
- [ ] aria2 번들 바이너리 경로 자동 탐지 (`resources/engine/*`)
|
- [x] aria2 바이너리 경로 자동 탐지 커맨드 추가 (`detect_aria2_binary`)
|
||||||
- [ ] 에러 메시지 분류(파일 없음, 권한 오류, 포트 충돌)
|
- [x] 시작 실패 에러 분류(파일 없음, 권한 오류, 즉시 종료)
|
||||||
|
- [x] aria2 번들 바이너리 리소스 구조 확정 (`src-tauri/resources/engine/*`)
|
||||||
|
- [~] 실배포 번들 검증 (플랫폼별 smoke test 진행 중)
|
||||||
|
|
||||||
### Phase 2: 다운로드 RPC/큐
|
### Phase 2: 다운로드 RPC/큐
|
||||||
- [ ] aria2 JSON-RPC client 계층 구현 (Rust 또는 Vue 중 1안 선택)
|
- [x] aria2 JSON-RPC client 계층 구현 (Rust command 래퍼)
|
||||||
- [ ] 작업 추가/중지/재시도/삭제
|
- [x] 작업 추가 (URI/Torrent)
|
||||||
- [ ] 작업 목록/속도/진행률 실시간 갱신
|
- [x] 작업 목록/속도/진행률 갱신
|
||||||
- [ ] Magnet/Torrent 입력 파이프라인
|
- [x] 작업 제어(개별 pause/resume/remove, 전체 pause/resume)
|
||||||
|
- [~] Magnet/Torrent 입력 파이프라인 (입력/추가는 완료, 메타데이터 UX 개선 필요)
|
||||||
|
- [ ] 작업 상세 패널(파일/피어/트래커/활동) 구현
|
||||||
|
- [ ] 선택 기반 배치 액션(다중 선택, 일괄 정지/재개/삭제)
|
||||||
|
|
||||||
### Phase 3: 설정/세션/마이그레이션
|
### Phase 3: 설정/세션/마이그레이션
|
||||||
- [ ] 설정 저장소 도입 (다운로드 폴더, 동시작업 수, 속도 제한 등)
|
- [~] 설정 저장소 도입 (다운로드 폴더, 동시작업 수, 속도 제한 등)
|
||||||
|
- [x] 기본 실행 설정(localStorage) 저장/복원
|
||||||
|
- [ ] aria2 글로벌 옵션과 완전 동기화
|
||||||
- [ ] 세션 파일 관리(종료 시 저장, 시작 시 복구)
|
- [ ] 세션 파일 관리(종료 시 저장, 시작 시 복구)
|
||||||
- [ ] Motrix 설정 키 매핑표 작성 및 자동 마이그레이션 도구
|
- [ ] Motrix 설정 키 매핑표 작성 및 자동 마이그레이션 도구
|
||||||
|
|
||||||
@@ -55,13 +62,32 @@
|
|||||||
- Electron API 차이: 기능별 대체표를 먼저 만들고 Tauri plugin으로 대응
|
- 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`: aria2 프로세스 시작/중지/상태 조회
|
||||||
|
- `src-tauri/src/engine.rs`: 바이너리 자동 탐지 + 에러 분류 + 작업 제어 RPC 커맨드
|
||||||
- `src-tauri/src/lib.rs`: Tauri invoke handler 연결
|
- `src-tauri/src/lib.rs`: Tauri invoke handler 연결
|
||||||
- `src/lib/engineApi.ts`: 프런트 command 호출 래퍼
|
- `src/lib/engineApi.ts`: 프런트 command 호출 래퍼(엔진 + 작업 제어)
|
||||||
- `src/App.vue`: 엔진 제어 UI
|
- `src/App.vue`: Motrix 스타일 사이드바/목록/추가 모달/액션 버튼
|
||||||
|
- `src/style.css`: 작업 액션 UI 스타일 보강
|
||||||
- 다음 우선순위:
|
- 다음 우선순위:
|
||||||
1. aria2 바이너리 경로 자동 탐지 + 리소스 번들 구조 확정
|
1. Motrix `Task Detail` 동등 기능(파일/피어/트래커/활동) 구현
|
||||||
2. JSON-RPC 기반 다운로드 목록 API 구현
|
2. 설정 저장소 도입(local persist + aria2 global option 적용)
|
||||||
3. 설정 저장소 도입
|
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",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"vue": "^3.5.25"
|
"vue": "^3.5.25"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -19,6 +20,9 @@
|
|||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vue-tsc": "^3.1.5"
|
"vue-tsc": "^3.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24 <25"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
@@ -1099,6 +1103,15 @@
|
|||||||
"node": ">= 10"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|||||||
@@ -11,10 +11,13 @@
|
|||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri:dev": "tauri dev",
|
"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": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"vue": "^3.5.25"
|
"vue": "^3.5.25"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -668,6 +669,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2955,6 +2958,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -3760,6 +3787,46 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-log"
|
name = "tauri-plugin-log"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.10.0", features = [] }
|
tauri = { version = "2.10.0", features = [] }
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
reqwest = { version = "0.12.24", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12.24", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"main"
|
"main"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"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 reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
use std::env;
|
||||||
|
use std::net::{SocketAddr, TcpStream};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::State;
|
use tauri::{Manager, State};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -46,6 +48,13 @@ pub struct Aria2AddTorrentRequest {
|
|||||||
pub split: Option<u16>,
|
pub split: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Aria2TaskCommandRequest {
|
||||||
|
pub rpc: Aria2RpcConfig,
|
||||||
|
pub gid: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EngineStatusResponse {
|
pub struct EngineStatusResponse {
|
||||||
@@ -66,6 +75,7 @@ pub struct Aria2TaskSummary {
|
|||||||
pub download_speed: String,
|
pub download_speed: String,
|
||||||
pub dir: String,
|
pub dir: String,
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
|
pub uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -84,9 +94,20 @@ pub struct TorrentFilePayload {
|
|||||||
pub size: u64,
|
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)]
|
#[derive(Debug)]
|
||||||
struct EngineRuntime {
|
struct EngineRuntime {
|
||||||
child: Option<Child>,
|
child: Option<Child>,
|
||||||
|
external_reuse: bool,
|
||||||
|
rpc_port: Option<u16>,
|
||||||
binary_path: Option<String>,
|
binary_path: Option<String>,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
started_at: Option<u64>,
|
started_at: Option<u64>,
|
||||||
@@ -96,6 +117,8 @@ impl Default for EngineRuntime {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
child: None,
|
child: None,
|
||||||
|
external_reuse: false,
|
||||||
|
rpc_port: None,
|
||||||
binary_path: None,
|
binary_path: None,
|
||||||
args: vec![],
|
args: vec![],
|
||||||
started_at: None,
|
started_at: None,
|
||||||
@@ -111,7 +134,7 @@ pub struct EngineState {
|
|||||||
impl EngineState {
|
impl EngineState {
|
||||||
fn status(runtime: &EngineRuntime) -> EngineStatusResponse {
|
fn status(runtime: &EngineRuntime) -> EngineStatusResponse {
|
||||||
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),
|
pid: runtime.child.as_ref().map(std::process::Child::id),
|
||||||
binary_path: runtime.binary_path.clone(),
|
binary_path: runtime.binary_path.clone(),
|
||||||
args: runtime.args.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 {
|
fn rpc_endpoint(config: &Aria2RpcConfig) -> String {
|
||||||
format!(
|
format!(
|
||||||
"http://127.0.0.1:{}/jsonrpc",
|
"http://127.0.0.1:{}/jsonrpc",
|
||||||
@@ -249,6 +399,17 @@ fn map_task(task: &Value) -> Aria2TaskSummary {
|
|||||||
download_speed: value_to_string(task.get("downloadSpeed")),
|
download_speed: value_to_string(task.get("downloadSpeed")),
|
||||||
dir: value_to_string(task.get("dir")),
|
dir: value_to_string(task.get("dir")),
|
||||||
file_name: pick_file_name(file_path),
|
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)
|
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]
|
#[tauri::command]
|
||||||
pub fn engine_start(
|
pub fn engine_start(
|
||||||
|
app: tauri::AppHandle,
|
||||||
state: State<'_, EngineState>,
|
state: State<'_, EngineState>,
|
||||||
request: EngineStartRequest,
|
request: EngineStartRequest,
|
||||||
) -> Result<EngineStatusResponse, String> {
|
) -> Result<EngineStatusResponse, String> {
|
||||||
|
let rpc_port = request.rpc_listen_port.unwrap_or(6800);
|
||||||
let mut runtime = state
|
let mut runtime = state
|
||||||
.runtime
|
.runtime
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|err| format!("failed to lock engine state: {err}"))?;
|
.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() {
|
if let Some(child) = runtime.child.as_mut() {
|
||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
Ok(Some(_)) => {
|
Ok(Some(_)) => {
|
||||||
@@ -302,7 +480,31 @@ pub fn engine_start(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let args = build_engine_args(&request);
|
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)
|
let child = Command::new(&binary)
|
||||||
.args(&args)
|
.args(&args)
|
||||||
@@ -310,9 +512,25 @@ pub fn engine_start(
|
|||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.spawn()
|
.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.child = Some(child);
|
||||||
|
runtime.external_reuse = false;
|
||||||
|
runtime.rpc_port = Some(rpc_port);
|
||||||
runtime.binary_path = Some(binary);
|
runtime.binary_path = Some(binary);
|
||||||
runtime.args = args;
|
runtime.args = args;
|
||||||
runtime.started_at = Some(
|
runtime.started_at = Some(
|
||||||
@@ -325,6 +543,21 @@ pub fn engine_start(
|
|||||||
Ok(EngineState::status(&runtime))
|
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]
|
#[tauri::command]
|
||||||
pub fn engine_stop(state: State<'_, EngineState>) -> Result<EngineStatusResponse, String> {
|
pub fn engine_stop(state: State<'_, EngineState>) -> Result<EngineStatusResponse, String> {
|
||||||
let mut runtime = state
|
let mut runtime = state
|
||||||
@@ -339,6 +572,8 @@ pub fn engine_stop(state: State<'_, EngineState>) -> Result<EngineStatusResponse
|
|||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runtime.external_reuse = false;
|
||||||
|
runtime.rpc_port = None;
|
||||||
runtime.started_at = None;
|
runtime.started_at = None;
|
||||||
Ok(EngineState::status(&runtime))
|
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))
|
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]
|
#[tauri::command]
|
||||||
pub fn load_torrent_file(path: String) -> Result<TorrentFilePayload, String> {
|
pub fn load_torrent_file(path: String) -> Result<TorrentFilePayload, String> {
|
||||||
if !path.to_ascii_lowercase().ends_with(".torrent") {
|
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,
|
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;
|
mod engine;
|
||||||
|
|
||||||
use engine::{
|
use engine::{
|
||||||
aria2_add_torrent, aria2_add_uri, aria2_list_tasks, engine_start, engine_status, engine_stop,
|
aria2_add_torrent, aria2_add_uri, aria2_list_tasks, aria2_pause_all, aria2_pause_task,
|
||||||
load_torrent_file, EngineState,
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(EngineState::default())
|
.manage(EngineState::default())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
app.handle().plugin(
|
app.handle().plugin(
|
||||||
@@ -23,10 +26,18 @@ pub fn run() {
|
|||||||
engine_start,
|
engine_start,
|
||||||
engine_stop,
|
engine_stop,
|
||||||
engine_status,
|
engine_status,
|
||||||
|
detect_aria2_binary,
|
||||||
aria2_add_torrent,
|
aria2_add_torrent,
|
||||||
aria2_add_uri,
|
aria2_add_uri,
|
||||||
aria2_list_tasks,
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -12,9 +12,12 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
"label": "main",
|
||||||
"title": "gdown",
|
"title": "gdown",
|
||||||
"width": 800,
|
"width": 1280,
|
||||||
"height": 600,
|
"height": 860,
|
||||||
|
"minWidth": 1080,
|
||||||
|
"minHeight": 720,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"fullscreen": false
|
"fullscreen": false
|
||||||
}
|
}
|
||||||
@@ -26,6 +29,9 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"resources": [
|
||||||
|
"resources/engine/**/*"
|
||||||
|
],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
457
src/App.vue
457
src/App.vue
@@ -2,14 +2,22 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import type { UnlistenFn } from '@tauri-apps/api/event'
|
import type { UnlistenFn } from '@tauri-apps/api/event'
|
||||||
|
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||||
import {
|
import {
|
||||||
addAria2Torrent,
|
addAria2Torrent,
|
||||||
addAria2Uri,
|
addAria2Uri,
|
||||||
|
detectAria2Binary,
|
||||||
getEngineStatus,
|
getEngineStatus,
|
||||||
listAria2Tasks,
|
listAria2Tasks,
|
||||||
loadTorrentFile,
|
loadTorrentFile,
|
||||||
|
pauseAllAria2,
|
||||||
|
pauseAria2Task,
|
||||||
|
openPathInFileManager,
|
||||||
|
removeAria2Task,
|
||||||
|
removeAria2TaskRecord,
|
||||||
|
resumeAllAria2,
|
||||||
|
resumeAria2Task,
|
||||||
startEngine,
|
startEngine,
|
||||||
stopEngine,
|
|
||||||
type Aria2Task,
|
type Aria2Task,
|
||||||
type Aria2TaskSnapshot,
|
type Aria2TaskSnapshot,
|
||||||
type EngineStatus,
|
type EngineStatus,
|
||||||
@@ -19,6 +27,17 @@ type TaskFilter = 'all' | 'active' | 'waiting' | 'stopped'
|
|||||||
type AddTab = 'url' | 'torrent'
|
type AddTab = 'url' | 'torrent'
|
||||||
type AppPage = 'downloads' | 'settings'
|
type AppPage = 'downloads' | 'settings'
|
||||||
type SettingsTab = 'basic' | 'advanced' | 'lab'
|
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 binaryPath = ref('aria2c')
|
||||||
const rpcPort = ref(6800)
|
const rpcPort = ref(6800)
|
||||||
@@ -30,10 +49,11 @@ const maxConcurrentDownloads = ref(5)
|
|||||||
const loadingEngine = ref(false)
|
const loadingEngine = ref(false)
|
||||||
const loadingTasks = ref(false)
|
const loadingTasks = ref(false)
|
||||||
const loadingAddTask = ref(false)
|
const loadingAddTask = ref(false)
|
||||||
|
const loadingTaskAction = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const successMessage = ref('')
|
const successMessage = ref('')
|
||||||
const autoRefresh = ref(true)
|
const autoRefresh = ref(true)
|
||||||
const filter = ref<TaskFilter>('all')
|
const filter = ref<TaskFilter>('active')
|
||||||
const page = ref<AppPage>('downloads')
|
const page = ref<AppPage>('downloads')
|
||||||
const settingsTab = ref<SettingsTab>('basic')
|
const settingsTab = ref<SettingsTab>('basic')
|
||||||
|
|
||||||
@@ -83,16 +103,45 @@ const tasks = ref<Aria2TaskSnapshot>({
|
|||||||
let refreshTimer: number | null = null
|
let refreshTimer: number | null = null
|
||||||
let unlistenDragDrop: UnlistenFn | 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(() => {
|
const filteredTasks = computed(() => {
|
||||||
if (filter.value === 'active') return tasks.value.active
|
if (filter.value === 'active') return tasks.value.active
|
||||||
if (filter.value === 'waiting') return tasks.value.waiting
|
if (filter.value === 'waiting') return tasks.value.waiting
|
||||||
if (filter.value === 'stopped') return tasks.value.stopped
|
if (filter.value === 'stopped') return tasks.value.stopped
|
||||||
return [...tasks.value.active, ...tasks.value.waiting, ...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) {
|
function pushError(message: string) {
|
||||||
successMessage.value = ''
|
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 {
|
function formatBytes(value: string): string {
|
||||||
const n = Number(value)
|
const n = Number(value)
|
||||||
if (!Number.isFinite(n)) return '-'
|
if (!Number.isFinite(n)) return '-'
|
||||||
@@ -187,39 +231,164 @@ function updateRefreshTimer() {
|
|||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onStartEngine() {
|
async function autoStartEngine(silent = true) {
|
||||||
loadingEngine.value = true
|
loadingEngine.value = true
|
||||||
try {
|
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({
|
status.value = await startEngine({
|
||||||
binaryPath: binaryPath.value.trim() || undefined,
|
binaryPath: resolvedBinaryPath,
|
||||||
rpcListenPort: rpcPort.value,
|
rpcListenPort: rpcPort.value,
|
||||||
rpcSecret: rpcSecret.value.trim() || undefined,
|
rpcSecret: rpcSecret.value.trim() || undefined,
|
||||||
downloadDir: downloadDir.value.trim() || undefined,
|
downloadDir: downloadDir.value.trim() || undefined,
|
||||||
maxConcurrentDownloads: maxConcurrentDownloads.value,
|
maxConcurrentDownloads: maxConcurrentDownloads.value,
|
||||||
split: split.value,
|
split: split.value,
|
||||||
})
|
})
|
||||||
pushSuccess('aria2 engine started.')
|
saveSettingsToStorage()
|
||||||
|
if (!silent) pushSuccess('aria2 engine started.')
|
||||||
await refreshTasks()
|
await refreshTasks()
|
||||||
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pushError(String(error))
|
pushError(String(error))
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
loadingEngine.value = false
|
loadingEngine.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onStopEngine() {
|
async function ensureEngineRunning() {
|
||||||
loadingEngine.value = true
|
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 {
|
try {
|
||||||
status.value = await stopEngine()
|
await pauseAllAria2(rpcConfig())
|
||||||
tasks.value = { active: [], waiting: [], stopped: [] }
|
pushSuccess('모든 작업을 일시정지했습니다.')
|
||||||
pushSuccess('aria2 engine stopped.')
|
await refreshTasks()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pushError(String(error))
|
pushError(String(error))
|
||||||
} finally {
|
} 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() {
|
function openAddModal() {
|
||||||
showAddModal.value = true
|
showAddModal.value = true
|
||||||
addTab.value = 'url'
|
addTab.value = 'url'
|
||||||
@@ -234,9 +403,28 @@ function openSettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
|
saveSettingsToStorage()
|
||||||
pushSuccess('설정이 저장되었습니다.')
|
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() {
|
function closeAddModal() {
|
||||||
showAddModal.value = false
|
showAddModal.value = false
|
||||||
modalDropActive.value = false
|
modalDropActive.value = false
|
||||||
@@ -388,26 +576,32 @@ async function onWindowDrop(event: DragEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitAddTask() {
|
async function onSubmitAddTask() {
|
||||||
if (!status.value.running) {
|
const ready = await ensureEngineRunning()
|
||||||
pushError('먼저 엔진을 시작하세요.')
|
if (!ready) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingAddTask.value = true
|
loadingAddTask.value = true
|
||||||
try {
|
try {
|
||||||
if (addTab.value === 'url') {
|
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을 입력하세요.')
|
pushError('URL을 입력하세요.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const gids: string[] = []
|
||||||
|
for (const uri of uris) {
|
||||||
const gid = await addAria2Uri({
|
const gid = await addAria2Uri({
|
||||||
rpc: rpcConfig(),
|
rpc: rpcConfig(),
|
||||||
uri: addUrl.value.trim(),
|
uri,
|
||||||
out: addOut.value.trim() || undefined,
|
out: uris.length === 1 ? (addOut.value.trim() || undefined) : undefined,
|
||||||
dir: downloadDir.value.trim() || undefined,
|
dir: downloadDir.value.trim() || undefined,
|
||||||
split: addSplit.value,
|
split: addSplit.value,
|
||||||
})
|
})
|
||||||
pushSuccess(`작업이 추가되었습니다. gid=${gid}`)
|
gids.push(gid)
|
||||||
|
}
|
||||||
|
pushSuccess(`${gids.length}개 작업이 추가되었습니다.`)
|
||||||
} else {
|
} else {
|
||||||
if (!torrentBase64.value.trim()) {
|
if (!torrentBase64.value.trim()) {
|
||||||
pushError('.torrent 파일을 선택하세요.')
|
pushError('.torrent 파일을 선택하세요.')
|
||||||
@@ -439,7 +633,11 @@ async function onSubmitAddTask() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
loadSettingsFromStorage()
|
||||||
await refreshEngineStatus()
|
await refreshEngineStatus()
|
||||||
|
if (!status.value.running) {
|
||||||
|
await autoStartEngine(true)
|
||||||
|
}
|
||||||
await refreshTasks()
|
await refreshTasks()
|
||||||
updateRefreshTimer()
|
updateRefreshTimer()
|
||||||
|
|
||||||
@@ -510,116 +708,85 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section v-if="page === 'downloads'" class="content">
|
<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 class="downloads-main">
|
||||||
<header class="toolbar">
|
<header class="toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<h1>다운로드 중</h1>
|
<h1>{{ filter === 'active' ? '다운로드 중' : filter === 'waiting' ? '대기 중' : '중단됨' }}</h1>
|
||||||
<span class="count">{{ totalCount }} tasks</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<label class="switcher">
|
<button
|
||||||
<input v-model="autoRefresh" type="checkbox" @change="updateRefreshTimer" />
|
class="icon-tool ghost"
|
||||||
<span>Auto refresh</span>
|
:disabled="loadingTaskAction || filteredTaskCount === 0"
|
||||||
</label>
|
title="목록 삭제"
|
||||||
<button class="ghost" :disabled="loadingTasks" @click="refreshTasks()">새로고침</button>
|
@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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section v-if="errorMessage" class="notice error">{{ errorMessage }}</section>
|
<section v-if="errorMessage" class="notice error">{{ errorMessage }}</section>
|
||||||
<section v-if="successMessage" class="notice success">{{ successMessage }}</section>
|
<section v-if="successMessage" class="notice success">{{ successMessage }}</section>
|
||||||
|
|
||||||
<section class="main-grid">
|
|
||||||
<article class="task-pane card">
|
<article class="task-pane card">
|
||||||
<div class="tabs">
|
<div class="task-card-list">
|
||||||
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">전체</button>
|
<article v-for="task in filteredTasks" :key="task.gid" class="task-card">
|
||||||
<button :class="{ active: filter === 'active' }" @click="filter = 'active'">
|
<div class="task-card-head">
|
||||||
다운로드 중 ({{ 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>
|
|
||||||
|
|
||||||
<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">
|
<div class="file-cell">
|
||||||
<strong>{{ task.fileName || '-' }}</strong>
|
<strong>{{ task.fileName || '-' }}</strong>
|
||||||
<small>{{ task.gid }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="task-actions-pill">
|
||||||
<td>
|
<button
|
||||||
<span class="status-tag" :class="task.status">{{ task.status }}</span>
|
class="icon-pill ghost"
|
||||||
</td>
|
:disabled="loadingTaskAction || task.status === 'stopped'"
|
||||||
<td>
|
: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="progress-row">
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
<span :style="{ width: `${progress(task).toFixed(1)}%` }" />
|
<span :style="{ width: `${progress(task).toFixed(1)}%` }" />
|
||||||
</div>
|
</div>
|
||||||
<small>{{ progress(task).toFixed(1) }}%</small>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
<td>{{ formatSpeed(task.downloadSpeed) }}</td>
|
<div class="task-meta-row">
|
||||||
<td>{{ formatBytes(task.totalLength) }}</td>
|
<span>{{ formatBytes(task.completedLength) }} / {{ formatBytes(task.totalLength) }}</span>
|
||||||
</tr>
|
<span class="task-meta-right">
|
||||||
<tr v-if="filteredTasks.length === 0">
|
<span>{{ formatSpeed(task.downloadSpeed) }}</span>
|
||||||
<td colspan="5" class="empty">현재 다운로드 없음</td>
|
</span>
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="right-pane card">
|
<div v-if="filteredTasks.length === 0" class="empty">현재 다운로드 없음</div>
|
||||||
<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>
|
</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>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -627,18 +794,32 @@ onUnmounted(() => {
|
|||||||
<section v-else class="content settings-content">
|
<section v-else class="content settings-content">
|
||||||
<section class="settings-layout">
|
<section class="settings-layout">
|
||||||
<aside class="settings-nav card">
|
<aside class="settings-nav card">
|
||||||
<h2>설정</h2>
|
<h2>Settings</h2>
|
||||||
<button :class="{ active: settingsTab === 'basic' }" @click="settingsTab = 'basic'">기본</button>
|
<p class="settings-nav-sub">다운로드 엔진과 앱 동작을 조정합니다.</p>
|
||||||
<button :class="{ active: settingsTab === 'advanced' }" @click="settingsTab = 'advanced'">고급</button>
|
<button :class="{ active: settingsTab === 'basic' }" @click="settingsTab = 'basic'">
|
||||||
<button :class="{ active: settingsTab === 'lab' }" @click="settingsTab = 'lab'">실험실</button>
|
<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>
|
</aside>
|
||||||
|
|
||||||
<article class="settings-panel card">
|
<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">
|
<div v-if="settingsTab === 'basic'" class="settings-form">
|
||||||
<section class="settings-group">
|
<section class="settings-group settings-card-section">
|
||||||
<label>테마</label>
|
<div class="group-title">UI & 표시</div>
|
||||||
|
<label class="field-label">테마</label>
|
||||||
<div class="theme-row">
|
<div class="theme-row">
|
||||||
<button class="theme-tile" :class="{ active: settingTheme === 'auto' }" @click="settingTheme = 'auto'">자동</button>
|
<button class="theme-tile" :class="{ active: settingTheme === 'auto' }" @click="settingTheme = 'auto'">자동</button>
|
||||||
<button class="theme-tile" :class="{ active: settingTheme === 'light' }" @click="settingTheme = 'light'">밝게</button>
|
<button class="theme-tile" :class="{ active: settingTheme === 'light' }" @click="settingTheme = 'light'">밝게</button>
|
||||||
@@ -646,13 +827,15 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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="settingHideWindowOnStartup" type="checkbox" /> 창 자동으로 숨기기</label>
|
||||||
<label class="check-row"><input v-model="settingTraySpeed" 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>
|
<label class="check-row"><input v-model="settingShowDockProgress" type="checkbox" /> 다운로드 진행률 막대 보기</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-group two-col">
|
<section class="settings-group settings-card-section two-col">
|
||||||
|
<div class="group-title full-row">기본 파라미터</div>
|
||||||
<label>언어
|
<label>언어
|
||||||
<select v-model="settingLanguage">
|
<select v-model="settingLanguage">
|
||||||
<option>한국어</option>
|
<option>한국어</option>
|
||||||
@@ -664,19 +847,26 @@ onUnmounted(() => {
|
|||||||
</label>
|
</label>
|
||||||
</section>
|
</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="settingRunOnLogin" type="checkbox" /> 로그인 시 실행</label>
|
||||||
<label class="check-row"><input v-model="settingRememberWindow" 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>
|
<label class="check-row"><input v-model="settingAutoResume" type="checkbox" /> 완료되지 않은 작업 자동 재개</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-group">
|
<section class="settings-group settings-card-section">
|
||||||
<label>기본 폴더
|
<div class="group-title">다운로드 디렉터리</div>
|
||||||
<input v-model="downloadDir" type="text" />
|
<label class="field-label">기본 폴더</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>
|
||||||
|
|
||||||
<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)
|
<label>업로드 제한 (KB/s)
|
||||||
<input v-model.number="settingUploadLimit" type="number" min="0" />
|
<input v-model.number="settingUploadLimit" type="number" min="0" />
|
||||||
</label>
|
</label>
|
||||||
@@ -685,8 +875,8 @@ onUnmounted(() => {
|
|||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-group">
|
<section class="settings-group settings-card-section">
|
||||||
<h3>BitTorrent</h3>
|
<div class="group-title">BitTorrent</div>
|
||||||
<label class="check-row"><input v-model="settingMagnetAsTorrent" type="checkbox" /> 마그넷 링크를 토렌트 파일로 저장</label>
|
<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="settingAutoDownloadTorrentMeta" type="checkbox" /> 마그넷 및 토렌트 내용 자동 다운로드</label>
|
||||||
<label class="check-row"><input v-model="settingBtForceEncryption" type="checkbox" /> BT 강제 암호화</label>
|
<label class="check-row"><input v-model="settingBtForceEncryption" type="checkbox" /> BT 강제 암호화</label>
|
||||||
@@ -702,8 +892,9 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="settings-placeholder">
|
<div v-else class="settings-placeholder card">
|
||||||
<p>{{ settingsTab === 'advanced' ? '고급 설정 화면(포팅 진행 중)' : '실험실 설정 화면(포팅 진행 중)' }}</p>
|
<h3>{{ settingsTab === 'advanced' ? '고급 설정' : '실험실 설정' }}</h3>
|
||||||
|
<p>{{ settingsTab === 'advanced' ? 'RPC, 프록시, 트래커 등 고급 항목을 Motrix 구조에 맞춰 포팅 중입니다.' : '실험 기능과 실험적 네트워크 옵션을 단계적으로 이식합니다.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ export interface EngineStatus {
|
|||||||
startedAt: number | null
|
startedAt: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BinaryProbeResult {
|
||||||
|
found: boolean
|
||||||
|
binaryPath: string | null
|
||||||
|
source: string | null
|
||||||
|
candidates: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface EngineStartPayload {
|
export interface EngineStartPayload {
|
||||||
binaryPath?: string
|
binaryPath?: string
|
||||||
rpcListenPort?: number
|
rpcListenPort?: number
|
||||||
@@ -30,6 +37,7 @@ export interface Aria2Task {
|
|||||||
downloadSpeed: string
|
downloadSpeed: string
|
||||||
dir: string
|
dir: string
|
||||||
fileName: string
|
fileName: string
|
||||||
|
uri: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Aria2TaskSnapshot {
|
export interface Aria2TaskSnapshot {
|
||||||
@@ -54,6 +62,11 @@ export interface AddTorrentPayload {
|
|||||||
split?: number
|
split?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TaskCommandPayload {
|
||||||
|
rpc: Aria2RpcConfig
|
||||||
|
gid: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface TorrentFilePayload {
|
export interface TorrentFilePayload {
|
||||||
name: string
|
name: string
|
||||||
base64: string
|
base64: string
|
||||||
@@ -72,6 +85,10 @@ export async function stopEngine(): Promise<EngineStatus> {
|
|||||||
return invoke<EngineStatus>('engine_stop')
|
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> {
|
export async function listAria2Tasks(config: Aria2RpcConfig): Promise<Aria2TaskSnapshot> {
|
||||||
return invoke<Aria2TaskSnapshot>('aria2_list_tasks', { config })
|
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> {
|
export async function loadTorrentFile(path: string): Promise<TorrentFilePayload> {
|
||||||
return invoke<TorrentFilePayload>('load_torrent_file', { path })
|
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;
|
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 {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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; }
|
.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 {
|
.switcher {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -170,7 +296,7 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
|||||||
|
|
||||||
.main-grid {
|
.main-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 280px;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,27 +319,24 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
|||||||
color: #e0e3ff;
|
color: #e0e3ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-table-wrap { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
.task-card-list {
|
||||||
|
display: grid;
|
||||||
.task-table {
|
gap: 10px;
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.84rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-table th,
|
.task-card {
|
||||||
.task-table td {
|
border: 1px solid #363b45;
|
||||||
text-align: left;
|
border-radius: 10px;
|
||||||
padding: 9px;
|
background: #20242c;
|
||||||
border-bottom: 1px solid #2b2f37;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-table th {
|
.task-card-head {
|
||||||
font-size: 0.72rem;
|
display: flex;
|
||||||
color: #8f9ab0;
|
align-items: flex-start;
|
||||||
background: #1a1c22;
|
justify-content: space-between;
|
||||||
text-transform: uppercase;
|
gap: 12px;
|
||||||
letter-spacing: 0.04em;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-cell { display: flex; flex-direction: column; gap: 2px; }
|
.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 {
|
.progress-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,35 +382,45 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
|||||||
background: linear-gradient(90deg, #5b86ff, #5860f0);
|
background: linear-gradient(90deg, #5b86ff, #5860f0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-row small { color: #a8b2c4; font-size: 0.72rem; }
|
|
||||||
|
|
||||||
.empty { text-align: center; color: var(--text-sub); }
|
.empty { text-align: center; color: var(--text-sub); }
|
||||||
|
|
||||||
.right-pane {
|
.task-actions-pill {
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 6px;
|
||||||
gap: 7px;
|
justify-content: flex-start;
|
||||||
|
background: #262b33;
|
||||||
|
border: 1px solid #3a404c;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-pane h2 { margin: 0; font-size: 0.98rem; }
|
.icon-pill {
|
||||||
.right-pane h3 { margin: 6px 0 2px; font-size: 0.86rem; }
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
.runtime { margin: 0; font-weight: 700; }
|
border-radius: 999px;
|
||||||
.runtime.ok { color: var(--success); }
|
display: inline-flex;
|
||||||
.runtime.off { color: #949db0; }
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
.right-pane p { margin: 0; color: #9aa3b8; font-size: 0.8rem; }
|
padding: 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
.right-pane label {
|
}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
.task-meta-row {
|
||||||
gap: 3px;
|
margin-top: 10px;
|
||||||
font-size: 0.75rem;
|
display: flex;
|
||||||
color: #a3acbe;
|
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 input,
|
||||||
.add-modal textarea {
|
.add-modal textarea {
|
||||||
height: 33px;
|
height: 33px;
|
||||||
@@ -305,7 +438,6 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
|||||||
min-height: 92px;
|
min-height: 92px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-pane input:focus,
|
|
||||||
.add-modal input:focus,
|
.add-modal input:focus,
|
||||||
.add-modal textarea:focus {
|
.add-modal textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -338,11 +470,19 @@ button.ghost:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.small { padding: 4px 8px; font-size: 0.74rem; }
|
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; }
|
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||||
|
|
||||||
.engine-actions { display: flex; gap: 7px; margin: 4px 0 6px; }
|
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -487,11 +627,278 @@ button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|||||||
gap: 8px;
|
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) {
|
@media (max-width: 1180px) {
|
||||||
.main-grid { grid-template-columns: 1fr; }
|
.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) {
|
@media (max-width: 900px) {
|
||||||
|
.downloads-view {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
.downloads-filter {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
.app-shell { grid-template-columns: 1fr; }
|
.app-shell { grid-template-columns: 1fr; }
|
||||||
.sidebar { display: none; }
|
.sidebar { display: none; }
|
||||||
|
.settings-group.two-col {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.theme-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user