Full Code of agalwood/Motrix for AI

master 7012040fec92 cached
475 files
896.6 KB
276.0k tokens
499 symbols
1 requests
Download .txt
Showing preview only (999K chars total). Download the full file or copy to clipboard to get everything.
Repository: agalwood/Motrix
Branch: master
Commit: 7012040fec92
Files: 475
Total size: 896.6 KB

Directory structure:
gitextract_m9s_ek03/

├── .babelrc
├── .editorconfig
├── .electron-vue/
│   ├── build.js
│   ├── dev-client.js
│   ├── dev-runner.js
│   ├── webpack.main.config.js
│   ├── webpack.renderer.config.js
│   └── webpack.web.config.js
├── .eslintignore
├── .eslintrc.js
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── 1_bug_report.yml
│   │   ├── 2_bug_report_cn.yml
│   │   ├── feature_request.md
│   │   └── feature_request_cn.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── lock.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       └── release.yml
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING-CN.md
├── CONTRIBUTING.md
├── LICENSE
├── README-CN.md
├── README.md
├── app-update.yml
├── appveyor.yml
├── build/
│   ├── afterPackHook.js
│   ├── afterSignHook.js
│   ├── background.tiff
│   ├── icon.icns
│   └── torrent.icns
├── electron-builder.json
├── extra/
│   ├── README.md
│   ├── darwin/
│   │   ├── arm64/
│   │   │   └── engine/
│   │   │       ├── aria2.conf
│   │   │       └── aria2c
│   │   └── x64/
│   │       └── engine/
│   │           ├── aria2.conf
│   │           └── aria2c
│   ├── linux/
│   │   ├── arm64/
│   │   │   └── engine/
│   │   │       ├── aria2.conf
│   │   │       └── aria2c
│   │   ├── armv7l/
│   │   │   └── engine/
│   │   │       ├── aria2.conf
│   │   │       └── aria2c
│   │   └── x64/
│   │       └── engine/
│   │           ├── aria2.conf
│   │           └── aria2c
│   └── win32/
│       ├── ia32/
│       │   └── engine/
│       │       └── aria2.conf
│       └── x64/
│           └── engine/
│               └── aria2.conf
├── jsconfig.json
├── package.json
├── src/
│   ├── index.ejs
│   ├── main/
│   │   ├── Application.js
│   │   ├── Launcher.js
│   │   ├── configs/
│   │   │   ├── engine.js
│   │   │   ├── page.js
│   │   │   └── protocol.js
│   │   ├── core/
│   │   │   ├── AutoLaunchManager.js
│   │   │   ├── ConfigManager.js
│   │   │   ├── Context.js
│   │   │   ├── EnergyManager.js
│   │   │   ├── Engine.js
│   │   │   ├── EngineClient.js
│   │   │   ├── ExceptionHandler.js
│   │   │   ├── Logger.js
│   │   │   ├── ProtocolManager.js
│   │   │   ├── UPnPManager.js
│   │   │   └── UpdateManager.js
│   │   ├── index.dev.js
│   │   ├── index.js
│   │   ├── menus/
│   │   │   ├── darwin.json
│   │   │   ├── linux.json
│   │   │   ├── touchBar.json
│   │   │   ├── tray.json
│   │   │   └── win32.json
│   │   ├── pages/
│   │   │   ├── about.html
│   │   │   └── index.html
│   │   ├── ui/
│   │   │   ├── DockManager.js
│   │   │   ├── Locale.js
│   │   │   ├── MenuManager.js
│   │   │   ├── ThemeManager.js
│   │   │   ├── TouchBarManager.js
│   │   │   ├── TrayManager.js
│   │   │   └── WindowManager.js
│   │   └── utils/
│   │       ├── index.js
│   │       └── menu.js
│   ├── renderer/
│   │   ├── api/
│   │   │   ├── Api.js
│   │   │   └── index.js
│   │   ├── assets/
│   │   │   └── .gitkeep
│   │   ├── components/
│   │   │   ├── About/
│   │   │   │   ├── AboutPanel.vue
│   │   │   │   ├── AppInfo.vue
│   │   │   │   └── Copyright.vue
│   │   │   ├── Aside/
│   │   │   │   └── Index.vue
│   │   │   ├── Browser/
│   │   │   │   └── index.vue
│   │   │   ├── CommandManager/
│   │   │   │   ├── index.js
│   │   │   │   └── instance.js
│   │   │   ├── DragSelect/
│   │   │   │   └── Index.vue
│   │   │   ├── Dragger/
│   │   │   │   └── Index.vue
│   │   │   ├── Icons/
│   │   │   │   ├── Icon.vue
│   │   │   │   ├── arrow-down.js
│   │   │   │   ├── arrow-up.js
│   │   │   │   ├── audio.js
│   │   │   │   ├── delete.js
│   │   │   │   ├── dice.js
│   │   │   │   ├── document.js
│   │   │   │   ├── folder.js
│   │   │   │   ├── image.js
│   │   │   │   ├── inbox.js
│   │   │   │   ├── info-circle.js
│   │   │   │   ├── info-square.js
│   │   │   │   ├── link.js
│   │   │   │   ├── magnet.js
│   │   │   │   ├── menu-about.js
│   │   │   │   ├── menu-add.js
│   │   │   │   ├── menu-preference.js
│   │   │   │   ├── menu-task.js
│   │   │   │   ├── more.js
│   │   │   │   ├── node.js
│   │   │   │   ├── preference-advanced.js
│   │   │   │   ├── preference-basic.js
│   │   │   │   ├── preference-lab.js
│   │   │   │   ├── purge.js
│   │   │   │   ├── refresh.js
│   │   │   │   ├── speedometer.js
│   │   │   │   ├── sync.js
│   │   │   │   ├── task-history.js
│   │   │   │   ├── task-pause-line.js
│   │   │   │   ├── task-pause.js
│   │   │   │   ├── task-restart.js
│   │   │   │   ├── task-start-line.js
│   │   │   │   ├── task-start.js
│   │   │   │   ├── task-stop-line.js
│   │   │   │   ├── task-stop.js
│   │   │   │   ├── trash.js
│   │   │   │   ├── video.js
│   │   │   │   ├── win-close.js
│   │   │   │   ├── win-maximize.js
│   │   │   │   └── win-minimize.js
│   │   │   ├── Locale/
│   │   │   │   └── index.js
│   │   │   ├── Logo/
│   │   │   │   ├── Logo.vue
│   │   │   │   └── LogoMini.vue
│   │   │   ├── Main.vue
│   │   │   ├── Msg/
│   │   │   │   └── index.js
│   │   │   ├── Native/
│   │   │   │   ├── DynamicTray.vue
│   │   │   │   ├── EngineClient.vue
│   │   │   │   ├── Ipc.vue
│   │   │   │   ├── SelectDirectory.vue
│   │   │   │   ├── ShowInFolder.vue
│   │   │   │   └── TitleBar.vue
│   │   │   ├── Preference/
│   │   │   │   ├── Advanced.vue
│   │   │   │   ├── Basic.vue
│   │   │   │   ├── HistoryDirectory.vue
│   │   │   │   ├── Index.vue
│   │   │   │   ├── Lab.vue
│   │   │   │   └── ThemeSwitcher.vue
│   │   │   ├── Speedometer/
│   │   │   │   └── Speedometer.vue
│   │   │   ├── Subnav/
│   │   │   │   ├── PreferenceSubnav.vue
│   │   │   │   ├── SubnavSwitcher.vue
│   │   │   │   └── TaskSubnav.vue
│   │   │   ├── Task/
│   │   │   │   ├── AddTask.vue
│   │   │   │   ├── Index.vue
│   │   │   │   ├── SelectTorrent.vue
│   │   │   │   ├── TaskActions.vue
│   │   │   │   ├── TaskItem.vue
│   │   │   │   ├── TaskItemActions.vue
│   │   │   │   ├── TaskList.vue
│   │   │   │   ├── TaskProgress.vue
│   │   │   │   ├── TaskProgressInfo.vue
│   │   │   │   └── TaskStatus.vue
│   │   │   ├── TaskDetail/
│   │   │   │   ├── Index.vue
│   │   │   │   ├── TaskActivity.vue
│   │   │   │   ├── TaskFiles.vue
│   │   │   │   ├── TaskGeneral.vue
│   │   │   │   ├── TaskPeers.vue
│   │   │   │   └── TaskTrackers.vue
│   │   │   ├── TaskGraphic/
│   │   │   │   ├── Atom.vue
│   │   │   │   └── Index.vue
│   │   │   └── Theme/
│   │   │       ├── Dark/
│   │   │       │   └── Variables.scss
│   │   │       ├── Dark.scss
│   │   │       ├── Default.scss
│   │   │       ├── Index.scss
│   │   │       ├── Light/
│   │   │       │   └── Variables.scss
│   │   │       └── Variables.scss
│   │   ├── pages/
│   │   │   └── index/
│   │   │       ├── App.vue
│   │   │       ├── commands.js
│   │   │       └── main.js
│   │   ├── router/
│   │   │   └── index.js
│   │   ├── store/
│   │   │   ├── index.js
│   │   │   └── modules/
│   │   │       ├── app.js
│   │   │       ├── index.js
│   │   │       ├── preference.js
│   │   │       └── task.js
│   │   ├── utils/
│   │   │   ├── native.js
│   │   │   └── task.js
│   │   └── workers/
│   │       └── tray.worker.js
│   └── shared/
│       ├── aria2/
│       │   ├── index.js
│       │   └── lib/
│       │       ├── Aria2.js
│       │       ├── Deferred.js
│       │       ├── JSONRPCClient.js
│       │       ├── JSONRPCError.js
│       │       ├── debug.js
│       │       └── promiseEvent.js
│       ├── colors.json
│       ├── configKeys.js
│       ├── constants.js
│       ├── keymap.json
│       ├── locales/
│       │   ├── LocaleManager.js
│       │   ├── all.js
│       │   ├── app.js
│       │   ├── ar/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── bg/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ca/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── de/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── el/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── en-US/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── es/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── fa/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── fr/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── hu/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── id/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── index.js
│       │   ├── it/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ja/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ko/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── nb/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── nl/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── pl/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── pt-BR/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ro/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ru/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── th/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── tr/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── uk/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── vi/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── zh-CN/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   └── zh-TW/
│       │       ├── about.js
│       │       ├── app.js
│       │       ├── edit.js
│       │       ├── help.js
│       │       ├── index.js
│       │       ├── menu.js
│       │       ├── preferences.js
│       │       ├── subnav.js
│       │       ├── task.js
│       │       └── window.js
│       ├── ua.js
│       └── utils/
│           ├── curl.js
│           ├── index.js
│           ├── rename.js
│           ├── tracker.js
│           └── tray.js
└── static/
    └── .gitkeep

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "comments": false,
  "env": {
    "main": {
      "presets": ["@babel/preset-env"]
    },
    "renderer": {
      "presets": [
        "@babel/preset-env"
      ],
      "plugins": [
        [
          "component",
          {
            "libraryName": "element-ui",
            "styleLibraryName": "theme-chalk"
          }
        ]
      ]
    },
    "web": {
      "presets": ["@babel/preset-env"],
      "plugins": [
        [
          "component",
          {
            "libraryName": "element-ui",
            "styleLibraryName": "theme-chalk"
          }
        ]
      ]
    }
  },
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-runtime"
  ]
}


================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .electron-vue/build.js
================================================
'use strict'

process.env.NODE_ENV = 'production'

const { say } = require('cfonts')
const chalk = require('chalk')
const del = require('del')
const Webpack = require('webpack')
const Multispinner = require('@motrix/multispinner')

const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
const webConfig = require('./webpack.web.config')

const doneLog = chalk.bgGreen.white(' DONE ') + ' '
const errorLog = chalk.bgRed.white(' ERROR ') + ' '
const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
const isCI = process.env.CI || false

if (process.env.BUILD_TARGET === 'clean') {
  clean()
} else if (process.env.BUILD_TARGET === 'web') {
  web()
} else {
  build()
}

function clean () {
  del.sync(['release/*', '!.gitkeep'])
  console.log(`\n${doneLog}\n`)
  process.exit()
}

function build () {
  greeting()

  del.sync(['dist/electron/*', '!.gitkeep'])

  const tasks = ['main', 'renderer']
  const m = new Multispinner(tasks, {
    preText: 'building',
    postText: 'process'
  })

  let results = ''

  m.on('success', () => {
    process.stdout.write('\x1B[2J\x1B[0f')
    console.log(`\n\n${results}`)
    console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
    process.exit()
  })

  pack(mainConfig).then(result => {
    results += result + '\n\n'
    m.success('main')
  }).catch(err => {
    m.error('main')
    console.log(`\n  ${errorLog}failed to build main process`)
    console.error(`\n${err}\n`)
    process.exit(1)
  })

  pack(rendererConfig).then(result => {
    results += result + '\n\n'
    m.success('renderer')
  }).catch(err => {
    m.error('renderer')
    console.log(`\n  ${errorLog}failed to build renderer process`)
    console.error(`\n${err}\n`)
    process.exit(1)
  })
}

function pack (config) {
  return new Promise((resolve, reject) => {
    config.mode = 'production'
    Webpack(config, (err, stats) => {
      if (err) {
        reject(err.stack || err)
      } else if (stats.hasErrors()) {
        let err = ''

        stats.toString({
          chunks: false,
          colors: true
        })
        .split(/\r?\n/)
        .forEach(line => {
          err += `    ${line}\n`
        })

        reject(err)
      } else {
        resolve(stats.toString({
          chunks: false,
          colors: true
        }))
      }
    })
  })
}

function web () {
  deleteSync(['dist/web/*', '!.gitkeep'])
  webConfig.mode = 'production'
  Webpack(webConfig, (err, stats) => {
    if (err || stats.hasErrors()) console.log(err)

    console.log(stats.toString({
      chunks: false,
      colors: true
    }))

    process.exit()
  })
}

function greeting () {
  const cols = process.stdout.columns
  let text = ''

  if (cols > 85) {
    text = 'lets-build'
  } else if (cols > 60) {
    text = 'lets-|build'
  } else {
    text = false
  }

  if (text && !isCI) {
    say(text, {
      colors: ['magentaBright'],
      font: 'simple3d',
      space: false
    })
  } else console.log(chalk.magentaBright.bold('\n  lets-build'))
  console.log()
}


================================================
FILE: .electron-vue/dev-client.js
================================================
const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')

hotClient.subscribe(event => {
  /**
   * Reload browser when HTMLWebpackPlugin emits a new index.html
   *
   * Currently disabled until jantimon/html-webpack-plugin#680 is resolved.
   * https://github.com/SimulatedGREG/electron-vue/issues/437
   * https://github.com/jantimon/html-webpack-plugin/issues/680
   */
  // if (event.action === 'reload') {
  //   window.location.reload()
  // }

  /**
   * Notify `mainWindow` when `main` process is compiling,
   * giving notice for an expected reload of the `electron` process
   */
  if (event.action === 'compiling') {
    document.body.innerHTML += `
      <style>
        #dev-client {
          background: #4fc08d;
          border-radius: 4px;
          bottom: 20px;
          box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
          color: #fff;
          font-family: 'Source Sans Pro', sans-serif;
          left: 20px;
          padding: 8px 12px;
          position: absolute;
        }
      </style>

      <div id="dev-client">
        Compiling Main Process...
      </div>
    `
  }
})


================================================
FILE: .electron-vue/dev-runner.js
================================================
'use strict'

const path = require('node:path')
const { spawn } = require('node:child_process')
const { say } = require('cfonts')
const electron = require('electron')
const chalk = require('chalk')
const Webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')

const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')

let electronProcess = null
let manualRestart = false

function logStats (proc, data) {
  let log = ''

  log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`)
  log += '\n\n'

  if (typeof data === 'object') {
    data.toString({
      colors: true,
      chunks: false
    }).split(/\r?\n/).forEach(line => {
      log += '  ' + line + '\n'
    })
  } else {
    log += `  ${data}\n`
  }

  log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n'

  console.log(log)
}

function startRenderer () {
  return new Promise(async (resolve, reject) => {
    rendererConfig.entry.index = rendererConfig.entry.index
    rendererConfig.mode = 'development'

    const compiler = Webpack(rendererConfig)
    const devServerOptions = {
      ...rendererConfig.devServer,
      port: 9080,
      static: {
        directory: path.resolve(__dirname, "../"),
      },
    };

    const server = new WebpackDevServer(devServerOptions, compiler)
    await server.start()
    resolve()
  })
}

function startMain () {
  return new Promise((resolve, reject) => {
    mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
    mainConfig.mode = 'development'
    const compiler = Webpack(mainConfig)

    compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => {
      logStats('Main', chalk.white.bold('compiling...'))
      // hotMiddleware.publish({ action: 'compiling' })
      done()
    })

    compiler.watch({}, (err, stats) => {
      if (err) {
        console.log(err)
        return
      }

      logStats('Main', stats)

      if (electronProcess && electronProcess.kill) {
        manualRestart = true
        process.kill(electronProcess.pid)
        electronProcess = null
        startElectron()

        setTimeout(() => {
          manualRestart = false
        }, 5000)
      }

      resolve()
    })
  })
}

function startElectron () {
  electronProcess = spawn(electron, ['--inspect=5858', path.join(__dirname, '../dist/electron/main.js')])

  electronProcess.stdout.on('data', data => {
    electronLog(data, 'blue')
  })
  electronProcess.stderr.on('data', data => {
    electronLog(data, 'red')
  })

  electronProcess.on('close', () => {
    if (!manualRestart) process.exit()
  })
}

function electronLog (data, color) {
  let log = ''
  data = data.toString().split(/\r?\n/)
  data.forEach(line => {
    log += `  ${line}\n`
  })
  if (/[0-9A-z]+/.test(log)) {
    console.log(
      chalk[color].bold('┏ Electron -------------------') +
      '\n\n' +
      log +
      chalk[color].bold('┗ ----------------------------') +
      '\n'
    )
  }
}

function greeting () {
  const cols = process.stdout.columns
  let text = ''

  if (cols > 104) {
    text = 'motrix-dev'
  } else if (cols > 76) {
    text = 'motrix-|dev'
  } else {
    text = false
  }

  if (text) {
    say(text, {
      colors: ['magentaBright'],
      font: 'simple3d',
      space: false
    })
  } else console.log(chalk.magentaBright.bold('\n  motrix-dev'))
  console.log(chalk.blue('  getting ready...') + '\n')
}

function init () {
  greeting()

  Promise.all([startRenderer(), startMain()])
    .then(() => {
      startElectron()
    })
    .catch(err => {
      console.error(err)
    })
}

init()


================================================
FILE: .electron-vue/webpack.main.config.js
================================================
'use strict'

process.env.BABEL_ENV = 'main'

const path = require('node:path')
const Webpack = require('webpack')
const ESLintPlugin = require('eslint-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const { dependencies } = require('../package.json')
const { appId } = require('../electron-builder.json')
const devMode = process.env.NODE_ENV !== 'production'

let mainConfig = {
  entry: {
    main: path.join(__dirname, '../src/main/index.js')
  },
  externals: [
    ...Object.keys(dependencies || {})
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.node$/,
        use: 'node-loader'
      }
    ]
  },
  node: {
    __dirname: devMode,
    __filename: devMode
  },
  output: {
    filename: '[name].js',
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '../dist/electron')
  },
  plugins: [
    new Webpack.NoEmitOnErrorsPlugin(),
    new ESLintPlugin({
      formatter: require('eslint-friendly-formatter')
    })
  ],
  resolve: {
    alias: {
      '@': path.join(__dirname, '../src/main'),
      '@shared': path.join(__dirname, '../src/shared')
    },
    extensions: ['.js', '.json', '.node']
  },
  target: 'electron-main',
  optimization: {
    minimize: !devMode,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      })
    ],
  },
}

/**
 * Adjust mainConfig for development settings
 */
if (devMode) {
  mainConfig.plugins.push(
    new Webpack.DefinePlugin({
      '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`,
      'appId': `"${appId}"`
    })
  )
}

/**
 * Adjust mainConfig for production settings
 */
if (!devMode) {
  mainConfig.plugins.push(
    new Webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"',
      'appId': `"${appId}"`
    })
  )
}

module.exports = mainConfig


================================================
FILE: .electron-vue/webpack.renderer.config.js
================================================
'use strict'

process.env.BABEL_ENV = 'renderer'

const path = require('node:path')
const Webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const { dependencies } = require('../package.json')
const devMode = process.env.NODE_ENV !== 'production'

/**
 * List of node_modules to include in webpack bundle
 *
 * Required for specific packages like Vue UI libraries
 * that provide pure *.vue files that need compiling
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
 */
let whiteListedModules = ['vue']

let rendererConfig = {
  entry: {
    index: path.join(__dirname, '../src/renderer/pages/index/main.js')
  },
  externals: [
    ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
  ],
  module: {
    rules: [
      {
        test: /\.worker\.js$/,
        use: {
          loader: 'worker-loader',
          options: { filename: '[name].js' }
        }
      },
      {
        test: /\.scss$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'sass-loader',
            options: {
              implementation: require('sass'),
              additionalData: '@import "@/components/Theme/Variables.scss";',
              sassOptions: {
                includePaths:[__dirname, 'src']
              }
            },
          }
        ]
      },
      {
        test: /\.sass$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'sass-loader',
            options: {
              implementation: require('sass'),
              indentedSyntax: true,
              additionalData: '@import "@/components/Theme/Variables.scss";',
              sassOptions: {
                includePaths:[__dirname, 'src']
              }
            },
          }
        ]
      },
      {
        test: /\.less$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.node$/,
        use: 'node-loader'
      },
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader',
          options: {
            extractCSS: process.env.NODE_ENV === 'production',
            loaders: {
              sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
              scss: 'vue-style-loader!css-loader!sass-loader',
              less: 'vue-style-loader!css-loader!less-loader'
            }
          }
        }
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        type: 'asset/inline'
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        type: 'asset/resource'
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        type: 'asset/inline'
      }
    ]
  },
  node: {
    __dirname: devMode,
    __filename: devMode
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    }),
    new HtmlWebpackPlugin({
      title: 'Motrix',
      filename: 'index.html',
      chunks: ['index'],
      template: path.resolve(__dirname, '../src/index.ejs'),
      // minify: {
      //   collapseWhitespace: true,
      //   removeAttributeQuotes: true,
      //   removeComments: true
      // },
      isBrowser: false,
      isDev: process.env.NODE_ENV !== 'production',
      nodeModules: devMode
        ? path.resolve(__dirname, '../node_modules')
        : false
    }),
    new Webpack.HotModuleReplacementPlugin(),
    new Webpack.NoEmitOnErrorsPlugin(),
    new ESLintPlugin({
      extensions: ['js', 'vue'],
      formatter: require('eslint-friendly-formatter')
    })
  ],
  output: {
    filename: '[name].js',
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '../dist/electron'),
    globalObject: 'this',
    publicPath: ''
  },
  resolve: {
    alias: {
      '@': path.join(__dirname, '../src/renderer'),
      '@shared': path.join(__dirname, '../src/shared'),
      'vue$': 'vue/dist/vue.esm.js'
    },
    extensions: ['.js', '.vue', '.json', '.css', '.node']
  },
  target: 'electron-renderer',
  optimization: {
    minimize: !devMode,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
      new CssMinimizerPlugin(),
    ],
  },
}

/**
 * Adjust rendererConfig for development settings
 */
if (devMode) {
  rendererConfig.devtool = 'eval-cheap-module-source-map'

  rendererConfig.plugins.push(
    new Webpack.DefinePlugin({
      '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
    })
  )
}

/**
 * Adjust rendererConfig for production settings
 */
if (!devMode) {
  rendererConfig.plugins.push(
    new CopyWebpackPlugin({
      patterns: [{
        from: path.join(__dirname, '../static'),
        to: path.join(__dirname, '../dist/electron/static'),
        globOptions: { ignore: [ '.*' ] }
      }]
    }),
    new Webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"'
    }),
    new Webpack.LoaderOptionsPlugin({
      minimize: false
    })
  )
}

module.exports = rendererConfig


================================================
FILE: .electron-vue/webpack.web.config.js
================================================
'use strict'

process.env.BABEL_ENV = 'web'

const path = require('node:path')
const { dependencies } = require('../package.json')
const Webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const devMode = process.env.NODE_ENV !== 'production'

/**
 * List of node_modules to include in webpack bundle
 *
 * Required for specific packages like Vue UI libraries
 * that provide pure *.vue files that need compiling
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
 */
let whiteListedModules = ['vue']

let webConfig = {
  entry: {
    index: path.join(__dirname, '../src/renderer/pages/index/main.js')
  },
  externals: [
    ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
  ],
  module: {
    rules: [
      {
        test: /\.worker\.js$/,
        use: {
          loader: 'worker-loader',
          options: { filename: '[name].js' }
        }
      },
      {
        test: /\.scss$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'sass-loader',
            options: {
              implementation: require('sass'),
              additionalData: '@import "@/components/Theme/Variables.scss"',
              sassOptions: {
                includePaths:[__dirname, 'src']
              }
            },
          }
        ]
      },
      {
        test: /\.sass$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'sass-loader',
            options: {
              implementation: require('sass'),
              indentedSyntax: true,
              additionalData: '@import "@/components/Theme/Variables.scss"',
              sassOptions: {
                includePaths:[__dirname, 'src']
              }
            },
          }
        ]
      },
      {
        test: /\.less$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [
          devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.js$/,
        use: 'babel-loader',
        include: [ path.resolve(__dirname, '../src/renderer') ],
        exclude: /node_modules/
      },
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader',
          options: {
            extractCSS: true,
            loaders: {
              sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
              scss: 'vue-style-loader!css-loader!sass-loader',
              less: 'vue-style-loader!css-loader!less-loader'
            }
          }
        }
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        type: 'asset/inline'
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        type: 'asset/inline'
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    }),
    new HtmlWebpackPlugin({
      title: 'Motrix',
      filename: 'index.html',
      chunks: ['index'],
      template: path.resolve(__dirname, '../src/index.ejs'),
      // minify: {
      //   collapseWhitespace: true,
      //   removeAttributeQuotes: true,
      //   removeComments: true
      // },
      isBrowser: true,
      isDev: process.env.NODE_ENV !== 'production',
      nodeModules: devMode
        ? path.resolve(__dirname, '../node_modules')
        : false
    }),
    new Webpack.DefinePlugin({
      'process.env.IS_WEB': 'true'
    }),
    new Webpack.HotModuleReplacementPlugin(),
    new Webpack.NoEmitOnErrorsPlugin(),
    new ESLintPlugin({
      extensions: ['js', 'vue'],
      formatter: require('eslint-friendly-formatter')
    })
  ],
  output: {
    filename: '[name].js',
    path: path.join(__dirname, '../dist/web'),
    globalObject: 'this',
    publicPath: ''
  },
  resolve: {
    alias: {
      '@': path.join(__dirname, '../src/renderer'),
      '@shared': path.join(__dirname, '../src/shared'),
      'vue$': 'vue/dist/vue.esm.js'
    },
    extensions: ['.js', '.vue', '.json', '.css']
  },
  target: 'web',
  optimization: {
    minimize: !devMode,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
      new CssMinimizerPlugin(),
    ],
  },
}

/**
 * Adjust webConfig for development settings
 */
if (devMode) {
  webConfig.devtool = 'eval-cheap-module-source-map'
}

/**
 * Adjust webConfig for production settings
 */
if (!devMode) {
  webConfig.plugins.push(
    new CopyWebpackPlugin({
      patterns: [{
        from: path.join(__dirname, '../static'),
        to: path.join(__dirname, '../dist/electron/static'),
        globOptions: { ignore: [ '.*' ] }
      }]
    }),
    new Webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"'
    }),
    new Webpack.LoaderOptionsPlugin({
      minimize: true
    })
  )
}

module.exports = webConfig


