Skip to content

Latest commit

 

History

History
1441 lines (1242 loc) · 38.7 KB

File metadata and controls

1441 lines (1242 loc) · 38.7 KB

Motrix to Aether Download Manager Migration Plan

Migration Roadmap and Technical Documentation
Comprehensive guide for porting Motrix download manager to Aether project


Table of Contents

  1. Project Overview
  2. Architecture Analysis
  3. Migration Roadmap
  4. Phase Implementation Details
  5. Technical Specifications
  6. Quality Assurance
  7. Deployment
  8. Summary

Project Overview

Executive Summary

This document outlines the complete migration plan for porting Motrix (Electron-based download manager) to the Aether project (Tauri + Nuxt 3 architecture). The migration will transform a traditional Electron application into a modern, performant Tauri-based desktop application while maintaining all core functionality.

Goals

  • Modernization: Migrate from Electron to Tauri for better performance and security
  • Framework Upgrade: Vue 2 → Vue 3 with Composition API
  • Type Safety: JavaScript → TypeScript throughout the application
  • Architecture: Implement clean, maintainable code structure
  • Performance: Leverage Rust backend for optimal performance
  • Cross-platform: Maintain support for Windows, macOS, and Linux

Architecture Analysis

Current State: Motrix

Architecture Overview:

Motrix Architecture
├── Frontend: Electron + Vue.js 2 + Element UI
├── Backend: Node.js with Aria2 integration
├── State: Vuex store management
├── Build: Webpack + Electron Builder
└── Language: JavaScript

Key Components:

  • Aria2 Engine: C++ binary for download management
  • Electron Main Process: System integration and window management
  • Vue.js Renderer: User interface and interaction
  • Vuex Store: Application state management
  • IPC Communication: Inter-process communication

Target State: Aether

Architecture Overview:

Aether Architecture
├── Frontend: Nuxt 3 + Vue.js 3 + Nuxt UI
├── Backend: Tauri (Rust) with Aria2 integration
├── State: Pinia store management
├── Build: Vite + Tauri bundler
└── Language: TypeScript + Rust

Key Improvements:

  • Rust Backend: Better performance and memory safety
  • Modern Frontend: Vue 3 Composition API with TypeScript
  • Smaller Bundle: Tauri produces significantly smaller binaries
  • Better Security: Rust's memory safety and Tauri's security model
  • Native Performance: Direct system integration through Rust

Migration Roadmap

Phase 1: Foundation & Architecture Setup

Duration: Weeks 1-2

1.1 Project Configuration

  • Package.json Updates

    {
      "name": "aether-download-manager",
      "description": "Modern download manager built with Tauri and Nuxt",
      "version": "1.0.0",
      "main": "src-tauri/target/release/aether-download-manager",
      "scripts": {
        "dev": "tauri dev",
        "build": "tauri build",
        "preview": "nuxt preview"
      }
    }
  • Tauri Configuration

    {
      "package": {
        "productName": "Aether Download Manager",
        "version": "1.0.0"
      },
      "tauri": {
        "allowlist": {
          "all": false,
          "fs": {
            "all": true,
            "scope": ["$DOWNLOAD/*", "$DOCUMENT/*"]
          },
          "shell": {
            "all": false,
            "sidecar": true,
            "scope": ["aria2c"]
          }
        }
      }
    }

1.2 Directory Structure