================================================
FILE: .eslintignore
================================================
src/renderer/components/Icons/*.js

src/shared/locales/*
!src/shared/locales/all.js
!src/shared/locales/app.js
!src/shared/locales/index.js
!src/shared/locales/LocalManager.js


================================================
FILE: .eslintrc.js
================================================
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  extends: [
    'plugin:vue/essential',
    '@vue/standard'
  ],
  parserOptions: {
    parser: 'babel-eslint'
  },
  globals: {
    appId: true,
    __static: true
  },
  rules: {
    'no-console': 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    indent: ['error', 2],
    'vue/script-indent': ['error', 2, {
      baseIndent: 1
    }]
  },
  overrides: [
    {
      files: ['*.vue'],
      rules: {
        indent: 'off'
      }
    }
  ]
}


================================================
FILE: .github/ISSUE_TEMPLATE/1_bug_report.yml
================================================
name: 🐛 [NEW] Bug Report
description: File a bug report here
title: "[BUG]: "
labels: ["bug"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report 🤗
        Make sure there aren't any open/closed issues for this topic 😃

  - type: textarea
    id: bug-description
    attributes:
      label: Description of the bug
      description: Give us a brief description of what happened and what should have happened
    validations:
      required: true

  - type: textarea
    id: app-version
    attributes:
      label: Motrix Version
      description: Please provide detailed version information and installation method, such as macOS Apple silicon dmg, Windows Universal installation file, etc.
    validations:
      required: true

  - type: textarea
    attributes:
      label: Environment
      description: |
        Run this command in your project's root folder and paste the result:

        ```sh
        npx envinfo --system --binaries --browsers
        ```
        add `| pbcopy` if you're in macOS for easy copy paste.
        Alternatively, you can manually gather the version information from your environment.
    validations:
      required: true

  - type: textarea
    id: steps-to-reproduce
    attributes:
      label: Steps To Reproduce
      description: Steps to reproduce the behavior.
      placeholder: |
        1. Go to '...'
        2. Click on '...'
        3. Scroll down to '...'
        4. See error
        More info: A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is **required**, otherwise the issue might be closed without further notice. [**Why & How?**](https://antfu.me/posts/why-reproductions-are-required)
    validations:
      required: true

  - type: textarea
    id: additional-information
    attributes:
      label: Additional Information
      description: |
        Provide any additional information such as logs, screenshots, likes, scenarios in which the bug occurs so that it facilitates resolving the issue.

  - type: checkboxes
    id: checkboxes
    attributes:
      label: Validations
      description: Before submitting the issue, please make sure you do the following
      options:
        - label: Follow our [Code of Conduct](https://github.com/agalwood/Motrix/blob/master/CODE_OF_CONDUCT.md)
          required: true
        - label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
          required: true
        - label: Check that this is a concrete bug. For Q&A, please open a [GitHub Discussion](https://github.com/agalwood/Motrix/discussions) instead.
          required: true
        - label: The provided reproduction is a [minimal reproducible](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.
          required: true


================================================
FILE: .github/ISSUE_TEMPLATE/2_bug_report_cn.yml
================================================
name: 🐛 [新] Bug 报告
description: 在这里提交 Bug 报告
title: "[BUG]: "
labels: ["bug"]
body:
  - type: markdown
    attributes:
      value: |
        感谢您抽出时间来填写这份 Bug 报告 🤗
        请确保此问题没有已存在的开放/关闭问题 😃

  - type: textarea
    id: bug-description
    attributes:
      label: Bug 描述
      description: 给我们一个简短的描述,说明发生了什么以及应该发生什么。
    validations:
      required: true

  - type: textarea
    id: app-version
    attributes:
      label: Motrix 版本
      description: 请提供详细的版本信息以及安装的方式,如 macOS Apple silicon dmg、Windows Universal 安装文件等
    validations:
      required: true

  - type: textarea
    attributes:
      label: 环境
      description: |
        在项目的根目录下运行以下命令,并将结果粘贴到下方:

        ```sh
        npx envinfo --system --binaries --browsers
        ```
        在 macOS 中,如果您需要轻松复制粘贴,可以添加 `| pbcopy`。
        或者,您也可以手动收集您的环境版本信息。
    validations:
      required: true

  - type: textarea
    id: steps-to-reproduce
    attributes:
      label: 复现步骤
      description: 复现该问题的步骤。
      placeholder: |
        1. 前往 '...'
        2. 点击 '...'
        3. 滚动到 '...'
        4. 查看错误
        更多信息:[最小复现例子](https://stackoverflow.com/help/minimal-reproducible-example) 是必需的,否则该问题可能会被关闭而没有进一步的通知。[**为什么 & 如何?**](https://antfu.me/posts/why-reproductions-are-required)
    validations:
      required: true

  - type: textarea
    id: additional-information
    attributes:
      label: 额外信息
      description: |
        提供任何额外的信息,例如日志、截图、喜欢、发生该 Bug 的场景,以便有助于解决问题。

  - type: checkboxes
    id: checkboxes
    attributes:
      label: 验证
      description: 在提交问题之前,请确保您完成了以下操作
      options:
        - label: 遵循我们的[行为准则](https://github.com/agalwood/Motrix/blob/master/CODE_OF_CONDUCT.md)
          required: true
        - label: 确认是否已经有一个报告了相同的 Bug,以避免创建重复的问题。
          required: true
        - label: 确认此问题是一个具体的 Bug。若要进行问答,请开启 [GitHub 讨论](https://github.com/agalwood/Motrix/discussions)。
          required: true
        - label: 提供的复现是该 Bug 的 [最小复现例子](https://stackoverflow.com/help/minimal-reproducible-example)。
          required: true


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement ✨
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request_cn.md
================================================
---
name: 新功能请求
about: 你期望 Motrix 未来添加的新功能
title: ''
labels: enhancement ✨
assignees: ''

---

<!--
反馈之前请搜索一下已有 issues 和 帮助文档,看是否已经有人提交了类似的新功能请求
https://github.com/agalwood/Motrix/issues
http://motrix.app/support

按以下格式填写反馈信息,谢谢
-->

**请描述一下你的新功能请求是否与已知问题有关?**
简明扼要地描述了问题所在。

**描述你想要的解决方案**
简明扼要地描述你想要的解决方案。

**描述你考虑过的替代方案**
简明扼要地描述你考虑过的任何替代解决方案或功能。

**更多信息**
补充有关该新功能的其他信息。


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- You can erase any parts of this template not applicable to your Pull Request. -->

## Description
<!-- Write a brief description of the changes introduced by this PR -->

## Related Issues
<!--
  Link to the issue that is fixed by this PR (if there is one)
  e.g. Fixes #1234, Addresses #1234, Related to #1234, etc.
-->

### Checklist:

* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../../pulls) for the same update/change?
* [ ] Have you linted your code locally prior to submission?
* [ ] Have you successfully ran app with your changes locally?


================================================
FILE: .github/lock.yml
================================================
# Configuration for Lock Threads - https://github.com/dessant/lock-threads

# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 60

# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false

# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []

# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false

# Comment to post before locking. Set to `false` to disable
lockComment: >
  This thread has been automatically locked since there has not been
  any recent activity after it was closed. Please open a new issue for
  related bugs.

# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true

# Limit to only `issues` or `pulls`
# only: issues

# Optionally, specify configuration settings just for `issues` or `pulls`
# issues:
#   exemptLabels:
#     - help-wanted
#   lockLabel: outdated

# pulls:
#   daysUntilLock: 30

# Repository to extend settings from
# _extends: repo


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ master ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ master ]
  schedule:
    - cron: '23 8 * * 5'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'javascript' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
        # Learn more:
        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v2
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v2

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v2


================================================
FILE: .github/workflows/release.yml
================================================
name: Build/release

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  release:
    runs-on: ${{ matrix.os }}

    # Platforms to build on/for
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]

    steps:
      - name: Check out Git repository
        uses: actions/checkout@v3

      - name: Install Node.js, NPM and Yarn
        uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Install Snapcraft
        uses: samuelmeuli/action-snapcraft@v2
        # Only install Snapcraft on Ubuntu
        if: startsWith(matrix.os, 'ubuntu')
        env:
          # Snapcraft
          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snapcraft_token }}

      - name: Test Snapcraft
        if: startsWith(matrix.os, 'ubuntu')
        run: snapcraft --help

      - name: Build/release Electron app
        uses: motrixapp/action-electron-builder@v2
        with:
          build_script_name: 'build:github'
          # GitHub token, automatically provided to the action
          # (No need to define this secret in the repo settings)
          github_token: ${{ secrets.github_token }}

          # macOS code signing certificate
          mac_certs: ${{ secrets.mac_certs }}
          mac_certs_password: ${{ secrets.mac_certs_password }}

          # If the commit is tagged with a version (e.g. "v1.0.0"),
          # release the app after building
          release: ${{ vars.skip_publish != 'true' }}
        env:
          # Snapcraft
          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snapcraft_token }}
          # macOS notarization
          TEAM_ID: ${{ secrets.team_id }}
          APPLE_ID: ${{ secrets.apple_id }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.apple_app_specific_password }}


================================================
FILE: .gitignore
================================================
!.gitkeep
.DS_Store
.env
.idea/
.vs/
.vscode/
*.log
node_modules/
thumbs.db

# npm package
.npmrc
npm-debug.log.*

# Eslint Cache
.eslintcache*

# electron builder
*.provisionprofile
build/*.plist
dist/electron/*
dist/web/*

# release
release/*


================================================
FILE: .travis.yml
================================================
sudo: required
dist: trusty

language: c

jobs:
  include:
  - os: osx
    osx_image: xcode11.3
  - os: linux
    env: CC=clang CXX=clang++ npm_config_clang=1
    compiler: clang

cache:
  directories:
  - node_modules
  - $HOME/.cache/electron
  - $HOME/.cache/electron-builder

addons:
  apt:
    packages:
    - libgnome-keyring-dev
    - icnsutils
    - rpm

before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi

install:
- nvm install 12.14.1
- source ~/.bashrc
- npm install -g xvfb-maybe
- npm install

script:
- npm run release

before_cache:
  - rm -rf $HOME/.cache/electron-builder/wine

branches:
  only:
  - master


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
 advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
 address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
 professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at agalwood.net@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq


================================================
FILE: CONTRIBUTING-CN.md
================================================
# Motrix 贡献指南

开始贡献之前,确保你已经理解了 [GitHub 的协作流程](https://guides.github.com/introduction/flow/)。

## 🌍 翻译指南

首先你要确定一个语言的英文简写作为 **locale**,如 en-US,这个 locale 值请严格参考 [Electron 的 Locales 文档](https://www.electronjs.org/docs/api/app#appgetlocale) 和 [Chromium 源代码](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/l10n/l10n_util.cc)。

Motrix 的国际化分两部分:

- Element UI
- 菜单和主界面

### Element UI

Element UI 的国际化由 [Element 社区](http://element.eleme.io/#/en-US/component/i18n)提供,找到 **locale** 对应的语言包文件「两者 locale 命名可能不一致」,在 `src/shared/locales/all.js` 中引入,如

```javascript
import eleLocaleEn from 'element-ui/lib/locale/lang/en'
import eleLocaleZhCN from 'element-ui/lib/locale/lang/zh-CN'
```

### 菜单和主界面

Motrix 使用 i18next 作为翻译支持库,所以你可能需要简单了解一下它的[使用方法](https://www.i18next.com/overview/getting-started)。
配置文件按照语言 (**locale**) 划分目录:`src/shared/locales`,如:`src/shared/locales/en-US` 和 `src/shared/locales/zh-CN`。

目录里面有按业务模块划分的语言文件

菜单模块经过重构之后,国际化已经打散到了以下文件里了,不再需要再复制 `src/main/menus` 里的配置。

- about.js
- app.js
- edit.js
- help.js
- index.js
- menu.js
- preferences.js
- subnav.js
- task.js
- window.js


================================================
FILE: CONTRIBUTING.md
================================================
# Motrix Contributing Guide

Before you start contributing, make sure you already understand [GitHub flow](https://guides.github.com/introduction/flow/).

## 🌍 Translation Guide

First you need to determine the English abbreviation of a language as **locale**, such as en-US, this locale value should strictly refer to the [Electron's Documentation](https://www.electronjs.org/docs/api/app#appgetlocale) and [Chromium Source Code](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/l10n/l10n_util.cc).

The internationalization of Motrix is divided into two parts:

- Element UI
- Menu & Main Interface

### Element UI

The internationalization of Element UI is provided by the [Element community](http://element.eleme.io/#/en-US/component/i18n), then find the language pack file corresponding to **locale** (both locale naming may be inconsistent), which is import in `src/shared/locales/all.js`, such as

```javascript
import eleLocaleEn from 'element-ui/lib/locale/lang/en'
import eleLocaleZhCN from 'element-ui/lib/locale/lang/zh-CN'
```

### Menu & Main Interface

Motrix uses the [i18next](https://www.i18next.com/overview/getting-started) library for internationalization, so you need a quick look at how to use it.
The configuration files are divided by **locale**: `src/shared/locales`, such as `src/shared/locales/en-US` and `src/shared/locales/zh-CN`.

There are language files in the directory according to the business module.

After the menu module is refactored, the internationalization of the menu has been dispersed into the following files, and there is no need to copy the configuration in `src/main/menus`.

- about.js
- app.js
- edit.js
- help.js
- index.js
- menu.js
- preferences.js
- subnav.js
- task.js
- window.js


================================================
FILE: LICENSE
================================================
The MIT License

Copyright 2018-present Dr_rOot

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README-CN.md
================================================
# Motrix

<p>
  <a href="https://motrix.app">
    <img src="./static/512x512.png" width="256" alt="Motrix App Icon" />
  </a>
</p>

## 一款全能的下载工具

[![GitHub release](https://img.shields.io/github/v/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) ![Build/release](https://github.com/agalwood/Motrix/workflows/Build/release/badge.svg) ![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)

[English](./README.md) | 简体中文

我是个兴趣使然的桌面应用开发者🤓,利用搬砖之余开发了 Motrix。

Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链等资源。它的界面简洁易用,希望大家喜欢 👻。

✈️ 去 [官网](https://motrix.app/zh-CN) 逛逛  |  📖 查看 [帮助手册](http://motrix.app/support/issues)

## 💽 安装稳定版

[GitHub](https://github.com/agalwood/Motrix/releases) 和 [官网](https://motrix.app/zh-CN) 提供了已经编译好的稳定版安装包,当然你也可以自己克隆代码编译打包。

### Windows

建议使用安装包(Motrix-Setup-x.y.z.exe)安装 Motrix 以确保完整的体验,例如关联 torrent 文件,捕获磁力链等。

如果你在 Windows 是用包管理工具来管理应用,如 [Chocolatey](https://chocolatey.org)、[scoop](https://github.com/lukesampson/scoop),你可以使用它们安装 Motrix。

#### Chocolatey
感谢 [@Yato](https://github.com/iYato) 持续维护着 [Motrix Chocolatey](https://community.chocolatey.org/packages/motrix) 包。要安装 Motrix,请从 `命令行` 或 `PowerShell` 中运行以下命令:

```bash
# 安装
choco install motrix

# 升级
choco upgrade motrix
```

#### scoop
如果你更喜欢便携版,你可以使用 [scoop](https://github.com/lukesampson/scoop)(需要 Windows 7+,天朝用户可能需要设置 Git 代理)安装最新便携版本的 Motrix。

```bash
scoop bucket add extras
scoop install motrix
```

### macOS

macOS 用户可以使用 `brew` 安装 Motrix,感谢 [@Mitscherlich](https://github.com/Mitscherlich) 的 [PR](https://github.com/Homebrew/homebrew-cask/pull/59494)。

```bash
brew update && brew install motrix
```

#### 自动更新
Motrix v1.8.0+ 版本更改了应用 BundleID ( `net.agalwood.Motrix` => `app.motrix.native` ), Motrix v1.6.11 的自动更新会因为签名不一致而失败。[Motrix 安装助手](https://github.com/motrixapp/motrix-install-assistant)将帮助您安装最新的 Motrix 应用程序。

<p>
  <a href="https://github.com/motrixapp/motrix-install-assistant">
    <img src="https://raw.githubusercontent.com/motrixapp/motrix-install-assistant/main/build/256x256.png" width="192" alt="Motrix Install Assistant Icon" />
  </a>
</p>

### Linux

你可以下载 `AppImage` (适用于所有 Linux 发行版)或 `snap` 来安装 Motrix,更多 Linux 安装包格式请查看 [GitHub/release](https://github.com/agalwood/Motrix/releases) 。

Motrix 在 Linux 中首次启动可能需要使用 `sudo` 运行,因为可能没有创建下载会话文件的权限 (`/var/cache/aria2.session`)。

如果你想自己通过编译源码来安装,请阅读 **编译打包** 部分。

#### AppImage
最新版的 Motrix AppImage 需要自己手动进执行桌面集成。请查看 [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) 的文档进行操作。

> 桌面集成
> electron-builder v21 之后,桌面集成不再是 AppImage 文件的一部分。
> 推荐使用 [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) 集成 AppImage。

Deepin 20 Beta 用户安装 Motrix 失败的问题,请按照以下方法处理:

打开`终端`,黏贴运行如下命令之后再次安装 Motrix。
```bash
sudo apt --fix-broken install
```

#### Snap
Motrix 已经上架 [Snapcraft](https://snapcraft.io/motrix) ,Ubuntu 用户推荐从 Snap 商店下载。

v1.5.10 提示

系统托盘可能无法正常显示指示器,导致退出应用程序不方便。
请取消勾选 偏好设置——基本设置——隐藏应用程序菜单(仅限Windows和Linux),点击保存并应用。然后点击 "文件 "菜单中的 "退出",退出应用程序。

请更新到 v1.5.12 及以上版本,可以使用键盘组合快捷键 <kbd>Ctrl</kbd> + <kbd>q</kbd> 快速退出应用。

#### AUR
对于 Arch Linux 用户,可以使用 [aur](https://aur.archlinux.org/packages/motrix/) 安装 Motrix,感谢维护者 [@weearc](https://github.com/weearc)。

运行以下命令进行安装:

```bash
yay -S motrix
```

#### Flatpak
感谢 [@proletarius101](https://github.com/proletarius101) 的 [PR](https://github.com/flathub/flathub/pull/2334),Motrix 已经上架 [Flathub](https://flathub.org/apps/details/net.agalwood.Motrix),喜欢 Flatpak 的 Linux 用户可以尝试。

```bash
# 安装
flatpak install flathub net.agalwood.Motrix

# 运行
flatpak run net.agalwood.Motrix
```

## ✨ 特性

- 🕹 简洁明了的图形操作界面
- 🦄 支持BT和磁力链任务
- ☑️ 支持选择性下载BT部分文件
- 📡 每天自动更新 Tracker 服务器列表
- 🔌 UPnP & NAT-PMP 端口映射
- 🎛 最高支持 10 个任务同时下载
- 🚀 单任务最高支持 64 线程下载
- 🚥 设置上传/下载限速
- 🕶 模拟用户代理UA
- 🔔 下载完成后通知
- 💻 支持触控栏快捷键 (Mac 专享)
- 🤖 常驻系统托盘,操作更加便捷
- 📟 系统托盘速度仪表显示实时速度 (Mac 专享)
- 🌑 深色模式
- 🗑 移除任务时可同时删除相关文件
- 🌍 国际化,[查看已可选的语言](#-国际化)
- 🛠 更多特性开发中

## 🖥 应用界面

![motrix-screenshot-task-cn.png](https://cdn.nlark.com/yuque/0/2020/png/129147/1589782239990-fecb9065-19ac-4c35-938b-0be45621ca3a.png)

## ⌨️ 本地开发

### 克隆代码

```bash
git clone git@github.com:agalwood/Motrix.git
```

### 安装依赖

```bash
cd Motrix
yarn
```

天朝大陆用户建议使用淘宝的 npm 源

```bash
yarn config set registry 'https://registry.npmmirror.com'
npm config set registry 'https://registry.npmmirror.com'
export ELECTRON_MIRROR='https://npm.taobao.org/mirrors/electron/'
export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
```

> Error: Electron failed to install correctly, please delete node_modules/electron and try installing again

`Electron` 下载安装失败的问题,解决方式请参考 https://github.com/electron/electron/issues/8466#issuecomment-571425574

### 开发模式

```bash
yarn run dev
```

### 编译打包

```bash
yarn run build
```
#### 编译 Apple Silicon 版本

```bash
yarn run build:applesilicon
```
完成之后可以在项目的 `release` 目录看到编译打包好的应用文件

## 🛠 技术栈

- [Electron](https://electronjs.org/)
- [Vue](https://vuejs.org/) + [VueX](https://vuex.vuejs.org/) + [Element](https://element.eleme.io)
- [Aria2](https://aria2.github.io/)

## ☑️ TODO

开发计划请移步 [Trello](https://trello.com/b/qNUzA0bv/motrix) 查看

## 🤝 参与共建 [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)

如果你有兴趣参与共同开发,欢迎 FORK 和 PR。

## 🌍 国际化

欢迎大家将 Motrix 翻译成更多的语言版本 🧐,开工之前请先阅读一下 [翻译指南](./CONTRIBUTING-CN.md#-翻译指南)。

| Key   | Name                | Status       |
|-------|:--------------------|:-------------|
| ar    | Arabic              | ✔️ [@hadialqattan](https://github.com/hadialqattan), [@AhmedElTabarani](https://github.com/AhmedElTabarani) |
| bg    | Българският език    | ✔️ [@null-none](https://github.com/null-none) |
| ca    | Català              | ✔️ [@marcizhu](https://github.com/marcizhu) |
| de    | Deutsch             | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| el    | Ελληνικά            | ✔️ [@Likecinema](https://github.com/Likecinema) |
| en-US | English             | ✔️           |
| es    | Español             | ✔️ [@Chofito](https://github.com/Chofito)|
| fa    | فارسی               | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
| fr    | Français            | ✔️ [@gpatarin](https://github.com/gpatarin) |
| hu    | Hungarian           | ✔️ [@zalnaRs](https://github.com/zalnaRs) |
| id    | Indonesia           | ✔️ [@aarestu](https://github.com/aarestu) |
| it    | Italiano            | ✔️ [@blackcat-917](https://github.com/blackcat-917) |
| ja    | 日本語               | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
| ko    | 한국어                | ✔️ [@KOZ39](https://github.com/KOZ39) |
| nb    | Norsk Bokmål        | ✔️ [@rubjo](https://github.com/rubjo) |
| nl    | Nederlands          | ✔️ [@nickbouwhuis](https://github.com/nickbouwhuis) |
| pl    | Polski              | ✔️ [@KanarekLife](https://github.com/KanarekLife) |
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
| ro    | Română              | ✔️ [@alyn3d](https://github.com/alyn3d) |
| ru    | Русский             | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| th    | แบบไทย              | ✔️ [@nxanywhere](https://github.com/nxanywhere) |
| tr    | Türkçe              | ✔️ [@abdullah](https://github.com/abdullah) |
| uk    | Українська          | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| vi    | Tiếng Việt          | ✔️ [@duythanhvn](https://github.com/duythanhvn) |
| zh-CN | 简体中文             | ✔️           |
| zh-TW | 繁體中文             | ✔️ [@Yukaii](https://github.com/Yukaii) [@5idereal](https://github.com/5idereal) |

## 📜 开源许可

基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。


================================================
FILE: README.md
================================================
# Motrix

<p>
  <a href="https://motrix.app">
    <img src="./static/512x512.png" width="256" alt="Motrix App Icon" />
  </a>
</p>

## A full-featured download manager

[![GitHub release](https://img.shields.io/github/v/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) ![Build/release](https://github.com/agalwood/Motrix/workflows/Build/release/badge.svg) ![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)

English | [简体中文](./README-CN.md)

Motrix is a full-featured download manager that supports downloading HTTP, FTP, BitTorrent, Magnet, etc.

Motrix has a clean and easy to use interface. I hope you will like it 👻.

✈️ [Official Website](https://motrix.app) | 📖 [Manual](https://github.com/agalwood/Motrix/wiki)

## 💽 Installation

Download from [GitHub Releases](https://github.com/agalwood/Motrix/releases) and install it.

### Windows

It is recommended to install Motrix using the installation package (Motrix-Setup-x.y.z.exe) to ensure a complete experience, such as associating torrent files, capturing magnet links, etc.

If you use package management tools to manage applications on Windows, such as [Chocolatey](https://chocolatey.org), [scoop](https://github.com/lukesampson/scoop). You can use them to install Motrix.

#### Chocolatey
Thanks to [@Yato](https://github.com/iYato) for continuing to maintain the [Motrix Chocolatey](https://community.chocolatey.org/packages/motrix) package. To install motrix, run the following command from the `command line` or from `PowerShell`:

```bash
# Install
choco install motrix

# Upgrade
choco upgrade motrix
```

#### scoop
If you prefer the portable version, you can use [scoop](https://github.com/lukesampson/scoop) (need Windows 7+) to install Motrix.

```bash
scoop bucket add extras
scoop install motrix
```

### macOS

The macOS users can install Motrix using `brew`, thanks to [PR](https://github.com/Homebrew/homebrew-cask/pull/59494) of [@Mitscherlich](https://github.com/Mitscherlich).

```bash
brew update && brew install motrix
```

#### Auto Update

Since Motrix v1.8.0 and later versions changed the App BundleID ( `net.agalwood.Motrix` => `app.motrix.native` ), the automatic update of Motrix v1.6.11 will fail. [Motrix Install Assistant](https://github.com/motrixapp/motrix-install-assistant) will help you install the latest Motrix application.

<p>
  <a href="https://github.com/motrixapp/motrix-install-assistant">
    <img src="https://raw.githubusercontent.com/motrixapp/motrix-install-assistant/main/build/256x256.png" width="192" alt="Motrix Install Assistant Icon" />
  </a>
</p>

### Linux

You can download the `AppImage` (for all Linux distributions) or `snap` to install Motrix, see [GitHub/release](https://github.com/agalwood/Motrix/releases) for more Linux installation package formats.

Motrix may need to run with `sudo` for the first time in Linux because there is no permission to create the download session file (`/var/cache/aria2.session`).

If you want to build from source code, please read the **Build** section.

#### AppImage
The latest version of Motrix AppImage requires you to manually perform desktop integration. Please check the documentation of [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) .

> Desktop Integration
> Since electron-builder 21 desktop integration is not a part of produced AppImage file.
> [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) is the recommended way to integrate AppImages.

Deepin 20 Beta users failed to install Motrix, please follow the steps below:

Open the `Terminal`, paste and run the following command to install Motrix again.

```bash
sudo apt --fix-broken install
```

#### Snap
Motrix has been listed on [Snapcraft](https://snapcraft.io/motrix) , Ubuntu users recommend downloading from the Snap Store.

Tips for v1.5.10

The tray may not display the indicator normally, which makes it inconvenient to exit the application.

Please unchecked Preferences--Basic Settings--Hide App Menu (Windows & Linux Only), click Save & Apply. Then click "Exit" in the File menu to exit the application.

Please update to v1.5.12 and above, you can use the keyboard shortcut <kbd>Ctrl</kbd> + <kbd>q</kbd> to quickly exit the application.

#### AUR
For Arch Linux users, Motrix is available in [aur](https://aur.archlinux.org/packages/motrix/), thanks to the maintainer [@weearc](https://github.com/weearc).

Run the following command to install:

```bash
yay -S motrix
```

#### Flatpak
Thanks to the [PR](https://github.com/flathub/flathub/pull/2334) of [@proletarius101](https://github.com/proletarius101), Motrix has been listed [Flathub](https://flathub.org/apps/details/net.agalwood.Motrix), Linux users who like the Flatpak can try it.

```bash
# Install
flatpak install flathub net.agalwood.Motrix

# Run
flatpak run net.agalwood.Motrix
```

## ✨ Features

- 🕹 Simple and clear user interface
- 🦄 Supports BitTorrent & Magnet
- ☑️ BitTorrent selective download
- 📡 Update tracker list every day automatically
- 🔌 UPnP & NAT-PMP Port Mapping
- 🎛 Up to 10 concurrent download tasks
- 🚀 Supports 64 threads in a single task
- 🚥 Supports speed limit
- 🕶 Mock User-Agent
- 🔔 Download completed Notification
- 💻 Ready for Touch Bar (Mac only)
- 🤖 Resident system tray for quick operation
- 📟 Tray speed meter displays real-time speed (Mac only)
- 🌑 Dark mode
- 🗑 Delete related files when removing tasks (optional)
- 🌍 I18n, [View supported languages](#-internationalization).
- 🛠 More features in development

## 🖥 User Interface

![motrix-screenshot-task-en.png](https://cdn.nlark.com/yuque/0/2020/png/129147/1589782238501-e7b39166-da58-4152-ae34-65a061cafa48.png)

## ⌨️ Development

### Clone Code

```bash
git clone git@github.com:agalwood/Motrix.git
```

### Install Dependencies

```bash
cd Motrix
yarn
```

> Error: Electron failed to install correctly, please delete node_modules/electron and try installing again

`Electron` failed to install correctly, please refer to https://github.com/electron/electron/issues/8466#issuecomment-571425574

### Dev Mode

```bash
yarn run dev
```

### Build Release

```bash
yarn run build
```
#### Build for Apple Silicon

```bash
yarn run build:applesilicon
```

After building, the application will be found in the project's `release` directory.

## 🛠 Technology Stack

- [Electron](https://electronjs.org/)
- [Vue](https://vuejs.org/) + [VueX](https://vuex.vuejs.org/) + [Element](https://element.eleme.io)
- [Aria2](https://aria2.github.io/)

## ☑️ TODO

Development Roadmap see: [Trello](https://trello.com/b/qNUzA0bv/motrix)

## 🤝 Contribute [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)

If you are interested in participating in joint development, PR and Forks are welcome!

## 🌍 Internationalization

Translations into versions for other languages are welcome 🧐! Please read the [translation guide](./CONTRIBUTING.md#-translation-guide) before starting translations.

| Key   | Name                | Status       |
|-------|:--------------------|:-------------|
| ar    | Arabic              | ✔️ [@hadialqattan](https://github.com/hadialqattan), [@AhmedElTabarani](https://github.com/AhmedElTabarani) |
| bg    | Българският език    | ✔️ [@null-none](https://github.com/null-none) |
| ca    | Català              | ✔️ [@marcizhu](https://github.com/marcizhu) |
| de    | Deutsch             | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| el    | Ελληνικά            | ✔️ [@Likecinema](https://github.com/Likecinema) |
| en-US | English             | ✔️           |
| es    | Español             | ✔️ [@Chofito](https://github.com/Chofito)|
| fa    | فارسی               | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
| fr    | Français            | ✔️ [@gpatarin](https://github.com/gpatarin) |
| hu    | Hungarian           | ✔️ [@zalnaRs](https://github.com/zalnaRs) |
| id    | Indonesia           | ✔️ [@aarestu](https://github.com/aarestu) |
| it    | Italiano            | ✔️ [@blackcat-917](https://github.com/blackcat-917) |
| ja    | 日本語               | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
| ko    | 한국어                | ✔️ [@KOZ39](https://github.com/KOZ39) |
| nb    | Norsk Bokmål        | ✔️ [@rubjo](https://github.com/rubjo) |
| nl    | Nederlands          | ✔️ [@nickbouwhuis](https://github.com/nickbouwhuis) |
| pl    | Polski              | ✔️ [@KanarekLife](https://github.com/KanarekLife) |
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
| ro    | Română              | ✔️ [@alyn3d](https://github.com/alyn3d) |
| ru    | Русский             | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| th    | แบบไทย              | ✔️ [@nxanywhere](https://github.com/nxanywhere) |
| tr    | Türkçe              | ✔️ [@abdullah](https://github.com/abdullah) |
| uk    | Українська          | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| vi    | Tiếng Việt          | ✔️ [@duythanhvn](https://github.com/duythanhvn) |
| zh-CN | 简体中文             | ✔️           |
| zh-TW | 繁體中文             | ✔️ [@Yukaii](https://github.com/Yukaii) [@5idereal](https://github.com/5idereal) |

## 📜 License

[MIT](https://opensource.org/licenses/MIT) Copyright (c) 2018-present Dr_rOot


================================================
FILE: app-update.yml
================================================
provider: generic
url: 'https://dl.motrix.app/releases/'


================================================
FILE: appveyor.yml
================================================
image: Visual Studio 2017

platform:
  - x64

cache:
  - node_modules
  - '%USERPROFILE%\.electron'

init:
  - git config --global core.autocrlf input

install:
  - ps: Install-Product node 12.14.1 x64
  - git reset --hard HEAD
  - npm install
  - node --version

build_script:
  - npm run release

test: off

branches:
  only:
    - master


================================================
FILE: build/afterPackHook.js
================================================
//  Forked from https://github.com/samuelmeuli/mini-diary/blob/master/scripts/after-pack.js

/**
 * Source: https://github.com/patrikx3/redis-ui/blob/master/src/build/after-pack.js
 *
 * Copyright (c) 2019 Patrik Laszlo / P3X / Corifeus and contributors.
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

// TODO: Remove script once https://github.com/electron/electron/issues/17972 is solved by
// `electron-builder`

const fs = require('node:fs')
const { spawn } = require('node:child_process')
const { chdir } = require('node:process')

const pkg = require('../package.json')
const binName = `${pkg.name}`.toLowerCase()

const exec = async function exec (cmd, args = []) {
  const child = spawn(cmd, args, { shell: true })
  redirectOutputFor(child)
  await waitFor(child)
}

const redirectOutputFor = child => {
  const printStdout = data => {
    process.stdout.write(data.toString())
  }
  const printStderr = data => {
    process.stderr.write(data.toString())
  }
  child.stdout.on('data', printStdout)
  child.stderr.on('data', printStderr)

  child.once('close', () => {
    child.stdout.off('data', printStdout)
    child.stderr.off('data', printStderr)
  })
}

const waitFor = async function (child) {
  return new Promise(resolve => {
    child.once('close', () => resolve())
  })
}

const linuxTargets = [
  'AppImage',
  'deb',
  'rpm',
  'snap'
]

module.exports = async function (context) {
  console.warn('after build; disable sandbox')
  const isLinux = context.targets.find(
    target => linuxTargets.includes(target)
  )
  if (!isLinux) {
    return
  }
  const originalDir = process.cwd()
  const dirname = context.appOutDir
  chdir(dirname)

  await exec('mv', [binName, binName + '.bin'])
  const wrapperScript = `#!/bin/bash
    "\${BASH_SOURCE%/*}"/${binName}.bin "$@" --no-sandbox
  `
  fs.writeFileSync(binName, wrapperScript)
  await exec('chmod', ['+x', binName])

  chdir(originalDir)
}


================================================
FILE: build/afterSignHook.js
================================================
require('dotenv').config()
const { join } = require('node:path')
const { notarize } = require('@electron/notarize')
const { appId } = require('../electron-builder.json')

exports.default = async function (context) {
  const { electronPlatformName, appOutDir } = context
  if (electronPlatformName !== 'darwin') {
    return
  }

  const skipNotarize = process.env.SKIP_NOTARIZE
  if (skipNotarize === 'true') {
    console.log('Skipping notarize')
    return
  }

  const appBundleId = appId
  const appName = context.packager.appInfo.productFilename
  const appPath = join(appOutDir, `${appName}.app`)

  try {
    await notarize({
      tool: 'notarytool',
      appBundleId,
      appPath,
      teamId: process.env.TEAM_ID,
      appleId: process.env.APPLE_ID,
      appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD
    })
  } catch (error) {
    console.error(error)
  }

  console.log(`Done notarizing ${appId}`)
}


================================================
FILE: electron-builder.json
================================================
{
    "productName": "Motrix",
    "appId": "app.motrix.native",
    "afterPack": "./build/afterPackHook.js",
    "afterSign": "./build/afterSignHook.js",
    "fileAssociations": [
      {
        "ext": "torrent",
        "mimeType": "application/x-bittorrent",
        "name": "Torrent",
        "role": "Viewer"
      }
    ],
    "asar": true,
    "directories": {
      "output": "release"
    },
    "files": [
      "dist/electron/**/*"
    ],
    "protocols": [
      {
        "name": "Motrix Protocol",
        "schemes": [
          "mo",
          "motrix"
        ]
      },
      {
        "name": "Magnet Protocol",
        "schemes": [
          "magnet"
        ]
      },
      {
        "name": "Thunder Protocol",
        "schemes": [
          "thunder"
        ]
      }
    ],
    "dmg": {
      "window": {
        "width": 540,
        "height": 380
      },
      "contents": [
        {
          "x": 410,
          "y": 230,
          "type": "link",
          "path": "/Applications"
        },
        {
          "x": 130,
          "y": 230,
          "type": "file"
        }
      ]
    },
    "mac": {
      "target": [
        {
          "target": "dmg",
          "arch": [
            "x64",
            "arm64",
            "universal"
          ]
        },
        {
          "target": "zip",
          "arch": [
            "x64",
            "arm64",
            "universal"
          ]
        }
      ],
      "type": "development",
      "darkModeSupport": true,
      "hardenedRuntime": false,
      "notarize": false,
      "extraResources": {
        "from": "./extra/darwin/${arch}/",
        "to": "./",
        "filter": [
          "**/*"
        ]
      },
      "category": "public.app-category.utilities"
    },
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64",
            "ia32"
          ]
        },
        {
          "target": "appx",
          "arch": [
            "x64",
            "ia32"
          ]
        },
        {
          "target": "zip",
          "arch": [
            "x64",
            "ia32"
          ]
        },
        {
          "target": "portable",
          "arch": [
            "x64",
            "ia32"
          ]
        }
      ],
      "extraResources": {
        "from": "./extra/win32/${arch}/",
        "to": "./",
        "filter": [
          "**/*"
        ]
      }
    },
    "nsis": {
      "artifactName": "${productName}-Setup-${version}.${ext}",
      "oneClick": false,
      "allowToChangeInstallationDirectory": true
    },
    "appx": {
      "artifactName": "${productName}-${version}-${arch}.${ext}",
      "applicationId": "app.motrix.native",
      "identityName": "59744DrrOot.Motrix",
      "publisher": "CN=5BB4961D-30D8-4993-9ADF-05E1E1F5A395",
      "publisherDisplayName": "Dr_rOot"
    },
    "portable": {
      "artifactName": "${productName}-${version}-${arch}.${ext}"
    },
    "linux": {
      "category": "Network",
      "mimeTypes": [
        "application/x-bittorrent",
        "x-scheme-handler/magnet"
      ],
      "target": [
        {
          "target": "AppImage",
          "arch": [
            "x64",
            "arm64",
            "armv7l"
          ]
        },
        {
          "target": "deb",
          "arch": [
            "x64",
            "arm64",
            "armv7l"
          ]
        },
        {
          "target": "rpm",
          "arch": [
            "x64"
          ]
        },
        {
          "target": "snap",
          "arch": [
            "x64"
          ]
        }
      ],
      "extraResources": {
        "from": "./extra/linux/${arch}/",
        "to": "./",
        "filter": [
          "**/*"
        ]
      }
    },
    "publish": [
      {
        "provider": "generic",
        "url": "https://dl.motrix.app/releases/"
      },
      {
        "provider": "github"
      }
    ]
  }


================================================
FILE: extra/README.md
================================================
# aria2

Source code: https://github.com/agalwood/aria2


================================================
FILE: extra/darwin/arm64/engine/aria2.conf
================================================
###############################
# 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-


================================================
FILE: extra/darwin/x64/engine/aria2.conf
================================================
###############################
# 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-


================================================
FILE: extra/linux/arm64/engine/aria2.conf
================================================
###############################
# 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-


================================================
FILE: extra/linux/armv7l/engine/aria2.conf
================================================
###############################
# 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-


================================================
FILE: extra/linux/x64/engine/aria2.conf
================================================
###############################
# 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-


================================================
FILE: extra/win32/ia32/engine/aria2.conf
================================================
###############################
# 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-


================================================
FILE: extra/win32/x64/engine/aria2.conf
================================================
###############################
# 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-


================================================
FILE: jsconfig.json
================================================
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/renderer/*"
      ],
      "@shared/*": [
        "./src/shared/*"
      ]
    }
  },
  "exclude": ["node_modules", "dist"]
}


================================================
FILE: package.json
================================================
{
  "name": "Motrix",
  "version": "1.8.19",
  "description": "A full-featured download manager",
  "homepage": "https://motrix.app",
  "author": {
    "name": "Dr_rOot",
    "email": "agalwood.net@gmail.com"
  },
  "copyright": "Copyright© Dr_rOot",
  "license": "MIT",
  "main": "./dist/electron/main.js",
  "repository": {
    "type": "git",
    "url": "git@github.com:agalwood/Motrix.git"
  },
  "scripts": {
    "release": "npm run build --publish onTagOrDraft",
    "build": "node .electron-vue/build.js && electron-builder",
    "build:applesilicon": "node .electron-vue/build.js && electron-builder --arm64 --mac",
    "build:github": "node .electron-vue/build.js",
    "build:dir": "node .electron-vue/build.js && electron-builder --dir",
    "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
    "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js",
    "dev": "node .electron-vue/dev-runner.js",
    "dev:renderer": "webpack serve --node-env development --hot --color --config .electron-vue/webpack.renderer.config.js --port 9080 --content-base app/dist",
    "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src",
    "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src",
    "pack": "npm run pack:main && npm run pack:renderer",
    "pack:main": "webpack --node-env production --progress --color --config .electron-vue/webpack.main.config.js",
    "pack:renderer": "webpack --node-env production --progress --color --config .electron-vue/webpack.renderer.config.js",
    "postinstall": "electron-builder install-app-deps && npm run lint:fix"
  },
  "engines": {
    "node": ">=16.0.0"
  },
  "dependencies": {
    "node-fetch": "^2.6.1",
    "ws": "^8.13.0"
  },
  "devDependencies": {
    "@babel/core": "^7.21.8",
    "@babel/plugin-proposal-class-properties": "^7.18.6",
    "@babel/plugin-transform-runtime": "^7.21.4",
    "@babel/preset-env": "^7.21.5",
    "@babel/register": "^7.21.0",
    "@babel/runtime": "^7.21.5",
    "@bany/curl-to-json": "^1.2.7",
    "@electron/notarize": "^1.2.3",
    "@electron/osx-sign": "^1.0.4",
    "@electron/remote": "^2.0.9",
    "@motrix/multispinner": "^0.2.4",
    "@motrix/nat-api": "^0.3.4",
    "@panter/vue-i18next": "^0.15.2",
    "@vue/eslint-config-standard": "^6.1.0",
    "ajv": "^8.12.0",
    "axios": "^1.4.0",
    "babel-eslint": "^10.1.0",
    "babel-loader": "^9.1.2",
    "babel-plugin-component": "^1.1.1",
    "bittorrent-peerid": "^1.3.6",
    "blob-util": "^2.0.2",
    "cfonts": "^3.2.0",
    "chalk": "^4.1.2",
    "copy-webpack-plugin": "^11.0.0",
    "cross-env": "^7.0.3",
    "css-loader": "^6.7.3",
    "css-minimizer-webpack-plugin": "^5.0.0",
    "del": "^6.1.1",
    "electron": "^22.3.9",
    "electron-builder": "^24.4.0",
    "electron-devtools-installer": "^3.2.0",
    "electron-is": "^3.0.0",
    "electron-log": "^4.4.8",
    "electron-store": "^8.1.0",
    "electron-updater": "^6.1.0",
    "element-ui": "^2.15.13",
    "eslint": "^7.32.0",
    "eslint-friendly-formatter": "^4.0.1",
    "eslint-plugin-import": "^2.27.5",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^6.1.1",
    "eslint-plugin-vue": "^9.12.0",
    "eslint-webpack-plugin": "^4.0.1",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.5.1",
    "i18next": "^22.4.15",
    "lodash": "^4.17.21",
    "mini-css-extract-plugin": "2.7.5",
    "node-loader": "^2.0.0",
    "normalize.css": "^8.0.1",
    "parse-torrent": "^9.1.5",
    "randomatic": "^3.1.1",
    "sass": "1.62.1",
    "sass-loader": "^12.6.0",
    "style-loader": "^3.3.2",
    "terser-webpack-plugin": "^5.3.8",
    "vue": "^2.7.14",
    "vue-electron": "^1.0.6",
    "vue-loader": "^15.10.1",
    "vue-router": "^3.6.5",
    "vue-selectable": "^0.5.0",
    "vue-style-loader": "^4.1.3",
    "vue-template-compiler": "^2.7.14",
    "vuex": "^3.6.2",
    "vuex-router-sync": "^5.0.0",
    "webpack": "^5.82.1",
    "webpack-cli": "^5.1.1",
    "webpack-dev-server": "^4.15.0",
    "webpack-hot-middleware": "^2.25.3",
    "webpack-merge": "^5.8.0",
    "worker-loader": "^3.0.8"
  }
}


================================================
FILE: src/index.ejs
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Motrix</title>
    <% if (htmlWebpackPlugin.options.nodeModules) { %>
      <!-- Add `node_modules/` to global paths so `require` works properly in development -->
      <script>
        require('module').globalPaths.push('<%= htmlWebpackPlugin.options.nodeModules.replace(/\\/g, '\\\\') %>')
      </script>
    <% } %>
  </head>

  <style>
    .skeleton-aside {
      background-color: rgba(0, 0, 0, 0.8);
      width: 78px;
    }
    .skeleton-subnav {
      background-color: #f4f5f7;
      width: 200px;
    }
    .skeleton-main {
      background-color: #fff;
    }

    @media (prefers-color-scheme: dark) {
      .skeleton-aside {
        background-color: rgba(0, 0, 0, 0.9);
      }
      .skeleton-subnav {
        background-color: #2D2D2D;
      }
      .skeleton-main {
        background-color: #343434;
      }
    }
  </style>

  <body>
    <div id="app">
      <div class="title-bar"></div>
      <section class="el-container" id="container">
        <aside class="el-aside skeleton-aside hidden-sm-and-down">
        </aside>
        <aside class="el-aside skeleton-subnav hidden-xs-only">
        </aside>
        <section class="el-container skeleton-main">
        </section>
      </section>
    </div>
    <!-- Set `__static` path to static files in production -->
    <% if (!htmlWebpackPlugin.options.isBrowser && !htmlWebpackPlugin.options.isDev) { %>
      <script>
        window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
      </script>
    <% } %>

    <!-- webpack builds are automatically injected -->
  </body>
</html>


================================================
FILE: src/main/Application.js
================================================
import { EventEmitter } from 'node:events'
import { readFile, unlink } from 'node:fs'
import { extname, basename } from 'node:path'
import { app, shell, dialog, ipcMain } from 'electron'
import is from 'electron-is'
import { isEmpty, isEqual } from 'lodash'

import {
  APP_RUN_MODE,
  AUTO_SYNC_TRACKER_INTERVAL,
  AUTO_CHECK_UPDATE_INTERVAL,
  PROXY_SCOPES
} from '@shared/constants'
import { checkIsNeedRun } from '@shared/utils'
import {
  convertTrackerDataToComma,
  fetchBtTrackerFromSource,
  reduceTrackerString
} from '@shared/utils/tracker'
import { showItemInFolder } from './utils'
import logger from './core/Logger'
import Context from './core/Context'
import ConfigManager from './core/ConfigManager'
import { setupLocaleManager } from './ui/Locale'
import Engine from './core/Engine'
import EngineClient from './core/EngineClient'
import UPnPManager from './core/UPnPManager'
import AutoLaunchManager from './core/AutoLaunchManager'
import UpdateManager from './core/UpdateManager'
import EnergyManager from './core/EnergyManager'
import ProtocolManager from './core/ProtocolManager'
import WindowManager from './ui/WindowManager'
import MenuManager from './ui/MenuManager'
import TouchBarManager from './ui/TouchBarManager'
import TrayManager from './ui/TrayManager'
import DockManager from './ui/DockManager'
import ThemeManager from './ui/ThemeManager'

export default class Application extends EventEmitter {
  constructor () {
    super()
    this.isReady = false
    this.init()
  }

  init () {
    this.initContext()

    this.initConfigManager()

    this.setupLogger()

    this.initLocaleManager()

    this.setupApplicationMenu()

    this.initWindowManager()

    this.initUPnPManager()

    this.startEngine()

    this.initEngineClient()

    this.initThemeManager()

    this.initTrayManager()

    this.initTouchBarManager()

    this.initDockManager()

    this.initAutoLaunchManager()

    this.initEnergyManager()

    this.initProtocolManager()

    this.initUpdaterManager()

    this.handleCommands()

    this.handleEvents()

    this.handleIpcMessages()

    this.handleIpcInvokes()

    this.emit('application:initialized')
  }

  initContext () {
    this.context = new Context()
  }

  initConfigManager () {
    this.configListeners = {}
    this.configManager = new ConfigManager()
  }

  offConfigListeners () {
    try {
      Object.keys(this.configListeners).forEach((key) => {
        this.configListeners[key]()
      })
    } catch (e) {
      logger.warn('[Motrix] offConfigListeners===>', e)
    }
    this.configListeners = {}
  }

  setupLogger () {
    const { userConfig } = this.configManager
    const key = 'log-level'
    const logLevel = userConfig.get(key)
    logger.transports.file.level = logLevel

    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)
      logger.transports.file.level = newValue
    })
  }

  initLocaleManager () {
    this.locale = this.configManager.getLocale()
    this.localeManager = setupLocaleManager(this.locale)
    this.i18n = this.localeManager.getI18n()
  }

  setupApplicationMenu () {
    this.menuManager = new MenuManager()
    this.menuManager.setup(this.locale)
  }

  adjustMenu () {
    if (is.mas()) {
      const visibleStates = {
        'app.check-for-updates': false,
        'task.new-bt-task': false
      }
      this.menuManager.updateMenuStates(visibleStates, null, null)
      this.trayManager.updateMenuStates(visibleStates, null, null)
    }
  }

  startEngine () {
    const self = this

    try {
      this.engine = new Engine({
        systemConfig: this.configManager.getSystemConfig(),
        userConfig: this.configManager.getUserConfig()
      })
      this.engine.start()
    } catch (err) {
      const { message } = err
      dialog.showMessageBox({
        type: 'error',
        title: this.i18n.t('app.system-error-title'),
        message: this.i18n.t('app.system-error-message', { message })
      }).then(_ => {
        setTimeout(() => {
          self.quit()
        }, 100)
      })
    }
  }

  async stopEngine () {
    logger.info('[Motrix] stopEngine===>')
    try {
      await this.engineClient.shutdown({ force: true })
      logger.info('[Motrix] stopEngine.setImmediate===>')
      setImmediate(() => {
        this.engine.stop()
      })
    } catch (err) {
      logger.warn('[Motrix] shutdown engine fail: ', err.message)
    } finally {
      // no finally
    }
  }

  initEngineClient () {
    const port = this.configManager.getSystemConfig('rpc-listen-port')
    const secret = this.configManager.getSystemConfig('rpc-secret')
    this.engineClient = new EngineClient({
      port,
      secret
    })
  }

  initAutoLaunchManager () {
    this.autoLaunchManager = new AutoLaunchManager()
  }

  initEnergyManager () {
    this.energyManager = new EnergyManager()
  }

  initTrayManager () {
    this.trayManager = new TrayManager({
      theme: this.configManager.getUserConfig('tray-theme'),
      systemTheme: this.themeManager.getSystemTheme(),
      speedometer: this.configManager.getUserConfig('tray-speedometer'),
      runMode: this.configManager.getUserConfig('run-mode')
    })

    this.watchTraySpeedometerEnabledChange()

    this.trayManager.on('mouse-down', ({ focused }) => {
      this.sendCommandToAll('application:update-tray-focused', { focused })
    })

    this.trayManager.on('mouse-up', ({ focused }) => {
      this.sendCommandToAll('application:update-tray-focused', { focused })
    })

    this.trayManager.on('drop-files', (files = []) => {
      this.handleFile(files[0])
    })

    this.trayManager.on('drop-text', (text) => {
      this.handleProtocol(text)
    })
  }

  watchTraySpeedometerEnabledChange () {
    const { userConfig } = this.configManager
    const key = 'tray-speedometer'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)
      this.trayManager.handleSpeedometerEnableChange(newValue)
    })
  }

  initDockManager () {
    this.dockManager = new DockManager({
      runMode: this.configManager.getUserConfig('run-mode')
    })
  }

  watchOpenAtLoginChange () {
    const { userConfig } = this.configManager
    const key = 'open-at-login'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)
      if (is.linux()) {
        return
      }

      if (newValue) {
        this.autoLaunchManager.enable()
      } else {
        this.autoLaunchManager.disable()
      }
    })
  }

  watchProtocolsChange () {
    const { userConfig } = this.configManager
    const key = 'protocols'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)

      if (!newValue || isEqual(newValue, oldValue)) {
        return
      }

      logger.info('[Motrix] setup protocols client:', newValue)
      this.protocolManager.setup(newValue)
    })
  }

  watchRunModeChange () {
    const { userConfig } = this.configManager
    const key = 'run-mode'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)
      this.trayManager.handleRunModeChange(newValue)

      if (newValue !== APP_RUN_MODE.TRAY) {
        this.dockManager.show()
      } else {
        this.dockManager.hide()
        // Hiding the dock icon will trigger the entire app to hide.
        this.show()
      }
    })
  }

  watchProxyChange () {
    const { userConfig } = this.configManager
    const key = 'proxy'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)
      this.updateManager.setupProxy(newValue)

      const { enable, server, bypass, scope = [] } = newValue
      const system = enable && server && scope.includes(PROXY_SCOPES.DOWNLOAD)
        ? {
          'all-proxy': server,
          'no-proxy': bypass
        }
        : {}
      this.configManager.setSystemConfig(system)
      this.engineClient.call('changeGlobalOption', system)
    })
  }

  watchLocaleChange () {
    const { userConfig } = this.configManager
    const key = 'locale'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)
      this.localeManager.changeLanguageByLocale(newValue)
        .then(() => {
          this.menuManager.handleLocaleChange(newValue)
          this.trayManager.handleLocaleChange(newValue)
        })
      this.sendCommandToAll('application:update-locale', { locale: newValue })
    })
  }

  watchThemeChange () {
    const { userConfig } = this.configManager
    const key = 'theme'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)
      this.themeManager.updateSystemTheme(newValue)
      this.sendCommandToAll('application:update-theme', { theme: newValue })
    })
  }

  watchShowProgressBarChange () {
    const { userConfig } = this.configManager
    const key = 'show-progress-bar'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info(`[Motrix] detected ${key} value change event:`, newValue, oldValue)

      if (newValue) {
        this.bindProgressChange()
      } else {
        this.unbindProgressChange()
      }
    })
  }

  initUPnPManager () {
    this.upnp = new UPnPManager()

    this.watchUPnPEnabledChange()

    this.watchUPnPPortsChange()

    const enabled = this.configManager.getUserConfig('enable-upnp')
    if (!enabled) {
      return
    }

    this.startUPnPMapping()
  }

  async startUPnPMapping () {
    const btPort = this.configManager.getSystemConfig('listen-port')
    const dhtPort = this.configManager.getSystemConfig('dht-listen-port')

    const promises = [
      this.upnp.map(btPort),
      this.upnp.map(dhtPort)
    ]
    try {
      await Promise.allSettled(promises)
    } catch (e) {
      logger.warn('[Motrix] start UPnP mapping fail', e.message)
    }
  }

  async stopUPnPMapping () {
    const btPort = this.configManager.getSystemConfig('listen-port')
    const dhtPort = this.configManager.getSystemConfig('dht-listen-port')

    const promises = [
      this.upnp.unmap(btPort),
      this.upnp.unmap(dhtPort)
    ]
    try {
      await Promise.allSettled(promises)
    } catch (e) {
      logger.warn('[Motrix] stop UPnP mapping fail', e)
    }
  }

  watchUPnPPortsChange () {
    const { systemConfig } = this.configManager
    const watchKeys = ['listen-port', 'dht-listen-port']

    watchKeys.forEach((key) => {
      this.configListeners[key] = systemConfig.onDidChange(key, async (newValue, oldValue) => {
        logger.info('[Motrix] detected port change event:', key, newValue, oldValue)
        const enable = this.configManager.getUserConfig('enable-upnp')
        if (!enable) {
          return
        }

        const promises = [
          this.upnp.unmap(oldValue),
          this.upnp.map(newValue)
        ]
        try {
          await Promise.allSettled(promises)
        } catch (e) {
          logger.info('[Motrix] change UPnP port mapping failed:', e)
        }
      })
    })
  }

  watchUPnPEnabledChange () {
    const { userConfig } = this.configManager
    const key = 'enable-upnp'
    this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
      logger.info('[Motrix] detected enable-upnp value change event:', newValue, oldValue)
      if (newValue) {
        this.startUPnPMapping()
      } else {
        await this.stopUPnPMapping()
        this.upnp.closeClient()
      }
    })
  }

  async shutdownUPnPManager () {
    const enable = this.configManager.getUserConfig('enable-upnp')
    if (enable) {
      await this.stopUPnPMapping()
    }

    this.upnp.closeClient()
  }

  syncTrackers (source, proxy) {
    if (isEmpty(source)) {
      return
    }

    setTimeout(() => {
      fetchBtTrackerFromSource(source, proxy).then((data) => {
        logger.warn('[Motrix] auto sync tracker data:', data)
        if (!data || data.length === 0) {
          return
        }

        let tracker = convertTrackerDataToComma(data)
        tracker = reduceTrackerString(tracker)
        this.savePreference({
          system: {
            'bt-tracker': tracker
          },
          user: {
            'last-sync-tracker-time': Date.now()
          }
        })
      }).catch((err) => {
        logger.warn('[Motrix] auto sync tracker failed:', err.message)
      })
    }, 500)
  }

  autoSyncTrackers () {
    const enable = this.configManager.getUserConfig('auto-sync-tracker')
    const lastTime = this.configManager.getUserConfig('last-sync-tracker-time')
    const result = checkIsNeedRun(enable, lastTime, AUTO_SYNC_TRACKER_INTERVAL)
    logger.info('[Motrix] auto sync tracker checkIsNeedRun:', result)
    if (!result) {
      return
    }

    const source = this.configManager.getUserConfig('tracker-source')
    const proxy = this.configManager.getUserConfig('proxy', { enable: false })

    this.syncTrackers(source, proxy)
  }

  autoResumeTask () {
    const enabled = this.configManager.getUserConfig('resume-all-when-app-launched')
    if (!enabled) {
      return
    }

    this.engineClient.call('unpauseAll')
  }

  initWindowManager () {
    this.windowManager = new WindowManager({
      userConfig: this.configManager.getUserConfig()
    })

    this.windowManager.on('window-resized', (data) => {
      this.storeWindowState(data)
    })

    this.windowManager.on('window-moved', (data) => {
      this.storeWindowState(data)
    })

    this.windowManager.on('window-closed', (data) => {
      this.storeWindowState(data)
    })

    this.windowManager.on('enter-full-screen', (window) => {
      this.dockManager.show()
    })

    this.windowManager.on('leave-full-screen', (window) => {
      const mode = this.configManager.getUserConfig('run-mode')
      if (mode === APP_RUN_MODE.TRAY) {
        this.dockManager.hide()
      }
    })
  }

  storeWindowState (data = {}) {
    const enabled = this.configManager.getUserConfig('keep-window-state')
    if (!enabled) {
      return
    }

    const state = this.configManager.getUserConfig('window-state', {})
    const { page, bounds } = data
    const newState = {
      ...state,
      [page]: bounds
    }
    this.configManager.setUserConfig('window-state', newState)
  }

  start (page, options = {}) {
    const win = this.showPage(page, options)

    win.once('ready-to-show', () => {
      this.isReady = true
      this.emit('ready')
    })

    if (is.macOS()) {
      this.touchBarManager.setup(page, win)
    }
  }

  showPage (page, options = {}) {
    const { openedAtLogin } = options
    const autoHideWindow = this.configManager.getUserConfig('auto-hide-window')
    return this.windowManager.openWindow(page, {
      hidden: openedAtLogin || autoHideWindow
    })
  }

  show (page = 'index') {
    this.windowManager.showWindow(page)
  }

  hide (page) {
    if (page) {
      this.windowManager.hideWindow(page)
    } else {
      this.windowManager.hideAllWindow()
    }
  }

  toggle (page = 'index') {
    this.windowManager.toggleWindow(page)
  }

  closePage (page) {
    this.windowManager.destroyWindow(page)
  }

  stop () {
    try {
      const promises = [
        this.stopEngine(),
        this.shutdownUPnPManager(),
        this.energyManager.stopPowerSaveBlocker(),
        this.trayManager.destroy()
      ]

      return promises
    } catch (err) {
      logger.warn('[Motrix] stop error: ', err.message)
    }
  }

  async stopAllSettled () {
    await Promise.allSettled(this.stop())
  }

  async quit () {
    await this.stopAllSettled()
    app.exit()
  }

  sendCommand (command, ...args) {
    if (!this.emit(command, ...args)) {
      const window = this.windowManager.getFocusedWindow()
      if (window) {
        this.windowManager.sendCommandTo(window, command, ...args)
      }
    }
  }

  sendCommandToAll (command, ...args) {
    if (!this.emit(command, ...args)) {
      this.windowManager.getWindowList().forEach(window => {
        this.windowManager.sendCommandTo(window, command, ...args)
      })
    }
  }

  sendMessageToAll (channel, ...args) {
    this.windowManager.getWindowList().forEach(window => {
      this.windowManager.sendMessageTo(window, channel, ...args)
    })
  }

  initThemeManager () {
    this.themeManager = new ThemeManager()
    this.themeManager.on('system-theme-change', (theme) => {
      this.trayManager.handleSystemThemeChange(theme)
      this.sendCommandToAll('application:update-system-theme', { theme })
    })
  }

  initTouchBarManager () {
    if (!is.macOS()) {
      return
    }

    this.touchBarManager = new TouchBarManager()
  }

  initProtocolManager () {
    const protocols = this.configManager.getUserConfig('protocols', {})
    this.protocolManager = new ProtocolManager({
      protocols
    })
  }

  handleProtocol (url) {
    this.show()

    this.protocolManager.handle(url)
  }

  handleFile (filePath) {
    if (!filePath) {
      return
    }

    if (extname(filePath).toLowerCase() !== '.torrent') {
      return
    }

    this.show()

    const name = basename(filePath)
    readFile(filePath, (err, data) => {
      if (err) {
        logger.warn(`[Motrix] read file error: ${filePath}`, err.message)
        return
      }
      const dataURL = Buffer.from(data).toString('base64')
      this.sendCommandToAll('application:new-bt-task-with-file', {
        name,
        dataURL
      })
    })
  }

  initUpdaterManager () {
    if (is.mas()) {
      return
    }

    const enabled = this.configManager.getUserConfig('auto-check-update')
    const proxy = this.configManager.getSystemConfig('all-proxy')
    const lastTime = this.configManager.getUserConfig('last-check-update-time')
    const autoCheck = checkIsNeedRun(enabled, lastTime, AUTO_CHECK_UPDATE_INTERVAL)
    this.updateManager = new UpdateManager({
      autoCheck,
      proxy
    })
    this.handleUpdaterEvents()
  }

  handleUpdaterEvents () {
    this.updateManager.on('checking', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', false)
      this.trayManager.updateMenuItemEnabledState('app.check-for-updates', false)
      this.configManager.setUserConfig('last-check-update-time', Date.now())
    })

    this.updateManager.on('download-progress', (event) => {
      const win = this.windowManager.getWindow('index')
      win.setProgressBar(event.percent / 100)
    })

    this.updateManager.on('update-not-available', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
      this.trayManager.updateMenuItemEnabledState('app.check-for-updates', true)
    })

    this.updateManager.on('update-downloaded', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
      this.trayManager.updateMenuItemEnabledState('app.check-for-updates', true)
      const win = this.windowManager.getWindow('index')
      win.setProgressBar(1)
    })

    this.updateManager.on('update-cancelled', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
      this.trayManager.updateMenuItemEnabledState('app.check-for-updates', true)
      const win = this.windowManager.getWindow('index')
      win.setProgressBar(-1)
    })

    this.updateManager.on('will-updated', async (event) => {
      this.windowManager.setWillQuit(true)
      await this.stopAllSettled()
    })

    this.updateManager.on('update-error', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
      this.trayManager.updateMenuItemEnabledState('app.check-for-updates', true)
    })
  }

  async relaunch () {
    await this.stopAllSettled()
    app.relaunch()
    app.exit()
  }

  async resetSession () {
    await this.stopEngine()

    app.clearRecentDocuments()

    const sessionPath = this.context.get('session-path')
    setTimeout(() => {
      unlink(sessionPath, (err) => {
        logger.info('[Motrix] Removed the download seesion file:', err)
      })

      this.engine.start()
    }, 3000)
  }

  savePreference (config = {}) {
    logger.info('[Motrix] save preference:', config)
    const { system, user } = config
    if (!isEmpty(system)) {
      console.info('[Motrix] main save system config: ', system)
      this.configManager.setSystemConfig(system)
      this.engineClient.changeGlobalOption(system)
    }

    if (!isEmpty(user)) {
      console.info('[Motrix] main save user config: ', user)
      this.configManager.setUserConfig(user)
    }
  }

  handleCommands () {
    this.on('application:save-preference', this.savePreference)

    this.on('application:update-tray', (tray) => {
      this.trayManager.updateTrayByImage(tray)
    })

    this.on('application:relaunch', () => {
      this.relaunch()
    })

    this.on('application:quit', () => {
      this.quit()
    })

    this.on('application:show', ({ page }) => {
      this.show(page)
    })

    this.on('application:hide', ({ page }) => {
      this.hide(page)
    })

    this.on('application:reset-session', () => this.resetSession())

    this.on('application:factory-reset', () => {
      this.offConfigListeners()
      this.configManager.reset()
      this.relaunch()
    })

    this.on('application:check-for-updates', () => {
      this.updateManager.check()
    })

    this.on('application:change-theme', (theme) => {
      this.themeManager.updateSystemTheme(theme)
      this.sendCommandToAll('application:update-theme', { theme })
    })

    this.on('application:change-locale', (locale) => {
      this.localeManager.changeLanguageByLocale(locale)
        .then(() => {
          this.menuManager.handleLocaleChange(locale)
          this.trayManager.handleLocaleChange(locale)
        })
    })

    this.on('application:toggle-dock', (visible) => {
      if (visible) {
        this.dockManager.show()
      } else {
        this.dockManager.hide()
        // Hiding the dock icon will trigger the entire app to hide.
        this.show()
      }
    })

    this.on('application:auto-hide-window', (hide) => {
      if (hide) {
        this.windowManager.handleWindowBlur()
      } else {
        this.windowManager.unbindWindowBlur()
      }
    })

    this.on('application:change-menu-states', (visibleStates, enabledStates, checkedStates) => {
      this.menuManager.updateMenuStates(visibleStates, enabledStates, checkedStates)
      this.trayManager.updateMenuStates(visibleStates, enabledStates, checkedStates)
    })

    this.on('application:open-file', (event) => {
      dialog.showOpenDialog({
        properties: ['openFile'],
        filters: [
          {
            name: 'Torrent',
            extensions: ['torrent']
          }
        ]
      }).then(({ canceled, filePaths }) => {
        if (canceled || filePaths.length === 0) {
          return
        }

        const [filePath] = filePaths
        this.handleFile(filePath)
      })
    })

    this.on('application:clear-recent-tasks', () => {
      app.clearRecentDocuments()
    })

    this.on('application:setup-protocols-client', (protocols) => {
      if (is.dev() || is.mas() || !protocols) {
        return
      }
      logger.info('[Motrix] setup protocols client:', protocols)
      this.protocolManager.setup(protocols)
    })

    this.on('application:open-external', (url) => {
      this.openExternal(url)
    })

    this.on('application:reveal-in-folder', (data) => {
      const { gid, path } = data
      logger.info('[Motrix] application:reveal-in-folder===>', path)
      if (path) {
        showItemInFolder(path)
      }
      if (gid) {
        this.sendCommandToAll('application:show-task-detail', { gid })
      }
    })

    this.on('help:official-website', () => {
      const url = 'https://motrix.app/'
      this.openExternal(url)
    })

    this.on('help:manual', () => {
      const url = 'https://motrix.app/manual'
      this.openExternal(url)
    })

    this.on('help:release-notes', () => {
      const url = 'https://motrix.app/release'
      this.openExternal(url)
    })

    this.on('help:report-problem', () => {
      const url = 'https://motrix.app/report'
      this.openExternal(url)
    })
  }

  openExternal (url) {
    if (!url) {
      return
    }

    shell.openExternal(url)
  }

  handleConfigChange (configName) {
    this.sendCommandToAll('application:update-preference-config', { configName })
  }

  handleEvents () {
    this.once('application:initialized', () => {
      this.autoSyncTrackers()

      this.autoResumeTask()

      this.adjustMenu()
    })

    this.configManager.userConfig.onDidAnyChange(() => this.handleConfigChange('user'))
    this.configManager.systemConfig.onDidAnyChange(() => this.handleConfigChange('system'))

    this.watchOpenAtLoginChange()
    this.watchProtocolsChange()
    this.watchRunModeChange()
    this.watchShowProgressBarChange()
    this.watchProxyChange()
    this.watchLocaleChange()
    this.watchThemeChange()

    this.on('download-status-change', (downloading) => {
      this.trayManager.handleDownloadStatusChange(downloading)
      if (downloading) {
        this.energyManager.startPowerSaveBlocker()
      } else {
        this.energyManager.stopPowerSaveBlocker()
      }
    })

    this.on('speed-change', (speed) => {
      this.dockManager.handleSpeedChange(speed)
      this.trayManager.handleSpeedChange(speed)
    })

    this.on('task-download-complete', (task, path) => {
      this.dockManager.openDock(path)

      if (is.linux()) {
        return
      }
      app.addRecentDocument(path)
    })

    if (this.configManager.userConfig.get('show-progress-bar')) {
      this.bindProgressChange()
    }
  }

  handleProgressChange (progress) {
    if (this.updateManager.isChecking) {
      return
    }
    if (!is.windows() && progress === 2) {
      progress = 0
    }
    this.windowManager.getWindow('index').setProgressBar(progress)
  }

  bindProgressChange () {
    if (this.listeners('progress-change').length > 0) {
      return
    }

    this.on('progress-change', this.handleProgressChange)
  }

  unbindProgressChange () {
    if (this.listeners('progress-change').length === 0) {
      return
    }

    this.off('progress-change', this.handleProgressChange)
    this.windowManager.getWindow('index').setProgressBar(-1)
  }

  handleIpcMessages () {
    ipcMain.on('command', (event, command, ...args) => {
      logger.log('[Motrix] ipc receive command', command, ...args)
      this.emit(command, ...args)
    })

    ipcMain.on('event', (event, eventName, ...args) => {
      logger.log('[Motrix] ipc receive event', eventName, ...args)
      this.emit(eventName, ...args)
    })
  }

  handleIpcInvokes () {
    ipcMain.handle('get-app-config', async () => {
      const systemConfig = this.configManager.getSystemConfig()
      const userConfig = this.configManager.getUserConfig()
      const context = this.context.get()

      const result = {
        ...systemConfig,
        ...userConfig,
        ...context
      }
      return result
    })
  }
}


================================================
FILE: src/main/Launcher.js
================================================
import { EventEmitter } from 'node:events'
import { app } from 'electron'
import is from 'electron-is'

import ExceptionHandler from './core/ExceptionHandler'
import logger from './core/Logger'
import Application from './Application'
import {
  splitArgv,
  parseArgvAsUrl,
  parseArgvAsFile
} from './utils'
import { EMPTY_STRING } from '@shared/constants'

export default class Launcher extends EventEmitter {
  constructor () {
    super()
    this.url = EMPTY_STRING
    this.file = EMPTY_STRING

    this.makeSingleInstance(() => {
      this.init()
    })
  }

  makeSingleInstance (callback) {
    // Mac App Store Sandboxed App not support requestSingleInstanceLock
    if (is.mas()) {
      callback && callback()
      return
    }

    const gotSingleLock = app.requestSingleInstanceLock()

    if (!gotSingleLock) {
      app.quit()
    } else {
      app.on('second-instance', (event, argv, workingDirectory) => {
        global.application.showPage('index')
        if (!is.macOS() && argv.length > 1) {
          this.handleAppLaunchArgv(argv)
        }
      })

      callback && callback()
    }
  }

  init () {
    this.exceptionHandler = new ExceptionHandler()

    this.openedAtLogin = is.macOS()
      ? app.getLoginItemSettings().wasOpenedAtLogin
      : false

    if (process.argv.length > 1) {
      this.handleAppLaunchArgv(process.argv)
    }

    logger.info('[Motrix] openedAtLogin:', this.openedAtLogin)

    this.handleAppEvents()
  }

  handleAppEvents () {
    this.handleRendererRemote()
    this.handleOpenUrl()
    this.handleOpenFile()

    this.handelAppReady()
    this.handleAppWillQuit()
  }

  handleRendererRemote () {
    app.on('browser-window-created', (_, window) => {
      require('@electron/remote/main').enable(window.webContents)
    })
  }

  /**
   * handleOpenUrl
   * Event 'open-url' macOS only
   * "name": "Motrix Protocol",
   * "schemes": ["mo", "motrix"]
   */
  handleOpenUrl () {
    if (is.mas() || !is.macOS()) {
      return
    }
    app.on('open-url', (event, url) => {
      logger.info(`[Motrix] open-url: ${url}`)
      event.preventDefault()
      this.url = url
      this.sendUrlToApplication()
    })
  }

  /**
   * handleOpenFile
   * Event 'open-file' macOS only
   * handle open torrent file
   */
  handleOpenFile () {
    if (!is.macOS()) {
      return
    }
    app.on('open-file', (event, path) => {
      logger.info(`[Motrix] open-file: ${path}`)
      event.preventDefault()
      this.file = path
      this.sendFileToApplication()
    })
  }

  /**
   * handleAppLaunchArgv
   * For Windows, Linux
   * @param {array} argv
   */
  handleAppLaunchArgv (argv) {
    logger.info('[Motrix] handleAppLaunchArgv:', argv)

    // args: array, extra: map
    const { args, extra } = splitArgv(argv)
    logger.info('[Motrix] split argv args:', args)
    logger.info('[Motrix] split argv extra:', extra)
    if (extra['--opened-at-login'] === '1') {
      this.openedAtLogin = true
    }

    const file = parseArgvAsFile(args)
    if (file) {
      this.file = file
      this.sendFileToApplication()
    }

    const url = parseArgvAsUrl(args)
    if (url) {
      this.url = url
      this.sendUrlToApplication()
    }
  }

  sendUrlToApplication () {
    if (this.url && global.application && global.application.isReady) {
      global.application.handleProtocol(this.url)
      this.url = EMPTY_STRING
    }
  }

  sendFileToApplication () {
    if (this.file && global.application && global.application.isReady) {
      global.application.handleFile(this.file)
      this.file = EMPTY_STRING
    }
  }

  handelAppReady () {
    app.on('ready', () => {
      global.application = new Application()

      const { openedAtLogin } = this
      global.application.start('index', {
        openedAtLogin
      })

      global.application.on('ready', () => {
        this.sendUrlToApplication()

        this.sendFileToApplication()
      })
    })

    app.on('activate', () => {
      if (global.application) {
        logger.info('[Motrix] activate')
        global.application.showPage('index')
      }
    })
  }

  handleAppWillQuit () {
    app.on('will-quit', () => {
      logger.info('[Motrix] will-quit')
      if (global.application) {
        logger.info('[Motrix] will-quit.application.stop')
        global.application.stop()
      }
    })
  }
}