aether-download-manager/
├── docs/                          # Documentation
│   ├── migration-plan.md          # This document
│   ├── api-reference.md           # API documentation
│   └── development-guide.md       # Development setup
├── src-tauri/                     # Rust backend
│   ├── src/
│   │   ├── main.rs               # Entry point
│   │   ├── lib.rs                # Library exports
│   │   ├── commands/             # Tauri commands
│   │   │   ├── mod.rs
│   │   │   ├── download.rs       # Download operations
│   │   │   ├── config.rs         # Configuration management
│   │   │   └── system.rs         # System integration
│   │   ├── aria2/                # Aria2 integration
│   │   │   ├── mod.rs
│   │   │   ├── client.rs         # Aria2 RPC client
│   │   │   ├── config.rs         # Aria2 configuration
│   │   │   └── process.rs        # Process management
│   │   ├── models/               # Data models
│   │   │   ├── mod.rs
│   │   │   ├── download.rs       # Download task models
│   │   │   └── config.rs         # Configuration models
│   │   └── utils/                # Utility functions
│   ├── binaries/                 # Aria2 binaries
│   │   ├── aria2c-macos-x64
│   │   ├── aria2c-macos-arm64
│   │   ├── aria2c-windows-x64.exe
│   │   ├── aria2c-linux-x64
│   │   └── aria2c-linux-arm64
│   └── icons/                    # Application icons
├── app/                          # Nuxt frontend
│   ├── components/               # Vue components
│   │   ├── Download/             # Download management
│   │   │   ├── TaskList.vue      # Task list component
│   │   │   ├── TaskItem.vue      # Individual task
│   │   │   ├── AddDialog.vue     # Add download dialog
│   │   │   └── ProgressBar.vue   # Progress visualization
│   │   ├── Layout/               # Layout components
│   │   │   ├── Sidebar.vue       # Navigation sidebar
│   │   │   ├── Header.vue        # Application header
│   │   │   └── StatusBar.vue     # Status information
│   │   └── Settings/             # Settings interface
│   │       ├── General.vue       # General settings
│   │       ├── Aria2.vue         # Aria2 configuration
│   │       └── Advanced.vue      # Advanced options
│   ├── composables/              # Vue composables
│   │   ├── useDownload.ts        # Download management
│   │   ├── useAria2.ts           # Aria2 client
│   │   ├── useConfig.ts          # Configuration
│   │   └── useNotifications.ts   # Notification system
│   ├── layouts/                  # Nuxt layouts
│   │   ├── default.vue           # Default layout
│   │   └── minimal.vue           # Minimal layout
│   ├── pages/                    # Application pages
│   │   ├── index.vue             # Home/Downloads page
│   │   ├── downloads/            # Download management
│   │   │   ├── index.vue         # Active downloads
│   │   │   ├── completed.vue     # Completed downloads
│   │   │   └── [id].vue          # Task details
│   │   ├── settings/             # Settings pages
│   │   │   ├── index.vue         # Settings overview
│   │   │   ├── general.vue       # General settings
│   │   │   └── advanced.vue      # Advanced settings
│   │   └── about.vue             # About page
│   ├── stores/                   # Pinia stores
│   │   ├── download.ts           # Download state
│   │   ├── config.ts             # Configuration state
│   │   └── ui.ts                 # UI state
│   └── types/                    # TypeScript types
│       ├── download.ts           # Download types
│       ├── config.ts             # Configuration types
│       └── aria2.ts              # Aria2 types
└── public/                       # Static assets
    ├── aria2/                    # Aria2 configurations
    │   ├── aria2.conf            # Default configuration
    │   └── dht.dat               # DHT nodes
    └── icons/                    # UI icons

Phase 2: Core Download Engine Integration

Duration: Weeks 3-5

2.1 Rust Backend Development

Aria2 Process Management:

// src-tauri/src/aria2/process.rs
use std::process::{Child, Command, Stdio};
use tauri::api::process::{Command as TauriCommand, CommandEvent};

pub struct Aria2Process {
    child: Option<Child>,
    rpc_port: u16,
    rpc_secret: String,
}

impl Aria2Process {
    pub fn new(port: u16, secret: String) -> Self {
        Self {
            child: None,
            rpc_port: port,
            rpc_secret: secret,
        }
    }

    pub async fn start(&mut self, config_path: &str) -> Result<(), Box<dyn std::error::Error>> {
        let binary_path = self.get_aria2_binary_path()?;
        
        let child = Command::new(binary_path)
            .args(&[
                "--enable-rpc",
                &format!("--rpc-listen-port={}", self.rpc_port),
                &format!("--rpc-secret={}", self.rpc_secret),
                "--rpc-allow-origin-all",
                &format!("--conf-path={}", config_path),
            ])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        self.child = Some(child);
        Ok(())
    }

    pub fn stop(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        if let Some(mut child) = self.child.take() {
            child.kill()?;
            child.wait()?;
        }
        Ok(())
    }

    fn get_aria2_binary_path(&self) -> Result<String, Box<dyn std::error::Error>> {
        let platform = std::env::consts::OS;
        let arch = std::env::consts::ARCH;
        
        let binary_name = match platform {
            "windows" => format!("aria2c-{}-{}.exe", platform, arch),
            _ => format!("aria2c-{}-{}", platform, arch),
        };
        
        // Get binary path from Tauri resources
        let resource_path = tauri::api::path::resource_dir(&tauri::generate_context!())?
            .join("binaries")
            .join(binary_name);
            
        Ok(resource_path.to_string_lossy().to_string())
    }
}

Aria2 RPC Client:

// src-tauri/src/aria2/client.rs
use serde_json::{json, Value};
use reqwest::Client;

pub struct Aria2Client {
    base_url: String,
    secret: String,
    client: Client,
}