================================================
FILE: src/main/configs/engine.js
================================================
export const engineBinMap = {
  darwin: 'aria2c',
  win32: 'aria2c.exe',
  linux: 'aria2c'
}

export const engineArchMap = {
  darwin: {
    x64: 'x64',
    arm64: 'arm64'
  },
  win32: {
    ia32: 'ia32',
    x64: 'x64',
    arm64: 'x64'
  },
  linux: {
    x64: 'x64',
    arm: 'armv7l',
    arm64: 'arm64'
  }
}


================================================
FILE: src/main/configs/page.js
================================================
import is from 'electron-is'

export default {
  index: {
    attrs: {
      title: 'Motrix',
      width: 1024,
      height: 768,
      minWidth: 478,
      minHeight: 420,
      transparent: is.macOS()
    },
    bindCloseToHide: true,
    openDevTools: is.dev(),
    url: is.dev() ? 'http://localhost:9080' : require('path').join('file://', __dirname, '/index.html')
  }
}


================================================
FILE: src/main/configs/protocol.js
================================================
/* eslint quote-props: ["error", "always"] */
export default {
  'task-list': 'application:task-list',
  'new-task': 'application:new-task',
  'new-bt-task': 'application:new-bt-task',
  'pause-all-task': 'application:pause-all-task',
  'resume-all-task': 'application:resume-all-task',
  'reveal-in-folder': 'application:reveal-in-folder',
  'preferences': 'application:preferences',
  'about': 'application:about'
}


================================================
FILE: src/main/core/AutoLaunchManager.js
================================================
import { app } from 'electron'

import { LOGIN_SETTING_OPTIONS } from '@shared/constants'

export default class AutoLaunchManager {
  enable () {
    return new Promise((resolve, reject) => {
      const enabled = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
      if (enabled) {
        resolve()
      }

      app.setLoginItemSettings({
        ...LOGIN_SETTING_OPTIONS,
        openAtLogin: true
      })
      resolve()
    })
  }

  disable () {
    return new Promise((resolve, reject) => {
      app.setLoginItemSettings({ openAtLogin: false })
      resolve()
    })
  }

  isEnabled () {
    return new Promise((resolve, reject) => {
      const enabled = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
      resolve(enabled)
    })
  }
}


================================================
FILE: src/main/core/ConfigManager.js
================================================
import { app } from 'electron'
import is from 'electron-is'
import Store from 'electron-store'

import {
  getConfigBasePath,
  getDhtPath,
  getMaxConnectionPerServer,
  getUserDownloadsPath
} from '../utils/index'
import {
  APP_RUN_MODE,
  APP_THEME,
  EMPTY_STRING,
  ENGINE_RPC_PORT,
  IP_VERSION,
  LOGIN_SETTING_OPTIONS,
  NGOSANG_TRACKERS_BEST_IP_URL_CDN,
  NGOSANG_TRACKERS_BEST_URL_CDN,
  PROXY_SCOPES,
  PROXY_SCOPE_OPTIONS
} from '@shared/constants'
import { CHROME_UA } from '@shared/ua'
import { separateConfig } from '@shared/utils'
import { reduceTrackerString } from '@shared/utils/tracker'

export default class ConfigManager {
  constructor () {
    this.systemConfig = {}
    this.userConfig = {}

    this.init()
  }

  init () {
    this.initUserConfig()
    this.initSystemConfig()
  }

  /**
   * Aria2 Configuration Priority
   * system.json > built-in aria2.conf
   * https://aria2.github.io/manual/en/html/aria2c.html
   *
   */
  initSystemConfig () {
    this.systemConfig = new Store({
      name: 'system',
      cwd: getConfigBasePath(),
      /* eslint-disable quote-props */
      defaults: {
        'all-proxy': EMPTY_STRING,
        'allow-overwrite': false,
        'auto-file-renaming': true,
        'bt-exclude-tracker': EMPTY_STRING,
        'bt-force-encryption': false,
        'bt-load-saved-metadata': true,
        'bt-save-metadata': true,
        'bt-tracker': EMPTY_STRING,
        'continue': true,
        'dht-file-path': getDhtPath(IP_VERSION.V4),
        'dht-file-path6': getDhtPath(IP_VERSION.V6),
        'dht-listen-port': 26701,
        'dir': getUserDownloadsPath(),
        'enable-dht6': true,
        'follow-metalink': true,
        'follow-torrent': true,
        'listen-port': 21301,
        'max-concurrent-downloads': 5,
        'max-connection-per-server': getMaxConnectionPerServer(),
        'max-download-limit': 0,
        'max-overall-download-limit': 0,
        'max-overall-upload-limit': 0,
        'no-proxy': EMPTY_STRING,
        'pause-metadata': false,
        'pause': true,
        'rpc-listen-port': ENGINE_RPC_PORT,
        'rpc-secret': EMPTY_STRING,
        'seed-ratio': 2,
        'seed-time': 2880,
        'split': getMaxConnectionPerServer(),
        'user-agent': CHROME_UA
      }
      /* eslint-enable quote-props */
    })
    this.fixSystemConfig()
  }

  initUserConfig () {
    this.userConfig = new Store({
      name: 'user',
      cwd: getConfigBasePath(),
      // Schema need electron-store upgrade to 3.x.x,
      // but it will cause the application build to fail.
      // schema: {
      //   theme: {
      //     type: 'string',
      //     enum: ['auto', 'light', 'dark']
      //   }
      // },
      /* eslint-disable quote-props */
      defaults: {
        'auto-check-update': is.macOS(),
        'auto-hide-window': false,
        'auto-sync-tracker': true,
        'enable-upnp': true,
        'engine-max-connection-per-server': getMaxConnectionPerServer(),
        'favorite-directories': [],
        'hide-app-menu': is.windows() || is.linux(),
        'history-directories': [],
        'keep-seeding': false,
        'keep-window-state': false,
        'last-check-update-time': 0,
        'last-sync-tracker-time': 0,
        'locale': app.getLocale(),
        'log-level': 'warn',
        'new-task-show-downloading': true,
        'no-confirm-before-delete-task': false,
        'open-at-login': false,
        'protocols': { 'magnet': true, 'thunder': false },
        'proxy': {
          'enable': false,
          'server': EMPTY_STRING,
          'bypass': EMPTY_STRING,
          'scope': PROXY_SCOPE_OPTIONS
        },
        'resume-all-when-app-launched': false,
        'run-mode': APP_RUN_MODE.STANDARD,
        'show-progress-bar': true,
        'task-notification': true,
        'theme': APP_THEME.AUTO,
        'tracker-source': [
          NGOSANG_TRACKERS_BEST_IP_URL_CDN,
          NGOSANG_TRACKERS_BEST_URL_CDN
        ],
        'tray-theme': APP_THEME.AUTO,
        'tray-speedometer': is.macOS(),
        'update-channel': 'latest',
        'window-state': {}
      }
      /* eslint-enable quote-props */
    })
    this.fixUserConfig()
  }

  fixSystemConfig () {
    // Remove aria2c unrecognized options
    const { others } = separateConfig(this.systemConfig.store)
    if (others && Object.keys(others).length > 0) {
      Object.keys(others).forEach(key => {
        this.systemConfig.delete(key)
      })
    }

    const proxy = this.getUserConfig('proxy', { enable: false })
    const { enable, server, bypass, scope = [] } = proxy
    if (enable && server && scope.includes(PROXY_SCOPES.DOWNLOAD)) {
      this.setSystemConfig('all-proxy', server)
      this.setSystemConfig('no-proxy', bypass)
    }

    // Fix spawn ENAMETOOLONG on Windows
    const tracker = reduceTrackerString(this.systemConfig.get('bt-tracker'))
    this.setSystemConfig('bt-tracker', tracker)
  }

  fixUserConfig () {
    // Fix the value of open-at-login when the user delete
    // the Motrix self-starting item through startup management.
    const openAtLogin = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
    if (this.getUserConfig('open-at-login') !== openAtLogin) {
      this.setUserConfig('open-at-login', openAtLogin)
    }

    if (this.getUserConfig('tracker-source').length === 0) {
      this.setUserConfig('tracker-source', [
        NGOSANG_TRACKERS_BEST_IP_URL_CDN,
        NGOSANG_TRACKERS_BEST_URL_CDN
      ])
    }
  }

  getSystemConfig (key, defaultValue) {
    if (typeof key === 'undefined' &&
        typeof defaultValue === 'undefined') {
      return this.systemConfig.store
    }

    return this.systemConfig.get(key, defaultValue)
  }

  getUserConfig (key, defaultValue) {
    if (typeof key === 'undefined' &&
        typeof defaultValue === 'undefined') {
      return this.userConfig.store
    }

    return this.userConfig.get(key, defaultValue)
  }

  getLocale () {
    return this.getUserConfig('locale') || app.getLocale()
  }

  setSystemConfig (...args) {
    this.systemConfig.set(...args)
  }

  setUserConfig (...args) {
    this.userConfig.set(...args)
  }

  reset () {
    this.systemConfig.clear()
    this.userConfig.clear()
  }
}


================================================
FILE: src/main/core/Context.js
================================================
import logger from './Logger'
import {
  getEnginePath,
  getAria2BinPath,
  getAria2ConfPath,
  getSessionPath
} from '../utils'

const { platform, arch } = process

export default class Context {
  constructor () {
    this.init()
  }

  getLogPath () {
    const { path } = logger.transports.file.getFile()
    return path
  }

  init () {
    // The key of Context cannot be the same as that of userConfig and systemConfig.
    this.context = {
      platform: platform,
      arch: arch,
      'log-path': this.getLogPath(),
      'session-path': getSessionPath(),
      'engine-path': getEnginePath(platform, arch),
      'aria2-bin-path': getAria2BinPath(platform, arch),
      'aria2-conf-path': getAria2ConfPath(platform, arch)
    }

    logger.info('[Motrix] Context.init===>', this.context)
  }

  get (key) {
    if (typeof key === 'undefined') {
      return this.context
    }

    return this.context[key]
  }
}


================================================
FILE: src/main/core/EnergyManager.js
================================================
import { powerSaveBlocker } from 'electron'

import logger from './Logger'

let psbId
export default class EnergyManager {
  startPowerSaveBlocker () {
    logger.info('[Motrix] EnergyManager.startPowerSaveBlocker', psbId)
    if (psbId && powerSaveBlocker.isStarted(psbId)) {
      return
    }

    psbId = powerSaveBlocker.start('prevent-app-suspension')
    logger.info('[Motrix] start power save blocker:', psbId)
  }

  stopPowerSaveBlocker () {
    logger.info('[Motrix] EnergyManager.stopPowerSaveBlocker', psbId)
    if (typeof psbId === 'undefined' || !powerSaveBlocker.isStarted(psbId)) {
      return
    }

    powerSaveBlocker.stop(psbId)
    logger.info('[Motrix] stop power save blocker:', psbId)
    psbId = undefined
  }
}


================================================
FILE: src/main/core/Engine.js
================================================
import { spawn } from 'node:child_process'
import { existsSync, writeFile, unlink } from 'node:fs'
import is from 'electron-is'

import logger from './Logger'
import { getI18n } from '../ui/Locale'
import {
  getEnginePidPath,
  getAria2BinPath,
  getAria2ConfPath,
  getSessionPath,
  transformConfig
} from '../utils/index'

const { platform, arch } = process

export default class Engine {
  // ChildProcess | null
  static instance = null

  constructor (options = {}) {
    this.options = options

    this.i18n = getI18n()
    this.systemConfig = options.systemConfig
    this.userConfig = options.userConfig
  }

  start () {
    const pidPath = getEnginePidPath()
    logger.info('[Motrix] Engie pid path:', pidPath)

    if (this.instance) {
      return
    }

    const binPath = this.getEngineBinPath()
    const args = this.getStartArgs()
    this.instance = spawn(binPath, args, {
      windowsHide: false,
      stdio: is.dev() ? 'pipe' : 'ignore'
    })
    const pid = this.instance.pid.toString()
    this.writePidFile(pidPath, pid)

    this.instance.once('close', () => {
      try {
        unlink(pidPath, (err) => {
          if (err) {
            logger.warn(`[Motrix] Unlink engine process pid file failed: ${err}`)
          }
        })
      } catch (err) {
        logger.warn(`[Motrix] Unlink engine process pid file failed: ${err}`)
      }
    })

    if (is.dev()) {
      this.instance.stdout.on('data', (data) => {
        logger.log('[Motrix] engine stdout===>', data.toString())
      })

      this.instance.stderr.on('data', (data) => {
        logger.log('[Motrix] engine stderr===>', data.toString())
      })
    }
  }

  stop () {
    logger.info('[Motrix] engine.stop.instance')
    if (this.instance) {
      this.instance.kill()
      this.instance = null
    }
  }

  writePidFile (pidPath, pid) {
    writeFile(pidPath, pid, (err) => {
      if (err) {
        logger.error(`[Motrix] Write engine process pid failed: ${err}`)
      }
    })
  }

  getEngineBinPath () {
    const result = getAria2BinPath(platform, arch)
    const binIsExist = existsSync(result)
    if (!binIsExist) {
      logger.error('[Motrix] engine bin is not exist:', result)
      throw new Error(this.i18n.t('app.engine-missing-message'))
    }

    return result
  }

  getStartArgs () {
    const confPath = getAria2ConfPath(platform, arch)

    const sessionPath = getSessionPath()
    const sessionIsExist = existsSync(sessionPath)

    let result = [`--conf-path=${confPath}`, `--save-session=${sessionPath}`]
    if (sessionIsExist) {
      result = [...result, `--input-file=${sessionPath}`]
    }

    const extraConfig = {
      ...this.systemConfig
    }
    const keepSeeding = this.userConfig['keep-seeding']
    const seedRatio = this.systemConfig['seed-ratio']
    if (keepSeeding || seedRatio === 0) {
      extraConfig['seed-ratio'] = 0
      delete extraConfig['seed-time']
    }
    console.log('extraConfig===>', extraConfig)

    const extra = transformConfig(extraConfig)
    result = [...result, ...extra]

    return result
  }

  isRunning (pid) {
    try {
      return process.kill(pid, 0)
    } catch (e) {
      return e.code === 'EPERM'
    }
  }

  restart () {
    this.stop()
    this.start()
  }
}


================================================
FILE: src/main/core/EngineClient.js
================================================
'use strict'

import { Aria2 } from '@shared/aria2'

import logger from './Logger'
import {
  compactUndefined,
  formatOptionsForEngine
} from '@shared/utils'
import {
  ENGINE_RPC_HOST,
  ENGINE_RPC_PORT,
  EMPTY_STRING
} from '@shared/constants'

const defaults = {
  host: ENGINE_RPC_HOST,
  port: ENGINE_RPC_PORT,
  secret: EMPTY_STRING
}

export default class EngineClient {
  static instance = null
  static client = null

  constructor (options = {}) {
    this.options = {
      ...defaults,
      ...options
    }

    this.init()
  }

  init () {
    this.connect()
  }

  connect () {
    logger.info('[Motrix] main engine client connect', this.options)
    const { host, port, secret } = this.options
    this.client = new Aria2({
      host,
      port,
      secret
    })
  }

  async call (method, ...args) {
    return this.client.call(method, ...args).catch((err) => {
      logger.warn('[Motrix] call client fail:', err.message)
    })
  }

  async changeGlobalOption (options) {
    logger.info('[Motrix] change engine global option:', options)
    const args = formatOptionsForEngine(options)

    return this.call('changeGlobalOption', args)
  }

  async shutdown (options = {}) {
    const { force = false } = options
    const { secret } = this.options

    const method = force ? 'forceShutdown' : 'shutdown'
    const args = compactUndefined([secret])
    return this.call(method, ...args)
  }
}


================================================
FILE: src/main/core/ExceptionHandler.js
================================================
import { app, dialog } from 'electron'
import is from 'electron-is'

import logger from './Logger'

const defaults = {
  showDialog: !is.dev()
}
export default class ExceptionHandler {
  constructor (options) {
    this.options = {
      ...defaults,
      ...options
    }

    this.setup()
  }

  setup () {
    if (is.dev()) {
      return
    }
    const { showDialog } = this.options
    process.on('uncaughtException', (err) => {
      const { message, stack } = err
      logger.error(`[Motrix] Uncaught exception: ${message}`)
      logger.error(stack)

      if (showDialog && app.isReady()) {
        dialog.showErrorBox('Error: ', message)
      }
    })
  }
}


================================================
FILE: src/main/core/Logger.js
================================================
import { join } from 'node:path'
import is from 'electron-is'
import logger from 'electron-log'

import { IS_PORTABLE, PORTABLE_EXECUTABLE_DIR } from '@shared/constants'

const level = is.production() ? 'info' : 'silly'
logger.transports.file.level = level

if (IS_PORTABLE) {
  logger.transports.file.resolvePath = () => join(PORTABLE_EXECUTABLE_DIR, 'main.log')
}

logger.info('[Motrix] Logger init')
logger.warn('[Motrix] Logger init')

export default logger


================================================
FILE: src/main/core/ProtocolManager.js
================================================
import { EventEmitter } from 'node:events'
import { app } from 'electron'
import is from 'electron-is'
import { parse } from 'querystring'

import logger from './Logger'
import protocolMap from '../configs/protocol'
import { ADD_TASK_TYPE } from '@shared/constants'

export default class ProtocolManager extends EventEmitter {
  constructor (options = {}) {
    super()
    this.options = options

    // package.json:build.protocols[].schemes[]
    // options.protocols: { 'magnet': true, 'thunder': false }
    this.protocols = {
      mo: true,
      motrix: true,
      ...options.protocols
    }

    this.init()
  }

  init () {
    const { protocols } = this
    this.setup(protocols)
  }

  setup (protocols = {}) {
    if (is.dev() || is.mas()) {
      return
    }

    Object.keys(protocols).forEach((protocol) => {
      const enabled = protocols[protocol]
      if (enabled) {
        if (!app.isDefaultProtocolClient(protocol)) {
          app.setAsDefaultProtocolClient(protocol)
        }
      } else {
        app.removeAsDefaultProtocolClient(protocol)
      }
    })
  }

  handle (url) {
    logger.info(`[Motrix] protocol url: ${url}`)

    if (
      url.toLowerCase().startsWith('ftp:') ||
      url.toLowerCase().startsWith('http:') ||
      url.toLowerCase().startsWith('https:') ||
      url.toLowerCase().startsWith('magnet:') ||
      url.toLowerCase().startsWith('thunder:')
    ) {
      return this.handleResourceProtocol(url)
    }

    if (
      url.toLowerCase().startsWith('mo:') ||
      url.toLowerCase().startsWith('motrix:')
    ) {
      return this.handleMoProtocol(url)
    }
  }

  handleResourceProtocol (url) {
    if (!url) {
      return
    }

    global.application.sendCommandToAll('application:new-task', {
      type: ADD_TASK_TYPE.URI,
      uri: url
    })
  }

  handleMoProtocol (url) {
    const parsed = new URL(url)
    const { host, search } = parsed
    logger.info('[Motrix] protocol parsed:', parsed, host)

    const command = protocolMap[host]
    if (!command) {
      return
    }

    const query = search.startsWith('?') ? search.replace('?', '') : search
    const args = parse(query)
    global.application.sendCommandToAll(command, args)
  }
}


================================================
FILE: src/main/core/UPnPManager.js
================================================
import NatAPI from '@motrix/nat-api'

import logger from './Logger'

let client = null
const mappingStatus = {}

export default class UPnPManager {
  constructor (options = {}) {
    this.options = {
      ...options
    }
  }

  init () {
    if (client) {
      return
    }

    client = new NatAPI({
      autoUpdate: true
    })
  }

  map (port) {
    this.init()

    return new Promise((resolve, reject) => {
      logger.info('[Motrix] UPnPManager port mapping: ', port)
      if (!port) {
        reject(new Error('[Motrix] port was not specified'))
        return
      }

      try {
        client.map(port, (err) => {
          if (err) {
            logger.warn(`[Motrix] UPnPManager map ${port} failed, error: `, err.message)
            reject(err.message)
            return
          }

          mappingStatus[port] = true
          logger.info(`[Motrix] UPnPManager port ${port} mapping succeeded`)
          resolve()
        })
      } catch (err) {
        reject(err.message)
      }
    })
  }

  unmap (port) {
    this.init()

    return new Promise((resolve, reject) => {
      logger.info('[Motrix] UPnPManager port unmapping: ', port)
      if (!port) {
        reject(new Error('[Motrix] port was not specified'))
        return
      }

      if (!mappingStatus[port]) {
        resolve()
        return
      }

      try {
        client.unmap(port, (err) => {
          if (err) {
            logger.warn(`[Motrix] UPnPManager unmap ${port} failed, error: `, err)
            reject(err.message)
            return
          }

          logger.info(`[Motrix] UPnPManager port ${port} unmapping succeeded`)
          mappingStatus[port] = false
          resolve()
        })
      } catch (err) {
        reject(err.message)
      }
    })
  }

  closeClient () {
    if (!client) {
      return
    }

    try {
      client.destroy(() => {
        client = null
      })
    } catch (err) {
      logger.warn('[Motrix] close UPnP client fail', err)
    }
  }
}


================================================
FILE: src/main/core/UpdateManager.js
================================================
import { EventEmitter } from 'node:events'
import { resolve } from 'node:path'
import { dialog } from 'electron'
import is from 'electron-is'
import { autoUpdater } from 'electron-updater'

import { PROXY_SCOPES } from '@shared/constants'
import logger from './Logger'
import { getI18n } from '../ui/Locale'

if (is.dev()) {
  autoUpdater.updateConfigPath = resolve(__dirname, '../../../app-update.yml')
}

export default class UpdateManager extends EventEmitter {
  constructor (options = {}) {
    super()
    this.options = options
    this.i18n = getI18n()

    this.isChecking = false
    this.updater = autoUpdater
    this.updater.autoDownload = false
    this.updater.autoInstallOnAppQuit = false
    this.updater.logger = logger
    logger.info('[Motrix] setup proxy:', this.options.proxy)
    this.setupProxy(this.options.proxy)

    this.autoCheckData = {
      checkEnable: this.options.autoCheck,
      userCheck: false
    }
    this.init()
  }

  setupProxy (proxy) {
    const { enable, server, scope = [] } = proxy
    if (!enable || !server || !scope.includes(PROXY_SCOPES.UPDATE_APP)) {
      this.updater.netSession.setProxy({
        proxyRules: undefined
      })
      return
    }

    const url = new URL(server)
    const { username, password, protocol = 'http:', host, port } = url
    const proxyRules = `${protocol}//${host}`

    logger.info(`[Motrix] setup proxy: ${proxyRules}`, username, password, protocol, host, port)
    this.updater.netSession.setProxy({
      proxyRules
    })

    if (server.includes('@')) {
      this.updater.signals.login((_authInfo, callback) => {
        callback(username, password)
      })
    }
  }

  init () {
    // Event: error
    // Event: checking-for-update
    // Event: update-available
    // Event: update-not-available
    // Event: download-progress
    // Event: update-downloaded

    this.updater.on('checking-for-update', this.checkingForUpdate.bind(this))
    this.updater.on('update-available', this.updateAvailable.bind(this))
    this.updater.on('update-not-available', this.updateNotAvailable.bind(this))
    this.updater.on('download-progress', this.updateDownloadProgress.bind(this))
    this.updater.on('update-downloaded', this.updateDownloaded.bind(this))
    this.updater.on('update-cancelled', this.updateCancelled.bind(this))
    this.updater.on('error', this.updateError.bind(this))

    if (this.autoCheckData.checkEnable && !this.isChecking) {
      this.autoCheckData.userCheck = false
      this.updater.checkForUpdates()
    }
  }

  check () {
    this.autoCheckData.userCheck = true
    this.updater.checkForUpdates()
  }

  checkingForUpdate () {
    this.isChecking = true
    this.emit('checking')
  }

  updateAvailable (event, info) {
    this.emit('update-available', info)
    dialog.showMessageBox({
      type: 'info',
      title: this.i18n.t('app.check-for-updates-title'),
      message: this.i18n.t('app.update-available-message'),
      buttons: [this.i18n.t('app.yes'), this.i18n.t('app.no')],
      cancelId: 1
    }).then(({ response }) => {
      if (response === 0) {
        this.updater.downloadUpdate()
      } else {
        this.emit('update-cancelled', info)
      }
    })
  }

  updateNotAvailable (event, info) {
    this.isChecking = false
    this.emit('update-not-available', info)
    if (this.autoCheckData.userCheck) {
      dialog.showMessageBox({
        title: this.i18n.t('app.check-for-updates-title'),
        message: this.i18n.t('app.update-not-available-message')
      })
    }
  }

  /**
   * autoUpdater:download-progress
   * @param {Object} event
   * progress,
   * bytesPerSecond,
   * percent,
   * total,
   * transferred
   */
  updateDownloadProgress (event) {
    this.emit('download-progress', event)
  }

  updateDownloaded (event, info) {
    this.emit('update-downloaded', info)
    this.updater.logger.log(`Update Downloaded: ${info}`)
    dialog.showMessageBox({
      title: this.i18n.t('app.check-for-updates-title'),
      message: this.i18n.t('app.update-downloaded-message')
    }).then(_ => {
      this.isChecking = false
      this.emit('will-updated')
      setTimeout(() => {
        this.updater.quitAndInstall()
      }, 200)
    })
  }

  updateCancelled () {
    this.isChecking = false
  }

  updateError (event, error) {
    this.isChecking = false
    this.emit('update-error', error)
    const msg = (error == null)
      ? this.i18n.t('app.update-error-message')
      : (error.stack || error).toString()

    this.updater.logger.warn(`[Motrix] update-error: ${msg}`)
    dialog.showErrorBox('Error', msg)
  }
}


================================================
FILE: src/main/index.dev.js
================================================
/**
 * This file is used specifically and only for development. It installs
 * `electron-debug` & `vue-devtools`. There shouldn't be any need to
 *  modify this file, but it can be used to extend your development
 *  environment.
 */

/* eslint-disable */

// Install `vue-devtools`
require('electron').app.whenReady().then(() => {
  let installExtension = require('electron-devtools-installer')
  installExtension.default(installExtension.VUEJS_DEVTOOLS)
    .then(() => {})
    .catch(err => {
      console.log('Unable to install `vue-devtools`: \n', err)
    })
})

// Require `main` process to boot app
require('./index')


================================================
FILE: src/main/index.js
================================================
import { app } from 'electron'
import is from 'electron-is'
import { initialize } from '@electron/remote/main'

import Launcher from './Launcher'

/**
 * initialize the main-process side of the remote module
 */
initialize()

process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'

if (process.env.NODE_ENV !== 'development') {
  global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}

/**
 * Fix Windows notification func
 * appId defined in .electron-vue/webpack.main.config.js
 */
if (is.windows()) {
  app.setAppUserModelId(appId)
}

global.launcher = new Launcher()