impl Aria2Client {
    pub fn new(host: &str, port: u16, secret: String) -> Self {
        Self {
            base_url: format!("http://{}:{}/jsonrpc", host, port),
            secret,
            client: Client::new(),
        }
    }

    pub async fn add_uri(&self, uris: Vec<String>, options: Value) -> Result<String, Box<dyn std::error::Error>> {
        let params = json!([
            format!("token:{}", self.secret),
            uris,
            options
        ]);

        let response = self.call_method("aria2.addUri", params).await?;
        Ok(response["result"].as_str().unwrap_or("").to_string())
    }

    pub async fn get_download_status(&self, gid: &str) -> Result<Value, Box<dyn std::error::Error>> {
        let params = json!([
            format!("token:{}", self.secret),
            gid
        ]);

        let response = self.call_method("aria2.tellStatus", params).await?;
        Ok(response["result"].clone())
    }

    pub async fn pause_download(&self, gid: &str) -> Result<(), Box<dyn std::error::Error>> {
        let params = json!([
            format!("token:{}", self.secret),
            gid
        ]);

        self.call_method("aria2.pause", params).await?;
        Ok(())
    }

    async fn call_method(&self, method: &str, params: Value) -> Result<Value, Box<dyn std::error::Error>> {
        let payload = json!({
            "jsonrpc": "2.0",
            "id": "1",
            "method": method,
            "params": params
        });

        let response = self.client
            .post(&self.base_url)
            .json(&payload)
            .send()
            .await?;

        let result: Value = response.json().await?;
        Ok(result)
    }
}

2.2 Tauri Commands

// src-tauri/src/commands/download.rs
use crate::aria2::Aria2Client;
use crate::models::download::{DownloadTask, DownloadOptions};
use tauri::State;
use std::sync::Mutex;

#[tauri::command]
pub async fn add_download_uri(
    client: State<'_, Mutex<Aria2Client>>,
    uris: Vec<String>,
    options: Option<DownloadOptions>,
) -> Result<String, String> {
    let client = client.lock().map_err(|e| e.to_string())?;
    let opts = options.unwrap_or_default();
    
    client
        .add_uri(uris, serde_json::to_value(opts).map_err(|e| e.to_string())?)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn pause_download(
    client: State<'_, Mutex<Aria2Client>>,
    gid: String,
) -> Result<(), String> {
    let client = client.lock().map_err(|e| e.to_string())?;
    
    client
        .pause_download(&gid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn resume_download(
    client: State<'_, Mutex<Aria2Client>>,
    gid: String,
) -> Result<(), String> {
    let client = client.lock().map_err(|e| e.to_string())?;
    
    client
        .resume_download(&gid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_all_downloads(
    client: State<'_, Mutex<Aria2Client>>,
) -> Result<Vec<DownloadTask>, String> {
    let client = client.lock().map_err(|e| e.to_string())?;
    
    client
        .get_all_downloads()
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn remove_download(
    client: State<'_, Mutex<Aria2Client>>,
    gid: String,
) -> Result<(), String> {
    let client = client.lock().map_err(|e| e.to_string())?;
    
    client
        .remove_download(&gid)
        .await
        .map_err(|e| e.to_string())
}

Phase 3: Frontend Development

Duration: Weeks 6-8

3.1 Vue 3 Composables

Download Management Composable:

// app/composables/useDownload.ts
import { ref, computed } from 'vue'
import { invoke } from '@tauri-apps/api/tauri'

export interface DownloadTask {
  gid: string
  status: 'active' | 'waiting' | 'paused' | 'error' | 'complete' | 'removed'
  totalLength: number
  completedLength: number
  downloadSpeed: number
  uploadSpeed: number
  connections: number
  files: DownloadFile[]
  dir: string
  createdTime: number
}

export interface DownloadFile {
  index: number
  path: string
  length: number
  completedLength: number
  selected: boolean
}

export interface DownloadOptions {
  dir?: string
  out?: string
  maxConnectionPerServer?: number
  split?: number
  minSplitSize?: string
  userAgent?: string
}

export const useDownload = () => {
  const downloads = ref<DownloadTask[]>([])
  const selectedDownloads = ref<string[]>([])
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  const activeDownloads = computed(() => 
    downloads.value.filter(d => d.status === 'active')
  )

  const completedDownloads = computed(() => 
    downloads.value.filter(d => d.status === 'complete')
  )

  const pausedDownloads = computed(() => 
    downloads.value.filter(d => d.status === 'paused')
  )

  const addDownload = async (uris: string[], options?: DownloadOptions): Promise<string> => {
    try {
      isLoading.value = true
      error.value = null
      
      const gid = await invoke<string>('add_download_uri', { uris, options })
      await refreshDownloads()
      
      return gid
    } catch (err) {
      error.value = err as string
      throw err
    } finally {
      isLoading.value = false
    }
  }

  const pauseDownload = async (gid: string): Promise<void> => {
    try {
      await invoke('pause_download', { gid })
      await refreshDownloads()
    } catch (err) {
      error.value = err as string
      throw err
    }
  }

  const resumeDownload = async (gid: string): Promise<void> => {
    try {
      await invoke('resume_download', { gid })
      await refreshDownloads()
    } catch (err) {
      error.value = err as string
      throw err
    }
  }

  const removeDownload = async (gid: string): Promise<void> => {
    try {
      await invoke('remove_download', { gid })
      downloads.value = downloads.value.filter(d => d.gid !== gid)
      selectedDownloads.value = selectedDownloads.value.filter(id => id !== gid)
    } catch (err) {
      error.value = err as string
      throw err
    }
  }

  const refreshDownloads = async (): Promise<void> => {
    try {
      const result = await invoke<DownloadTask[]>('get_all_downloads')
      downloads.value = result
    } catch (err) {
      error.value = err as string
      throw err
    }
  }

  const selectDownload = (gid: string, selected: boolean = true): void => {
    if (selected && !selectedDownloads.value.includes(gid)) {
      selectedDownloads.value.push(gid)
    } else if (!selected) {
      selectedDownloads.value = selectedDownloads.value.filter(id => id !== gid)
    }
  }

  const selectAllDownloads = (): void => {
    selectedDownloads.value = downloads.value.map(d => d.gid)
  }

  const clearSelection = (): void => {
    selectedDownloads.value = []
  }

  // Auto-refresh downloads every 2 seconds
  const { pause: pauseAutoRefresh, resume: resumeAutoRefresh } = useIntervalFn(
    refreshDownloads, 
    2000, 
    { immediate: true }
  )

  return {
    // State
    downloads: readonly(downloads),
    selectedDownloads: readonly(selectedDownloads),
    isLoading: readonly(isLoading),
    error: readonly(error),
    
    // Computed
    activeDownloads,
    completedDownloads,
    pausedDownloads,
    
    // Actions
    addDownload,
    pauseDownload,
    resumeDownload,
    removeDownload,
    refreshDownloads,
    selectDownload,
    selectAllDownloads,
    clearSelection,
    pauseAutoRefresh,
    resumeAutoRefresh,
  }
}

3.2 Core Vue Components

Task List Component:

<!-- app/components/Download/TaskList.vue -->
<template>
  <div class="task-list">
    <div v-if="isLoading" class="loading-state">
      <USkeleton class="h-20 w-full" v-for="i in 3" :key="i" />
    </div>
    
    <div v-else-if="tasks.length === 0" class="empty-state">
      <div class="text-center py-12">
        <Icon name="lucide:download" class="w-16 h-16 mx-auto mb-4 text-gray-400" />
        <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
          No downloads
        </h3>
        <p class="text-gray-500 dark:text-gray-400">
          Add a new download to get started
        </p>
      </div>
    </div>
    
    <div v-else class="space-y-2">
      <TaskItem
        v-for="task in tasks"
        :key="task.gid"
        :task="task"
        :selected="selectedDownloads.includes(task.gid)"
        @select="selectDownload"
        @pause="pauseDownload"
        @resume="resumeDownload"
        @remove="removeDownload"
        @click="$emit('taskClick', task)"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import type { DownloadTask } from '~/composables/useDownload'

interface Props {
  tasks: DownloadTask[]
  selectedDownloads: string[]
  isLoading?: boolean
}

interface Emits {
  taskClick: [task: DownloadTask]
}

defineProps<Props>()
defineEmits<Emits>()

const { selectDownload, pauseDownload, resumeDownload, removeDownload } = useDownload()
</script>

<style scoped>
.task-list {
  @apply min-h-full;
}

.loading-state {
  @apply space-y-4 p-4;
}

.empty-state {
  @apply flex items-center justify-center min-h-96;
}
</style>

Task Item Component:

<!-- app/components/Download/TaskItem.vue -->
<template>
  <UCard 
    :class="[
      'task-item cursor-pointer transition-all duration-200',
      { 'ring-2 ring-primary-500': selected }
    ]"
    @click="$emit('click')"
  >
    <div class="flex items-center space-x-4">
      <!-- Selection Checkbox -->
      <UCheckbox 
        :model-value="selected" 
        @update:model-value="$emit('select', task.gid, $event)"
        @click.stop
      />
      
      <!-- File Icon -->
      <div class="flex-shrink-0">
        <Icon :name="getFileIcon(task)" class="w-8 h-8" />
      </div>
      
      <!-- Task Info -->
      <div class="flex-1 min-w-0">
        <div class="flex items-center justify-between mb-2">
          <h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
            {{ getFileName(task) }}
          </h3>
          <TaskStatus :status="task.status" />
        </div>
        
        <!-- Progress Bar -->
        <ProgressBar 
          :total="task.totalLength"
          :completed="task.completedLength"
          :status="task.status"
          class="mb-2"
        />
        
        <!-- Stats -->
        <div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
          <span>{{ formatFileSize(task.completedLength) }} / {{ formatFileSize(task.totalLength) }}</span>
          <span v-if="task.status === 'active'" class="flex items-center space-x-4">
            <span>↓ {{ formatSpeed(task.downloadSpeed) }}</span>
            <span v-if="task.uploadSpeed > 0">↑ {{ formatSpeed(task.uploadSpeed) }}</span>
          </span>
          <span v-else-if="task.status === 'complete'">
            {{ $dayjs(task.createdTime * 1000).format('MMM D, YYYY') }}
          </span>
        </div>
      </div>
      
      <!-- Actions -->
      <div class="flex items-center space-x-2" @click.stop>
        <UButton
          v-if="task.status === 'active'"
          variant="ghost"
          size="sm"
          icon="lucide:pause"
          @click="$emit('pause', task.gid)"
        />
        <UButton
          v-else-if="task.status === 'paused'"
          variant="ghost"
          size="sm"
          icon="lucide:play"
          @click="$emit('resume', task.gid)"
        />
        
        <UDropdown :items="getActionItems(task)">
          <UButton variant="ghost" size="sm" icon="lucide:more-horizontal" />
        </UDropdown>
      </div>
    </div>
  </UCard>
</template>

<script setup lang="ts">
import type { DownloadTask } from '~/composables/useDownload'

interface Props {
  task: DownloadTask
  selected: boolean
}

interface Emits {
  click: []
  select: [gid: string, selected: boolean]
  pause: [gid: string]
  resume: [gid: string]
  remove: [gid: string]
}

defineProps<Props>()
defineEmits<Emits>()

const getFileName = (task: DownloadTask): string => {
  if (task.files.length > 0) {
    return task.files[0].path.split('/').pop() || 'Unknown'
  }
  return 'Unknown'
}

const getFileIcon = (task: DownloadTask): string => {
  const fileName = getFileName(task)
  const ext = fileName.split('.').pop()?.toLowerCase()
  
  const iconMap: Record<string, string> = {
    mp4: 'lucide:video',
    avi: 'lucide:video',
    mkv: 'lucide:video',
    mp3: 'lucide:music',
    flac: 'lucide:music',
    wav: 'lucide:music',
    pdf: 'lucide:file-text',
    doc: 'lucide:file-text',
    docx: 'lucide:file-text',
    zip: 'lucide:archive',
    rar: 'lucide:archive',
    '7z': 'lucide:archive',
  }
  
  return iconMap[ext || ''] || 'lucide:file'
}

const getActionItems = (task: DownloadTask) => {
  const items = [
    {
      label: 'Open folder',
      icon: 'lucide:folder-open',
      click: () => openFolder(task.dir),
    },
    {
      label: 'Copy download link',
      icon: 'lucide:copy',
      click: () => copyDownloadLink(task),
    },
  ]
  
  if (task.status !== 'active') {
    items.push({
      label: 'Remove',
      icon: 'lucide:trash-2',
      click: () => $emit('remove', task.gid),
    })
  }
  
  return [items]
}

const formatFileSize = (bytes: number): string => {
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
  if (bytes === 0) return '0 B'
  const i = Math.floor(Math.log(bytes) / Math.log(1024))
  return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`
}

const formatSpeed = (bytesPerSec: number): string => {
  return `${formatFileSize(bytesPerSec)}/s`
}
</script>

Phase 4: Advanced Features

Duration: Weeks 9-11

4.1 Settings Management

Settings Store:

// app/stores/config.ts
export const useConfigStore = defineStore('config', () => {
  const config = ref<AppConfig>({
    general: {
      downloadPath: '',
      maxConcurrentDownloads: 5,
      maxConnectionPerServer: 16,
      minSplitSize: '20M',
      enableNotifications: true,
      autoStartDownloads: true,
    },
    aria2: {
      rpcPort: 6800,
      rpcSecret: '',
      userAgent: 'Mozilla/5.0 (compatible; Aether Download Manager)',
      retryTimes: 5,
      retryDelay: 10,
      timeout: 60,
      checkCertificate: false,
    },
    ui: {
      theme: 'auto',
      language: 'en',
      showSpeedInTitle: true,
      minimizeToTray: true,
    },
    advanced: {
      enableProxy: false,
      proxyType: 'http',
      proxyHost: '',
      proxyPort: 8080,
      proxyUsername: '',
      proxyPassword: '',
      enableDHT: true,
      enablePEX: true,
      enableLPD: true,
    },
  })

  const loadConfig = async (): Promise<void> => {
    try {
      const savedConfig = await invoke<AppConfig>('get_config')
      config.value = { ...config.value, ...savedConfig }
    } catch (error) {
      console.error('Failed to load config:', error)
    }
  }

  const saveConfig = async (): Promise<void> => {
    try {
      await invoke('save_config', { config: config.value })
    } catch (error) {
      console.error('Failed to save config:', error)
      throw error
    }
  }

  const updateConfig = async <T extends keyof AppConfig>(
    section: T,
    updates: Partial<AppConfig[T]>
  ): Promise<void> => {
    config.value[section] = { ...config.value[section], ...updates }
    await saveConfig()
  }

  const resetConfig = async (): Promise<void> => {
    try {
      await invoke('reset_config')
      await loadConfig()
    } catch (error) {
      console.error('Failed to reset config:', error)
      throw error
    }
  }

  return {
    config: readonly(config),
    loadConfig,
    saveConfig,
    updateConfig,
    resetConfig,
  }
})

export interface AppConfig {
  general: {
    downloadPath: string
    maxConcurrentDownloads: number
    maxConnectionPerServer: number
    minSplitSize: string
    enableNotifications: boolean
    autoStartDownloads: boolean
  }
  aria2: {
    rpcPort: number
    rpcSecret: string
    userAgent: string
    retryTimes: number
    retryDelay: number
    timeout: number
    checkCertificate: boolean
  }
  ui: {
    theme: 'auto' | 'light' | 'dark'
    language: string
    showSpeedInTitle: boolean
    minimizeToTray: boolean
  }
  advanced: {
    enableProxy: boolean
    proxyType: 'http' | 'https' | 'socks5'
    proxyHost: string
    proxyPort: number
    proxyUsername: string
    proxyPassword: string
    enableDHT: boolean
    enablePEX: boolean
    enableLPD: boolean
  }
}

4.2 Real-time Updates with WebSocket

WebSocket Integration:

// app/composables/useAria2WebSocket.ts
export const useAria2WebSocket = () => {
  const { config } = storeToRefs(useConfigStore())
  const { refreshDownloads } = useDownload()
  
  const ws = ref<WebSocket | null>(null)
  const isConnected = ref(false)
  const reconnectAttempts = ref(0)
  const maxReconnectAttempts = 5

  const connect = (): void => {
    try {
      const wsUrl = `ws://localhost:${config.value.aria2.rpcPort}/jsonrpc`
      ws.value = new WebSocket(wsUrl)

      ws.value.onopen = () => {
        console.log('WebSocket connected')
        isConnected.value = true
        reconnectAttempts.value = 0
        
        // Subscribe to aria2 notifications
        subscribeToNotifications()
      }

      ws.value.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data)
          handleNotification(message)
        } catch (error) {
          console.error('Failed to parse WebSocket message:', error)
        }
      }

      ws.value.onclose = () => {
        console.log('WebSocket disconnected')
        isConnected.value = false
        
        // Attempt to reconnect
        if (reconnectAttempts.value < maxReconnectAttempts) {
          setTimeout(() => {
            reconnectAttempts.value++
            connect()
          }, 2000 * reconnectAttempts.value)
        }
      }

      ws.value.onerror = (error) => {
        console.error('WebSocket error:', error)
      }
    } catch (error) {
      console.error('Failed to connect WebSocket:', error)
    }
  }

  const disconnect = (): void => {
    if (ws.value) {
      ws.value.close()
      ws.value = null
    }
  }

  const subscribeToNotifications = (): void => {
    if (!ws.value || ws.value.readyState !== WebSocket.OPEN) return

    const notifications = [
      'aria2.onDownloadStart',
      'aria2.onDownloadPause',
      'aria2.onDownloadStop',
      'aria2.onDownloadComplete',
      'aria2.onDownloadError',
      'aria2.onBtDownloadComplete',
    ]

    notifications.forEach(method => {
      ws.value?.send(JSON.stringify({
        jsonrpc: '2.0',
        id: Date.now(),
        method: 'system.multicall',
        params: [[['system.listNotifications']]]
      }))
    })
  }

  const handleNotification = (message: any): void => {
    if (message.method && message.method.startsWith('aria2.')) {
      const event = message.method.replace('aria2.', '')
      const params = message.params || []
      
      console.log(`Aria2 event: ${event}`, params)
      
      // Refresh downloads on any notification
      refreshDownloads()
      
      // Handle specific events
      switch (event) {
        case 'onDownloadStart':
          showNotification('Download started', 'A new download has started')
          break
        case 'onDownloadComplete':
          showNotification('Download completed', 'A download has finished')
          break
        case 'onDownloadError':
          showNotification('Download error', 'A download has encountered an error')
          break
      }
    }
  }

  const showNotification = (title: string, body: string): void => {
    if ('Notification' in window && Notification.permission === 'granted') {
      new Notification(title, { body })
    }
  }

  // Auto-connect when composable is used
  onMounted(() => {
    connect()
  })

  onUnmounted(() => {
    disconnect()
  })

  return {
    isConnected: readonly(isConnected),
    connect,
    disconnect,
  }
}