================================================
FILE: src/main/menus/darwin.json
================================================
{
  "menu": [
    {
      "id": "menu.app",
      "submenu": [
        { "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
        { "type": "separator" },
        { "id": "app.preferences", "command": "application:preferences" },
        { "id": "app.check-for-updates", "command": "application:check-for-updates" },
        { "id": "app.hide", "role": "hide" },
        { "id": "app.hide-others", "role": "hideothers" },
        { "id": "app.unhide", "role": "unhide" },
        { "type": "separator" },
        { "id": "app.quit", "role": "quit" }
      ]
    },
    {
      "id": "menu.task",
      "submenu": [
        { "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
        { "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
        { "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
        { "type": "separator" },
        { "id": "app.task-list", "command": "application:task-list" },
        { "id": "task.pause-task", "command": "application:pause-task" },
        { "id": "task.resume-task", "command": "application:resume-task" },
        { "id": "task.delete-task", "command": "application:delete-task" },
        { "id": "task.move-task-up", "command": "application:move-task-up" },
        { "id": "task.move-task-down", "command": "application:move-task-down" },
        { "type": "separator" },
        { "id": "task.pause-all-task", "command": "application:pause-all-task" },
        { "id": "task.resume-all-task", "command": "application:resume-all-task" },
        { "id": "task.select-all-task", "command": "application:select-all-task" },
        { "type": "separator" },
        { "id": "task.clear-recent-tasks", "command": "application:clear-recent-tasks" }
      ]
    },
    {
      "id": "menu.edit",
      "submenu": [
        { "id": "edit.undo", "role": "undo" },
        { "id": "edit.redo", "role": "redo" },
        { "type": "separator" },
        { "id": "edit.cut", "role": "cut" },
        { "id": "edit.copy", "role": "copy" },
        { "id": "edit.paste", "role": "paste" },
        { "id": "edit.delete", "role": "delete" },
        { "id": "edit.select-all", "role": "selectall" }
      ]
    },
    {
      "role": "window",
      "id": "menu.window",
      "submenu": [
        { "id": "window.reload", "role": "reload" },
        { "id": "window.close", "role": "close" },
        { "id": "window.minimize", "role": "minimize" },
        { "id": "window.zoom", "role": "zoom" },
        { "id": "window.toggle-fullscreen", "role": "togglefullscreen" },
        { "type": "separator" },
        { "id": "window.front", "role": "front" }
      ]
    },
    {
      "role": "help",
      "id": "menu.help",
      "submenu": [
        { "id": "help.official-website", "command": "help:official-website" },
        { "id": "help.manual", "command": "help:manual" },
        { "id": "help.release-notes", "command": "help:release-notes" },
        { "type": "separator" },
        { "id": "help.report-problem", "command": "help:report-problem" },
        { "type": "separator" },
        { "id": "help.toggle-dev-tools", "role": "toggledevtools" }
      ]
    }
  ]
}


================================================
FILE: src/main/menus/linux.json
================================================
{
  "menu": [
    {
      "id": "menu.file",
      "submenu": [
        { "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
        { "type": "separator" },
        { "id": "app.preferences", "command": "application:preferences" },
        { "id": "app.check-for-updates", "command": "application:check-for-updates" },
        { "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
        { "type": "separator" },
        { "id": "app.quit", "role": "quit" }
      ]
    },
    {
      "id": "menu.task",
      "submenu": [
        { "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
        { "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
        { "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
        { "type": "separator" },
        { "id": "app.task-list", "command": "application:task-list" },
        { "id": "task.pause-task", "command": "application:pause-task" },
        { "id": "task.resume-task", "command": "application:resume-task" },
        { "id": "task.delete-task", "command": "application:delete-task" },
        { "id": "task.move-task-up", "command": "application:move-task-up" },
        { "id": "task.move-task-down", "command": "application:move-task-down" },
        { "type": "separator" },
        { "id": "task.pause-all-task", "command": "application:pause-all-task" },
        { "id": "task.resume-all-task", "command": "application:resume-all-task" },
        { "id": "task.select-all-task", "command": "application:select-all-task" }
      ]
    },
    {
      "id": "menu.edit",
      "submenu": [
        { "id": "edit.undo", "role": "undo" },
        { "id": "edit.redo", "role": "redo" },
        { "type": "separator" },
        { "id": "edit.cut", "role": "cut" },
        { "id": "edit.copy", "role": "copy" },
        { "id": "edit.paste", "role": "paste" },
        { "id": "edit.delete", "role": "delete" },
        { "id": "edit.select-all", "role": "selectall" }
      ]
    },
    {
      "role": "window",
      "id": "menu.window",
      "submenu": [
        { "id": "window.reload", "role": "reload" },
        { "id": "window.close", "role": "close" },
        { "id": "window.minimize", "role": "minimize" },
        { "id": "window.zoom", "role": "zoom" },
        { "id": "window.toggle-fullscreen", "role": "togglefullscreen" },
        { "type": "separator" },
        { "id": "window.front", "role": "front" }
      ]
    },
    {
      "role": "help",
      "id": "menu.help",
      "submenu": [
        { "id": "help.official-website", "command": "help:official-website" },
        { "id": "help.manual", "command": "help:manual" },
        { "id": "help.release-notes", "command": "help:release-notes" },
        { "type": "separator" },
        { "id": "help.report-problem", "command": "help:report-problem" },
        { "type": "separator" },
        { "id": "help.toggle-dev-tools", "role": "toggledevtools" }
      ]
    }
  ]
}


================================================
FILE: src/main/menus/touchBar.json
================================================
[
  {
    "type": "button", "icon": "new-task", "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index"
  },
  {
    "type": "spacer", "size": "small"
  },
  {
    "type": "group",
    "id": "task.task-list",
    "items": [
      {
        "type": "button", "icon": "task-active", "command": "application:task-list", "command-arg": { "status": "active" }
      },
      {
        "type": "button", "icon": "task-waiting", "command": "application:task-list", "command-arg": { "status": "waiting" }
      },
      {
        "type": "button", "icon": "task-stopped", "command": "application:task-list", "command-arg": { "status": "stopped" }
      }
    ]
  },
  {
    "type": "spacer", "size": "large"
  },
  {
    "type": "button", "icon": "preferences", "id": "app.preferences", "command": "application:preferences"
  },
  {
    "type": "spacer", "size": "small"
  },
  {
    "type": "button", "icon": "about", "id": "app.about", "command": "application:about", "command-before": "application:show?page=index"
  }
]


================================================
FILE: src/main/menus/tray.json
================================================
[
  { "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
  { "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": { "type": "torrent" }, "command-after": "application:show?page=index" },
  { "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
  { "type": "separator" },
  { "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
  { "id": "help.manual", "command": "help:manual" },
  { "id": "app.check-for-updates", "command": "application:check-for-updates" },
  { "type": "separator" },
  { "id": "app.task-list", "command": "application:task-list", "command-before": "application:show?page=index" },
  { "id": "app.preferences", "command": "application:preferences", "command-before": "application:show?page=index" },
  { "id": "app.quit", "command": "application:quit" }
]


================================================
FILE: src/main/menus/win32.json
================================================
{
  "menu": [
    {
      "id": "menu.file",
      "submenu": [
        { "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
        { "type": "separator" },
        { "id": "app.preferences", "command": "application:preferences" },
        { "id": "app.check-for-updates", "command": "application:check-for-updates" },
        { "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
        { "type": "separator" },
        { "id": "app.quit", "role": "quit" }
      ]
    },
    {
      "id": "menu.task",
      "submenu": [
        { "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
        { "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
        { "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
        { "type": "separator" },
        { "id": "app.task-list", "command": "application:task-list" },
        { "id": "task.pause-task", "command": "application:pause-task" },
        { "id": "task.resume-task", "command": "application:resume-task" },
        { "id": "task.delete-task", "command": "application:delete-task" },
        { "id": "task.move-task-up", "command": "application:move-task-up" },
        { "id": "task.move-task-down", "command": "application:move-task-down" },
        { "type": "separator" },
        { "id": "task.pause-all-task", "command": "application:pause-all-task" },
        { "id": "task.resume-all-task", "command": "application:resume-all-task" },
        { "id": "task.select-all-task", "command": "application:select-all-task" },
        { "type": "separator" },
        { "id": "task.clear-recent-tasks", "command": "application:clear-recent-tasks" }
      ]
    },
    {
      "id": "menu.edit",
      "submenu": [
        { "id": "edit.undo", "role": "undo" },
        { "id": "edit.redo", "role": "redo" },
        { "type": "separator" },
        { "id": "edit.cut", "role": "cut" },
        { "id": "edit.copy", "role": "copy" },
        { "id": "edit.paste", "role": "paste" },
        { "id": "edit.delete", "role": "delete" },
        { "id": "edit.select-all", "role": "selectall" }
      ]
    },
    {
      "role": "window",
      "id": "menu.window",
      "submenu": [
        { "id": "window.reload", "role": "reload" },
        { "id": "window.close", "role": "close" },
        { "id": "window.minimize", "role": "minimize" },
        { "id": "window.zoom", "role": "zoom" },
        { "id": "window.toggle-fullscreen", "role": "togglefullscreen" },
        { "type": "separator" },
        { "id": "window.front", "role": "front" }
      ]
    },
    {
      "role": "help",
      "id": "menu.help",
      "submenu": [
        { "id": "help.official-website", "command": "help:official-website" },
        { "id": "help.manual", "command": "help:manual" },
        { "id": "help.release-notes", "command": "help:release-notes" },
        { "type": "separator" },
        { "id": "help.report-problem", "command": "help:report-problem" },
        { "type": "separator" },
        { "id": "help.toggle-dev-tools", "role": "toggledevtools" }
      ]
    }
  ]
}


================================================
FILE: src/main/pages/about.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>

</body>
</html>


================================================
FILE: src/main/pages/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>

</body>
</html>


================================================
FILE: src/main/ui/DockManager.js
================================================
import is from 'electron-is'
import { EventEmitter } from 'node:events'
import { app } from 'electron'

import { bytesToSize } from '@shared/utils'

import {
  APP_RUN_MODE
} from '@shared/constants'

const enabled = is.macOS()

export default class DockManager extends EventEmitter {
  constructor (options) {
    super()
    this.options = options
    const { runMode } = this.options
    if (runMode === APP_RUN_MODE.TRAY) {
      this.hide()
    }
  }

  show = enabled
    ? () => {
      if (app.dock.isVisible()) {
        return
      }

      return app.dock.show()
    }
    : () => {}

  hide = enabled
    ? () => {
      if (!app.dock.isVisible()) {
        return
      }

      app.dock.hide()
    }
    : () => {}

  // macOS setBadge not working
  // @see https://github.com/electron/electron/issues/25745#issuecomment-702826143
  setBadge = enabled
    ? (text) => {
      app.dock.setBadge(text)
    }
    : (text) => {}

  handleSpeedChange = enabled
    ? (speed) => {
      const { downloadSpeed } = speed
      const text = downloadSpeed > 0 ? `${bytesToSize(downloadSpeed)}/s` : ''
      this.setBadge(text)
    }
    : (text) => {}

  openDock = enabled
    ? (path) => {
      app.dock.downloadFinished(path)
    }
    : (path) => {}
}


================================================
FILE: src/main/ui/Locale.js
================================================
import resources from '@shared/locales/app'
import LocaleManager from '@shared/locales/LocaleManager'

const localeManager = new LocaleManager({
  resources
})

export const getLocaleManager = () => {
  return localeManager
}

export const setupLocaleManager = (locale) => {
  localeManager.changeLanguageByLocale(locale)

  return localeManager
}

export const getI18n = () => {
  return localeManager.getI18n()
}

export const getI18nTranslator = () => {
  return localeManager.getI18n().t
}


================================================
FILE: src/main/ui/MenuManager.js
================================================
import { EventEmitter } from 'node:events'
import { Menu } from 'electron'

import keymap from '@shared/keymap'
import {
  translateTemplate,
  flattenMenuItems,
  updateStates
} from '../utils/menu'
import { getI18n } from '../ui/Locale'

export default class MenuManager extends EventEmitter {
  constructor (options) {
    super()
    this.options = options
    this.i18n = getI18n()

    this.keymap = keymap
    this.items = {}

    this.load()

    this.setup()
  }

  load () {
    const template = require(`../menus/${process.platform}.json`)
    this.template = template.menu
  }

  build () {
    const keystrokesByCommand = {}
    for (const item in this.keymap) {
      keystrokesByCommand[this.keymap[item]] = item
    }

    // Deepclone the menu template to refresh menu
    const template = JSON.parse(JSON.stringify(this.template))
    const tpl = translateTemplate(template, keystrokesByCommand, this.i18n)
    const menu = Menu.buildFromTemplate(tpl)
    return menu
  }

  setup () {
    const menu = this.build()
    Menu.setApplicationMenu(menu)
    this.items = flattenMenuItems(menu)
  }

  handleLocaleChange (locale) {
    this.setup()
  }

  updateMenuStates (visibleStates, enabledStates, checkedStates) {
    updateStates(this.items, visibleStates, enabledStates, checkedStates)
  }

  updateMenuItemVisibleState (id, flag) {
    const visibleStates = {
      [id]: flag
    }
    this.updateMenuStates(visibleStates, null, null)
  }

  updateMenuItemEnabledState (id, flag) {
    const enabledStates = {
      [id]: flag
    }
    this.updateMenuStates(null, enabledStates, null)
  }
}


================================================
FILE: src/main/ui/ThemeManager.js
================================================
import { EventEmitter } from 'node:events'
import { nativeTheme } from 'electron'

import { APP_THEME } from '@shared/constants'
import logger from '../core/Logger'
import { getSystemTheme } from '../utils'

export default class ThemeManager extends EventEmitter {
  constructor (options = {}) {
    super()

    this.options = options
    this.init()
  }

  init () {
    this.systemTheme = getSystemTheme()

    this.handleEvents()
  }

  getSystemTheme () {
    return this.systemTheme
  }

  handleEvents () {
    nativeTheme.on('updated', () => {
      const theme = getSystemTheme()
      this.systemTheme = theme
      logger.info('[Motrix] nativeTheme updated===>', theme)
      this.emit('system-theme-change', theme)
    })
  }

  updateSystemTheme (theme) {
    theme = theme === APP_THEME.AUTO ? 'system' : theme
    nativeTheme.themeSource = theme
  }
}


================================================
FILE: src/main/ui/TouchBarManager.js
================================================
import { EventEmitter } from 'node:events'
import { join } from 'node:path'
import { TouchBar, nativeImage } from 'electron'

import { handleCommand } from '../utils/menu'
import logger from '../core/Logger'

const { TouchBarButton, TouchBarLabel, TouchBarSpacer, TouchBarGroup } = TouchBar

export default class TouchBarManager extends EventEmitter {
  constructor (options) {
    super()
    this.options = options
    this.bars = {}
    this.load()
  }

  load () {
    this.template = require('../menus/touchBar.json')
  }

  getClickFn (item) {
    let fn = () => {}
    if (item.command) {
      fn = () => {
        handleCommand(item)
      }
    }
    return fn
  }

  getIconImage (icon) {
    if (!icon) {
      return
    }
    const img = join(__static, `./icons/${icon}.png`)
    return nativeImage.createFromPath(img)
  }

  buildItem (type, options) {
    let result = null
    const { label, backgroundColor, textColor, size } = options

    switch (type) {
    case 'button':
      result = new TouchBarButton({
        label,
        backgroundColor,
        icon: this.getIconImage(options.icon),
        click: this.getClickFn(options)
      })
      break
    case 'label':
      result = new TouchBarLabel({
        label,
        textColor
      })
      break
    case 'spacer':
      result = new TouchBarSpacer({ size })
      break
    case 'group':
      result = new TouchBarGroup({
        items: new TouchBar({
          items: options.items
        })
      })
      break
    default:
      result = null
    }

    return result
  }

  build (template) {
    const result = []

    template.forEach(tpl => {
      const { id, type, ...rest } = tpl
      let options = { ...rest }
      if (type === 'group') {
        options = { type, items: this.build(options.items) }
      }
      const item = this.buildItem(type, options)
      result.push(item)
    })
    return result
  }

  getTouchBarByPage (page) {
    let bar = this.bars[page] || null
    if (!bar) {
      try {
        const items = this.build(this.template)
        bar = new TouchBar({ items })
        this.bars[page] = bar
      } catch (e) {
        logger.info('getTouchBarByPage fail', e)
      }
    }
    return bar
  }

  setup (page, window) {
    const bar = this.getTouchBarByPage(page)
    window.setTouchBar(bar)
  }
}


================================================
FILE: src/main/ui/TrayManager.js
================================================
import { EventEmitter } from 'node:events'
import { join } from 'node:path'
import { Tray, Menu, nativeImage } from 'electron'
import is from 'electron-is'

import { APP_RUN_MODE, APP_THEME } from '@shared/constants'
import { getInverseTheme } from '@shared/utils'
import logger from '../core/Logger'
import { getI18n } from './Locale'
import {
  translateTemplate,
  flattenMenuItems,
  updateStates
} from '../utils/menu'
import { convertArrayBufferToBuffer } from '../utils/index'

let tray = null
const { platform } = process

export default class TrayManager extends EventEmitter {
  constructor (options = {}) {
    super()

    this.options = options
    this.theme = options.theme || APP_THEME.AUTO

    this.systemTheme = options.systemTheme
    this.inverseSystemTheme = getInverseTheme(this.systemTheme)
    this.macOS = platform === 'darwin'

    this.speedometer = options.speedometer
    this.runMode = options.runMode

    this.i18n = getI18n()
    this.menu = null
    this.cache = {}

    this.uploadSpeed = 0
    this.downloadSpeed = 0
    this.status = false
    this.focused = false
    this.initialized = false

    this.init()
  }

  init () {
    if (tray || this.initialized || this.runMode === APP_RUN_MODE.HIDE_TRAY) {
      return
    }

    this.loadTemplate()
    this.loadImages()
    this.initTray()
    this.setupMenu()
    this.bindEvents()

    this.initialized = true
  }

  loadTemplate () {
    this.template = require('../menus/tray.json')
  }

  loadImages () {
    switch (platform) {
    case 'darwin':
      this.loadImagesForMacOS()
      break
    case 'win32':
      this.loadImagesForWindows()
      break
    case 'linux':
      this.loadImagesForLinux()
      break

    default:
      this.loadImagesForDefault()
      break
    }
  }

  loadImagesForMacOS () {
    this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-light-normal.png')
  }

  loadImagesForWindows () {
    this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-colorful-normal.png')
    this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-colorful-active.png')
  }

  loadImagesForLinux () {
    const { theme } = this
    if (theme === APP_THEME.AUTO) {
      this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-dark-normal.png')
      this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-dark-active.png')
    } else {
      this.normalIcon = this.getFromCacheOrCreateImage(`mo-tray-${theme}-normal.png`)
      this.activeIcon = this.getFromCacheOrCreateImage(`mo-tray-${theme}-active.png`)
    }
  }

  loadImagesForDefault () {
    this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-light-normal.png')
    this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-light-active.png')
  }

  getFromCacheOrCreateImage (key) {
    let file = this.getCache(key)
    if (file) {
      return file
    }

    file = nativeImage.createFromPath(join(__static, `./${key}`))
    file.setTemplateImage(this.macOS)
    this.setCache(key, file)
    return file
  }

  getCache (key) {
    return this.cache[key]
  }

  setCache (key, value) {
    this.cache[key] = value
  }

  buildMenu () {
    const keystrokesByCommand = {}
    for (const item in this.keymap) {
      keystrokesByCommand[this.keymap[item]] = item
    }

    // Deepclone the menu template to refresh menu
    const template = JSON.parse(JSON.stringify(this.template))
    const tpl = translateTemplate(template, keystrokesByCommand, this.i18n)
    this.menu = Menu.buildFromTemplate(tpl)
    this.items = flattenMenuItems(this.menu)
  }

  setupMenu () {
    this.buildMenu()

    this.updateContextMenu()
  }

  initTray () {
    const { icon } = this.getIcons()
    tray = new Tray(icon)
    // tray.setPressedImage(inverseIcon)

    if (!this.macOS) {
      tray.setToolTip('Motrix')
    }
  }

  bindEvents () {
    // All OS
    tray.on('click', this.handleTrayClick)

    // macOS, Windows
    // tray.on('double-click', this.handleTrayDbClick)
    tray.on('right-click', this.handleTrayRightClick)
    tray.on('mouse-down', this.handleTrayMouseDown)
    tray.on('mouse-up', this.handleTrayMouseUp)

    // macOS only
    tray.setIgnoreDoubleClickEvents(true)
    tray.on('drop-files', this.handleTrayDropFiles)
    tray.on('drop-text', this.handleTrayDropText)
  }

  unbindEvents () {
    // All OS
    tray.removeListener('click', this.handleTrayClick)

    // macOS, Windows
    tray.removeListener('right-click', this.handleTrayRightClick)
    tray.removeListener('mouse-down', this.handleTrayMouseDown)
    tray.removeListener('mouse-up', this.handleTrayMouseUp)

    // macOS only
    tray.removeListener('drop-files', this.handleTrayDropFiles)
    tray.removeListener('drop-text', this.handleTrayDropText)
  }

  handleTrayClick = (event) => {
    global.application.toggle()
  }

  handleTrayDbClick = (event) => {
    global.application.show()
  }

  handleTrayRightClick = (event) => {
    tray.popUpContextMenu(this.menu)
  }

  handleTrayMouseDown = (event) => {
    this.focused = true
    this.emit('mouse-down', {
      focused: true,
      theme: this.inverseSystemTheme
    })
    this.renderTray()
  }

  handleTrayMouseUp = (event) => {
    this.focused = false
    this.emit('mouse-up', {
      focused: false,
      theme: this.theme
    })
    this.renderTray()
  }

  handleTrayDropFiles = (event, files) => {
    this.emit('drop-files', files)
  }

  handleTrayDropText = (event, text) => {
    this.emit('drop-text', text)
  }

  toggleSpeedometer (enabled) {
    this.speedometer = enabled
  }

  async renderTray () {
    if (!tray || this.speedometer) {
      return
    }

    const { icon } = this.getIcons()

    tray.setImage(icon)
    // tray.setPressedImage(inverseIcon)

    this.updateContextMenu()
  }

  getIcons () {
    if (this.macOS) {
      return { icon: this.normalIcon }
    }

    const { focused, status, systemTheme } = this

    const icon = status ? this.activeIcon : this.normalIcon
    if (systemTheme === APP_THEME.DARK) {
      return {
        icon
      }
    }

    const inverseIcon = status ? this.inverseActiveIcon : this.inverseNormalIcon

    return {
      icon: focused ? inverseIcon : icon
      // inverseIcon: focused ? icon : inverseIcon
    }
  }

  updateContextMenu () {
    /**
     * Linux requires setContextMenu to be called
     * in order for the context menu to populate correctly
     */
    if (!tray || process.platform !== 'linux') {
      return
    }

    tray.setContextMenu(this.menu)
  }

  updateMenuStates (visibleStates, enabledStates, checkedStates) {
    updateStates(this.items, visibleStates, enabledStates, checkedStates)

    this.updateContextMenu()
  }

  updateMenuItemVisibleState (id, flag) {
    const visibleStates = {
      [id]: flag
    }
    this.updateMenuStates(visibleStates, null, null)
  }

  updateMenuItemEnabledState (id, flag) {
    const enabledStates = {
      [id]: flag
    }
    this.updateMenuStates(null, enabledStates, null)
  }

  handleLocaleChange (locale) {
    this.setupMenu()
  }

  handleRunModeChange (mode) {
    this.runMode = mode

    if (mode === APP_RUN_MODE.HIDE_TRAY) {
      this.destroy()
    } else {
      this.init()
    }
  }

  handleSpeedometerEnableChange (enabled) {
    this.toggleSpeedometer(enabled)

    this.renderTray()
  }

  handleSystemThemeChange (systemTheme = APP_THEME.LIGHT) {
    if (!is.macOS()) {
      return
    }

    this.systemTheme = systemTheme
    this.inverseSystemTheme = getInverseTheme(systemTheme)

    this.loadImages()

    this.renderTray()
  }

  handleDownloadStatusChange (status) {
    this.status = status

    this.renderTray()
  }

  async handleSpeedChange ({ uploadSpeed, downloadSpeed }) {
    if (!this.speedometer) {
      return
    }

    this.uploadSpeed = uploadSpeed
    this.downloadSpeed = downloadSpeed

    await this.renderTray()
  }

  async updateTrayByImage (ab) {
    if (!tray) {
      return
    }

    const buffer = convertArrayBufferToBuffer(ab)
    const image = nativeImage.createFromBuffer(buffer, {
      scaleFactor: 2
    })
    image.setTemplateImage(this.macOS)
    tray.setImage(image)
  }

  destroy () {
    logger.info('[Motrix] TrayManager.destroy')
    if (tray) {
      this.unbindEvents()
    }

    tray.destroy()
    tray = null
    this.initialized = false
  }
}


================================================
FILE: src/main/ui/WindowManager.js
================================================
import { join } from 'node:path'
import { EventEmitter } from 'node:events'
import { debounce } from 'lodash'
import { app, shell, screen, BrowserWindow } from 'electron'
import is from 'electron-is'

import pageConfig from '../configs/page'
import logger from '../core/Logger'

const baseBrowserOptions = {
  titleBarStyle: 'hiddenInset',
  show: false,
  width: 1024,
  height: 768,
  backgroundColor: '#fff',
  webPreferences: {
    nodeIntegration: true
  }
}

// fix: BrowserWindow rendering bug under linux
const defaultBrowserOptions = is.macOS()
  ? {
    ...baseBrowserOptions,
    vibrancy: 'ultra-dark',
    visualEffectState: 'active',
    backgroundColor: '#00000000'
  }
  : {
    ...baseBrowserOptions
  }

export default class WindowManager extends EventEmitter {
  constructor (options = {}) {
    super()
    this.userConfig = options.userConfig || {}

    this.windows = {}

    this.willQuit = false

    this.handleBeforeQuit()

    this.handleAllWindowClosed()
  }

  setWillQuit (flag) {
    this.willQuit = flag
  }

  getPageOptions (page) {
    const result = pageConfig[page] || {}
    const hideAppMenu = this.userConfig['hide-app-menu']
    if (hideAppMenu) {
      result.attrs.frame = false
    }

    // Optimized for small screen users
    const { width, height } = screen.getPrimaryDisplay().workAreaSize
    const widthScale = width >= 1280 ? 1 : 0.875
    const heightScale = height >= 800 ? 1 : 0.875
    result.attrs.width *= widthScale
    result.attrs.height *= heightScale

    // fix AppImage Dock Icon Missing
    // https://github.com/AppImage/AppImageKit/wiki/Bundling-Electron-apps
    if (is.linux()) {
      result.attrs.icon = join(__static, './512x512.png')
    }

    return result
  }

  getPageBounds (page) {
    const enabled = this.userConfig['keep-window-state']
    const windowStateMap = this.userConfig['window-state'] || {}
    let result = null
    if (enabled) {
      result = windowStateMap[page]
    }

    return result
  }

  openWindow (page, options = {}) {
    const pageOptions = this.getPageOptions(page)
    const { hidden } = options
    const autoHideWindow = this.userConfig['auto-hide-window']
    let window = this.windows[page] || null
    if (window) {
      window.show()
      window.focus()
      return window
    }

    window = new BrowserWindow({
      ...defaultBrowserOptions,
      ...pageOptions.attrs,
      webPreferences: {
        enableRemoteModule: true,
        contextIsolation: false,
        nodeIntegration: true,
        nodeIntegrationInWorker: true
      }
    })

    const bounds = this.getPageBounds(page)
    if (bounds) {
      window.setBounds(bounds)
    }

    if (is.dev() && pageOptions.openDevTools) {
      window.webContents.openDevTools()
    }

    window.webContents.setWindowOpenHandler(({ url }) => {
      shell.openExternal(url)
      return { action: 'deny' }
    })

    if (pageOptions.url) {
      window.loadURL(pageOptions.url)
    }

    window.once('ready-to-show', () => {
      if (!hidden) {
        window.show()
      }
    })

    window.on('enter-full-screen', () => {
      this.emit('enter-full-screen', window)
    })

    window.on('leave-full-screen', () => {
      this.emit('leave-full-screen', window)
    })

    this.handleWindowState(page, window)

    this.handleWindowClose(pageOptions, page, window)

    this.bindAfterClosed(page, window)

    this.addWindow(page, window)
    if (autoHideWindow) {
      this.handleWindowBlur()
    }

    return window
 
Download .txt
gitextract_m9s_ek03/

├── .babelrc
├── .editorconfig
├── .electron-vue/
│   ├── build.js
│   ├── dev-client.js
│   ├── dev-runner.js
│   ├── webpack.main.config.js
│   ├── webpack.renderer.config.js
│   └── webpack.web.config.js
├── .eslintignore
├── .eslintrc.js
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── 1_bug_report.yml
│   │   ├── 2_bug_report_cn.yml
│   │   ├── feature_request.md
│   │   └── feature_request_cn.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── lock.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       └── release.yml
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING-CN.md
├── CONTRIBUTING.md
├── LICENSE
├── README-CN.md
├── README.md
├── app-update.yml
├── appveyor.yml
├── build/
│   ├── afterPackHook.js
│   ├── afterSignHook.js
│   ├── background.tiff
│   ├── icon.icns
│   └── torrent.icns
├── electron-builder.json
├── extra/
│   ├── README.md
│   ├── darwin/
│   │   ├── arm64/
│   │   │   └── engine/
│   │   │       ├── aria2.conf
│   │   │       └── aria2c
│   │   └── x64/
│   │       └── engine/
│   │           ├── aria2.conf
│   │           └── aria2c
│   ├── linux/
│   │   ├── arm64/
│   │   │   └── engine/
│   │   │       ├── aria2.conf
│   │   │       └── aria2c
│   │   ├── armv7l/
│   │   │   └── engine/
│   │   │       ├── aria2.conf
│   │   │       └── aria2c
│   │   └── x64/
│   │       └── engine/
│   │           ├── aria2.conf
│   │           └── aria2c
│   └── win32/
│       ├── ia32/
│       │   └── engine/
│       │       └── aria2.conf
│       └── x64/
│           └── engine/
│               └── aria2.conf
├── jsconfig.json
├── package.json
├── src/
│   ├── index.ejs
│   ├── main/
│   │   ├── Application.js
│   │   ├── Launcher.js
│   │   ├── configs/
│   │   │   ├── engine.js
│   │   │   ├── page.js
│   │   │   └── protocol.js
│   │   ├── core/
│   │   │   ├── AutoLaunchManager.js
│   │   │   ├── ConfigManager.js
│   │   │   ├── Context.js
│   │   │   ├── EnergyManager.js
│   │   │   ├── Engine.js
│   │   │   ├── EngineClient.js
│   │   │   ├── ExceptionHandler.js
│   │   │   ├── Logger.js
│   │   │   ├── ProtocolManager.js
│   │   │   ├── UPnPManager.js
│   │   │   └── UpdateManager.js
│   │   ├── index.dev.js
│   │   ├── index.js
│   │   ├── menus/
│   │   │   ├── darwin.json
│   │   │   ├── linux.json
│   │   │   ├── touchBar.json
│   │   │   ├── tray.json
│   │   │   └── win32.json
│   │   ├── pages/
│   │   │   ├── about.html
│   │   │   └── index.html
│   │   ├── ui/
│   │   │   ├── DockManager.js
│   │   │   ├── Locale.js
│   │   │   ├── MenuManager.js
│   │   │   ├── ThemeManager.js
│   │   │   ├── TouchBarManager.js
│   │   │   ├── TrayManager.js
│   │   │   └── WindowManager.js
│   │   └── utils/
│   │       ├── index.js
│   │       └── menu.js
│   ├── renderer/
│   │   ├── api/
│   │   │   ├── Api.js
│   │   │   └── index.js
│   │   ├── assets/
│   │   │   └── .gitkeep
│   │   ├── components/
│   │   │   ├── About/
│   │   │   │   ├── AboutPanel.vue
│   │   │   │   ├── AppInfo.vue
│   │   │   │   └── Copyright.vue
│   │   │   ├── Aside/
│   │   │   │   └── Index.vue
│   │   │   ├── Browser/
│   │   │   │   └── index.vue
│   │   │   ├── CommandManager/
│   │   │   │   ├── index.js
│   │   │   │   └── instance.js
│   │   │   ├── DragSelect/
│   │   │   │   └── Index.vue
│   │   │   ├── Dragger/
│   │   │   │   └── Index.vue
│   │   │   ├── Icons/
│   │   │   │   ├── Icon.vue
│   │   │   │   ├── arrow-down.js
│   │   │   │   ├── arrow-up.js
│   │   │   │   ├── audio.js
│   │   │   │   ├── delete.js
│   │   │   │   ├── dice.js
│   │   │   │   ├── document.js
│   │   │   │   ├── folder.js
│   │   │   │   ├── image.js
│   │   │   │   ├── inbox.js
│   │   │   │   ├── info-circle.js
│   │   │   │   ├── info-square.js
│   │   │   │   ├── link.js
│   │   │   │   ├── magnet.js
│   │   │   │   ├── menu-about.js
│   │   │   │   ├── menu-add.js
│   │   │   │   ├── menu-preference.js
│   │   │   │   ├── menu-task.js
│   │   │   │   ├── more.js
│   │   │   │   ├── node.js
│   │   │   │   ├── preference-advanced.js
│   │   │   │   ├── preference-basic.js
│   │   │   │   ├── preference-lab.js
│   │   │   │   ├── purge.js
│   │   │   │   ├── refresh.js
│   │   │   │   ├── speedometer.js
│   │   │   │   ├── sync.js
│   │   │   │   ├── task-history.js
│   │   │   │   ├── task-pause-line.js
│   │   │   │   ├── task-pause.js
│   │   │   │   ├── task-restart.js
│   │   │   │   ├── task-start-line.js
│   │   │   │   ├── task-start.js
│   │   │   │   ├── task-stop-line.js
│   │   │   │   ├── task-stop.js
│   │   │   │   ├── trash.js
│   │   │   │   ├── video.js
│   │   │   │   ├── win-close.js
│   │   │   │   ├── win-maximize.js
│   │   │   │   └── win-minimize.js
│   │   │   ├── Locale/
│   │   │   │   └── index.js
│   │   │   ├── Logo/
│   │   │   │   ├── Logo.vue
│   │   │   │   └── LogoMini.vue
│   │   │   ├── Main.vue
│   │   │   ├── Msg/
│   │   │   │   └── index.js
│   │   │   ├── Native/
│   │   │   │   ├── DynamicTray.vue
│   │   │   │   ├── EngineClient.vue
│   │   │   │   ├── Ipc.vue
│   │   │   │   ├── SelectDirectory.vue
│   │   │   │   ├── ShowInFolder.vue
│   │   │   │   └── TitleBar.vue
│   │   │   ├── Preference/
│   │   │   │   ├── Advanced.vue
│   │   │   │   ├── Basic.vue
│   │   │   │   ├── HistoryDirectory.vue
│   │   │   │   ├── Index.vue
│   │   │   │   ├── Lab.vue
│   │   │   │   └── ThemeSwitcher.vue
│   │   │   ├── Speedometer/
│   │   │   │   └── Speedometer.vue
│   │   │   ├── Subnav/
│   │   │   │   ├── PreferenceSubnav.vue
│   │   │   │   ├── SubnavSwitcher.vue
│   │   │   │   └── TaskSubnav.vue
│   │   │   ├── Task/
│   │   │   │   ├── AddTask.vue
│   │   │   │   ├── Index.vue
│   │   │   │   ├── SelectTorrent.vue
│   │   │   │   ├── TaskActions.vue
│   │   │   │   ├── TaskItem.vue
│   │   │   │   ├── TaskItemActions.vue
│   │   │   │   ├── TaskList.vue
│   │   │   │   ├── TaskProgress.vue
│   │   │   │   ├── TaskProgressInfo.vue
│   │   │   │   └── TaskStatus.vue
│   │   │   ├── TaskDetail/
│   │   │   │   ├── Index.vue
│   │   │   │   ├── TaskActivity.vue
│   │   │   │   ├── TaskFiles.vue
│   │   │   │   ├── TaskGeneral.vue
│   │   │   │   ├── TaskPeers.vue
│   │   │   │   └── TaskTrackers.vue
│   │   │   ├── TaskGraphic/
│   │   │   │   ├── Atom.vue
│   │   │   │   └── Index.vue
│   │   │   └── Theme/
│   │   │       ├── Dark/
│   │   │       │   └── Variables.scss
│   │   │       ├── Dark.scss
│   │   │       ├── Default.scss
│   │   │       ├── Index.scss
│   │   │       ├── Light/
│   │   │       │   └── Variables.scss
│   │   │       └── Variables.scss
│   │   ├── pages/
│   │   │   └── index/
│   │   │       ├── App.vue
│   │   │       ├── commands.js
│   │   │       └── main.js
│   │   ├── router/
│   │   │   └── index.js
│   │   ├── store/
│   │   │   ├── index.js
│   │   │   └── modules/
│   │   │       ├── app.js
│   │   │       ├── index.js
│   │   │       ├── preference.js
│   │   │       └── task.js
│   │   ├── utils/
│   │   │   ├── native.js
│   │   │   └── task.js
│   │   └── workers/
│   │       └── tray.worker.js
│   └── shared/
│       ├── aria2/
│       │   ├── index.js
│       │   └── lib/
│       │       ├── Aria2.js
│       │       ├── Deferred.js
│       │       ├── JSONRPCClient.js
│       │       ├── JSONRPCError.js
│       │       ├── debug.js
│       │       └── promiseEvent.js
│       ├── colors.json
│       ├── configKeys.js
│       ├── constants.js
│       ├── keymap.json
│       ├── locales/
│       │   ├── LocaleManager.js
│       │   ├── all.js
│       │   ├── app.js
│       │   ├── ar/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── bg/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ca/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── de/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── el/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── en-US/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── es/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── fa/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── fr/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── hu/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── id/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── index.js
│       │   ├── it/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ja/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ko/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── nb/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── nl/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── pl/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── pt-BR/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ro/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── ru/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── th/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── tr/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── uk/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── vi/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   ├── zh-CN/
│       │   │   ├── about.js
│       │   │   ├── app.js
│       │   │   ├── edit.js
│       │   │   ├── help.js
│       │   │   ├── index.js
│       │   │   ├── menu.js
│       │   │   ├── preferences.js
│       │   │   ├── subnav.js
│       │   │   ├── task.js
│       │   │   └── window.js
│       │   └── zh-TW/
│       │       ├── about.js
│       │       ├── app.js
│       │       ├── edit.js
│       │       ├── help.js
│       │       ├── index.js
│       │       ├── menu.js
│       │       ├── preferences.js
│       │       ├── subnav.js
│       │       ├── task.js
│       │       └── window.js
│       ├── ua.js
│       └── utils/
│           ├── curl.js
│           ├── index.js
│           ├── rename.js
│           ├── tracker.js
│           └── tray.js
└── static/
    └── .gitkeep
Download .txt
SYMBOL INDEX (499 symbols across 37 files)

FILE: .electron-vue/build.js
  function clean (line 28) | function clean () {
  function build (line 34) | function build () {
  function pack (line 75) | function pack (config) {
  function web (line 104) | function web () {
  function greeting (line 119) | function greeting () {

FILE: .electron-vue/dev-runner.js
  function logStats (line 17) | function logStats (proc, data) {
  function startRenderer (line 39) | function startRenderer () {
  function startMain (line 59) | function startMain () {
  function startElectron (line 95) | function startElectron () {
  function electronLog (line 110) | function electronLog (data, color) {
  function greeting (line 127) | function greeting () {
  function init (line 149) | function init () {

FILE: src/main/Application.js
  class Application (line 39) | class Application extends EventEmitter {
    method constructor (line 40) | constructor () {
    method init (line 46) | init () {
    method initContext (line 92) | initContext () {
    method initConfigManager (line 96) | initConfigManager () {
    method offConfigListeners (line 101) | offConfigListeners () {
    method setupLogger (line 112) | setupLogger () {
    method initLocaleManager (line 124) | initLocaleManager () {
    method setupApplicationMenu (line 130) | setupApplicationMenu () {
    method adjustMenu (line 135) | adjustMenu () {
    method startEngine (line 146) | startEngine () {
    method stopEngine (line 169) | async stopEngine () {
    method initEngineClient (line 184) | initEngineClient () {
    method initAutoLaunchManager (line 193) | initAutoLaunchManager () {
    method initEnergyManager (line 197) | initEnergyManager () {
    method initTrayManager (line 201) | initTrayManager () {
    method watchTraySpeedometerEnabledChange (line 228) | watchTraySpeedometerEnabledChange () {
    method initDockManager (line 237) | initDockManager () {
    method watchOpenAtLoginChange (line 243) | watchOpenAtLoginChange () {
    method watchProtocolsChange (line 260) | watchProtocolsChange () {
    method watchRunModeChange (line 275) | watchRunModeChange () {
    method watchProxyChange (line 292) | watchProxyChange () {
    method watchLocaleChange (line 311) | watchLocaleChange () {
    method watchThemeChange (line 325) | watchThemeChange () {
    method watchShowProgressBarChange (line 335) | watchShowProgressBarChange () {
    method initUPnPManager (line 349) | initUPnPManager () {
    method startUPnPMapping (line 364) | async startUPnPMapping () {
    method stopUPnPMapping (line 379) | async stopUPnPMapping () {
    method watchUPnPPortsChange (line 394) | watchUPnPPortsChange () {
    method watchUPnPEnabledChange (line 419) | watchUPnPEnabledChange () {
    method shutdownUPnPManager (line 433) | async shutdownUPnPManager () {
    method syncTrackers (line 442) | syncTrackers (source, proxy) {
    method autoSyncTrackers (line 470) | autoSyncTrackers () {
    method autoResumeTask (line 485) | autoResumeTask () {
    method initWindowManager (line 494) | initWindowManager () {
    method storeWindowState (line 523) | storeWindowState (data = {}) {
    method start (line 538) | start (page, options = {}) {
    method showPage (line 551) | showPage (page, options = {}) {
    method show (line 559) | show (page = 'index') {
    method hide (line 563) | hide (page) {
    method toggle (line 571) | toggle (page = 'index') {
    method closePage (line 575) | closePage (page) {
    method stop (line 579) | stop () {
    method stopAllSettled (line 594) | async stopAllSettled () {
    method quit (line 598) | async quit () {
    method sendCommand (line 603) | sendCommand (command, ...args) {
    method sendCommandToAll (line 612) | sendCommandToAll (command, ...args) {
    method sendMessageToAll (line 620) | sendMessageToAll (channel, ...args) {
    method initThemeManager (line 626) | initThemeManager () {
    method initTouchBarManager (line 634) | initTouchBarManager () {
    method initProtocolManager (line 642) | initProtocolManager () {
    method handleProtocol (line 649) | handleProtocol (url) {
    method handleFile (line 655) | handleFile (filePath) {
    method initUpdaterManager (line 680) | initUpdaterManager () {
    method handleUpdaterEvents (line 696) | handleUpdaterEvents () {
    method relaunch (line 738) | async relaunch () {
    method resetSession (line 744) | async resetSession () {
    method savePreference (line 759) | savePreference (config = {}) {
    method handleCommands (line 774) | handleCommands () {
    method openExternal (line 912) | openExternal (url) {
    method handleConfigChange (line 920) | handleConfigChange (configName) {
    method handleEvents (line 924) | handleEvents () {
    method handleProgressChange (line 972) | handleProgressChange (progress) {
    method bindProgressChange (line 982) | bindProgressChange () {
    method unbindProgressChange (line 990) | unbindProgressChange () {
    method handleIpcMessages (line 999) | handleIpcMessages () {
    method handleIpcInvokes (line 1011) | handleIpcInvokes () {

FILE: src/main/Launcher.js
  class Launcher (line 15) | class Launcher extends EventEmitter {
    method constructor (line 16) | constructor () {
    method makeSingleInstance (line 26) | makeSingleInstance (callback) {
    method init (line 49) | init () {
    method handleAppEvents (line 65) | handleAppEvents () {
    method handleRendererRemote (line 74) | handleRendererRemote () {
    method handleOpenUrl (line 86) | handleOpenUrl () {
    method handleOpenFile (line 103) | handleOpenFile () {
    method handleAppLaunchArgv (line 120) | handleAppLaunchArgv (argv) {
    method sendUrlToApplication (line 144) | sendUrlToApplication () {
    method sendFileToApplication (line 151) | sendFileToApplication () {
    method handelAppReady (line 158) | handelAppReady () {
    method handleAppWillQuit (line 182) | handleAppWillQuit () {

FILE: src/main/core/AutoLaunchManager.js
  class AutoLaunchManager (line 5) | class AutoLaunchManager {
    method enable (line 6) | enable () {
    method disable (line 21) | disable () {
    method isEnabled (line 28) | isEnabled () {

FILE: src/main/core/ConfigManager.js
  class ConfigManager (line 27) | class ConfigManager {
    method constructor (line 28) | constructor () {
    method init (line 35) | init () {
    method initSystemConfig (line 46) | initSystemConfig () {
    method initUserConfig (line 89) | initUserConfig () {
    method fixSystemConfig (line 146) | fixSystemConfig () {
    method fixUserConfig (line 167) | fixUserConfig () {
    method getSystemConfig (line 183) | getSystemConfig (key, defaultValue) {
    method getUserConfig (line 192) | getUserConfig (key, defaultValue) {
    method getLocale (line 201) | getLocale () {
    method setSystemConfig (line 205) | setSystemConfig (...args) {
    method setUserConfig (line 209) | setUserConfig (...args) {
    method reset (line 213) | reset () {

FILE: src/main/core/Context.js
  class Context (line 11) | class Context {
    method constructor (line 12) | constructor () {
    method getLogPath (line 16) | getLogPath () {
    method init (line 21) | init () {
    method get (line 36) | get (key) {

FILE: src/main/core/EnergyManager.js
  class EnergyManager (line 6) | class EnergyManager {
    method startPowerSaveBlocker (line 7) | startPowerSaveBlocker () {
    method stopPowerSaveBlocker (line 17) | stopPowerSaveBlocker () {

FILE: src/main/core/Engine.js
  class Engine (line 17) | class Engine {
    method constructor (line 21) | constructor (options = {}) {
    method start (line 29) | start () {
    method stop (line 69) | stop () {
    method writePidFile (line 77) | writePidFile (pidPath, pid) {
    method getEngineBinPath (line 85) | getEngineBinPath () {
    method getStartArgs (line 96) | getStartArgs () {
    method isRunning (line 124) | isRunning (pid) {
    method restart (line 132) | restart () {

FILE: src/main/core/EngineClient.js
  class EngineClient (line 22) | class EngineClient {
    method constructor (line 26) | constructor (options = {}) {
    method init (line 35) | init () {
    method connect (line 39) | connect () {
    method call (line 49) | async call (method, ...args) {
    method changeGlobalOption (line 55) | async changeGlobalOption (options) {
    method shutdown (line 62) | async shutdown (options = {}) {

FILE: src/main/core/ExceptionHandler.js
  class ExceptionHandler (line 9) | class ExceptionHandler {
    method constructor (line 10) | constructor (options) {
    method setup (line 19) | setup () {

FILE: src/main/core/ProtocolManager.js
  class ProtocolManager (line 10) | class ProtocolManager extends EventEmitter {
    method constructor (line 11) | constructor (options = {}) {
    method init (line 26) | init () {
    method setup (line 31) | setup (protocols = {}) {
    method handle (line 48) | handle (url) {
    method handleResourceProtocol (line 69) | handleResourceProtocol (url) {
    method handleMoProtocol (line 80) | handleMoProtocol (url) {

FILE: src/main/core/UPnPManager.js
  class UPnPManager (line 8) | class UPnPManager {
    method constructor (line 9) | constructor (options = {}) {
    method init (line 15) | init () {
    method map (line 25) | map (port) {
    method unmap (line 53) | unmap (port) {
    method closeClient (line 86) | closeClient () {

FILE: src/main/core/UpdateManager.js
  class UpdateManager (line 15) | class UpdateManager extends EventEmitter {
    method constructor (line 16) | constructor (options = {}) {
    method setupProxy (line 36) | setupProxy (proxy) {
    method init (line 61) | init () {
    method check (line 83) | check () {
    method checkingForUpdate (line 88) | checkingForUpdate () {
    method updateAvailable (line 93) | updateAvailable (event, info) {
    method updateNotAvailable (line 110) | updateNotAvailable (event, info) {
    method updateDownloadProgress (line 130) | updateDownloadProgress (event) {
    method updateDownloaded (line 134) | updateDownloaded (event, info) {
    method updateCancelled (line 149) | updateCancelled () {
    method updateError (line 153) | updateError (event, error) {

FILE: src/main/ui/DockManager.js
  class DockManager (line 13) | class DockManager extends EventEmitter {
    method constructor (line 14) | constructor (options) {

FILE: src/main/ui/MenuManager.js
  class MenuManager (line 12) | class MenuManager extends EventEmitter {
    method constructor (line 13) | constructor (options) {
    method load (line 26) | load () {
    method build (line 31) | build () {
    method setup (line 44) | setup () {
    method handleLocaleChange (line 50) | handleLocaleChange (locale) {
    method updateMenuStates (line 54) | updateMenuStates (visibleStates, enabledStates, checkedStates) {
    method updateMenuItemVisibleState (line 58) | updateMenuItemVisibleState (id, flag) {
    method updateMenuItemEnabledState (line 65) | updateMenuItemEnabledState (id, flag) {

FILE: src/main/ui/ThemeManager.js
  class ThemeManager (line 8) | class ThemeManager extends EventEmitter {
    method constructor (line 9) | constructor (options = {}) {
    method init (line 16) | init () {
    method getSystemTheme (line 22) | getSystemTheme () {
    method handleEvents (line 26) | handleEvents () {
    method updateSystemTheme (line 35) | updateSystemTheme (theme) {

FILE: src/main/ui/TouchBarManager.js
  class TouchBarManager (line 10) | class TouchBarManager extends EventEmitter {
    method constructor (line 11) | constructor (options) {
    method load (line 18) | load () {
    method getClickFn (line 22) | getClickFn (item) {
    method getIconImage (line 32) | getIconImage (icon) {
    method buildItem (line 40) | buildItem (type, options) {
    method build (line 76) | build (template) {
    method getTouchBarByPage (line 91) | getTouchBarByPage (page) {
    method setup (line 105) | setup (page, window) {

FILE: src/main/ui/TrayManager.js
  class TrayManager (line 20) | class TrayManager extends EventEmitter {
    method constructor (line 21) | constructor (options = {}) {
    method init (line 47) | init () {
    method loadTemplate (line 61) | loadTemplate () {
    method loadImages (line 65) | loadImages () {
    method loadImagesForMacOS (line 83) | loadImagesForMacOS () {
    method loadImagesForWindows (line 87) | loadImagesForWindows () {
    method loadImagesForLinux (line 92) | loadImagesForLinux () {
    method loadImagesForDefault (line 103) | loadImagesForDefault () {
    method getFromCacheOrCreateImage (line 108) | getFromCacheOrCreateImage (key) {
    method getCache (line 120) | getCache (key) {
    method setCache (line 124) | setCache (key, value) {
    method buildMenu (line 128) | buildMenu () {
    method setupMenu (line 141) | setupMenu () {
    method initTray (line 147) | initTray () {
    method bindEvents (line 157) | bindEvents () {
    method unbindEvents (line 173) | unbindEvents () {
    method toggleSpeedometer (line 225) | toggleSpeedometer (enabled) {
    method renderTray (line 229) | async renderTray () {
    method getIcons (line 242) | getIcons () {
    method updateContextMenu (line 264) | updateContextMenu () {
    method updateMenuStates (line 276) | updateMenuStates (visibleStates, enabledStates, checkedStates) {
    method updateMenuItemVisibleState (line 282) | updateMenuItemVisibleState (id, flag) {
    method updateMenuItemEnabledState (line 289) | updateMenuItemEnabledState (id, flag) {
    method handleLocaleChange (line 296) | handleLocaleChange (locale) {
    method handleRunModeChange (line 300) | handleRunModeChange (mode) {
    method handleSpeedometerEnableChange (line 310) | handleSpeedometerEnableChange (enabled) {
    method handleSystemThemeChange (line 316) | handleSystemThemeChange (systemTheme = APP_THEME.LIGHT) {
    method handleDownloadStatusChange (line 329) | handleDownloadStatusChange (status) {
    method handleSpeedChange (line 335) | async handleSpeedChange ({ uploadSpeed, downloadSpeed }) {
    method updateTrayByImage (line 346) | async updateTrayByImage (ab) {
    method destroy (line 359) | destroy () {

FILE: src/main/ui/WindowManager.js
  class WindowManager (line 33) | class WindowManager extends EventEmitter {
    method constructor (line 34) | constructor (options = {}) {
    method setWillQuit (line 47) | setWillQuit (flag) {
    method getPageOptions (line 51) | getPageOptions (page) {
    method getPageBounds (line 74) | getPageBounds (page) {
    method openWindow (line 85) | openWindow (page, options = {}) {
    method getWindow (line 153) | getWindow (page) {
    method getWindows (line 157) | getWindows () {
    method getWindowList (line 161) | getWindowList () {
    method addWindow (line 165) | addWindow (page, window) {
    method destroyWindow (line 169) | destroyWindow (page) {
    method removeWindow (line 182) | removeWindow (page) {
    method bindAfterClosed (line 186) | bindAfterClosed (page, window) {
    method handleWindowState (line 192) | handleWindowState (page, window) {
    method handleWindowClose (line 204) | handleWindowClose (pageOptions, page, window) {
    method showWindow (line 223) | showWindow (page) {
    method hideWindow (line 232) | hideWindow (page) {
    method hideAllWindow (line 240) | hideAllWindow () {
    method toggleWindow (line 246) | toggleWindow (page) {
    method getFocusedWindow (line 259) | getFocusedWindow () {
    method handleBeforeQuit (line 263) | handleBeforeQuit () {
    method onWindowBlur (line 269) | onWindowBlur (event, window) {
    method handleWindowBlur (line 273) | handleWindowBlur () {
    method unbindWindowBlur (line 277) | unbindWindowBlur () {
    method handleAllWindowClosed (line 281) | handleAllWindowClosed () {
    method sendCommandTo (line 287) | sendCommandTo (window, command, ...args) {
    method sendMessageTo (line 295) | sendMessageTo (window, channel, ...args) {

FILE: src/main/utils/menu.js
  function findById (line 58) | function findById (template, id) {
  function handleCommandBefore (line 116) | function handleCommandBefore (item) {
  function handleCommandAfter (line 125) | function handleCommandAfter (item) {
  function acceleratorForCommand (line 134) | function acceleratorForCommand (command, keystrokesByCommand) {

FILE: src/renderer/api/Api.js
  class Api (line 15) | class Api {
    method constructor (line 16) | constructor (options = {}) {
    method init (line 22) | async init () {
    method loadConfigFromLocalStorage (line 29) | loadConfigFromLocalStorage () {
    method loadConfigFromNativeStore (line 35) | async loadConfigFromNativeStore () {
    method loadConfig (line 40) | async loadConfig () {
    method initClient (line 49) | initClient () {
    method closeClient (line 62) | closeClient () {
    method fetchPreference (line 72) | fetchPreference () {
    method savePreference (line 79) | savePreference (params = {}) {
    method savePreferenceToLocalStorage (line 88) | savePreferenceToLocalStorage () {
    method savePreferenceToNativeStore (line 92) | savePreferenceToNativeStore (params = {}) {
    method getVersion (line 114) | getVersion () {
    method changeGlobalOption (line 118) | changeGlobalOption (options) {
    method getGlobalOption (line 124) | getGlobalOption () {
    method getOption (line 133) | getOption (params = {}) {
    method updateActiveTaskOption (line 145) | updateActiveTaskOption (options) {
    method changeOption (line 157) | changeOption (params = {}) {
    method getGlobalStat (line 166) | getGlobalStat () {
    method addUri (line 170) | addUri (params) {
    method addTorrent (line 187) | addTorrent (params) {
    method addMetalink (line 197) | addMetalink (params) {
    method fetchDownloadingTaskList (line 207) | fetchDownloadingTaskList (params = {}) {
    method fetchWaitingTaskList (line 226) | fetchWaitingTaskList (params = {}) {
    method fetchStoppedTaskList (line 232) | fetchStoppedTaskList (params = {}) {
    method fetchActiveTaskList (line 238) | fetchActiveTaskList (params = {}) {
    method fetchTaskList (line 244) | fetchTaskList (params = {}) {
    method fetchTaskItem (line 258) | fetchTaskItem (params = {}) {
    method fetchTaskItemWithPeers (line 264) | fetchTaskItemWithPeers (params = {}) {
    method fetchTaskItemPeers (line 288) | fetchTaskItemPeers (params = {}) {
    method pauseTask (line 294) | pauseTask (params = {}) {
    method pauseAllTask (line 300) | pauseAllTask (params = {}) {
    method forcePauseTask (line 304) | forcePauseTask (params = {}) {
    method forcePauseAllTask (line 310) | forcePauseAllTask (params = {}) {
    method resumeTask (line 314) | resumeTask (params = {}) {
    method resumeAllTask (line 320) | resumeAllTask (params = {}) {
    method removeTask (line 324) | removeTask (params = {}) {
    method forceRemoveTask (line 330) | forceRemoveTask (params = {}) {
    method saveSession (line 336) | saveSession (params = {}) {
    method purgeTaskRecord (line 340) | purgeTaskRecord (params = {}) {
    method removeTaskRecord (line 344) | removeTaskRecord (params = {}) {
    method multicall (line 350) | multicall (method, params = {}) {
    method batchChangeOption (line 362) | batchChangeOption (params = {}) {
    method batchRemoveTask (line 366) | batchRemoveTask (params = {}) {
    method batchResumeTask (line 370) | batchResumeTask (params = {}) {
    method batchPauseTask (line 374) | batchPauseTask (params = {}) {
    method batchForcePauseTask (line 378) | batchForcePauseTask (params = {}) {

FILE: src/renderer/components/CommandManager/index.js
  class CommandManager (line 3) | class CommandManager extends EventEmitter {
    method constructor (line 4) | constructor () {
    method register (line 10) | register (id, fn) {
    method unregister (line 24) | unregister (id) {
    method execute (line 32) | execute (id, ...args) {

FILE: src/renderer/components/Locale/index.js
  function getLocaleManager (line 8) | function getLocaleManager () {

FILE: src/renderer/components/Msg/index.js
  method get (line 7) | get (obj, prop) {

FILE: src/renderer/pages/index/main.js
  function initTrayWorker (line 32) | function initTrayWorker () {
  function init (line 54) | function init (config) {

FILE: src/renderer/store/modules/app.js
  constant BASE_INTERVAL (line 5) | const BASE_INTERVAL = 1000
  constant PER_INTERVAL (line 6) | const PER_INTERVAL = 100
  constant MIN_INTERVAL (line 7) | const MIN_INTERVAL = 500
  constant MAX_INTERVAL (line 8) | const MAX_INTERVAL = 6000
  method UPDATE_SYSTEM_THEME (line 39) | UPDATE_SYSTEM_THEME (state, theme) {
  method UPDATE_TRAY_FOCUSED (line 42) | UPDATE_TRAY_FOCUSED (state, focused) {
  method UPDATE_ABOUT_PANEL_VISIBLE (line 45) | UPDATE_ABOUT_PANEL_VISIBLE (state, visible) {
  method UPDATE_ENGINE_INFO (line 48) | UPDATE_ENGINE_INFO (state, engineInfo) {
  method UPDATE_ENGINE_OPTIONS (line 51) | UPDATE_ENGINE_OPTIONS (state, engineOptions) {
  method UPDATE_GLOBAL_STAT (line 54) | UPDATE_GLOBAL_STAT (state, stat) {
  method UPDATE_ADD_TASK_VISIBLE (line 57) | UPDATE_ADD_TASK_VISIBLE (state, visible) {
  method UPDATE_ADD_TASK_TYPE (line 60) | UPDATE_ADD_TASK_TYPE (state, taskType) {
  method UPDATE_ADD_TASK_URL (line 63) | UPDATE_ADD_TASK_URL (state, text) {
  method UPDATE_ADD_TASK_TORRENTS (line 66) | UPDATE_ADD_TASK_TORRENTS (state, fileList) {
  method UPDATE_ADD_TASK_OPTIONS (line 69) | UPDATE_ADD_TASK_OPTIONS (state, options) {
  method UPDATE_INTERVAL (line 74) | UPDATE_INTERVAL (state, millisecond) {
  method INCREASE_INTERVAL (line 87) | INCREASE_INTERVAL (state, millisecond) {
  method DECREASE_INTERVAL (line 92) | DECREASE_INTERVAL (state, millisecond) {
  method UPDATE_PROGRESS (line 97) | UPDATE_PROGRESS (state, progress) {
  method updateSystemTheme (line 103) | updateSystemTheme ({ commit }, theme) {
  method updateTrayFocused (line 106) | updateTrayFocused ({ commit }, focused) {
  method showAboutPanel (line 109) | showAboutPanel ({ commit }) {
  method hideAboutPanel (line 112) | hideAboutPanel ({ commit }) {
  method fetchEngineInfo (line 115) | fetchEngineInfo ({ commit }) {
  method fetchEngineOptions (line 121) | fetchEngineOptions ({ commit }) {
  method fetchGlobalStat (line 130) | fetchGlobalStat ({ commit, dispatch }) {
  method increaseInterval (line 150) | increaseInterval ({ commit }, millisecond = 100) {
  method showAddTaskDialog (line 153) | showAddTaskDialog ({ commit }, taskType) {
  method hideAddTaskDialog (line 157) | hideAddTaskDialog ({ commit }) {
  method changeAddTaskType (line 162) | changeAddTaskType ({ commit }, taskType) {
  method updateAddTaskUrl (line 165) | updateAddTaskUrl ({ commit }, uri = '') {
  method addTaskAddTorrents (line 168) | addTaskAddTorrents ({ commit }, { fileList }) {
  method updateAddTaskOptions (line 171) | updateAddTaskOptions ({ commit }, options = {}) {
  method updateInterval (line 174) | updateInterval ({ commit }, millisecond) {
  method resetInterval (line 177) | resetInterval ({ commit }) {
  method fetchProgress (line 180) | fetchProgress ({ commit }) {

FILE: src/renderer/store/modules/preference.js
  method UPDATE_PREFERENCE_DATA (line 24) | UPDATE_PREFERENCE_DATA (state, config) {
  method fetchPreference (line 30) | fetchPreference ({ dispatch }) {
  method save (line 39) | save ({ dispatch }, config) {
  method recordHistoryDirectory (line 48) | recordHistoryDirectory ({ state, dispatch }, directory) {
  method addHistoryDirectory (line 57) | addHistoryDirectory ({ state, dispatch }, directory) {
  method favoriteDirectory (line 67) | favoriteDirectory ({ state, dispatch }, directory) {
  method cancelFavoriteDirectory (line 87) | cancelFavoriteDirectory ({ state, dispatch }, directory) {
  method removeDirectory (line 106) | removeDirectory ({ state, dispatch }, directory) {
  method updateAppTheme (line 117) | updateAppTheme ({ dispatch }, theme) {
  method updateAppLocale (line 120) | updateAppLocale ({ dispatch }, locale) {
  method updatePreference (line 123) | updatePreference  ({ commit }, config) {
  method fetchBtTracker (line 126) | fetchBtTracker (_, trackerSource = []) {
  method toggleEngineMode (line 131) | toggleEngineMode () {

FILE: src/renderer/store/modules/task.js
  method UPDATE_SEEDING_LIST (line 22) | UPDATE_SEEDING_LIST (state, seedingList) {
  method UPDATE_TASK_LIST (line 25) | UPDATE_TASK_LIST (state, taskList) {
  method UPDATE_SELECTED_GID_LIST (line 28) | UPDATE_SELECTED_GID_LIST (state, gidList) {
  method CHANGE_CURRENT_LIST (line 31) | CHANGE_CURRENT_LIST (state, currentList) {
  method CHANGE_TASK_DETAIL_VISIBLE (line 34) | CHANGE_TASK_DETAIL_VISIBLE (state, visible) {
  method UPDATE_CURRENT_TASK_GID (line 37) | UPDATE_CURRENT_TASK_GID (state, gid) {
  method UPDATE_ENABLED_FETCH_PEERS (line 40) | UPDATE_ENABLED_FETCH_PEERS (state, enabled) {
  method UPDATE_CURRENT_TASK_ITEM (line 43) | UPDATE_CURRENT_TASK_ITEM (state, task) {
  method UPDATE_CURRENT_TASK_FILES (line 46) | UPDATE_CURRENT_TASK_FILES (state, files) {
  method UPDATE_CURRENT_TASK_PEERS (line 49) | UPDATE_CURRENT_TASK_PEERS (state, peers) {
  method changeCurrentList (line 55) | changeCurrentList ({ commit, dispatch }, currentList) {
  method fetchList (line 60) | fetchList ({ commit, state }) {
  method selectTasks (line 71) | selectTasks ({ commit }, list) {
  method selectAllTask (line 74) | selectAllTask ({ commit, state }) {
  method fetchItem (line 78) | fetchItem ({ dispatch }, gid) {
  method fetchItemWithPeers (line 84) | fetchItemWithPeers ({ dispatch }, gid) {
  method showTaskDetailByGid (line 91) | showTaskDetailByGid ({ commit, dispatch }, gid) {
  method showTaskDetail (line 99) | showTaskDetail ({ commit, dispatch }, task) {
  method hideTaskDetail (line 104) | hideTaskDetail ({ commit }) {
  method toggleEnabledFetchPeers (line 107) | toggleEnabledFetchPeers ({ commit }, enabled) {
  method updateCurrentTaskItem (line 110) | updateCurrentTaskItem ({ commit }, task) {
  method updateCurrentTaskGid (line 120) | updateCurrentTaskGid ({ commit }, gid) {
  method addUri (line 123) | addUri ({ dispatch }, data) {
  method addTorrent (line 131) | addTorrent ({ dispatch }, data) {
  method addMetalink (line 139) | addMetalink ({ dispatch }, data) {
  method getTaskOption (line 147) | getTaskOption (_, gid) {
  method changeTaskOption (line 155) | changeTaskOption (_, payload) {
  method removeTask (line 159) | removeTask ({ state, dispatch }, task) {
  method forcePauseTask (line 171) | forcePauseTask ({ dispatch }, task) {
  method pauseTask (line 183) | pauseTask ({ dispatch }, task) {
  method resumeTask (line 193) | resumeTask ({ dispatch }, task) {
  method pauseAllTask (line 201) | pauseAllTask ({ dispatch }) {
  method resumeAllTask (line 211) | resumeAllTask ({ dispatch }) {
  method addToSeedingList (line 218) | addToSeedingList ({ state, commit }, gid) {
  method removeFromSeedingList (line 230) | removeFromSeedingList ({ state, commit }, gid) {
  method stopSeeding (line 240) | stopSeeding ({ dispatch }, { gid }) {
  method removeTaskRecord (line 246) | removeTaskRecord ({ state, dispatch }, task) {
  method saveSession (line 259) | saveSession () {
  method purgeTaskRecord (line 262) | purgeTaskRecord ({ dispatch }) {
  method toggleTask (line 266) | toggleTask ({ dispatch }, task) {
  method batchResumeSelectedTasks (line 275) | batchResumeSelectedTasks ({ state }) {
  method batchPauseSelectedTasks (line 283) | batchPauseSelectedTasks ({ state }) {
  method batchForcePauseTask (line 291) | batchForcePauseTask (_, gids) {
  method batchResumeTask (line 294) | batchResumeTask (_, gids) {
  method batchRemoveTask (line 297) | batchRemoveTask ({ dispatch }, gids) {

FILE: src/shared/aria2/lib/Aria2.js
  class Aria2 (line 5) | class Aria2 extends JSONRPCClient {
    method prefix (line 6) | prefix (str) {
    method unprefix (line 13) | unprefix (str) {
    method addSecret (line 18) | addSecret (parameters) {
    method _onnotification (line 26) | _onnotification (notification) {
    method call (line 33) | async call (method, ...params) {
    method multicall (line 37) | async multicall (calls) {
    method batch (line 46) | async batch (calls) {
    method listNotifications (line 55) | async listNotifications () {
    method listMethods (line 60) | async listMethods () {

FILE: src/shared/aria2/lib/JSONRPCClient.js
  class JSONRPCClient (line 14) | class JSONRPCClient extends EventEmitter {
    method constructor (line 15) | constructor (options) {
    method id (line 23) | id () {
    method url (line 27) | url (protocol) {
    method websocket (line 39) | websocket (message) {
    method http (line 50) | async http (message) {
    method _buildMessage (line 70) | _buildMessage (method, params) {
    method batch (line 85) | async batch (calls) {
    method call (line 98) | async call (method, parameters) {
    method _send (line 107) | async _send (message) {
    method _onresponse (line 116) | _onresponse ({ id, error, result }) {
    method _onrequest (line 124) | _onrequest ({ method, params }) {
    method _onnotification (line 128) | _onnotification ({ method, params }) {
    method _onobject (line 144) | _onobject (message) {
    method open (line 150) | async open () {
    method close (line 176) | async close () {

FILE: src/shared/aria2/lib/JSONRPCError.js
  class JSONRPCError (line 3) | class JSONRPCError extends Error {
    method constructor (line 4) | constructor ({ message, code, data }) {

FILE: src/shared/aria2/lib/promiseEvent.js
  function cleanup (line 5) | function cleanup () {
  function onEvent (line 9) | function onEvent (data) {
  function onError (line 13) | function onError (err) {

FILE: src/shared/constants.js
  constant EMPTY_STRING (line 1) | const EMPTY_STRING = ''
  constant PORTABLE_EXECUTABLE_DIR (line 2) | const PORTABLE_EXECUTABLE_DIR = process.env.PORTABLE_EXECUTABLE_DIR
  constant IS_PORTABLE (line 3) | const IS_PORTABLE = PORTABLE_EXECUTABLE_DIR && PORTABLE_EXECUTABLE_DIR !...
  constant APP_THEME (line 5) | const APP_THEME = {
  constant APP_RUN_MODE (line 11) | const APP_RUN_MODE = {
  constant ADD_TASK_TYPE (line 17) | const ADD_TASK_TYPE = {
  constant TASK_STATUS (line 22) | const TASK_STATUS = {
  constant LOG_LEVELS (line 32) | const LOG_LEVELS = [
  constant MAX_NUM_OF_DIRECTORIES (line 41) | const MAX_NUM_OF_DIRECTORIES = 5
  constant ENGINE_RPC_HOST (line 43) | const ENGINE_RPC_HOST = '127.0.0.1'
  constant ENGINE_RPC_PORT (line 44) | const ENGINE_RPC_PORT = 16800
  constant ENGINE_MAX_CONCURRENT_DOWNLOADS (line 45) | const ENGINE_MAX_CONCURRENT_DOWNLOADS = 10
  constant ENGINE_MAX_CONNECTION_PER_SERVER (line 46) | const ENGINE_MAX_CONNECTION_PER_SERVER = 64
  constant UNKNOWN_PEERID (line 48) | const UNKNOWN_PEERID = '%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00...
  constant UNKNOWN_PEERID_NAME (line 49) | const UNKNOWN_PEERID_NAME = 'unknown'
  constant GRAPHIC (line 50) | const GRAPHIC = '░▒▓█'
  constant ONE_SECOND (line 52) | const ONE_SECOND = 1000
  constant ONE_MINUTE (line 53) | const ONE_MINUTE = ONE_SECOND * 60
  constant ONE_HOUR (line 54) | const ONE_HOUR = ONE_MINUTE * 60
  constant ONE_DAY (line 55) | const ONE_DAY = ONE_HOUR * 24
  constant AUTO_SYNC_TRACKER_INTERVAL (line 58) | const AUTO_SYNC_TRACKER_INTERVAL = ONE_HOUR * 12
  constant AUTO_CHECK_UPDATE_INTERVAL (line 61) | const AUTO_CHECK_UPDATE_INTERVAL = ONE_DAY * 7
  constant MAX_BT_TRACKER_LENGTH (line 63) | const MAX_BT_TRACKER_LENGTH = 6144
  constant NGOSANG_TRACKERS_BEST_URL (line 68) | const NGOSANG_TRACKERS_BEST_URL = 'https://raw.githubusercontent.com/ngo...
  constant NGOSANG_TRACKERS_BEST_IP_URL (line 69) | const NGOSANG_TRACKERS_BEST_IP_URL = 'https://raw.githubusercontent.com/...
  constant NGOSANG_TRACKERS_ALL_URL (line 70) | const NGOSANG_TRACKERS_ALL_URL = 'https://raw.githubusercontent.com/ngos...
  constant NGOSANG_TRACKERS_ALL_IP_URL (line 71) | const NGOSANG_TRACKERS_ALL_IP_URL = 'https://raw.githubusercontent.com/n...
  constant NGOSANG_TRACKERS_BEST_URL_CDN (line 73) | const NGOSANG_TRACKERS_BEST_URL_CDN = 'https://cdn.jsdelivr.net/gh/ngosa...
  constant NGOSANG_TRACKERS_BEST_IP_URL_CDN (line 74) | const NGOSANG_TRACKERS_BEST_IP_URL_CDN = 'https://cdn.jsdelivr.net/gh/ng...
  constant NGOSANG_TRACKERS_ALL_URL_CDN (line 75) | const NGOSANG_TRACKERS_ALL_URL_CDN = 'https://cdn.jsdelivr.net/gh/ngosan...
  constant NGOSANG_TRACKERS_ALL_IP_URL_CDN (line 76) | const NGOSANG_TRACKERS_ALL_IP_URL_CDN = 'https://cdn.jsdelivr.net/gh/ngo...
  constant XIU2_TRACKERS_BEST_URL (line 81) | const XIU2_TRACKERS_BEST_URL = 'https://raw.githubusercontent.com/XIU2/T...
  constant XIU2_TRACKERS_ALL_URL (line 82) | const XIU2_TRACKERS_ALL_URL = 'https://raw.githubusercontent.com/XIU2/Tr...
  constant XIU2_TRACKERS_HTTP_URL (line 83) | const XIU2_TRACKERS_HTTP_URL = 'https://raw.githubusercontent.com/XIU2/T...
  constant XIU2_TRACKERS_BEST_URL_CDN (line 85) | const XIU2_TRACKERS_BEST_URL_CDN = 'https://cdn.jsdelivr.net/gh/XIU2/Tra...
  constant XIU2_TRACKERS_ALL_URL_CDN (line 86) | const XIU2_TRACKERS_ALL_URL_CDN = 'https://cdn.jsdelivr.net/gh/XIU2/Trac...
  constant XIU2_TRACKERS_HTTP_URL_CDN (line 87) | const XIU2_TRACKERS_HTTP_URL_CDN = 'https://cdn.jsdelivr.net/gh/XIU2/Tra...
  constant XIU2_TRACKERS_BLACK_URL (line 90) | const XIU2_TRACKERS_BLACK_URL = 'https://cdn.jsdelivr.net/gh/XIU2/Tracke...
  constant TRACKER_SOURCE_OPTIONS (line 92) | const TRACKER_SOURCE_OPTIONS = [
  constant PROXY_SCOPES (line 175) | const PROXY_SCOPES = {
  constant PROXY_SCOPE_OPTIONS (line 181) | const PROXY_SCOPE_OPTIONS = [
  constant NONE_SELECTED_FILES (line 187) | const NONE_SELECTED_FILES = 'none'
  constant SELECTED_ALL_FILES (line 188) | const SELECTED_ALL_FILES = 'all'
  constant IP_VERSION (line 190) | const IP_VERSION = {
  constant LOGIN_SETTING_OPTIONS (line 195) | const LOGIN_SETTING_OPTIONS = {
  constant TRAY_CANVAS_CONFIG (line 202) | const TRAY_CANVAS_CONFIG = {
  constant COMMON_RESOURCE_TAGS (line 211) | const COMMON_RESOURCE_TAGS = ['http://', 'https://', 'ftp://', 'magnet:']
  constant THUNDER_RESOURCE_TAGS (line 212) | const THUNDER_RESOURCE_TAGS = ['thunder://']
  constant RESOURCE_TAGS (line 214) | const RESOURCE_TAGS = [
  constant SUPPORT_RTL_LOCALES (line 219) | const SUPPORT_RTL_LOCALES = [
  constant IMAGE_SUFFIXES (line 240) | const IMAGE_SUFFIXES = [
  constant AUDIO_SUFFIXES (line 261) | const AUDIO_SUFFIXES = [
  constant VIDEO_SUFFIXES (line 273) | const VIDEO_SUFFIXES = [
  constant SUB_SUFFIXES (line 285) | const SUB_SUFFIXES = [
  constant DOCUMENT_SUFFIXES (line 295) | const DOCUMENT_SUFFIXES = [

FILE: src/shared/locales/LocaleManager.js
  class LocaleManager (line 4) | class LocaleManager {
    method constructor (line 5) | constructor (options = {}) {
    method changeLanguage (line 14) | changeLanguage (lng) {
    method changeLanguageByLocale (line 18) | changeLanguageByLocale (locale) {
    method getI18n (line 23) | getI18n () {

FILE: src/shared/ua.js
  constant ARIA2_UA (line 1) | const ARIA2_UA = 'aria2/1.36.0'
  constant TRANSMISSION_UA (line 2) | const TRANSMISSION_UA = 'Transmission/3.00'
  constant CHROME_UA (line 3) | const CHROME_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Apple...
  constant DU_UA (line 4) | const DU_UA = 'netdisk;6.0.0.12;PC;PC-Windows;10.0.16299;WindowsBaiduYun...

FILE: src/shared/utils/rename.js
  constant RULE_REGEX (line 1) | const RULE_REGEX = /\(([^)]*)\)/
  constant PLUS (line 2) | const PLUS = '+'
  constant MINUS (line 3) | const MINUS = '-'
  constant OPERATORS (line 4) | const OPERATORS = [PLUS, MINUS]
Condensed preview — 475 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (984K chars).
[
  {
    "path": ".babelrc",
    "chars": 707,
    "preview": "{\n  \"comments\": false,\n  \"env\": {\n    \"main\": {\n      \"presets\": [\"@babel/preset-env\"]\n    },\n    \"renderer\": {\n      \"p"
  },
  {
    "path": ".editorconfig",
    "chars": 130,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newli"
  },
  {
    "path": ".electron-vue/build.js",
    "chars": 3071,
    "preview": "'use strict'\n\nprocess.env.NODE_ENV = 'production'\n\nconst { say } = require('cfonts')\nconst chalk = require('chalk')\ncons"
  },
  {
    "path": ".electron-vue/dev-client.js",
    "chars": 1205,
    "preview": "const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')\n\nhotClient.subscribe(event => {\n  /**"
  },
  {
    "path": ".electron-vue/dev-runner.js",
    "chars": 3714,
    "preview": "'use strict'\n\nconst path = require('node:path')\nconst { spawn } = require('node:child_process')\nconst { say } = require("
  },
  {
    "path": ".electron-vue/webpack.main.config.js",
    "chars": 1915,
    "preview": "'use strict'\n\nprocess.env.BABEL_ENV = 'main'\n\nconst path = require('node:path')\nconst Webpack = require('webpack')\nconst"
  },
  {
    "path": ".electron-vue/webpack.renderer.config.js",
    "chars": 5899,
    "preview": "'use strict'\n\nprocess.env.BABEL_ENV = 'renderer'\n\nconst path = require('node:path')\nconst Webpack = require('webpack')\nc"
  },
  {
    "path": ".electron-vue/webpack.web.config.js",
    "chars": 5511,
    "preview": "'use strict'\n\nprocess.env.BABEL_ENV = 'web'\n\nconst path = require('node:path')\nconst { dependencies } = require('../pack"
  },
  {
    "path": ".eslintignore",
    "chars": 176,
    "preview": "src/renderer/components/Icons/*.js\n\nsrc/shared/locales/*\n!src/shared/locales/all.js\n!src/shared/locales/app.js\n!src/shar"
  },
  {
    "path": ".eslintrc.js",
    "chars": 563,
    "preview": "module.exports = {\n  root: true,\n  env: {\n    browser: true,\n    node: true\n  },\n  extends: [\n    'plugin:vue/essential'"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1_bug_report.yml",
    "chars": 2878,
    "preview": "name: 🐛 [NEW] Bug Report\ndescription: File a bug report here\ntitle: \"[BUG]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2_bug_report_cn.yml",
    "chars": 2025,
    "preview": "name: 🐛 [新] Bug 报告\ndescription: 在这里提交 Bug 报告\ntitle: \"[BUG]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 606,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement ✨\nassignees: ''\n\n---\n\n**"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request_cn.md",
    "chars": 375,
    "preview": "---\nname: 新功能请求\nabout: 你期望 Motrix 未来添加的新功能\ntitle: ''\nlabels: enhancement ✨\nassignees: ''\n\n---\n\n<!--\n反馈之前请搜索一下已有 issues 和"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 584,
    "preview": "<!-- You can erase any parts of this template not applicable to your Pull Request. -->\n\n## Description\n<!-- Write a brie"
  },
  {
    "path": ".github/lock.yml",
    "chars": 1150,
    "preview": "# Configuration for Lock Threads - https://github.com/dessant/lock-threads\n\n# Number of days of inactivity before a clos"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 2440,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1803,
    "preview": "name: Build/release\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  release:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 245,
    "preview": "!.gitkeep\n.DS_Store\n.env\n.idea/\n.vs/\n.vscode/\n*.log\nnode_modules/\nthumbs.db\n\n# npm package\n.npmrc\nnpm-debug.log.*\n\n# Esl"
  },
  {
    "path": ".travis.yml",
    "chars": 722,
    "preview": "sudo: required\ndist: trusty\n\nlanguage: c\n\njobs:\n  include:\n  - os: osx\n    osx_image: xcode11.3\n  - os: linux\n    env: C"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3354,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING-CN.md",
    "chars": 1101,
    "preview": "# Motrix 贡献指南\n\n开始贡献之前,确保你已经理解了 [GitHub 的协作流程](https://guides.github.com/introduction/flow/)。\n\n## 🌍 翻译指南\n\n首先你要确定一个语言的英文简写"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1755,
    "preview": "# Motrix Contributing Guide\n\nBefore you start contributing, make sure you already understand [GitHub flow](https://guide"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "The MIT License\n\nCopyright 2018-present Dr_rOot\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "README-CN.md",
    "chars": 7829,
    "preview": "# Motrix\n\n<p>\n  <a href=\"https://motrix.app\">\n    <img src=\"./static/512x512.png\" width=\"256\" alt=\"Motrix App Icon\" />\n "
  },
  {
    "path": "README.md",
    "chars": 9580,
    "preview": "# Motrix\n\n<p>\n  <a href=\"https://motrix.app\">\n    <img src=\"./static/512x512.png\" width=\"256\" alt=\"Motrix App Icon\" />\n "
  },
  {
    "path": "app-update.yml",
    "chars": 57,
    "preview": "provider: generic\nurl: 'https://dl.motrix.app/releases/'\n"
  },
  {
    "path": "appveyor.yml",
    "chars": 341,
    "preview": "image: Visual Studio 2017\n\nplatform:\n  - x64\n\ncache:\n  - node_modules\n  - '%USERPROFILE%\\.electron'\n\ninit:\n  - git confi"
  },
  {
    "path": "build/afterPackHook.js",
    "chars": 2967,
    "preview": "//  Forked from https://github.com/samuelmeuli/mini-diary/blob/master/scripts/after-pack.js\n\n/**\n * Source: https://gith"
  },
  {
    "path": "build/afterSignHook.js",
    "chars": 929,
    "preview": "require('dotenv').config()\nconst { join } = require('node:path')\nconst { notarize } = require('@electron/notarize')\ncons"
  },
  {
    "path": "electron-builder.json",
    "chars": 3938,
    "preview": "{\n    \"productName\": \"Motrix\",\n    \"appId\": \"app.motrix.native\",\n    \"afterPack\": \"./build/afterPackHook.js\",\n    \"after"
  },
  {
    "path": "extra/README.md",
    "chars": 56,
    "preview": "# aria2\n\nSource code: https://github.com/agalwood/aria2\n"
  },
  {
    "path": "extra/darwin/arm64/engine/aria2.conf",
    "chars": 3865,
    "preview": "###############################\n# Motrix macOS Aria2 config file\n#\n# @see https://aria2.github.io/manual/en/html/aria2c."
  },
  {
    "path": "extra/darwin/x64/engine/aria2.conf",
    "chars": 3865,
    "preview": "###############################\n# Motrix macOS Aria2 config file\n#\n# @see https://aria2.github.io/manual/en/html/aria2c."
  },
  {
    "path": "extra/linux/arm64/engine/aria2.conf",
    "chars": 3866,
    "preview": "###############################\n# Motrix Linux Aria2 config file\n#\n# @see https://aria2.github.io/manual/en/html/aria2c."
  },
  {
    "path": "extra/linux/armv7l/engine/aria2.conf",
    "chars": 3866,
    "preview": "###############################\n# Motrix Linux Aria2 config file\n#\n# @see https://aria2.github.io/manual/en/html/aria2c."
  },
  {
    "path": "extra/linux/x64/engine/aria2.conf",
    "chars": 3866,
    "preview": "###############################\n# Motrix Linux Aria2 config file\n#\n# @see https://aria2.github.io/manual/en/html/aria2c."
  },
  {
    "path": "extra/win32/ia32/engine/aria2.conf",
    "chars": 3867,
    "preview": "###############################\n# Motrix Windows Aria2 config file\n#\n# @see https://aria2.github.io/manual/en/html/aria2"
  },
  {
    "path": "extra/win32/x64/engine/aria2.conf",
    "chars": 3869,
    "preview": "###############################\n# Motrix Windows Aria2 config file\n#\n# @see https://aria2.github.io/manual/en/html/aria2"
  },
  {
    "path": "jsconfig.json",
    "chars": 216,
    "preview": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"./src/renderer/*\"\n      ],\n      \"@s"
  },
  {
    "path": "package.json",
    "chars": 4147,
    "preview": "{\n  \"name\": \"Motrix\",\n  \"version\": \"1.8.19\",\n  \"description\": \"A full-featured download manager\",\n  \"homepage\": \"https:/"
  },
  {
    "path": "src/index.ejs",
    "chars": 1658,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Motrix</title>\n    <% if (htmlWebpackPlugin.option"
  },
  {
    "path": "src/main/Application.js",
    "chars": 27724,
    "preview": "import { EventEmitter } from 'node:events'\nimport { readFile, unlink } from 'node:fs'\nimport { extname, basename } from "
  },
  {
    "path": "src/main/Launcher.js",
    "chars": 4347,
    "preview": "import { EventEmitter } from 'node:events'\nimport { app } from 'electron'\nimport is from 'electron-is'\n\nimport Exception"
  },
  {
    "path": "src/main/configs/engine.js",
    "chars": 315,
    "preview": "export const engineBinMap = {\n  darwin: 'aria2c',\n  win32: 'aria2c.exe',\n  linux: 'aria2c'\n}\n\nexport const engineArchMap"
  },
  {
    "path": "src/main/configs/page.js",
    "chars": 377,
    "preview": "import is from 'electron-is'\n\nexport default {\n  index: {\n    attrs: {\n      title: 'Motrix',\n      width: 1024,\n      h"
  },
  {
    "path": "src/main/configs/protocol.js",
    "chars": 418,
    "preview": "/* eslint quote-props: [\"error\", \"always\"] */\nexport default {\n  'task-list': 'application:task-list',\n  'new-task': 'ap"
  },
  {
    "path": "src/main/core/AutoLaunchManager.js",
    "chars": 777,
    "preview": "import { app } from 'electron'\n\nimport { LOGIN_SETTING_OPTIONS } from '@shared/constants'\n\nexport default class AutoLaun"
  },
  {
    "path": "src/main/core/ConfigManager.js",
    "chars": 6239,
    "preview": "import { app } from 'electron'\nimport is from 'electron-is'\nimport Store from 'electron-store'\n\nimport {\n  getConfigBase"
  },
  {
    "path": "src/main/core/Context.js",
    "chars": 928,
    "preview": "import logger from './Logger'\nimport {\n  getEnginePath,\n  getAria2BinPath,\n  getAria2ConfPath,\n  getSessionPath\n} from '"
  },
  {
    "path": "src/main/core/EnergyManager.js",
    "chars": 741,
    "preview": "import { powerSaveBlocker } from 'electron'\n\nimport logger from './Logger'\n\nlet psbId\nexport default class EnergyManager"
  },
  {
    "path": "src/main/core/Engine.js",
    "chars": 3256,
    "preview": "import { spawn } from 'node:child_process'\nimport { existsSync, writeFile, unlink } from 'node:fs'\nimport is from 'elect"
  },
  {
    "path": "src/main/core/EngineClient.js",
    "chars": 1423,
    "preview": "'use strict'\n\nimport { Aria2 } from '@shared/aria2'\n\nimport logger from './Logger'\nimport {\n  compactUndefined,\n  format"
  },
  {
    "path": "src/main/core/ExceptionHandler.js",
    "chars": 672,
    "preview": "import { app, dialog } from 'electron'\nimport is from 'electron-is'\n\nimport logger from './Logger'\n\nconst defaults = {\n "
  },
  {
    "path": "src/main/core/Logger.js",
    "chars": 462,
    "preview": "import { join } from 'node:path'\nimport is from 'electron-is'\nimport logger from 'electron-log'\n\nimport { IS_PORTABLE, P"
  },
  {
    "path": "src/main/core/ProtocolManager.js",
    "chars": 2218,
    "preview": "import { EventEmitter } from 'node:events'\nimport { app } from 'electron'\nimport is from 'electron-is'\nimport { parse } "
  },
  {
    "path": "src/main/core/UPnPManager.js",
    "chars": 2000,
    "preview": "import NatAPI from '@motrix/nat-api'\n\nimport logger from './Logger'\n\nlet client = null\nconst mappingStatus = {}\n\nexport "
  },
  {
    "path": "src/main/core/UpdateManager.js",
    "chars": 4597,
    "preview": "import { EventEmitter } from 'node:events'\nimport { resolve } from 'node:path'\nimport { dialog } from 'electron'\nimport "
  },
  {
    "path": "src/main/index.dev.js",
    "chars": 627,
    "preview": "/**\n * This file is used specifically and only for development. It installs\n * `electron-debug` & `vue-devtools`. There "
  },
  {
    "path": "src/main/index.js",
    "chars": 603,
    "preview": "import { app } from 'electron'\nimport is from 'electron-is'\nimport { initialize } from '@electron/remote/main'\n\nimport L"
  },
  {
    "path": "src/main/menus/darwin.json",
    "chars": 3352,
    "preview": "{\n  \"menu\": [\n    {\n      \"id\": \"menu.app\",\n      \"submenu\": [\n        { \"id\": \"app.about\", \"command\": \"application:abou"
  },
  {
    "path": "src/main/menus/linux.json",
    "chars": 3172,
    "preview": "{\n  \"menu\": [\n    {\n      \"id\": \"menu.file\",\n      \"submenu\": [\n        { \"id\": \"app.about\", \"command\": \"application:abo"
  },
  {
    "path": "src/main/menus/touchBar.json",
    "chars": 1065,
    "preview": "[\n  {\n    \"type\": \"button\", \"icon\": \"new-task\", \"id\": \"task.new-task\", \"command\": \"application:new-task\", \"command-after"
  },
  {
    "path": "src/main/menus/tray.json",
    "chars": 952,
    "preview": "[\n  { \"id\": \"task.new-task\", \"command\": \"application:new-task\", \"command-after\": \"application:show?page=index\" },\n  { \"i"
  },
  {
    "path": "src/main/menus/win32.json",
    "chars": 3295,
    "preview": "{\n  \"menu\": [\n    {\n      \"id\": \"menu.file\",\n      \"submenu\": [\n        { \"id\": \"app.about\", \"command\": \"application:abo"
  },
  {
    "path": "src/main/pages/about.html",
    "chars": 252,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, in"
  },
  {
    "path": "src/main/pages/index.html",
    "chars": 252,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, in"
  },
  {
    "path": "src/main/ui/DockManager.js",
    "chars": 1262,
    "preview": "import is from 'electron-is'\nimport { EventEmitter } from 'node:events'\nimport { app } from 'electron'\n\nimport { bytesTo"
  },
  {
    "path": "src/main/ui/Locale.js",
    "chars": 494,
    "preview": "import resources from '@shared/locales/app'\nimport LocaleManager from '@shared/locales/LocaleManager'\n\nconst localeManag"
  },
  {
    "path": "src/main/ui/MenuManager.js",
    "chars": 1616,
    "preview": "import { EventEmitter } from 'node:events'\nimport { Menu } from 'electron'\n\nimport keymap from '@shared/keymap'\nimport {"
  },
  {
    "path": "src/main/ui/ThemeManager.js",
    "chars": 867,
    "preview": "import { EventEmitter } from 'node:events'\nimport { nativeTheme } from 'electron'\n\nimport { APP_THEME } from '@shared/co"
  },
  {
    "path": "src/main/ui/TouchBarManager.js",
    "chars": 2335,
    "preview": "import { EventEmitter } from 'node:events'\nimport { join } from 'node:path'\nimport { TouchBar, nativeImage } from 'elect"
  },
  {
    "path": "src/main/ui/TrayManager.js",
    "chars": 8317,
    "preview": "import { EventEmitter } from 'node:events'\nimport { join } from 'node:path'\nimport { Tray, Menu, nativeImage } from 'ele"
  },
  {
    "path": "src/main/ui/WindowManager.js",
    "chars": 6559,
    "preview": "import { join } from 'node:path'\nimport { EventEmitter } from 'node:events'\nimport { debounce } from 'lodash'\nimport { a"
  },
  {
    "path": "src/main/utils/index.js",
    "chars": 4900,
    "preview": "import { resolve } from 'node:path'\nimport { access, constants, existsSync, lstatSync } from 'node:fs'\nimport { app, nat"
  },
  {
    "path": "src/main/utils/menu.js",
    "chars": 5124,
    "preview": "import { parse } from 'querystring'\n\nexport const concat = (template, submenu, submenuToAdd) => {\n  submenuToAdd.forEach"
  },
  {
    "path": "src/renderer/api/Api.js",
    "chars": 9596,
    "preview": "import { ipcRenderer } from 'electron'\nimport is from 'electron-is'\nimport { isEmpty, clone } from 'lodash'\nimport { Ari"
  },
  {
    "path": "src/renderer/api/index.js",
    "chars": 67,
    "preview": "import Api from './Api'\n\nconst api = new Api()\n\nexport default api\n"
  },
  {
    "path": "src/renderer/assets/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/renderer/components/About/AboutPanel.vue",
    "chars": 1330,
    "preview": "<template>\n  <el-dialog\n    custom-class=\"app-about-dialog\"\n    width=\"61.8vw\"\n    :visible=\"visible\"\n    @open=\"handleO"
  },
  {
    "path": "src/renderer/components/About/AppInfo.vue",
    "chars": 1947,
    "preview": "<template>\n  <div class=\"app-info\">\n    <div class=\"app-version\">\n      <mo-logo :width=\"93\" :height=\"21\" style=\"vertica"
  },
  {
    "path": "src/renderer/components/About/Copyright.vue",
    "chars": 1278,
    "preview": "<template>\n  <el-row class=\"copyright\">\n    <el-col :span=\"6\" class=\"copyright-left\">\n      <a target=\"_blank\" rel=\"noop"
  },
  {
    "path": "src/renderer/components/Aside/Index.vue",
    "chars": 2572,
    "preview": "<template>\n  <el-aside width=\"78px\" :class=\"['aside', 'hidden-sm-and-down', { 'draggable': asideDraggable }]\" :style=\"vi"
  },
  {
    "path": "src/renderer/components/Browser/index.vue",
    "chars": 1620,
    "preview": "<template>\n  <div ref=\"webviewViewport\" class=\"webview-viewport\">\n    <iframe\n      class=\"mo-webview\"\n      ref=\"iframe"
  },
  {
    "path": "src/renderer/components/CommandManager/index.js",
    "chars": 971,
    "preview": "import { EventEmitter } from 'node:events'\n\nexport default class CommandManager extends EventEmitter {\n  constructor () "
  },
  {
    "path": "src/renderer/components/CommandManager/instance.js",
    "chars": 77,
    "preview": "import CommandManager from '.'\n\nexport const commands = new CommandManager()\n"
  },
  {
    "path": "src/renderer/components/DragSelect/Index.vue",
    "chars": 4242,
    "preview": "<template>\n  <div\n    ref=\"container\"\n    style=\"position: relative; user-select: none; overflow-x: hidden; touch-action"
  },
  {
    "path": "src/renderer/components/Dragger/Index.vue",
    "chars": 1525,
    "preview": "<template>\r\n  <div v-if=\"false\"></div>\r\n</template>\r\n\r\n<script>\r\n  import { ADD_TASK_TYPE } from '@shared/constants'\r\n\r\n"
  },
  {
    "path": "src/renderer/components/Icons/Icon.vue",
    "chars": 5460,
    "preview": "<template>\n  <svg\n    version=\"1.1\"\n    :class=\"klass\"\n    :role=\"label ? 'img' : 'presentation'\"\n    :aria-label=\"label"
  },
  {
    "path": "src/renderer/components/Icons/arrow-down.js",
    "chars": 618,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'arrow-down': {\n    'width': 24,\n    'height': 24,\n    'ra"
  },
  {
    "path": "src/renderer/components/Icons/arrow-up.js",
    "chars": 613,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'arrow-up': {\n    'width': 24,\n    'height': 24,\n    'raw'"
  },
  {
    "path": "src/renderer/components/Icons/audio.js",
    "chars": 834,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'audio': {\n    'width': 24,\n    'height': 24,\n    'raw': `"
  },
  {
    "path": "src/renderer/components/Icons/delete.js",
    "chars": 419,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'delete': {\n    'width': 24,\n    'height': 24,\n    'raw': "
  },
  {
    "path": "src/renderer/components/Icons/dice.js",
    "chars": 1169,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'dice': {\n    'width': 24,\n    'height': 24,\n    'raw': `<"
  },
  {
    "path": "src/renderer/components/Icons/document.js",
    "chars": 602,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'document': {\n    'width': 24,\n    'height': 24,\n    'raw'"
  },
  {
    "path": "src/renderer/components/Icons/folder.js",
    "chars": 431,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'folder': {\n    'width': 24,\n    'height': 24,\n    'raw': "
  },
  {
    "path": "src/renderer/components/Icons/image.js",
    "chars": 590,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'image': {\n    'width': 24,\n    'height': 24,\n    'raw': `"
  },
  {
    "path": "src/renderer/components/Icons/inbox.js",
    "chars": 657,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'inbox': {\n    'width': 24,\n    'height': 24,\n    'raw': `"
  },
  {
    "path": "src/renderer/components/Icons/info-circle.js",
    "chars": 550,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'info-circle': {\n    'width': 24,\n    'height': 24,\n    'r"
  },
  {
    "path": "src/renderer/components/Icons/info-square.js",
    "chars": 548,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'info-square': {\n    'width': 24,\n    'height': 24,\n    'r"
  },
  {
    "path": "src/renderer/components/Icons/link.js",
    "chars": 570,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'link': {\n    'width': 24,\n    'height': 24,\n    'raw': `<"
  },
  {
    "path": "src/renderer/components/Icons/magnet.js",
    "chars": 682,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'magnet': {\n    'width': 24,\n    'height': 24,\n    'raw': "
  },
  {
    "path": "src/renderer/components/Icons/menu-about.js",
    "chars": 591,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'menu-about': {\n    'width': 24,\n    'height': 24,\n    'ra"
  },
  {
    "path": "src/renderer/components/Icons/menu-add.js",
    "chars": 430,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'menu-add': {\n    'width': 24,\n    'height': 24,\n    'raw'"
  },
  {
    "path": "src/renderer/components/Icons/menu-preference.js",
    "chars": 983,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'menu-preference': {\n    'width': 24,\n    'height': 24,\n  "
  },
  {
    "path": "src/renderer/components/Icons/menu-task.js",
    "chars": 405,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'menu-task': {\n    'width': 24,\n    'height': 24,\n    'pat"
  },
  {
    "path": "src/renderer/components/Icons/more.js",
    "chars": 2635,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'more': {\n    'width': 24,\n    'height': 24,\n    'paths': "
  },
  {
    "path": "src/renderer/components/Icons/node.js",
    "chars": 754,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'node': {\n    'width': 24,\n    'height': 24,\n    'raw': `\n"
  },
  {
    "path": "src/renderer/components/Icons/preference-advanced.js",
    "chars": 849,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'preference-advanced': {\n    'width': 24,\n    'height': 24"
  },
  {
    "path": "src/renderer/components/Icons/preference-basic.js",
    "chars": 500,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'preference-basic': {\n    'width': 24,\n    'height': 24,\n "
  },
  {
    "path": "src/renderer/components/Icons/preference-lab.js",
    "chars": 423,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'preference-lab': {\n    'width': 24,\n    'height': 24,\n   "
  },
  {
    "path": "src/renderer/components/Icons/purge.js",
    "chars": 743,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'purge': {\n    'width': 24,\n    'height': 24,\n    'raw': `"
  },
  {
    "path": "src/renderer/components/Icons/refresh.js",
    "chars": 494,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'refresh': {\n    'width': 24,\n    'height': 24,\n    'raw':"
  },
  {
    "path": "src/renderer/components/Icons/speedometer.js",
    "chars": 5293,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'speedometer': {\n    'width': 24,\n    'height': 24,\n    'p"
  },
  {
    "path": "src/renderer/components/Icons/sync.js",
    "chars": 1237,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'sync': {\n    'width': 24,\n    'height': 24,\n    'raw': `<"
  },
  {
    "path": "src/renderer/components/Icons/task-history.js",
    "chars": 252,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-history': {\n    'width': 24,\n    'height': 24,\n    '"
  },
  {
    "path": "src/renderer/components/Icons/task-pause-line.js",
    "chars": 437,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-pause-line': {\n    'width': 24,\n    'height': 24,\n  "
  },
  {
    "path": "src/renderer/components/Icons/task-pause.js",
    "chars": 375,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-pause': {\n    'width': 24,\n    'height': 24,\n    'pa"
  },
  {
    "path": "src/renderer/components/Icons/task-restart.js",
    "chars": 697,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-restart': {\n    'width': 24,\n    'height': 24,\n    '"
  },
  {
    "path": "src/renderer/components/Icons/task-start-line.js",
    "chars": 352,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-start-line': {\n    'width': 24,\n    'height': 24,\n  "
  },
  {
    "path": "src/renderer/components/Icons/task-start.js",
    "chars": 399,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-start': {\n    'width': 24,\n    'height': 24,\n    'pa"
  },
  {
    "path": "src/renderer/components/Icons/task-stop-line.js",
    "chars": 359,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-stop-line': {\n    'width': 24,\n    'height': 24,\n   "
  },
  {
    "path": "src/renderer/components/Icons/task-stop.js",
    "chars": 255,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'task-stop': {\n    'width': 24,\n    'height': 24,\n    'pat"
  },
  {
    "path": "src/renderer/components/Icons/trash.js",
    "chars": 754,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'trash': {\n    'width': 24,\n    'height': 24,\n    'raw': `"
  },
  {
    "path": "src/renderer/components/Icons/video.js",
    "chars": 1184,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'video': {\n    'width': 24,\n    'height': 24,\n    'raw': `"
  },
  {
    "path": "src/renderer/components/Icons/win-close.js",
    "chars": 491,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'win-close': {\n    'width': 12,\n    'height': 12,\n    'raw"
  },
  {
    "path": "src/renderer/components/Icons/win-maximize.js",
    "chars": 497,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'win-maximize': {\n    'width': 12,\n    'height': 12,\n    '"
  },
  {
    "path": "src/renderer/components/Icons/win-minimize.js",
    "chars": 374,
    "preview": "import Icon from '@/components/Icons/Icon'\n\nIcon.register({\n  'win-minimize': {\n    'width': 12,\n    'height': 12,\n    '"
  },
  {
    "path": "src/renderer/components/Locale/index.js",
    "chars": 224,
    "preview": "import resources from '@shared/locales/all'\nimport LocaleManager from '@shared/locales/LocaleManager'\n\nconst localeManag"
  },
  {
    "path": "src/renderer/components/Logo/Logo.vue",
    "chars": 1504,
    "preview": "<template>\n  <div class=\"logo\">\n    <a target=\"_blank\" href=\"https://motrix.app/\">\n      <svg xmlns=\"http://www.w3.org/2"
  },
  {
    "path": "src/renderer/components/Logo/LogoMini.vue",
    "chars": 794,
    "preview": "<template>\n  <h1 class=\"logo-mini\">\n    <a target=\"_blank\" href=\"https://motrix.app/\">\n      <svg xmlns=\"http://www.w3.o"
  },
  {
    "path": "src/renderer/components/Main.vue",
    "chars": 1805,
    "preview": "<template>\n  <el-container id=\"container\">\n    <mo-aside />\n    <router-view />\n    <mo-speedometer />\n    <mo-add-task "
  },
  {
    "path": "src/renderer/components/Msg/index.js",
    "chars": 987,
    "preview": "const queue = []\nconst maxLength = 5\n\nexport default {\n  install: function (Vue, Message, defaultOption = {}) {\n    Vue."
  },
  {
    "path": "src/renderer/components/Native/DynamicTray.vue",
    "chars": 2497,
    "preview": "<template>\n  <div style=\"display: none;\">\n    <img\n      id=\"tray-icon-light-normal\"\n      src=\"static/mo-tray-light-nor"
  },
  {
    "path": "src/renderer/components/Native/EngineClient.vue",
    "chars": 8681,
    "preview": "<template>\n  <div v-if=\"false\"></div>\n</template>\n\n<script>\n  import is from 'electron-is'\n  import { mapState } from 'v"
  },
  {
    "path": "src/renderer/components/Native/Ipc.vue",
    "chars": 577,
    "preview": "<template>\n  <div v-if=\"false\"></div>\n</template>\n\n<script>\n  import { commands } from '@/components/CommandManager/inst"
  },
  {
    "path": "src/renderer/components/Native/SelectDirectory.vue",
    "chars": 734,
    "preview": "<template>\n  <el-button\n    class=\"select-directory\"\n    @click.stop=\"onFolderClick\"\n  >\n    <mo-icon name=\"folder\" widt"
  },
  {
    "path": "src/renderer/components/Native/ShowInFolder.vue",
    "chars": 580,
    "preview": "<template>\n  <i @click.stop=\"onFolderClick\">\n    <mo-icon name=\"folder\" width=\"10\" height=\"10\" />\n  </i>\n</template>\n\n<s"
  },
  {
    "path": "src/renderer/components/Native/TitleBar.vue",
    "chars": 2150,
    "preview": "<template>\n  <div class=\"title-bar\">\n    <div class=\"title-bar-dragger\"></div>\n    <ul v-if=\"showActions\" class=\"window-"
  },
  {
    "path": "src/renderer/components/Preference/Advanced.vue",
    "chars": 26642,
    "preview": "<template>\n  <el-container class=\"content panel\" direction=\"vertical\">\n    <el-header class=\"panel-header\" height=\"84\">\n"
  },
  {
    "path": "src/renderer/components/Preference/Basic.vue",
    "chars": 20593,
    "preview": "<template>\n  <el-container class=\"content panel\" direction=\"vertical\">\n    <el-header class=\"panel-header\" height=\"84\">\n"
  },
  {
    "path": "src/renderer/components/Preference/HistoryDirectory.vue",
    "chars": 5977,
    "preview": "<template>\n  <div class=\"mo-history-directory\">\n    <el-popover\n      popper-class=\"mo-directory-popper\"\n      trigger=\""
  },
  {
    "path": "src/renderer/components/Preference/Index.vue",
    "chars": 1514,
    "preview": "<template>\n  <el-container class=\"main panel\" direction=\"horizontal\">\n    <el-aside width=\"200px\" class=\"subnav hidden-x"
  },
  {
    "path": "src/renderer/components/Preference/Lab.vue",
    "chars": 2315,
    "preview": "<template>\n  <el-container class=\"content panel\" direction=\"vertical\">\n    <el-header class=\"panel-header\" height=\"84\">\n"
  },
  {
    "path": "src/renderer/components/Preference/ThemeSwitcher.vue",
    "chars": 2530,
    "preview": "<template>\n  <div>\n    <ul class=\"theme-switcher\">\n      <li\n        v-for=\"item in themeOptions\"\n        :class=\"['them"
  },
  {
    "path": "src/renderer/components/Speedometer/Speedometer.vue",
    "chars": 3045,
    "preview": "<template>\n  <div class=\"mo-speedometer\" :class=\"{ stopped: stat.numActive === 0 }\">\n    <div\n      class=\"mode\"\n      @"
  },
  {
    "path": "src/renderer/components/Subnav/PreferenceSubnav.vue",
    "chars": 1580,
    "preview": "<template>\n  <nav class=\"subnav-inner\">\n    <h3>{{ title }}</h3>\n    <ul>\n      <li\n        @click=\"() => nav('basic')\"\n"
  },
  {
    "path": "src/renderer/components/Subnav/SubnavSwitcher.vue",
    "chars": 1635,
    "preview": "<template>\n  <el-dropdown @command=\"handleRoute\" class=\"subnav-switch\" size=\"medium\">\n    <h4 class=\"subnav-title\">\n    "
  },
  {
    "path": "src/renderer/components/Subnav/TaskSubnav.vue",
    "chars": 1509,
    "preview": "<template>\n  <nav class=\"subnav-inner\">\n    <h3>{{ title }}</h3>\n    <ul>\n      <li\n        @click=\"() => nav('active')\""
  },
  {
    "path": "src/renderer/components/Task/AddTask.vue",
    "chars": 12510,
    "preview": "<template>\n  <el-dialog\n    custom-class=\"tab-title-dialog add-task-dialog\"\n    width=\"67vw\"\n    :visible=\"visible\"\n    "
  },
  {
    "path": "src/renderer/components/Task/Index.vue",
    "chars": 12918,
    "preview": "<template>\n  <el-container\n    class=\"main panel\"\n    direction=\"horizontal\"\n  >\n    <el-aside\n      width=\"200px\"\n     "
  },
  {
    "path": "src/renderer/components/Task/SelectTorrent.vue",
    "chars": 4658,
    "preview": "<template>\n  <el-upload\n    class=\"upload-torrent\"\n    drag\n    action=\"/\"\n    v-if=\"isTorrentsEmpty\"\n    :limit=\"1\"\n   "
  },
  {
    "path": "src/renderer/components/Task/TaskActions.vue",
    "chars": 4973,
    "preview": "<template>\n  <div class=\"task-actions\">\n    <el-tooltip\n      class=\"item hidden-md-and-up\"\n      effect=\"dark\"\n      pl"
  },
  {
    "path": "src/renderer/components/Task/TaskItem.vue",
    "chars": 3254,
    "preview": "<template>\n  <div :key=\"task.gid\" class=\"task-item\" v-on:dblclick=\"onDbClick\">\n    <div class=\"task-name\" :title=\"taskFu"
  },
  {
    "path": "src/renderer/components/Task/TaskItemActions.vue",
    "chars": 6313,
    "preview": "<template>\n  <ul :key=\"task.gid\" class=\"task-item-actions\" v-on:dblclick.stop=\"() => null\">\n    <li v-for=\"action in tas"
  },
  {
    "path": "src/renderer/components/Task/TaskList.vue",
    "chars": 1977,
    "preview": "<template>\n  <mo-drag-select\n    class=\"task-list\"\n    v-if=\"taskList.length > 0\"\n    attribute=\"attr\"\n    @change=\"hand"
  },
  {
    "path": "src/renderer/components/Task/TaskProgress.vue",
    "chars": 927,
    "preview": "<template>\n  <el-progress\n    v-if=\"isActive\"\n    :percentage=\"percent\"\n    :show-text=\"false\"\n    status=\"success\"\n    "
  },
  {
    "path": "src/renderer/components/Task/TaskProgressInfo.vue",
    "chars": 3660,
    "preview": "<template>\n  <el-row class=\"task-progress-info\">\n    <el-col\n      class=\"task-progress-info-left\"\n      :xs=\"12\"\n      "
  },
  {
    "path": "src/renderer/components/Task/TaskStatus.vue",
    "chars": 1045,
    "preview": "<template>\n  <el-tag :effect=\"theme\" class=\"tag-task-status\" :type=\"type\">\n    {{ status && status.toUpperCase() }}\n  </"
  },
  {
    "path": "src/renderer/components/TaskDetail/Index.vue",
    "chars": 9722,
    "preview": "<template>\n  <el-drawer\n    custom-class=\"panel task-detail-drawer\"\n    size=\"61.8%\"\n    v-if=\"gid\"\n    :title=\"$t('task"
  },
  {
    "path": "src/renderer/components/TaskDetail/TaskActivity.vue",
    "chars": 5781,
    "preview": "<template>\n  <el-form\n    class=\"mo-task-activity\"\n    ref=\"form\"\n    :model=\"form\"\n    :label-width=\"formLabelWidth\"\n  "
  },
  {
    "path": "src/renderer/components/TaskDetail/TaskFiles.vue",
    "chars": 6282,
    "preview": "<template>\n  <div class=\"mo-task-files\" v-if=\"files\">\n    <div class=\"mo-table-wrapper\">\n      <el-table\n        stripe\n"
  },
  {
    "path": "src/renderer/components/TaskDetail/TaskGeneral.vue",
    "chars": 4903,
    "preview": "<template>\n  <el-form\n    class=\"mo-task-general\"\n    ref=\"form\"\n    :model=\"form\"\n    :label-width=\"formLabelWidth\"\n   "
  },
  {
    "path": "src/renderer/components/TaskDetail/TaskPeers.vue",
    "chars": 1940,
    "preview": "<template>\n  <div class=\"mo-task-peers\">\n    <div class=\"mo-table-wrapper\">\n      <el-table\n        stripe\n        ref=\""
  },
  {
    "path": "src/renderer/components/TaskDetail/TaskTrackers.vue",
    "chars": 1556,
    "preview": "<template>\n  <el-form\n    ref=\"form\"\n    :model=\"form\"\n    :label-width=\"formLabelWidth\"\n    v-if=\"task\"\n  >\n    <div\n  "
  },
  {
    "path": "src/renderer/components/TaskGraphic/Atom.vue",
    "chars": 1380,
    "preview": "<template>\n  <rect\n    :class=\"klass\"\n    :status=\"status\"\n    :width=\"width\"\n    :height=\"height\"\n    :rx=\"radius\"\n    "
  },
  {
    "path": "src/renderer/components/TaskGraphic/Index.vue",
    "chars": 3301,
    "preview": "<template>\n  <svg version=\"1.1\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    class=\"svg-task-graphic\"\n    :width=\"width\"\n "
  },
  {
    "path": "src/renderer/components/Theme/Dark/Variables.scss",
    "chars": 3711,
    "preview": "/* Color\n-------------------------- */\n$--dk-border-color-base: #555;\n$--dk-font-color-base: #eee;\n$--dk-action-color-ba"
  },
  {
    "path": "src/renderer/components/Theme/Dark.scss",
    "chars": 9221,
    "preview": ".theme-dark {\n  .title-bar .window-actions > li {\n    color: $--dk-titlebar-actions-color;\n    &:hover {\n      backgroun"
  },
  {
    "path": "src/renderer/components/Theme/Default.scss",
    "chars": 5447,
    "preview": "html,\nbody {\n  height: 100%;\n  padding: 0;\n}\n\nbody {\n  font-family: \"Monospaced Number\", \"Chinese Quote\", -apple-system,"
  },
  {
    "path": "src/renderer/components/Theme/Index.scss",
    "chars": 336,
    "preview": "/* Normalize.css\n-------------------------- */\n@import '~normalize.css/normalize.css';\n\n/* Element UI\n------------------"
  },
  {
    "path": "src/renderer/components/Theme/Light/Variables.scss",
    "chars": 3233,
    "preview": "/* App\n-------------------------- */\n$--app-background: transparent !default;\n$--titlebar-actions-color: #1f1f1f !defaul"
  },
  {
    "path": "src/renderer/components/Theme/Variables.scss",
    "chars": 34963,
    "preview": "/* Element Chalk Variables */\n\n// Special comment for theme configurator\n// type|skipAutoTranslation|Category|Order\n// s"
  },
  {
    "path": "src/renderer/pages/index/App.vue",
    "chars": 2808,
    "preview": "<template>\n  <div id=\"app\">\n    <mo-title-bar\n      v-if=\"isRenderer\"\n      :showActions=\"showWindowActions\"\n    />\n    "
  },
  {
    "path": "src/renderer/pages/index/commands.js",
    "chars": 5255,
    "preview": "import { Message } from 'element-ui'\nimport { base64StringToBlob } from 'blob-util'\n\nimport router from '@/router'\nimpor"
  },
  {
    "path": "src/renderer/pages/index/main.js",
    "chars": 2504,
    "preview": "import is from 'electron-is'\nimport { ipcRenderer } from 'electron'\nimport Vue from 'vue'\nimport VueI18Next from '@pante"
  },
  {
    "path": "src/renderer/router/index.js",
    "chars": 1910,
    "preview": "import Vue from 'vue'\nimport Router from 'vue-router'\n\nVue.use(Router)\n\nexport default new Router({\n  routes: [\n    {\n  "
  },
  {
    "path": "src/renderer/store/index.js",
    "chars": 189,
    "preview": "import Vue from 'vue'\nimport Vuex from 'vuex'\n\nimport modules from './modules'\n\nVue.use(Vuex)\n\nexport default new Vuex.S"
  },
  {
    "path": "src/renderer/store/modules/app.js",
    "chars": 5578,
    "preview": "import { ADD_TASK_TYPE } from '@shared/constants'\nimport api from '@/api'\nimport { getSystemTheme } from '@/utils/native"
  },
  {
    "path": "src/renderer/store/modules/index.js",
    "chars": 377,
    "preview": "/**\n * The file enables `@/store/index.js` to import all vuex modules\n * in a one-shot manner. There should not be any r"
  },
  {
    "path": "src/renderer/store/modules/preference.js",
    "chars": 3756,
    "preview": "import { isEmpty } from 'lodash'\n\nimport api from '@/api'\nimport {\n  getLangDirection,\n  pushItemToFixedLengthArray,\n  r"
  },
  {
    "path": "src/renderer/store/modules/task.js",
    "chars": 8352,
    "preview": "import api from '@/api'\nimport { EMPTY_STRING, TASK_STATUS } from '@shared/constants'\nimport { checkTaskIsBT, intersecti"
  },
  {
    "path": "src/renderer/utils/native.js",
    "chars": 3104,
    "preview": "import { access, constants } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { shell, nativeTheme } from '@ele"
  },
  {
    "path": "src/renderer/utils/task.js",
    "chars": 2892,
    "preview": "import { isEmpty } from 'lodash'\n\nimport {\n  ADD_TASK_TYPE,\n  NONE_SELECTED_FILES,\n  SELECTED_ALL_FILES\n} from '@shared/"
  },
  {
    "path": "src/renderer/workers/tray.worker.js",
    "chars": 1082,
    "preview": "/* eslint no-unused-vars: 'off' */\nimport { TRAY_CANVAS_CONFIG } from '@shared/constants'\nimport { draw } from '@shared/"
  },
  {
    "path": "src/shared/aria2/index.js",
    "chars": 75,
    "preview": "'use strict'\n\nconst Aria2 = require('./lib/Aria2')\n\nmodule.exports = Aria2\n"
  },
  {
    "path": "src/shared/aria2/lib/Aria2.js",
    "chars": 1744,
    "preview": "'use strict'\n\nimport { JSONRPCClient } from './JSONRPCClient'\n\nexport class Aria2 extends JSONRPCClient {\n  prefix (str)"
  },
  {
    "path": "src/shared/aria2/lib/Deferred.js",
    "chars": 165,
    "preview": "'use strict'\n\nmodule.exports = function Deferred () {\n  this.promise = new Promise((resolve, reject) => {\n    this.resol"
  },
  {
    "path": "src/shared/aria2/lib/JSONRPCClient.js",
    "chars": 4139,
    "preview": "'use strict'\n\nimport { EventEmitter } from 'node:events'\nimport _fetch from 'node-fetch'\nimport _WebSocket from 'ws'\nimp"
  },
  {
    "path": "src/shared/aria2/lib/JSONRPCError.js",
    "chars": 213,
    "preview": "'use strict'\n\nexport class JSONRPCError extends Error {\n  constructor ({ message, code, data }) {\n    super(message)\n   "
  },
  {
    "path": "src/shared/aria2/lib/debug.js",
    "chars": 463,
    "preview": "'use strict'\n\nimport { inspect } from 'util'\n\nmodule.exports = (aria2) => {\n  aria2.on('open', () => {\n    console.log('"
  },
  {
    "path": "src/shared/aria2/lib/promiseEvent.js",
    "chars": 465,
    "preview": "'use strict'\n\nmodule.exports = function promiseEvent (target, event) {\n  return new Promise((resolve, reject) => {\n    f"
  },
  {
    "path": "src/shared/colors.json",
    "chars": 168,
    "preview": "{\n  \"active\": \"#5b5bea\",\n  \"waiting\": \"#737373\",\n  \"paused\": \"#737373\",\n  \"error\": \"#FF6157\",\n  \"complete\": \"#2ACB42\",\n "
  },
  {
    "path": "src/shared/configKeys.js",
    "chars": 3374,
    "preview": "const userKeys = [\n  'auto-check-update',\n  'auto-hide-window',\n  'auto-sync-tracker',\n  'cookie',\n  'enable-upnp',\n  'e"
  },
  {
    "path": "src/shared/constants.js",
    "chars": 7011,
    "preview": "export const EMPTY_STRING = ''\nexport const PORTABLE_EXECUTABLE_DIR = process.env.PORTABLE_EXECUTABLE_DIR\nexport const I"
  },
  {
    "path": "src/shared/keymap.json",
    "chars": 399,
    "preview": "{\n  \"cmdctrl-q\": \"application:quit\",\n  \"cmdctrl-n\": \"application:new-task\",\n  \"cmdctrl-shift-n\": \"application:new-bt-tas"
  },
  {
    "path": "src/shared/locales/LocaleManager.js",
    "chars": 489,
    "preview": "import i18next from 'i18next'\nimport { getLanguage } from '@shared/locales'\n\nexport default class LocaleManager {\n  cons"
  },
  {
    "path": "src/shared/locales/all.js",
    "chars": 5020,
    "preview": "import eleLocaleAr from 'element-ui/lib/locale/lang/ar'\nimport eleLocaleBg from 'element-ui/lib/locale/lang/bg'\nimport e"
  },
  {
    "path": "src/shared/locales/app.js",
    "chars": 2750,
    "preview": "import appLocaleAr from '@shared/locales/ar'\nimport appLocaleBg from '@shared/locales/bg'\nimport appLocaleCa from '@shar"
  },
  {
    "path": "src/shared/locales/ar/about.js",
    "chars": 141,
    "preview": "export default {\n  'engine-version': 'إصدار المحرك',\n  'license': 'الرخصة',\n  'about': 'حول',\n  'release': 'الإصدار',\n  "
  }
]

// ... and 275 more files (download for full content)

About this extraction

This page contains the full source code of the agalwood/Motrix GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 475 files (896.6 KB), approximately 276.0k tokens, and a symbol index with 499 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!