Phase 5: Testing & Quality Assurance

Duration: Weeks 12-13

5.1 Testing Strategy

Unit Tests:

// tests/composables/useDownload.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useDownload } from '~/composables/useDownload'

// Mock Tauri invoke
vi.mock('@tauri-apps/api/tauri', () => ({
  invoke: vi.fn(),
}))

describe('useDownload', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should add download successfully', async () => {
    const { invoke } = await import('@tauri-apps/api/tauri')
    vi.mocked(invoke).mockResolvedValueOnce('test-gid-123')

    const { addDownload } = useDownload()
    const gid = await addDownload(['https://example.com/file.zip'])

    expect(gid).toBe('test-gid-123')
    expect(invoke).toHaveBeenCalledWith('add_download_uri', {
      uris: ['https://example.com/file.zip'],
      options: undefined,
    })
  })

  it('should handle download errors', async () => {
    const { invoke } = await import('@tauri-apps/api/tauri')
    vi.mocked(invoke).mockRejectedValueOnce(new Error('Network error'))

    const { addDownload } = useDownload()

    await expect(addDownload(['invalid-url'])).rejects.toThrow('Network error')
  })
})

Integration Tests:

// src-tauri/src/tests/integration.rs
#[cfg(test)]
mod tests {
    use super::*;
    use crate::aria2::Aria2Client;
    use tokio_test;

    #[tokio_test]
    async fn test_aria2_client_connection() {
        let client = Aria2Client::new("localhost", 6800, "test-secret".to_string());
        
        // This would require a running aria2 instance for full integration test
        // For now, we test the client creation
        assert_eq!(client.base_url, "http://localhost:6800/jsonrpc");
    }

    #[tokio_test]
    async fn test_download_commands() {
        // Mock test for download commands
        let result = add_download_uri(
            vec!["https://httpbin.org/uuid".to_string()],
            None,
        ).await;
        
        // This would test the actual command in a test environment
        assert!(result.is_ok());
    }
}

5.2 Performance Testing

Benchmark Tests:

// src-tauri/benches/aria2_performance.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use aether_download_manager::aria2::Aria2Client;

fn benchmark_aria2_calls(c: &mut Criterion) {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let client = Aria2Client::new("localhost", 6800, "".to_string());

    c.bench_function("get_version", |b| {
        b.to_async(&rt).iter(|| async {
            black_box(client.get_version().await)
        })
    });
}

criterion_group!(benches, benchmark_aria2_calls);
criterion_main!(benches);

Phase 6: Deployment & Distribution

Duration: Week 14

6.1 Build Configuration

Tauri Configuration:

{
  "package": {
    "productName": "Aether Download Manager",
    "version": "1.0.0"
  },
  "build": {
    "beforeBuildCommand": "npm run build",
    "beforeDevCommand": "npm run dev",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "tauri": {
    "allowlist": {
      "all": false,
      "fs": {
        "all": true,
        "scope": [
          "$DOWNLOAD/*",
          "$DOCUMENT/*",
          "$HOME/.config/aether-download-manager/*"
        ]
      },
      "shell": {
        "all": false,
        "sidecar": true,
        "scope": ["aria2c"]
      },
      "window": {
        "all": false,
        "create": false,
        "center": true,
        "requestUserAttention": true,
        "setResizable": true,
        "setTitle": true,
        "maximize": true,
        "minimize": true,
        "show": true,
        "close": true,
        "hide": true,
        "setSize": true,
        "setMinSize": true,
        "setMaxSize": true,
        "setPosition": true,
        "setFullscreen": true,
        "setFocus": true,
        "setIcon": true,
        "setSkipTaskbar": true,
        "setCursorGrab": true,
        "setCursorVisible": true,
        "setCursorIcon": true,
        "setCursorPosition": true,
        "setIgnoreCursorEvents": true,
        "startDragging": true,
        "print": true
      },
      "notification": {
        "all": true
      },
      "app": {
        "all": false,
        "show": true,
        "hide": true
      }
    },
    "bundle": {
      "active": true,
      "targets": "all",
      "identifier": "com.aether.download-manager",
      "icon": [
        "icons/32x32.png",
        "icons/128x128.png",
        "icons/128x128@2x.png",
        "icons/icon.icns",
        "icons/icon.ico"
      ],
      "resources": [
        "binaries/*",
        "aria2/*"
      ],
      "externalBin": [
        "binaries/aria2c-macos-x64",
        "binaries/aria2c-macos-arm64",
        "binaries/aria2c-windows-x64.exe",
        "binaries/aria2c-linux-x64",
        "binaries/aria2c-linux-arm64"
      ],
      "copyright": "Copyright (c) 2025 Aether Team",
      "category": "Utility",
      "shortDescription": "Modern download manager",
      "longDescription": "A modern, fast, and reliable download manager built with Tauri and Nuxt.",
      "macOS": {
        "entitlements": null,
        "exceptionDomain": "",
        "frameworks": [],
        "providerShortName": null,
        "signingIdentity": null
      },
      "windows": {
        "certificateThumbprint": null,
        "digestAlgorithm": "sha256",
        "timestampUrl": ""
      },
      "linux": {
        "deb": {
          "depends": []
        }
      }
    },
    "security": {
      "csp": null
    },
    "updater": {
      "active": true,
      "endpoints": [
        "https://releases.aether-dm.com/{{target}}/{{current_version}}"
      ],
      "dialog": true,
      "pubkey": "YOUR_PUBLIC_KEY_HERE"
    },
    "windows": [
      {
        "fullscreen": false,
        "height": 720,
        "resizable": true,
        "title": "Aether Download Manager",
        "width": 1200,
        "minWidth": 800,
        "minHeight": 600
      }
    ]
  }
}

6.2 CI/CD Pipeline

GitHub Actions Workflow:

# .github/workflows/build.yml
name: Build and Release

on:
  push:
    tags:
      - 'v*'
  pull_request:
    branches: [main]

jobs:
  build:
    strategy:
      matrix:
        platform: [macos-latest, ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'npm'

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Install dependencies (Ubuntu)
        if: matrix.platform == 'ubuntu-latest'
        run: |
          sudo apt-get update
          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf

      - name: Install Node dependencies
        run: npm ci

      - name: Install Tauri dependencies
        run: npm install -g @tauri-apps/cli

      - name: Build application
        run: npm run tauri build

      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: ${{ matrix.platform }}-build
          path: src-tauri/target/release/bundle/

  release:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')

    steps:
      - uses: actions/checkout@v3

      - name: Download artifacts
        uses: actions/download-artifact@v3

      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            **/*.dmg
            **/*.exe
            **/*.AppImage
            **/*.deb
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Summary

This comprehensive migration plan provides a structured approach to transforming Motrix into a modern Tauri-based application. The plan emphasizes:

  1. Incremental Migration: Phased approach minimizing risk
  2. Modern Architecture: Leveraging Rust backend with Vue 3 frontend
  3. Type Safety: Full TypeScript implementation
  4. Performance: Optimized with Tauri's efficiency
  5. Maintainability: Clean, modular code structure
  6. Cross-platform: Support for all major desktop platforms

The migration will result in a faster, more secure, and maintainable download manager while preserving all the functionality that makes Motrix popular among users.

Key Benefits:

  • 🚀 50-70% smaller binary size compared to Electron
  • Better performance with Rust backend
  • 🔒 Enhanced security with Tauri's security model
  • 🛠️ Modern development experience with Vue 3 + TypeScript
  • 📱 Future-ready architecture for potential mobile support

This roadmap serves as a comprehensive guide for the development team to execute a successful migration from Motrix to Aether Download Manager.