Compare commits
33 Commits
release
...
nightly-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9994f152c | ||
|
|
a28c31ef1f | ||
|
|
083b896056 | ||
|
|
f9210b8ddb | ||
|
|
1cb51ac7c2 | ||
|
|
f28cfa5c62 | ||
|
|
9c2d613084 | ||
|
|
21dc76beec | ||
|
|
79e515a3ef | ||
|
|
f0b8fa16d6 | ||
|
|
5e19572ba3 | ||
|
|
8e21873afa | ||
|
|
a34a4d2626 | ||
|
|
408facb707 | ||
|
|
64ae782549 | ||
|
|
825fa74373 | ||
|
|
48ca9f6c7e | ||
|
|
6ac35db455 | ||
|
|
29b47444f7 | ||
|
|
54aaa7fdfe | ||
|
|
1a216bc2d0 | ||
|
|
14792bcb0a | ||
|
|
908f2990e0 | ||
|
|
6b3f8a593a | ||
|
|
857c9a0522 | ||
|
|
a2fb45fed1 | ||
|
|
17fbf642da | ||
|
|
7c1ba217b7 | ||
|
|
933413eaee | ||
|
|
809d5bab4b | ||
|
|
163b39d618 | ||
|
|
f2351cc963 | ||
|
|
639bad6e5b |
17
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-node/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Node.js version: 16, 14, 12
|
||||
ARG VARIANT="16-buster"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node packages
|
||||
# RUN su node -c "npm install -g <your-package-list -here>"
|
||||
|
||||
41
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,41 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-node
|
||||
{
|
||||
"name": "qBittorrent",
|
||||
|
||||
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
|
||||
// The 'service' property is the name of the service for the container that VS Code should
|
||||
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
|
||||
"service": "app",
|
||||
|
||||
// The optional 'workspaceFolder' property is the path VS Code should open by default when
|
||||
// connected. This is typically a volume mount in .devcontainer/docker-compose.yml
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "/bin/bash"
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"jcbuisson.vue",
|
||||
"eamodio.gitlens",
|
||||
"donjayamanne.githistory"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
8000,
|
||||
8080
|
||||
],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "yarn install",
|
||||
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node"
|
||||
}
|
||||
41
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
qb.test:
|
||||
image: linuxserver/qbittorrent
|
||||
container_name: qbittorrent
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- UMASK_SET=022
|
||||
- WEBUI_PORT=8080
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./qbittorrent:/config
|
||||
- ./downloads:/downloads
|
||||
restart: unless-stopped
|
||||
app:
|
||||
container_name: qb-web
|
||||
depends_on:
|
||||
- qb.test
|
||||
# Using a Dockerfile is optional, but included for completeness.
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
# [Optional] You can use build args to set options. e.g. 'VARIANT' below affects the image in the Dockerfile
|
||||
args:
|
||||
VARIANT: 16-buster
|
||||
|
||||
volumes:
|
||||
# This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json
|
||||
- ..:/workspace:cached
|
||||
|
||||
# Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
|
||||
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:qb.test
|
||||
@@ -1,32 +1,13 @@
|
||||
name: CI
|
||||
name: Release
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
TZ: Asia/Shanghai
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- nightly-*
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
|
||||
- run: yarn run lint --no-fix --max-warnings 0
|
||||
- run: yarn run test:unit
|
||||
|
||||
build:
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/release'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
@@ -38,10 +19,9 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
|
||||
- run: echo ::set-env name=RELEASE_DATE::$(date +'%Y%m%d')
|
||||
- run: echo ::set-env name=GIT_TAG::nightly-$RELEASE_DATE
|
||||
- run: echo ::set-env name=RELEASE_FILE::qb-web-$GIT_TAG.zip
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_FILE=qb-web-${GITHUB_REF#refs/*/}.zip" >> $GITHUB_ENV
|
||||
|
||||
- name: Pack Release
|
||||
run: |
|
||||
@@ -62,19 +42,16 @@ jobs:
|
||||
git add --all
|
||||
git commit -m "Publish"
|
||||
git push origin gh-pages -f
|
||||
|
||||
- name: Add tag
|
||||
run: |
|
||||
git tag $GIT_TAG
|
||||
git push origin $GIT_TAG
|
||||
|
||||
- id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ env.GIT_TAG }}
|
||||
release_name: ${{ env.GIT_TAG }}
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
prerelease: true
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
21
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
|
||||
- run: yarn run lint --no-fix --max-warnings 0
|
||||
- run: yarn run test:unit
|
||||
3
.gitignore
vendored
@@ -20,3 +20,6 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/.devcontainer/downloads
|
||||
/.devcontainer/qbittorrent
|
||||
@@ -8,7 +8,7 @@
|
||||
## Features
|
||||
Keywords: SPA, RSS, Search, Responsive Design, Modern Design, i18n
|
||||
|
||||
Languages: English, 中文
|
||||
Languages: English, 中文, Русский, Türkçe
|
||||
|
||||
[TODO](https://github.com/CzBiX/qb-web/projects/2)
|
||||
|
||||
@@ -17,7 +17,7 @@ see: [Wiki](https://github.com/CzBiX/qb-web/wiki/How-to-use)
|
||||
|
||||
## Wiki
|
||||
|
||||
[Use Nginx to running multi WebUI at the same time](https://github.com/CzBiX/qb-web/wiki/Use-Nginx-to-running-multi-WebUI-at-the-same-time)
|
||||
[Running multi WebUI at the same time](https://github.com/CzBiX/qb-web/wiki/Running-multi-WebUI-at-the-same-time)
|
||||
|
||||
## Screenshot
|
||||
|
||||
|
||||
74
bin/fix-site-icon-size.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
/* eslint-disable no-console */
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
const ICON_DIR = 'src/assets/site_icons'
|
||||
const ICON_SIZE = 48
|
||||
|
||||
function execShell(command) {
|
||||
const output = execSync(command)
|
||||
return output.toString().trim()
|
||||
}
|
||||
|
||||
function isIconFile(iconPath) {
|
||||
const output = execShell(`file -b --mime-type ${iconPath}`)
|
||||
return output.includes('image/vnd.microsoft.icon')
|
||||
}
|
||||
|
||||
function getIconIndex(iconPath) {
|
||||
const output = execShell(`magick identify -format "%s:%w\\n" ${iconPath}`)
|
||||
let lastIndex = 0
|
||||
for (const line of output.split('\n')) {
|
||||
const [index, width] = line.split(':')
|
||||
const size = parseInt(width)
|
||||
if (size >= ICON_SIZE) {
|
||||
return index
|
||||
}
|
||||
|
||||
lastIndex = index
|
||||
}
|
||||
|
||||
return lastIndex
|
||||
}
|
||||
|
||||
function convertIcon(iconPath, outputPath) {
|
||||
const iconIndex = getIconIndex(iconPath)
|
||||
execShell(`magick convert -thumbnail "${ICON_SIZE}x${ICON_SIZE}>" ${iconPath}[${iconIndex}] ${outputPath}`)
|
||||
}
|
||||
|
||||
async function fixSiteIcon(name) {
|
||||
const iconPath = path.join(ICON_DIR, name)
|
||||
if (!isIconFile(iconPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const tmpPath = path.join(ICON_DIR, '_tmp.ico')
|
||||
await fs.copyFile(iconPath, tmpPath)
|
||||
try {
|
||||
convertIcon(tmpPath, iconPath)
|
||||
} finally {
|
||||
await fs.unlink(tmpPath)
|
||||
}
|
||||
|
||||
console.log(`Converted ${name}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let files = []
|
||||
if (process.argv.length > 2) {
|
||||
files = process.argv.slice(2)
|
||||
} else {
|
||||
files = await fs.readdir(ICON_DIR)
|
||||
}
|
||||
|
||||
files = files.filter(name => {
|
||||
return name.endsWith('.png')
|
||||
})
|
||||
|
||||
for (const name of files) {
|
||||
await fixSiteIcon(name)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
15
package.json
@@ -11,20 +11,20 @@
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.0.45",
|
||||
"@types/node-polyglot": "^0.4.34",
|
||||
"@vue/composition-api": "^0.5.0",
|
||||
"axios": "^0.19.2",
|
||||
"@vue/composition-api": "^1.0.5",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.6.5",
|
||||
"dayjs": "^1.8.23",
|
||||
"debug": "^4.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.21",
|
||||
"node-polyglot": "^2.4.0",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-property-decorator": "^8.4.2",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.2.20",
|
||||
"vuetify": "^2.5.8",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,10 +60,5 @@
|
||||
"*.{js,vue,ts}": [
|
||||
"vue-cli-service lint"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"babel-jest": "^25.0.0",
|
||||
"ts-jest": "^25.0.0",
|
||||
"jest": "^25.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
35
src/Api.ts
@@ -1,6 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import Axios, { AxiosInstance, AxiosPromise, AxiosResponse } from 'axios';
|
||||
import { RssNode, RssRule, SearchPlugin, ApiCategory, SearchTaskResponse, Preferences } from '@/types';
|
||||
import {
|
||||
RssNode,
|
||||
RssRule,
|
||||
SearchPlugin,
|
||||
ApiCategory,
|
||||
SearchTaskResponse,
|
||||
Preferences,
|
||||
MainData,
|
||||
} from '@/types'
|
||||
|
||||
const apiEndpoint = 'api/v2';
|
||||
|
||||
@@ -62,7 +70,7 @@ class Api {
|
||||
return this.axios.post('/app/shutdown');
|
||||
}
|
||||
|
||||
public getMainData(rid?: number) {
|
||||
public getMainData(rid?: number): AxiosPromise<MainData> {
|
||||
const params = {
|
||||
rid,
|
||||
};
|
||||
@@ -149,6 +157,10 @@ class Api {
|
||||
return this.actionTorrents('setForceStart', hashes, { value: 'true' });
|
||||
}
|
||||
|
||||
public toggleSequentialTorrents(hashes: string[]) {
|
||||
return this.actionTorrents('toggleSequentialDownload', hashes);
|
||||
}
|
||||
|
||||
public reannounceTorrents(hashes: string[]) {
|
||||
return this.actionTorrents('reannounce', hashes);
|
||||
}
|
||||
@@ -162,13 +174,7 @@ class Api {
|
||||
}
|
||||
|
||||
public getTorrentTracker(hash: string) {
|
||||
const params = {
|
||||
hash,
|
||||
};
|
||||
|
||||
return this.axios.get('/torrents/trackers', {
|
||||
params,
|
||||
}).then(Api.handleResponse);
|
||||
return this.actionTorrent('trackers', hash);
|
||||
}
|
||||
|
||||
public getTorrentPeers(hash: string, rid?: number) {
|
||||
@@ -183,7 +189,7 @@ class Api {
|
||||
}
|
||||
|
||||
public editTracker(hash: string, origUrl: string, newUrl: string) {
|
||||
return this.actionTorrents('editTracker', [hash], { origUrl, newUrl });
|
||||
return this.actionTorrent('editTracker', hash, { origUrl, newUrl });
|
||||
}
|
||||
|
||||
public setTorrentLocation(hashes: string[], location: string) {
|
||||
@@ -339,6 +345,15 @@ class Api {
|
||||
return this.axios.post('/search/enablePlugin', body).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
private actionTorrent(action: string, hash: string, extra?: any) {
|
||||
const params: any = {
|
||||
hash,
|
||||
...extra,
|
||||
};
|
||||
const data = new URLSearchParams(params);
|
||||
return this.axios.post(`/torrents/${action}`, data).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
private actionTorrents(action: string, hashes: string[], extra?: any) {
|
||||
const params: any = {
|
||||
hashes: hashes.join('|'),
|
||||
|
||||
16
src/App.vue
@@ -82,6 +82,7 @@ import { Watch } from 'vue-property-decorator';
|
||||
import { MainData } from './types';
|
||||
import { Config } from './store/config';
|
||||
import Api from './Api';
|
||||
import {formatSize} from '@/filters'
|
||||
|
||||
let appWrapEl: HTMLElement;
|
||||
|
||||
@@ -120,7 +121,7 @@ let appWrapEl: HTMLElement;
|
||||
},
|
||||
})
|
||||
export default class App extends Vue {
|
||||
drawer = true
|
||||
drawer = !this.phoneLayout
|
||||
drawerOptions = {
|
||||
showLogs: false,
|
||||
showRss: false,
|
||||
@@ -205,7 +206,18 @@ export default class App extends Vue {
|
||||
const mainData = resp.data;
|
||||
|
||||
this.updateMainData(mainData);
|
||||
|
||||
if(this.config.displaySpeedInTitle) {
|
||||
const upInfoSpeed = mainData.server_state.up_info_speed
|
||||
const dlInfoSpeed = mainData.server_state.dl_info_speed
|
||||
let dl = '', up = ''
|
||||
if (dlInfoSpeed > 1024) {
|
||||
dl = `D ${formatSize(dlInfoSpeed)}/s`
|
||||
}
|
||||
if (upInfoSpeed > 1024) {
|
||||
up = `U ${formatSize(upInfoSpeed)}/s`
|
||||
}
|
||||
document.title = `[${up} ${dl}] qBittorrent Web UI`
|
||||
}
|
||||
this.task = setTimeout(this.getMainData, this.config.updateInterval);
|
||||
}
|
||||
|
||||
|
||||
BIN
src/assets/site_icons/2xfree.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 138 B After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/site_icons/hares.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/site_icons/hdsky.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/site_icons/hdtime.png
Normal file
|
After Width: | Height: | Size: 818 B |
BIN
src/assets/site_icons/kamept.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/assets/site_icons/lemonhd.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/site_icons/opencd.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/assets/site_icons/ourbits.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/site_icons/pterclub.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/site_icons/pthome.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/site_icons/ptsbao.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/assets/site_icons/pttime.png
Normal file
|
After Width: | Height: | Size: 1010 B |
BIN
src/assets/site_icons/soulvoice.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/site_icons/totheglory.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 742 B After Width: | Height: | Size: 1.9 KiB |
@@ -60,58 +60,58 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { sortBy, sumBy, defaultTo, isUndefined } from 'lodash';
|
||||
import { sortBy, sumBy, isUndefined } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import { Torrent, Category } from '@/types';
|
||||
import { Torrent, Category, Tag } from '@/types';
|
||||
import FilterGroup from './drawer/FilterGroup.vue';
|
||||
import api from '../Api';
|
||||
import { formatSize } from '../filters';
|
||||
import { StateType } from '../consts';
|
||||
import { formatSize } from '@/filters';
|
||||
import { StateType } from '@/consts';
|
||||
import SiteMap from '@/sites'
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Emit } from 'vue-property-decorator';
|
||||
|
||||
const stateList = [
|
||||
{
|
||||
title: tr('state.downloading'),
|
||||
title: tr('category_state.downloading'),
|
||||
state: StateType.Downloading,
|
||||
icon: 'download',
|
||||
},
|
||||
{
|
||||
title: tr('state.seeding'),
|
||||
title: tr('category_state.seeding'),
|
||||
state: StateType.Seeding,
|
||||
icon: 'upload',
|
||||
},
|
||||
{
|
||||
title: tr('state.completed'),
|
||||
title: tr('category_state.completed'),
|
||||
state: StateType.Completed,
|
||||
icon: 'check',
|
||||
},
|
||||
{
|
||||
title: tr('state.resumed'),
|
||||
title: tr('category_state.resumed'),
|
||||
state: StateType.Resumed,
|
||||
icon: 'play',
|
||||
},
|
||||
{
|
||||
title: tr('state.paused'),
|
||||
title: tr('category_state.paused'),
|
||||
state: StateType.Paused,
|
||||
icon: 'pause',
|
||||
},
|
||||
{
|
||||
title: tr('state.active'),
|
||||
title: tr('category_state.active'),
|
||||
state: StateType.Active,
|
||||
icon: 'filter',
|
||||
},
|
||||
{
|
||||
title: tr('state.inactive'),
|
||||
title: tr('category_state.inactive'),
|
||||
state: StateType.Inactive,
|
||||
icon: 'filter-outline',
|
||||
},
|
||||
{
|
||||
title: tr('state.errored'),
|
||||
title: tr('category_state.errored'),
|
||||
state: StateType.Errored,
|
||||
icon: 'alert',
|
||||
},
|
||||
@@ -132,6 +132,14 @@ interface MenuChildrenItem extends MenuItem {
|
||||
append?: string;
|
||||
}
|
||||
|
||||
function getTopDomain(host: string) {
|
||||
const parts = host.split('.');
|
||||
if (parts.length > 2) {
|
||||
return parts.slice(-2).join('.');
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
FilterGroup,
|
||||
@@ -141,7 +149,9 @@ interface MenuChildrenItem extends MenuItem {
|
||||
'isDataReady',
|
||||
'allTorrents',
|
||||
'allCategories',
|
||||
'allTags',
|
||||
'torrentGroupByCategory',
|
||||
'torrentGroupByTag',
|
||||
'torrentGroupBySite',
|
||||
'torrentGroupByState',
|
||||
]),
|
||||
@@ -151,41 +161,32 @@ export default class Drawer extends Vue {
|
||||
@Prop()
|
||||
readonly value: any
|
||||
|
||||
basicItems: MenuItem[] = [
|
||||
{ icon: 'mdi-cog-box', title: tr('settings'), click: () => this.updateOptions('showSettings', true) },
|
||||
]
|
||||
|
||||
endItems: MenuItem[] = [
|
||||
{ icon: 'mdi-delta', title: tr('logs'), click: () => this.updateOptions('showLogs', true) },
|
||||
{ icon: 'mdi-card-search-outline', title: tr('search'), click: () => this.updateOptions('showSearch', true) },
|
||||
]
|
||||
|
||||
pcItems: MenuItem[] = [
|
||||
{ icon: 'mdi-rss-box', title: 'RSS', click: () => this.updateOptions('showRss', true) },
|
||||
{ icon: 'mdi-cog-box', title: tr('settings'), click: () => this.updateOptions('showSettings', true) },
|
||||
{ icon: 'mdi-history', title: tr('label.switch_to_old_ui'), click: this.switchUi },
|
||||
]
|
||||
|
||||
isDataReady!: boolean
|
||||
allTorrents!: Torrent[]
|
||||
allCategories!: Category[]
|
||||
allTags!: Tag[]
|
||||
torrentGroupByCategory!: {[category: string]: Torrent[]}
|
||||
torrentGroupByTag!: {[tag: string]: Torrent[]}
|
||||
torrentGroupBySite!: {[site: string]: Torrent[]}
|
||||
torrentGroupByState!: {[state: string]: Torrent[]}
|
||||
|
||||
created() {
|
||||
const searchMenuItem = {
|
||||
icon: 'mdi-card-search-outline',
|
||||
title: tr('search'),
|
||||
click: () => this.updateOptions('showSearch', true),
|
||||
};
|
||||
|
||||
if (this.phoneLayout) {
|
||||
this.endItems = this.endItems.concat([
|
||||
searchMenuItem,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.endItems = this.endItems.concat([
|
||||
{ icon: 'mdi-rss-box', title: 'RSS', click: () => this.updateOptions('showRss', true) },
|
||||
searchMenuItem,
|
||||
{ icon: 'mdi-history', title: tr('label.switch_to_old_ui'), click: this.switchUi },
|
||||
])
|
||||
this.endItems = this.endItems.concat(this.pcItems)
|
||||
}
|
||||
|
||||
get phoneLayout() {
|
||||
@@ -225,12 +226,31 @@ export default class Drawer extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
buildTagGroup(): MenuChildrenItem[] {
|
||||
return [{
|
||||
key: '',
|
||||
name: tr('untagged'),
|
||||
}].concat(this.allTags).map((tag) => {
|
||||
let value = this.torrentGroupByTag[tag.key];
|
||||
if (isUndefined(value)) {
|
||||
value = [];
|
||||
}
|
||||
const size = formatSize(sumBy(value, 'size'));
|
||||
const title = `${tag.name} (${value.length})`;
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon: 'mdi-folder', title, key: tag.key, append,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
buildSiteGroup(): MenuChildrenItem[] {
|
||||
return sortBy(Object.entries(this.torrentGroupBySite).map(([key, value]) => {
|
||||
const size = formatSize(sumBy(value, 'size'));
|
||||
const site = SiteMap[key];
|
||||
const domain = getTopDomain(key);
|
||||
const site = SiteMap[domain];
|
||||
const title = `${site ? site.name : (key || tr('others'))} (${value.length})`;
|
||||
const icon = defaultTo(site ? site.icon : null, 'mdi-server');
|
||||
const icon = site?.icon ?? 'mdi-server';
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon, title, key, append,
|
||||
@@ -240,7 +260,7 @@ export default class Drawer extends Vue {
|
||||
|
||||
get items() {
|
||||
if (!this.isDataReady) {
|
||||
return this.basicItems.concat(this.endItems);
|
||||
return this.endItems
|
||||
}
|
||||
|
||||
const filterGroups: MenuItem[] = [];
|
||||
@@ -249,7 +269,7 @@ export default class Drawer extends Vue {
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('state._'),
|
||||
title: tr('category_state._'),
|
||||
model: null,
|
||||
select: 'state',
|
||||
children: [
|
||||
@@ -274,6 +294,20 @@ export default class Drawer extends Vue {
|
||||
],
|
||||
});
|
||||
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('tag', 0),
|
||||
model: null,
|
||||
select: 'tag',
|
||||
children: [
|
||||
{
|
||||
icon: 'mdi-folder', title: `${tr('all')} (${this.allTorrents.length})`, key: null, append: `[${totalSize}]`,
|
||||
},
|
||||
...this.buildTagGroup(),
|
||||
],
|
||||
});
|
||||
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
@@ -288,7 +322,7 @@ export default class Drawer extends Vue {
|
||||
],
|
||||
});
|
||||
|
||||
return this.basicItems.concat([{filterGroups}] as any, this.endItems);
|
||||
return ([] as MenuItem[]).concat([{filterGroups}] as any, this.endItems);
|
||||
}
|
||||
|
||||
async switchUi() {
|
||||
|
||||
@@ -39,10 +39,24 @@
|
||||
class="mx-2"
|
||||
v-if="!phoneLayout"
|
||||
/>
|
||||
<div class="icon-label">
|
||||
<v-icon>mdi-nas</v-icon>
|
||||
{{ info.free_space_on_disk | formatSize }}
|
||||
</div>
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on }">
|
||||
<div
|
||||
class="icon-label"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-nas</v-icon>
|
||||
{{ info.free_space_on_disk | formatSize }}
|
||||
</div>
|
||||
</template>
|
||||
<span>
|
||||
Queued I/O jobs: {{ info.queued_io_jobs }}
|
||||
</span>
|
||||
<br>
|
||||
<span>
|
||||
Avg queue time: {{ info.average_time_queue }} ms
|
||||
</span>
|
||||
</v-tooltip>
|
||||
<v-divider
|
||||
vertical
|
||||
class="mx-2"
|
||||
@@ -189,7 +203,7 @@ import api from '../Api';
|
||||
import buildInfo from '@/buildInfo';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Watch } from 'vue-property-decorator';
|
||||
import { Torrent, ServerState } from '../types';
|
||||
import { Torrent, ServerState } from '@/types';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -314,7 +328,7 @@ export default class Footer extends Vue {
|
||||
align-items: center;
|
||||
|
||||
.v-icon {
|
||||
margin-right: 4px;
|
||||
//margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
}
|
||||
|
||||
clickBtn(null);
|
||||
}, { lazy: true });
|
||||
});
|
||||
|
||||
const btns = computed(() => {
|
||||
const c = config.value;
|
||||
|
||||
@@ -45,12 +45,11 @@ import { mapMutations } from 'vuex';
|
||||
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Emit } from 'vue-property-decorator';
|
||||
import { ConfigPayload } from '@/store/types';
|
||||
|
||||
@Component({
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'updateConfig',
|
||||
'setQuery',
|
||||
]),
|
||||
},
|
||||
})
|
||||
@@ -58,12 +57,12 @@ export default class MainToolbar extends Vue {
|
||||
@Prop(Boolean)
|
||||
readonly value!: boolean
|
||||
|
||||
updateConfig!: (_: ConfigPayload) => void
|
||||
setQuery!: (_: string | null) => void
|
||||
|
||||
focusedSearch = false
|
||||
|
||||
get searchQuery() {
|
||||
return this.$store.getters.config.filter.query;
|
||||
return this.$store.state.query;
|
||||
}
|
||||
|
||||
get phoneLayout() {
|
||||
@@ -82,12 +81,7 @@ export default class MainToolbar extends Vue {
|
||||
onSearch = throttle(async (v: string) => {
|
||||
// avoid input lag
|
||||
await this.$nextTick();
|
||||
this.updateConfig({
|
||||
key: 'filter',
|
||||
value: {
|
||||
query: v,
|
||||
},
|
||||
});
|
||||
this.setQuery(v || null);
|
||||
}, 400)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -102,11 +102,19 @@
|
||||
vertical
|
||||
inset
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
@click="toggleSequentialTorrents"
|
||||
:title="$t('toggle_sequential')"
|
||||
:disabled="!hasSelected"
|
||||
>
|
||||
<v-icon>mdi-transit-connection-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
@click="setTorrentLocation"
|
||||
:title="$t('title.set_location')"
|
||||
:disabled="selectedRows.length == 0"
|
||||
:disabled="selectedRows.length === 0"
|
||||
>
|
||||
<v-icon>mdi-folder-marker</v-icon>
|
||||
</v-btn>
|
||||
@@ -128,7 +136,7 @@
|
||||
icon
|
||||
@click="recheckTorrents"
|
||||
:title="$t('recheck')"
|
||||
:disabled="selectedRows.length == 0"
|
||||
:disabled="selectedRows.length === 0"
|
||||
>
|
||||
<v-icon>mdi-backup-restore</v-icon>
|
||||
</v-btn>
|
||||
@@ -180,7 +188,7 @@
|
||||
<v-progress-linear
|
||||
height="1.4em"
|
||||
:value="row.item.progress * 100"
|
||||
:color="row.item.state | stateColor(true)"
|
||||
:color="row.item.state | stateColor(true, row.item.seq_dl)"
|
||||
class="text-center ma-0"
|
||||
>
|
||||
<span :class="getProgressColorClass(row.item.progress)">
|
||||
@@ -188,7 +196,7 @@
|
||||
</span>
|
||||
</v-progress-linear>
|
||||
</td>
|
||||
<td>{{ row.item.state }}</td>
|
||||
<td>{{ $t('torrent_state.' + row.item.state) }}</td>
|
||||
<td>{{ row.item.num_seeds }}/{{ row.item.num_complete }}</td>
|
||||
<td>{{ row.item.num_leechs }}/{{ row.item.num_incomplete }}</td>
|
||||
<td>{{ row.item.dlspeed | formatNetworkSpeed }}</td>
|
||||
@@ -237,10 +245,10 @@ import ConfirmSetCategoryDialog from './dialogs/ConfirmSetCategoryDialog.vue'
|
||||
import EditTrackerDialog from './dialogs/EditTrackerDialog.vue'
|
||||
import InfoDialog from './dialogs/InfoDialog.vue'
|
||||
import api from '../Api'
|
||||
import { formatSize } from '../filters'
|
||||
import { DialogType, TorrentFilter, ConfigPayload, DialogConfig, SnackBarConfig } from '../store/types'
|
||||
import { formatSize } from '@/filters'
|
||||
import { DialogType, TorrentFilter, ConfigPayload, DialogConfig, SnackBarConfig } from '@/store/types'
|
||||
import Component from 'vue-class-component'
|
||||
import { Torrent, Category } from '../types'
|
||||
import { Torrent, Category, Tag } from '@/types'
|
||||
import { Watch } from 'vue-property-decorator'
|
||||
|
||||
function getStateInfo(state: string) {
|
||||
@@ -329,8 +337,10 @@ function getStateInfo(state: string) {
|
||||
...mapGetters([
|
||||
'isDataReady',
|
||||
'allTorrents',
|
||||
'allTags',
|
||||
'allCategories',
|
||||
'torrentGroupByCategory',
|
||||
'torrentGroupByTag',
|
||||
'torrentGroupBySite',
|
||||
'torrentGroupByState',
|
||||
]),
|
||||
@@ -338,6 +348,9 @@ function getStateInfo(state: string) {
|
||||
filter(state, getters) {
|
||||
return getters.config.filter;
|
||||
},
|
||||
query(state: any) {
|
||||
return state.query;
|
||||
},
|
||||
}),
|
||||
},
|
||||
filters: {
|
||||
@@ -352,11 +365,14 @@ function getStateInfo(state: string) {
|
||||
const item = getStateInfo(state);
|
||||
return `mdi-${item.icon}`;
|
||||
},
|
||||
stateColor(state: string, isProgress?: boolean) {
|
||||
stateColor(state: string, isProgress?: boolean, isSeqDL?: boolean) {
|
||||
const item = getStateInfo(state);
|
||||
if (!isProgress) {
|
||||
return item.color;
|
||||
}
|
||||
if (isSeqDL) {
|
||||
return '#e33371' // icon.color.secondary;
|
||||
}
|
||||
|
||||
return item.color || '#0008';
|
||||
},
|
||||
@@ -402,10 +418,13 @@ export default class Torrents extends Vue {
|
||||
isDataReady!: boolean
|
||||
allTorrents!: Torrent[]
|
||||
allCategories!: Category[]
|
||||
allTags!: Tag[]
|
||||
torrentGroupByCategory!: {[category: string]: Torrent[]}
|
||||
torrentGroupByTag!: {[tag: string]: Torrent[]}
|
||||
torrentGroupBySite!: {[site: string]: Torrent[]}
|
||||
torrentGroupByState!: {[state: string]: Torrent[]}
|
||||
filter!: TorrentFilter
|
||||
query!: string | null
|
||||
|
||||
updateConfig!: (_: ConfigPayload) => void
|
||||
showSnackBar!: (_: SnackBarConfig) => void
|
||||
@@ -433,11 +452,14 @@ export default class Torrents extends Vue {
|
||||
if (this.filter.category !== null) {
|
||||
list = intersection(list, this.torrentGroupByCategory[this.filter.category]);
|
||||
}
|
||||
if (this.filter.tag !== null) {
|
||||
list = intersection(list, this.torrentGroupByTag[this.filter.tag]);
|
||||
}
|
||||
if (this.filter.state !== null) {
|
||||
list = intersection(list, this.torrentGroupByState[this.filter.state]);
|
||||
}
|
||||
if (this.filter.query) {
|
||||
const q = this.filter.query.toLowerCase();
|
||||
if (this.query) {
|
||||
const q = this.query.toLowerCase();
|
||||
|
||||
list = list.filter(t => {
|
||||
return t.name.toLowerCase().includes(q) ||
|
||||
@@ -480,6 +502,9 @@ export default class Torrents extends Vue {
|
||||
await api.setForceStartTorrents(this.selectedHashes);
|
||||
}
|
||||
|
||||
async toggleSequentialTorrents() {
|
||||
await api.toggleSequentialTorrents(this.selectedHashes);
|
||||
}
|
||||
async pauseTorrents() {
|
||||
await api.pauseTorrents(this.selectedHashes);
|
||||
}
|
||||
|
||||
@@ -76,17 +76,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { chunk, countBy } from 'lodash';
|
||||
import {chunk, countBy} from 'lodash'
|
||||
|
||||
import api from '../../Api';
|
||||
import {
|
||||
formatDuration, formatSize, formatTimestamp, toPrecision,
|
||||
} from '@/filters';
|
||||
import api from '../../Api'
|
||||
import {formatDuration, formatSize, formatTimestamp, toPrecision} from '@/filters'
|
||||
|
||||
import { TorrentProperties, Torrent } from '@/types'
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Watch } from 'vue-property-decorator';
|
||||
import BaseTorrentInfo from './baseTorrentInfo';
|
||||
import {Torrent, TorrentProperties} from '@/types'
|
||||
import Component from 'vue-class-component'
|
||||
import {Prop, Watch} from 'vue-property-decorator'
|
||||
import BaseTorrentInfo from './baseTorrentInfo'
|
||||
|
||||
interface Item {
|
||||
label: string;
|
||||
@@ -153,8 +151,7 @@ export default class TorrentInfo extends BaseTorrentInfo {
|
||||
el.height = clientHeight;
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
const ctx = el.getContext('2d')!;
|
||||
return ctx;
|
||||
return el.getContext('2d')!;
|
||||
}
|
||||
|
||||
fetchInfo() {
|
||||
|
||||
@@ -1,35 +1,146 @@
|
||||
<template>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header> Downloads </v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<h4>When adding a torrent</h4>
|
||||
<v-divider />
|
||||
|
||||
<v-container
|
||||
class="px-0"
|
||||
fluid
|
||||
>
|
||||
<v-switch
|
||||
:input-value="preferences.create_subfolder_enabled"
|
||||
:label="`Create subfolder for torrents with multiple files`"
|
||||
@change="changeSettings('create_subfolder_enabled', !preferences.create_subfolder_enabled)"
|
||||
<v-container>
|
||||
<h4>{{ $t('preferences.adding_torrent') }}</h4>
|
||||
<v-divider />
|
||||
<v-container
|
||||
class="px-0"
|
||||
fluid
|
||||
>
|
||||
<v-switch
|
||||
:input-value="preferences.create_subfolder_enabled"
|
||||
:label="$t('preferences.create_subfolder_enabled')"
|
||||
@change="changeSettings('create_subfolder_enabled', !preferences.create_subfolder_enabled)"
|
||||
/>
|
||||
<v-switch
|
||||
:input-value="preferences.start_paused_enabled"
|
||||
:label="$t('preferences.start_paused_enabled')"
|
||||
@change="changeSettings('start_paused_enabled', !preferences.start_paused_enabled)"
|
||||
/>
|
||||
<v-switch
|
||||
:input-value="preferences.auto_delete_mode"
|
||||
:label="$t('preferences.auto_delete_mode')"
|
||||
@change="changeSettings('auto_delete_mode', !preferences.auto_delete_mode)"
|
||||
/>
|
||||
</v-container>
|
||||
<v-divider />
|
||||
<v-container
|
||||
class="px-0"
|
||||
fluid
|
||||
>
|
||||
<v-switch
|
||||
:input-value="preferences.preallocate_all"
|
||||
:label="$t('preferences.preallocate_all')"
|
||||
@change="changeSettings('preallocate_all', !preferences.preallocate_all)"
|
||||
/>
|
||||
<v-switch
|
||||
:input-value="preferences.incomplete_files_ext"
|
||||
:label="$t('preferences.incomplete_files_ext')"
|
||||
@change="changeSettings('incomplete_files_ext', !preferences.incomplete_files_ext)"
|
||||
/>
|
||||
</v-container>
|
||||
<h4>{{ $t('preferences.saving_management') }}</h4>
|
||||
<v-divider />
|
||||
<v-container
|
||||
class="px-0"
|
||||
fluid
|
||||
>
|
||||
<preference-row i18n-key="auto_tmm_enabled">
|
||||
<v-select
|
||||
dense
|
||||
:items="torrentMode"
|
||||
:value="preferences.auto_tmm_enabled ? torrentMode[0] : torrentMode[1]"
|
||||
@change="changeSettings('auto_tmm_enabled', $event == torrentMode[0])"
|
||||
/>
|
||||
</v-container>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="torrent_changed_tmm_enabled">
|
||||
<v-select
|
||||
dense
|
||||
:items="torrentAction"
|
||||
:value="preferences.category_changed_tmm_enabled ? torrentAction[1] : torrentAction[0]"
|
||||
@change="changeSettings('torrent_changed_tmm_enabled', $event == torrentAction[1])"
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="save_path_changed_tmm_enabled">
|
||||
<v-select
|
||||
dense
|
||||
:items="torrentAction"
|
||||
:value="preferences.category_changed_tmm_enabled ? torrentAction[1] : torrentAction[0]"
|
||||
@change="changeSettings('save_path_changed_tmm_enabled', $event == torrentAction[1])"
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="category_changed_tmm_enabled">
|
||||
<v-select
|
||||
dense
|
||||
:items="torrentAction"
|
||||
:value="preferences.category_changed_tmm_enabled ? torrentAction[1] : torrentAction[0]"
|
||||
@change="changeSettings('category_changed_tmm_enabled', $event == torrentAction[1])"
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="save_path">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="preferences.save_path"
|
||||
@change="changeSettings('save_path', $event)"
|
||||
lazy
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="temp_path">
|
||||
<template v-slot:header>
|
||||
<v-checkbox
|
||||
dense
|
||||
:value="preferences.temp_path_enabled"
|
||||
@change="changeSettings('temp_path_enabled', $event)"
|
||||
/>
|
||||
</template>
|
||||
<v-text-field
|
||||
:disabled="!preferences.temp_path_enabled"
|
||||
:value="preferences.temp_path"
|
||||
@change="changeSettings('temp_path', $event)"
|
||||
lazy
|
||||
dense
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row
|
||||
i18n-key="export_dir"
|
||||
can-be-enabled="true"
|
||||
>
|
||||
<v-text-field
|
||||
:value="preferences.export_dir"
|
||||
@change="changeSettings('export_dir', $event)"
|
||||
lazy
|
||||
clearable
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row
|
||||
i18n-key="export_dir_fin"
|
||||
can-be-enabled="true"
|
||||
>
|
||||
<v-text-field
|
||||
:value="preferences.export_dir_fin"
|
||||
@change="changeSettings('export_dir_fin', $event)"
|
||||
lazy
|
||||
clearable
|
||||
/>
|
||||
</preference-row>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { Preferences } from "@/types";
|
||||
import { Component } from "vue-property-decorator";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import Vue from 'vue'
|
||||
import {Preferences} from '@/types'
|
||||
import {Component} from 'vue-property-decorator'
|
||||
import {mapActions, mapGetters} from 'vuex'
|
||||
import PreferenceRow from './PreferenceRow.vue'
|
||||
import { tr } from '@/locale'
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
components: {
|
||||
PreferenceRow,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
preferences: "allPreferences",
|
||||
preferences: 'allPreferences',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
@@ -39,12 +150,14 @@ import { mapActions, mapGetters } from "vuex";
|
||||
},
|
||||
})
|
||||
export default class DownloadSettings extends Vue {
|
||||
preferences!: Preferences;
|
||||
preferences!: Preferences
|
||||
torrentAction = [tr('preferences.switch_torrent_mode_to_manual'), tr('preferences.move_affected_torrent')]
|
||||
torrentMode = [tr('preferences.auto_mode'), tr('preferences.manual_mode')]
|
||||
|
||||
updatePreferencesRequest!: (_: any) => void;
|
||||
updatePreferencesRequest!: (_: any) => void
|
||||
|
||||
changeSettings(property: string, value: string | boolean) {
|
||||
this.updatePreferencesRequest({ [property]: value });
|
||||
this.updatePreferencesRequest({[property]: value})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -52,5 +165,14 @@ export default class DownloadSettings extends Vue {
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/assets/styles.scss";
|
||||
|
||||
h4 {
|
||||
margin-top: 8px;
|
||||
padding-left: 4px
|
||||
}
|
||||
|
||||
.v-input--switch {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@include dialog-title;
|
||||
</style>
|
||||
|
||||
37
src/components/dialogs/settingsDialog/PreferenceRow.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<v-row
|
||||
align="center"
|
||||
dense
|
||||
>
|
||||
<v-col
|
||||
cols="auto"
|
||||
v-if="$slots.header"
|
||||
class="header"
|
||||
>
|
||||
<slot name="header" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<span v-text="$t('preferences.' + this.$props.i18nKey)" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<slot />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from 'vue-property-decorator'
|
||||
import Vue from 'vue'
|
||||
|
||||
@Component
|
||||
export default class PreferenceRow extends Vue {
|
||||
@Prop(String)
|
||||
readonly i18nKey?: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
height: 48px;
|
||||
}
|
||||
</style>
|
||||
61
src/components/dialogs/settingsDialog/RssSettings.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-switch
|
||||
:input-value="preferences.rss_processing_enabled"
|
||||
:label="$t('preferences.rss_processing_enabled')"
|
||||
@change="changeSettings('rss_processing_enabled', !preferences.rss_processing_enabled)"
|
||||
/>
|
||||
<v-switch
|
||||
:input-value="preferences.rss_auto_downloading_enabled"
|
||||
:label="$t('preferences.rss_auto_downloading_enabled')"
|
||||
@change="changeSettings('rss_auto_downloading_enabled', !preferences.rss_auto_downloading_enabled)"
|
||||
/>
|
||||
<v-text-field
|
||||
suffix="min"
|
||||
type="number"
|
||||
:value="preferences.rss_refresh_interval"
|
||||
:label="$t('preferences.rss_refresh_interval')"
|
||||
@change="changeSettings('rss_refresh_interval', $event)"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {Preferences} from '@/types'
|
||||
import {Component} from 'vue-property-decorator'
|
||||
import {mapActions, mapGetters} from 'vuex'
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
preferences: 'allPreferences',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
updatePreferencesRequest: 'updatePreferencesRequest',
|
||||
}),
|
||||
},
|
||||
})
|
||||
export default class SpeedSettings extends Vue {
|
||||
preferences!: Preferences
|
||||
|
||||
updatePreferencesRequest!: (_: any) => void
|
||||
|
||||
changeSettings(property: string, value: string | boolean | number) {
|
||||
this.updatePreferencesRequest({[property]: value})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/assets/styles.scss";
|
||||
|
||||
.v-input--switch {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@include dialog-title;
|
||||
</style>
|
||||
@@ -1,57 +1,109 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-dialog
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
scrollable
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<v-icon class="mr-2">mdi-cog</v-icon>
|
||||
<span v-text="$t('settings')" />
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
@click="closeDialog"
|
||||
<v-dialog
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
scrollable
|
||||
persistent
|
||||
max-width="720px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<v-icon class="mr-2">mdi-cog</v-icon>
|
||||
<span v-text="$t('settings')" />
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
@click="closeDialog"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-tabs v-model="tab">
|
||||
<v-tab
|
||||
v-for="item of tabList"
|
||||
:key="item"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panels
|
||||
v-model="panelsOpen"
|
||||
multiple
|
||||
{{ $t('preferences.' + item) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-fade-transition>
|
||||
<v-alert
|
||||
dense
|
||||
text
|
||||
type="success"
|
||||
v-show="preferenceUpdated"
|
||||
>
|
||||
{{ $t('preferences.change_applied') }}
|
||||
</v-alert>
|
||||
</v-fade-transition>
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item key="downloads">
|
||||
<download-settings />
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
<v-card-actions />
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</v-tab-item>
|
||||
<v-tab-item key="speed">
|
||||
<speed-settings />
|
||||
</v-tab-item>
|
||||
<v-tab-item key="rss">
|
||||
<rss-settings />
|
||||
</v-tab-item>
|
||||
<v-tab-item key="webui">
|
||||
<web-u-i-settings />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { Component, Emit, Prop } from "vue-property-decorator";
|
||||
import DownloadSettings from "./DownloadSettings.vue";
|
||||
import Vue from 'vue'
|
||||
import {Component, Emit, Prop, Watch} from 'vue-property-decorator'
|
||||
import DownloadSettings from './DownloadSettings.vue'
|
||||
import SpeedSettings from './SpeedSettings.vue'
|
||||
import {mapGetters} from 'vuex'
|
||||
import {Preferences} from '@/types'
|
||||
import WebUISettings from './WebUISettings.vue'
|
||||
import RssSettings from './RssSettings.vue'
|
||||
import {Config} from '@/store/config'
|
||||
import { timeout } from '@/utils'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
DownloadSettings,
|
||||
SpeedSettings,
|
||||
WebUISettings,
|
||||
RssSettings,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
config: 'config',
|
||||
preferences: 'allPreferences',
|
||||
}),
|
||||
},
|
||||
methods: {},
|
||||
})
|
||||
export default class SearchDialog extends Vue {
|
||||
export default class SettingsDialog extends Vue {
|
||||
@Prop(Boolean)
|
||||
readonly value!: boolean;
|
||||
readonly value!: boolean
|
||||
preferences!: Preferences
|
||||
config!: Config
|
||||
|
||||
panelsOpen = [ 0 ];
|
||||
preferenceUpdated = false
|
||||
tabList = ['downloads', 'speed', 'rss', 'webui']
|
||||
tab = 'download'
|
||||
|
||||
@Emit("input")
|
||||
@Watch('preferences')
|
||||
@Watch('config')
|
||||
async onPreferenceUpdate() {
|
||||
this.preferenceUpdated = true
|
||||
await timeout(3000)
|
||||
this.preferenceUpdated = false
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -60,4 +112,7 @@ export default class SearchDialog extends Vue {
|
||||
@import "~@/assets/styles.scss";
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
::v-deep .v-card__text {
|
||||
}
|
||||
</style>
|
||||
|
||||
157
src/components/dialogs/settingsDialog/SpeedSettings.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-container
|
||||
fluid
|
||||
>
|
||||
<v-container>
|
||||
<v-row justify="center">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<h4> {{ $t('preferences.global_rate_limits') }}</h4>
|
||||
<v-text-field
|
||||
@change="changeSettings('dl_limit', convertToBytes($event))"
|
||||
:label="$t('preferences.dl_limit')"
|
||||
:placeholder="convertToKB(preferences.dl_limit)"
|
||||
lazy
|
||||
/>
|
||||
<v-text-field
|
||||
@change="changeSettings('up_limit', convertToBytes($event))"
|
||||
:label="$t('preferences.up_limit')"
|
||||
:placeholder="convertToKB(preferences.up_limit)"
|
||||
lazy
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<h4> {{ $t('preferences.alternate_rate_limits') }}</h4>
|
||||
<v-text-field
|
||||
type="number"
|
||||
@change="changeSettings('alt_dl_limit', convertToBytes($event))"
|
||||
:label="$t('preferences.dl_limit')"
|
||||
:placeholder="convertToKB(preferences.alt_dl_limit)"
|
||||
lazy
|
||||
/>
|
||||
<v-text-field
|
||||
type="number"
|
||||
@change="changeSettings('alt_up_limit', convertToBytes($event))"
|
||||
:label="$t('preferences.up_limit')"
|
||||
:placeholder="convertToKB(preferences.alt_up_limit)"
|
||||
lazy
|
||||
/>
|
||||
<v-checkbox
|
||||
:label="$t('preferences.alternate_schedule_enable_time')"
|
||||
@change="changeSettings('scheduler_enabled', $event)"
|
||||
:input-value="preferences.scheduler_enabled"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-if="preferences.scheduler_enabled"
|
||||
class="justify-center"
|
||||
>
|
||||
<v-col
|
||||
cols="auto"
|
||||
>
|
||||
<v-time-picker
|
||||
:value="preferences.schedule_from_hour + ':' + preferences.schedule_from_min"
|
||||
color="green lighten-1"
|
||||
format="24hr"
|
||||
header-color="primary"
|
||||
@input="updateSchedulerFrom($event)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="auto"
|
||||
>
|
||||
<v-time-picker
|
||||
:value="preferences.schedule_to_hour + ':' + preferences.schedule_to_min"
|
||||
color="green lighten-1"
|
||||
format="24hr"
|
||||
@input="updateSchedulerTo($event)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-container>
|
||||
<v-container
|
||||
class="px-0"
|
||||
fluid
|
||||
>
|
||||
<v-switch
|
||||
:input-value="preferences.limit_utp_rate"
|
||||
:label="$t('preferences.limit_utp_rate')"
|
||||
@change="changeSettings('limit_utp_rate', !preferences.limit_utp_rate)"
|
||||
/>
|
||||
<v-switch
|
||||
:input-value="preferences.limit_tcp_overhead"
|
||||
:label="$t('preferences.limit_tcp_overhead')"
|
||||
@change="changeSettings('limit_tcp_overhead', !preferences.limit_tcp_overhead)"
|
||||
/>
|
||||
<v-switch
|
||||
:input-value="preferences.limit_lan_peers"
|
||||
:label="$t('preferences.limit_lan_peers')"
|
||||
@change="changeSettings('limit_lan_peers', !preferences.limit_lan_peers)"
|
||||
/>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {Preferences} from '@/types'
|
||||
import {Component} from 'vue-property-decorator'
|
||||
import {mapActions, mapGetters} from 'vuex'
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
preferences: 'allPreferences',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
updatePreferencesRequest: 'updatePreferencesRequest',
|
||||
}),
|
||||
convertToKB(value: number): string {
|
||||
return (value / 1024).toString()
|
||||
},
|
||||
convertToBytes(value: number): number {
|
||||
return value * 1024
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class SpeedSettings extends Vue {
|
||||
preferences!: Preferences
|
||||
|
||||
updatePreferencesRequest!: (_: any) => void
|
||||
|
||||
changeSettings(property: string, value: string | boolean | number) {
|
||||
this.updatePreferencesRequest({[property]: value})
|
||||
}
|
||||
|
||||
updateSchedulerFrom(event: string) {
|
||||
const strings = event.split(':')
|
||||
this.updatePreferencesRequest({'schedule_from_hour': strings[0], 'schedule_from_min': strings[1]})
|
||||
}
|
||||
|
||||
updateSchedulerTo(event: string) {
|
||||
const strings = event.split(':')
|
||||
this.updatePreferencesRequest({'schedule_to_hour': strings[0], 'schedule_to_min': strings[1]})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/assets/styles.scss";
|
||||
|
||||
.v-input--switch {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@include dialog-title;
|
||||
</style>
|
||||
172
src/components/dialogs/settingsDialog/WebUISettings.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<h4>{{ $t("preferences.webui_remote_control") }}}</h4>
|
||||
<v-divider />
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col cols="2">
|
||||
<v-subheader>{{ $t("preferences.data_update_interval") }}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
:value="config.updateInterval"
|
||||
type="number"
|
||||
lazy
|
||||
@change="updateConfig({key: 'updateInterval', value: $event})"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col cols="2">
|
||||
<v-subheader>{{ $t("preferences.ip_address") }}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
:value="preferences.web_ui_address"
|
||||
@change="changeSettings('web_ui_address', $event)"
|
||||
lazy
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-subheader>{{ $t("preferences.ip_port") }}</v-subheader>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-text-field
|
||||
:value="preferences.web_ui_port"
|
||||
@change="changeSettings('web_ui_port', $event)"
|
||||
lazy
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
:label="$t('preferences.display_speed_in_title')"
|
||||
:input-value="config.displaySpeedInTitle"
|
||||
@change="updateTitleSpeedConfig($event)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<h4>{{ $t("preferences.authentication") }}</h4>
|
||||
<v-divider />
|
||||
<preference-row i18n-key="web_ui_username">
|
||||
<v-text-field
|
||||
:value="preferences.web_ui_username"
|
||||
@change="changeSettings('web_ui_username', $event)"
|
||||
lazy
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="web_ui_password">
|
||||
<v-text-field
|
||||
:value="preferences.web_ui_password"
|
||||
@change="changeSettings('web_ui_password', $event)"
|
||||
:placeholder="$t('preferences.new_password')"
|
||||
lazy
|
||||
/>
|
||||
</preference-row>
|
||||
<v-row dense>
|
||||
<v-col cols="auto">
|
||||
{{ $t("preferences.web_ui_max_auth_fail_count") }}
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-text-field
|
||||
:value="preferences.web_ui_max_auth_fail_count"
|
||||
@change="changeSettings('web_ui_max_auth_fail_count', $event)"
|
||||
lazy
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
{{ $t("preferences.web_ui_ban_duration") }}
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-text-field
|
||||
:value="preferences.web_ui_ban_duration"
|
||||
@change="changeSettings('web_ui_ban_duration', $event)"
|
||||
lazy
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
{{ $t("preferences.web_ui_seconds") }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
:input-value="preferences.bypass_auth_subnet_whitelist_enabled"
|
||||
:label="$t('preferences.bypass_auth_subnet_whitelist')"
|
||||
@change="changeSettings('bypass_auth_subnet_whitelist_enabled', $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
:input-value="preferences.bypass_local_auth"
|
||||
:label="$t('preferences.bypass_local_auth')"
|
||||
@change="changeSettings('bypass_local_auth', $event)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="4">
|
||||
<v-textarea
|
||||
:value="preferences.bypass_auth_subnet_whitelist"
|
||||
@change="changeSettings('bypass_auth_subnet_whitelist', $event)"
|
||||
lazy
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {Preferences} from '@/types'
|
||||
import {Component} from 'vue-property-decorator'
|
||||
import {mapActions, mapGetters, mapMutations} from 'vuex'
|
||||
import {Config} from '@/store/config'
|
||||
import { ConfigPayload } from '@/store/types';
|
||||
import PreferenceRow from '@/components/dialogs/settingsDialog/PreferenceRow.vue'
|
||||
|
||||
@Component({
|
||||
components: {PreferenceRow},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
config: 'config',
|
||||
preferences: 'allPreferences',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'updateConfig',
|
||||
]),
|
||||
...mapActions({
|
||||
updatePreferencesRequest: 'updatePreferencesRequest',
|
||||
}),
|
||||
},
|
||||
})
|
||||
export default class WebUISettings extends Vue {
|
||||
preferences!: Preferences
|
||||
config!: Config
|
||||
|
||||
updateConfig!: (_: ConfigPayload) => void
|
||||
updatePreferencesRequest!: (_: any) => void
|
||||
|
||||
changeSettings(property: string, value: string | boolean) {
|
||||
this.updatePreferencesRequest({[property]: value})
|
||||
}
|
||||
|
||||
updateTitleSpeedConfig(event: boolean) {
|
||||
this.updateConfig({
|
||||
key: 'displaySpeedInTitle',
|
||||
value: event,
|
||||
})
|
||||
if(!event) {
|
||||
document.title = 'qBittorrent Web UI'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -3,9 +3,15 @@ import Vue from 'vue';
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
export function toPrecision(value: number, precision: number) {
|
||||
if (value >= (10 ** precision)) {
|
||||
const limit = 10 ** precision;
|
||||
if (value >= limit) {
|
||||
return value.toString();
|
||||
} if (value >= 1) {
|
||||
}
|
||||
if (value >= 1) {
|
||||
if (value >= limit - 1) {
|
||||
return limit.toString();
|
||||
}
|
||||
|
||||
return value.toPrecision(precision);
|
||||
}
|
||||
|
||||
@@ -14,18 +20,20 @@ export function toPrecision(value: number, precision: number) {
|
||||
|
||||
export function formatSize(value: number): string {
|
||||
const units = 'KMGTP';
|
||||
let index = -1;
|
||||
let index = value ? Math.floor(Math.log2(value) / 10) : 0;
|
||||
|
||||
while (value >= 1000) {
|
||||
value = value / (1024 ** index);
|
||||
if (value >= 999) {
|
||||
value /= 1024;
|
||||
index++;
|
||||
}
|
||||
|
||||
const unit = index < 0 ? 'B' : `${units[index]}iB`;
|
||||
const unit = index === 0 ? 'B' : `${units[index - 1]}iB`;
|
||||
|
||||
if (index < 0) {
|
||||
if (index === 0) {
|
||||
return `${value} ${unit}`;
|
||||
}
|
||||
|
||||
return `${toPrecision(value, 3)} ${unit}`;
|
||||
}
|
||||
|
||||
|
||||
107
src/locale/en.ts
@@ -18,6 +18,7 @@ export default {
|
||||
resume: 'Resume',
|
||||
pause: 'Pause',
|
||||
force_start: 'Force Start',
|
||||
toggle_sequential: 'Toggle Sequential Download',
|
||||
info: 'Info',
|
||||
reset: 'Reset',
|
||||
login: 'Login',
|
||||
@@ -52,6 +53,8 @@ export default {
|
||||
all: 'All',
|
||||
category: 'Category |||| Categories',
|
||||
uncategorized: 'Uncategorized',
|
||||
tag: 'Tag |||| Tags',
|
||||
untagged: 'Untagged',
|
||||
others: 'Others',
|
||||
sites: 'Sites',
|
||||
files: 'Files',
|
||||
@@ -67,6 +70,72 @@ export default {
|
||||
plugin_manager: 'Plugin manager',
|
||||
update_plugins: 'Update plugins',
|
||||
|
||||
preferences: {
|
||||
change_applied: 'New preferences saved',
|
||||
downloads: 'Downloads',
|
||||
adding_torrent: 'When adding a torrent',
|
||||
create_subfolder_enabled: 'Create subfolder for torrents with multiple files',
|
||||
start_paused_enabled: 'Do not start the download automatically',
|
||||
auto_delete_mode: 'Delete .torrent files afterwards',
|
||||
preallocate_all: 'Pre-allocate disk space for all files',
|
||||
incomplete_files_ext: 'Append .!qB extension to incomplete files',
|
||||
saving_management: 'Saving Management',
|
||||
auto_tmm_enabled: 'Default Torrent Management Mode',
|
||||
torrent_changed_tmm_enabled: 'When Torrent Category changed',
|
||||
save_path_changed_tmm_enabled: 'When Default Save Path changed',
|
||||
category_changed_tmm_enabled: 'When Category Save Path changed',
|
||||
auto_mode: 'Automatic',
|
||||
manual_mode: 'Manual',
|
||||
switch_torrent_mode_to_manual: 'Switch affected torrent to manual mode',
|
||||
move_affected_torrent: 'Relocate affected torrents',
|
||||
save_path: 'Default Save Path',
|
||||
temp_path: 'Keep incomplete torrents in',
|
||||
export_dir: 'Copy .torrent files to',
|
||||
export_dir_fin: 'Copy .torrent files for finished downloads to',
|
||||
|
||||
speed: 'Speed',
|
||||
global_rate_limits: 'Global Rate Limits',
|
||||
alternate_rate_limits: 'Alternative Rate Limits',
|
||||
alternate_schedule_enable_time: 'Schedule the use of alternative rate limits',
|
||||
apply_speed_limit: 'Rate Limits Settings',
|
||||
dl_limit: 'Download (KiB/s)',
|
||||
up_limit: 'Upload (KiB/s)',
|
||||
zero_for_unlimited: '0 means unlimited',
|
||||
schedule_from: 'From',
|
||||
schedule_to: 'To',
|
||||
scheduler_days: 'When',
|
||||
limit_utp_rate: 'Apply rate limit to µTP protocol',
|
||||
limit_tcp_overhead: 'Apply rate limit to transport overhead',
|
||||
limit_lan_peers: 'Apply rate limit to peers on LAN',
|
||||
|
||||
connection: 'Connections',
|
||||
bittorrent: 'BitTorrent',
|
||||
|
||||
rss: 'RSS',
|
||||
rss_processing_enabled: 'Enable fetching RSS feeds',
|
||||
rss_auto_downloading_enabled: 'Enable auto downloading of RSS torrents',
|
||||
rss_refresh_interval: 'Feeds refresh interval',
|
||||
|
||||
webui: 'Web UI',
|
||||
data_update_interval: 'Data Update Interval (ms)',
|
||||
webui_remote_control: 'Web User Interface (Remote control)',
|
||||
ip_address: 'IP address',
|
||||
ip_port: 'Port',
|
||||
enable_upnp: 'Use UPnP / NAT-PMP to forward the port from my router',
|
||||
authentication: 'Authentication',
|
||||
web_ui_username: 'Username',
|
||||
web_ui_password: 'Password',
|
||||
bypass_local_auth: 'Bypass authentication for clients on localhost',
|
||||
bypass_auth_subnet_whitelist: 'Bypass authentication for clients in whitelisted IP subnets',
|
||||
web_ui_session_timeout: 'Session timeout',
|
||||
web_ui_max_auth_fail_count: 'Ban client after consecutive failures',
|
||||
web_ui_ban_duration: 'ban for',
|
||||
web_ui_seconds: 'seconds',
|
||||
new_password: 'Change current password...',
|
||||
|
||||
display_speed_in_title: 'Display download speed in page title',
|
||||
},
|
||||
|
||||
title: {
|
||||
_: 'Title',
|
||||
add_torrents: 'Add Torrents',
|
||||
@@ -113,30 +182,30 @@ export default {
|
||||
text: 'Are you sure you want to exit qBittorrent?',
|
||||
},
|
||||
add_torrents: {
|
||||
placeholder: 'Upload torrents by drop them here,\nor click attachment button at right to select.',
|
||||
placeholder: 'Upload torrents by dropping them here,\nor click attachment button at right to select.',
|
||||
hint: 'One link per line',
|
||||
},
|
||||
delete_torrents: {
|
||||
msg: 'Are you sure to delete selected torrents from transfer list?',
|
||||
msg: 'Are you sure you want to delete selected torrents from transfer list?',
|
||||
also_delete_same_name_torrents: 'Also delete one same named torrent |||| Also delete %{smart_count} same named torrents',
|
||||
},
|
||||
set_category: {
|
||||
move: 'Are you sure to move selected torrents to category %{category}?',
|
||||
reset: 'Are you sure to reset category of selected torrents?',
|
||||
move: 'Are you sure you want to move selected torrents to category %{category}?',
|
||||
reset: 'Are you sure you want to reset category of selected torrents?',
|
||||
also_move_same_name_torrents: 'Also move one same named torrent |||| Also move %{smart_count} same named torrents',
|
||||
},
|
||||
switch_locale: {
|
||||
msg: 'Are you sure to switch language to %{lang}?\nThis action will reload page.',
|
||||
msg: 'Are you sure you want to switch language to %{lang}?\nThis action will reload page.',
|
||||
},
|
||||
recheck_torrents: {
|
||||
msg: 'Are you sure want to recheck torrents?',
|
||||
msg: 'Are you sure you want to recheck torrents?',
|
||||
},
|
||||
rss: {
|
||||
add_feed: 'Add Feed',
|
||||
feed_url: 'Feed URL',
|
||||
auto_refresh: 'Auto Refresh',
|
||||
auto_download: 'Auto Download',
|
||||
delete_feeds: 'Are you sure to delete selected feeds?',
|
||||
delete_feeds: 'Are you sure you want to delete selected feeds?',
|
||||
date_format: '%{date} (%{duration} ago)',
|
||||
},
|
||||
rss_rule: {
|
||||
@@ -157,7 +226,7 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
state: {
|
||||
category_state: {
|
||||
_: 'State',
|
||||
|
||||
downloading: 'Downloading',
|
||||
@@ -169,4 +238,26 @@ export default {
|
||||
inactive: 'Inactive',
|
||||
errored: 'Errored',
|
||||
},
|
||||
|
||||
torrent_state: {
|
||||
error: 'error',
|
||||
missingFiles: 'missingFiles',
|
||||
uploading: 'uploading',
|
||||
pausedUP: 'pausedUP',
|
||||
queuedUP: 'queuedUP',
|
||||
stalledUP: 'stalledUP',
|
||||
checkingUP: 'checkingUP',
|
||||
forcedUP: 'forcedUP',
|
||||
allocating: 'allocating',
|
||||
downloading: 'downloading',
|
||||
metaDL: 'metaDL',
|
||||
pausedDL: 'pausedDL',
|
||||
queuedDL: 'queuedDL',
|
||||
stalledDL: 'stalledDL',
|
||||
checkingDL: 'checkingDL',
|
||||
forceDL: 'forceDL',
|
||||
checkingResumeData: 'checkingResumeData',
|
||||
moving: 'moving',
|
||||
unknown: 'unknown',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import langEn from './en';
|
||||
import langRu from './ru';
|
||||
import langTr from './tr';
|
||||
import langZhCn from './zh-CN';
|
||||
import langZhTw from './zh-TW';
|
||||
import langNl from './nl';
|
||||
|
||||
import { loadConfig } from '@/store/config';
|
||||
|
||||
@@ -11,6 +13,8 @@ export const translations = {
|
||||
'ru': langRu,
|
||||
'tr': langTr,
|
||||
'zh-CN': langZhCn,
|
||||
'zh-TW': langZhTw,
|
||||
'nl': langNl,
|
||||
}
|
||||
|
||||
export type LocaleKey = keyof typeof translations | null;
|
||||
|
||||
263
src/locale/nl.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
export default {
|
||||
lang: 'Nederlands',
|
||||
auto: 'Automatisch',
|
||||
|
||||
close: 'Afsluiten',
|
||||
no: 'Nee',
|
||||
yes: 'Ja',
|
||||
cancel: 'Annuleren',
|
||||
ok: 'OK',
|
||||
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
submit: 'Verzend',
|
||||
edit: 'Aanpassen',
|
||||
delete: 'Verwijderen',
|
||||
todo: 'Takenlijst',
|
||||
resume: 'Hervat',
|
||||
pause: 'Pauzeren',
|
||||
force_start: 'Forceer Starten',
|
||||
toggle_sequential: 'Sequentiële Downloads Inschakelen',
|
||||
info: 'Informatie',
|
||||
reset: 'Reset',
|
||||
login: 'Inloggen',
|
||||
search: 'Zoeken',
|
||||
refresh: 'Vernieuwen',
|
||||
location: 'Locatie',
|
||||
rename: 'Hernoemen',
|
||||
trigger_application_shutdown: 'qBittorrent afsluiten',
|
||||
reannounce: 'Opnieuw Aankondigen',
|
||||
recheck: 'Opnieuw Controleren',
|
||||
|
||||
username: 'Gebruikersnaam',
|
||||
password: 'Wachtwoord',
|
||||
|
||||
name: 'Naam',
|
||||
size: 'Grootte',
|
||||
progress: 'Vooruitgang',
|
||||
status: 'Status',
|
||||
seeds: 'Seeds',
|
||||
peers: 'Peers',
|
||||
dl_speed: 'DL Snelheid',
|
||||
up_speed: 'UP Snelheid',
|
||||
eta: 'Resterende Tijd',
|
||||
ratio: 'Deelverhouding',
|
||||
added_on: 'Toegevoegd Op',
|
||||
|
||||
settings: 'Instellingen',
|
||||
logs: 'Logs',
|
||||
light: 'Licht',
|
||||
dark: 'Donker',
|
||||
|
||||
all: 'Alles',
|
||||
category: 'Categorie |||| Categoriën',
|
||||
uncategorized: 'Zonder Categorie',
|
||||
tag: 'Label |||| Labels',
|
||||
untagged: 'Zonder Label',
|
||||
others: 'Overige',
|
||||
sites: 'Sites',
|
||||
files: 'Bestanden',
|
||||
less: 'Minder',
|
||||
more: 'Meer',
|
||||
feed: 'Feed',
|
||||
date: 'Datum',
|
||||
query: 'Aanvraag',
|
||||
plugin: 'Plugin |||| Plugins',
|
||||
action: 'Actie |||| Acties',
|
||||
search_engine: 'Zoekmachine',
|
||||
usage: 'Gebruik',
|
||||
plugin_manager: 'Plugins Beheren',
|
||||
update_plugins: 'Plugins Updaten',
|
||||
|
||||
preferences: {
|
||||
change_applied: 'Nieuwe voorkeuren opgeslagen',
|
||||
downloads: 'Downloads',
|
||||
adding_torrent: 'Bij het toevoegen van een torrent',
|
||||
create_subfolder_enabled: 'Maak een subfolder voor torrents met meerdere bestanden',
|
||||
start_paused_enabled: 'Downloads niet automatisch starten',
|
||||
auto_delete_mode: '.torrent bestanden automatisch verwijderen',
|
||||
preallocate_all: 'Schijfruime toewijzen voor alle bestanden',
|
||||
incomplete_files_ext: 'Voeg .!qB-extensie toe aan onvolledige bestanden',
|
||||
saving_management: 'Opslag Beheer',
|
||||
auto_tmm_enabled: 'Standaard Torrent Beheermodus',
|
||||
torrent_changed_tmm_enabled: 'Bij wijziging van Torrent Categorie',
|
||||
save_path_changed_tmm_enabled: 'Bij wijziging van standaard opslagpad',
|
||||
category_changed_tmm_enabled: 'Bij wijziging van Categorie opslagpad',
|
||||
auto_mode: 'Automatisch',
|
||||
manual_mode: 'Handmatig',
|
||||
switch_torrent_mode_to_manual: 'Schakel deze torrent over naar handmatige modus',
|
||||
move_affected_torrent: 'Verplaats de betreffende torrents',
|
||||
save_path: 'Standaard opslagpad',
|
||||
temp_path: 'Opslagpad onvolledige torrents',
|
||||
export_dir: 'Kopieer .torrent bestanden naar',
|
||||
export_dir_fin: 'Kopieer .torrent bestanden voor voltooide downloads naar',
|
||||
|
||||
speed: 'Snelheid',
|
||||
global_rate_limits: 'Globale snelheidslimieten',
|
||||
alternate_rate_limits: 'Alternatieve snelheidslimieten',
|
||||
alternate_schedule_enable_time: 'Alternatieve snelheidlimieten inplannen',
|
||||
apply_speed_limit: 'Instellingen snelheidslimieten',
|
||||
dl_limit: 'Downloaden (KiB/s)',
|
||||
up_limit: 'Uploaden (KiB/s)',
|
||||
zero_for_unlimited: '0 betekent onbeperkt',
|
||||
schedule_from: 'Van',
|
||||
schedule_to: 'Tot',
|
||||
scheduler_days: 'Dagen',
|
||||
limit_utp_rate: 'Pas snelheidslimiet toe op het µTP-protocol',
|
||||
limit_tcp_overhead: 'Pas snelheidslimiet toe op transport overhead',
|
||||
limit_lan_peers: 'Pas snelheidslimiet toe op peers op LAN',
|
||||
|
||||
connection: 'Verbindingen',
|
||||
bittorrent: 'BitTorrent',
|
||||
|
||||
rss: 'RSS',
|
||||
rss_processing_enabled: 'Ophalen van RSS-feeds inschakelen',
|
||||
rss_auto_downloading_enabled: 'Automatisch downloaden van RSS-torrents inschakelen',
|
||||
rss_refresh_interval: 'RSS-feed verversingsinterval',
|
||||
|
||||
webui: 'Web Gebruikersinterface',
|
||||
data_update_interval: 'Gegevens Update Interval (ms)',
|
||||
webui_remote_control: 'Web Gebruikersinterface (Bediening op afstand)',
|
||||
ip_address: 'IP-adres',
|
||||
ip_port: 'Poort',
|
||||
enable_upnp: 'Gebruik UPnP / NAT-PMP om de poort van mijn router door te sturen',
|
||||
authentication: 'Authenticatie',
|
||||
web_ui_username: 'Gebruikersnaam',
|
||||
web_ui_password: 'Wachtwoord',
|
||||
bypass_local_auth: 'Authenticatie omzeilen voor clients op localhost',
|
||||
bypass_auth_subnet_whitelist: 'Authenticatie omzeilen voor clients in gewhiteliste IP-subnetten',
|
||||
web_ui_session_timeout: 'Sessie time-out',
|
||||
web_ui_max_auth_fail_count: 'Verban client na opeenvolgende mislukte pogingen',
|
||||
web_ui_ban_duration: 'verban voor',
|
||||
web_ui_seconds: 'seconden',
|
||||
new_password: 'Wijzig huidig wachtwoord...',
|
||||
|
||||
display_speed_in_title: 'Toon downloadsnelheid in paginatitel',
|
||||
},
|
||||
|
||||
title: {
|
||||
_: 'Titel',
|
||||
add_torrents: 'Torrents Toevoegen',
|
||||
delete_torrents: 'Torrents Verwijderen',
|
||||
set_category: 'Categorie Instellen',
|
||||
edit_tracker: 'Tracker Bewerken',
|
||||
set_location: 'Locatie Instellen',
|
||||
recheck_torrents: 'Torrents Opnieuw Controleren',
|
||||
},
|
||||
|
||||
label: {
|
||||
switch_to_old_ui: 'Schakel naar oude gebruikersinterface',
|
||||
create_subfolder: 'Maak submap aan',
|
||||
start_torrent: 'Start torrent',
|
||||
skip_hash_check: 'Sla hashcontroler over',
|
||||
in_sequential_order: 'Op volgorde',
|
||||
first_and_last_pieces_first: 'Eerste en laatste delen eerst',
|
||||
|
||||
also_delete_files: 'Verwijder ook bestanden',
|
||||
|
||||
auto_tmm: 'Automatische TMM',
|
||||
|
||||
adding: 'Toevoegen…',
|
||||
reloading: 'Herladen…',
|
||||
deleting: 'Verwijderen…',
|
||||
moving: 'Verplaatsen…',
|
||||
moved: 'Verplaatst',
|
||||
next: 'Volgende',
|
||||
back: 'Vorige',
|
||||
confirm: 'Bevestigen',
|
||||
reannounced: 'Heraangekondigd',
|
||||
rechecking: 'Hercontroleren…',
|
||||
dht_nodes: '%{smart_count} node |||| %{smart_count} nodes',
|
||||
base_url: 'Base URL',
|
||||
},
|
||||
|
||||
msg: {
|
||||
item_is_required: '%{item} is vereist',
|
||||
},
|
||||
|
||||
dialog: {
|
||||
trigger_exit_qb: {
|
||||
title: 'qBittorrent afsluiten',
|
||||
text: 'Weet u zeker dat u qBittorrent wilt afsluiten?',
|
||||
},
|
||||
add_torrents: {
|
||||
placeholder: 'Upload torrents door ze naar hier te slepen,\nof click rechts op de bijlageknop om ze te selecteren.',
|
||||
hint: 'Één link per regel',
|
||||
},
|
||||
delete_torrents: {
|
||||
msg: 'Weet u zeker dat u de geselecteerde torrents uit de tranferlijst wilt verwijderen?',
|
||||
also_delete_same_name_torrents: 'Verwijder ook een torrent met dezelfde naam |||| Verwijder ook %{smart_count} torrents met dezelfde naam',
|
||||
},
|
||||
set_category: {
|
||||
move: 'Weet u zeker dat u de geselecteerde torrents naar de categorie %{category} wilt verplaatsen?',
|
||||
reset: 'Weet u zeker dat u de categorie van geselecteerde torrents wilt resetten?',
|
||||
also_move_same_name_torrents: 'Verplaats ook een torrent met dezelfde naam |||| Verplaats ook %{smart_count} torrents met dezelfde naam',
|
||||
},
|
||||
switch_locale: {
|
||||
msg: 'Weet u zeker dat u de taal wilt veranderen naar %{lang}?\nDeze actie zal de pagina herladen.',
|
||||
},
|
||||
recheck_torrents: {
|
||||
msg: 'Weet u zeker dat u de torrents opnieuw wilt controleren?',
|
||||
},
|
||||
rss: {
|
||||
add_feed: 'Feed toevoegen',
|
||||
feed_url: 'Feed URL',
|
||||
auto_refresh: 'Automatisch Vernieuwen',
|
||||
auto_download: 'Automatisch Downloaden',
|
||||
delete_feeds: 'Weet u zeker dat u de selecteerde feeds wilt verwijderen?',
|
||||
date_format: '%{date} (%{duration} geleden)',
|
||||
},
|
||||
rss_rule: {
|
||||
add_rule: 'Regel Toevoegen',
|
||||
new_rule_name: 'Naam van de nieuwe regel',
|
||||
delete_rule: 'Weet u zeker dat u de geselecteerde regel wilt verwijderen?',
|
||||
title: 'RSS Downloader',
|
||||
rule_settings: 'Regelinstellingen',
|
||||
|
||||
use_regex: 'Gebruik Regex',
|
||||
must_contain: 'Moet Bevatten',
|
||||
must_not_contain: 'Mag Niet Bevatten',
|
||||
episode_filter: 'Filter Op Aflevering',
|
||||
smart_episode: 'Gebruik Slimme Aflevering Filter',
|
||||
assign_category: 'Assign Category',
|
||||
|
||||
apply_to_feeds: 'Pas Regel Toe op Feeds',
|
||||
},
|
||||
},
|
||||
|
||||
category_state: {
|
||||
_: 'Status',
|
||||
|
||||
downloading: 'Downloaden',
|
||||
seeding: 'Seeding',
|
||||
completed: 'Voltooid',
|
||||
resumed: 'Hervat',
|
||||
paused: 'Gepauzeerd',
|
||||
active: 'Actief',
|
||||
inactive: 'Niet Actief',
|
||||
errored: 'Fout',
|
||||
},
|
||||
|
||||
torrent_state: {
|
||||
error: 'fout',
|
||||
missingFiles: 'ontbrekendeBestanden',
|
||||
uploading: 'uploaden',
|
||||
pausedUP: 'gepauzeerdUP',
|
||||
queuedUP: 'wachtrijdUP',
|
||||
stalledUP: 'vastgelopenUP',
|
||||
checkingUP: 'controleUP',
|
||||
forcedUP: 'geforceerdUP',
|
||||
allocating: 'toewijzen',
|
||||
downloading: 'downloaden',
|
||||
metaDL: 'metaDL',
|
||||
pausedDL: 'gepauzeerdDL',
|
||||
queuedDL: 'wachtrijdDL',
|
||||
stalledDL: 'vastgelopenDL',
|
||||
checkingDL: 'controleDL',
|
||||
forceDL: 'geforceerdDL',
|
||||
checkingResumeData: 'controleHervattingsData',
|
||||
moving: 'verplaatsen',
|
||||
unknown: 'onbekend',
|
||||
},
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
edit: 'Изменить',
|
||||
delete: 'Удалить',
|
||||
todo: 'Список дел',
|
||||
resume: 'Продлжить',
|
||||
resume: 'Продолжить',
|
||||
pause: 'Приостановить',
|
||||
force_start: 'Запустить принудительно',
|
||||
info: 'Информация',
|
||||
@@ -38,8 +38,8 @@ export default {
|
||||
status: 'Статус',
|
||||
seeds: 'Сиды',
|
||||
peers: 'Пиры',
|
||||
dl_speed: 'Скорость скачивания',
|
||||
up_speed: 'Скорость раздачи',
|
||||
dl_speed: 'Качает',
|
||||
up_speed: 'Раздаёт',
|
||||
eta: 'Осталось',
|
||||
ratio: 'Ратио',
|
||||
added_on: 'Добавлен',
|
||||
@@ -65,6 +65,64 @@ export default {
|
||||
search_engine: 'Поисковый движок',
|
||||
usage: 'применение',
|
||||
plugin_manager: 'Управление плагинами',
|
||||
update_plugins: 'Обновить плагины',
|
||||
|
||||
preferences: {
|
||||
change_applied: 'Настройки сохранены',
|
||||
downloads: 'Загрузки',
|
||||
adding_torrent: 'При добавлении торрента',
|
||||
create_subfolder_enabled: 'Создавать подпапку для торрентов со множеством файлов',
|
||||
start_paused_enabled: 'Не начинать загрузку автоматически',
|
||||
auto_delete_mode: 'Удалять торрент-файлы после добавления',
|
||||
preallocate_all: 'Предварительно резервировать место для всех файлов',
|
||||
incomplete_files_ext: 'Добавлять расширение .!qB к незавершённым файлам',
|
||||
saving_management: 'Управление сохранением',
|
||||
auto_tmm_enabled: 'Режим управления торрентом по умолчанию',
|
||||
torrent_changed_tmm_enabled: 'При изменении категории торрента',
|
||||
save_path_changed_tmm_enabled: 'При изменении пути сохранения по умолчанию',
|
||||
category_changed_tmm_enabled: 'При изменении пути сохранения категории',
|
||||
auto_mode: 'Автоматический',
|
||||
manual_mode: 'Ручной',
|
||||
switch_torrent_mode_to_manual: 'Переключить затронутые торренты в Ручной режим',
|
||||
move_affected_torrent: 'Переместить затронутые торренты',
|
||||
save_path: 'Путь сохранения по умолчанию',
|
||||
temp_path: 'Хранить незавершённые торренты в',
|
||||
export_dir: 'Копировать торрент-файлы в',
|
||||
export_dir_fin: 'Копировать торрент-файлы завершённых загрузок в',
|
||||
|
||||
speed: 'Скорость',
|
||||
global_rate_limits: 'Общие ограничения скорости',
|
||||
alternate_rate_limits: 'Альтернативные ограничения скорости',
|
||||
alternate_schedule_enable_time: 'Запланировать использование особых ограничений скорости',
|
||||
dl_limit: 'Загрузка (KiB/s)',
|
||||
up_limit: 'Отдача (KiB/s)',
|
||||
zero_for_unlimited: '«0» — без ограничений',
|
||||
schedule_from: 'С',
|
||||
schedule_to: 'До',
|
||||
scheduler_days: 'Когда',
|
||||
limit_utp_rate: 'Применять ограничения скорости к протоколу µTP',
|
||||
limit_tcp_overhead: 'Применять ограничения скорости к служебному трафику',
|
||||
limit_lan_peers: 'Применять ограничения скорости к локальным пирам',
|
||||
|
||||
webui: 'Веб-интерфейс',
|
||||
data_update_interval: 'Интервал обновления (ms)',
|
||||
webui_remote_control: 'Веб-интерфейс (удалённое управление)',
|
||||
ip_address: 'IP-адрес',
|
||||
ip_port: 'Порт',
|
||||
enable_upnp: 'Использовать UPnP / NAT-PMP для проброса порта через мой роутер',
|
||||
authentication: 'Аутентификация',
|
||||
web_ui_username: 'Имя пользователя',
|
||||
web_ui_password: 'Пароль',
|
||||
bypass_local_auth: 'Пропускать аутентификацию клиентов для localhost',
|
||||
bypass_auth_subnet_whitelist: 'Пропускать аутентификацию клиентов для разрешённых подсетей',
|
||||
web_ui_session_timeout: 'Таймаут сессии',
|
||||
web_ui_max_auth_fail_count: 'Блокировать клиента после серии сбоев',
|
||||
web_ui_ban_duration: 'заблокировать на',
|
||||
web_ui_seconds: 'секунд',
|
||||
new_password: 'Изменить текущий пароль...',
|
||||
|
||||
display_speed_in_title: 'Показывать скорость загрузки в заголовке окна',
|
||||
},
|
||||
|
||||
title: {
|
||||
_: 'Заголовок',
|
||||
@@ -86,7 +144,7 @@ export default {
|
||||
|
||||
also_delete_files: 'Также удалить файлы',
|
||||
|
||||
auto_tmm: 'Авто ТММ',
|
||||
auto_tmm: 'Автоуправление торрентом',
|
||||
|
||||
adding: 'Добавление…',
|
||||
reloading: 'Перезагрузка…',
|
||||
@@ -156,7 +214,7 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
state: {
|
||||
category_state: {
|
||||
_: 'Статистика',
|
||||
|
||||
downloading: 'Скачивается',
|
||||
@@ -168,4 +226,26 @@ export default {
|
||||
inactive: 'Не активный',
|
||||
errored: 'Ошибочный',
|
||||
},
|
||||
|
||||
torrent_state: {
|
||||
error: 'error',
|
||||
missingFiles: 'missingFiles',
|
||||
uploading: 'uploading',
|
||||
pausedUP: 'pausedUP',
|
||||
queuedUP: 'queuedUP',
|
||||
stalledUP: 'stalledUP',
|
||||
checkingUP: 'checkingUP',
|
||||
forcedUP: 'forcedUP',
|
||||
allocating: 'allocating',
|
||||
downloading: 'downloading',
|
||||
metaDL: 'metaDL',
|
||||
pausedDL: 'pausedDL',
|
||||
queuedDL: 'queuedDL',
|
||||
stalledDL: 'stalledDL',
|
||||
checkingDL: 'checkingDL',
|
||||
forceDL: 'forceDL',
|
||||
checkingResumeData: 'checkingResumeData',
|
||||
moving: 'moving',
|
||||
unknown: 'unknown',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
export default {
|
||||
lang: '中文',
|
||||
lang: '简体中文',
|
||||
auto: '自动',
|
||||
|
||||
close: '关闭',
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
added_on: '添加时间',
|
||||
|
||||
settings: '设置',
|
||||
|
||||
logs: '日志',
|
||||
light: '亮色',
|
||||
dark: '暗色',
|
||||
@@ -52,6 +53,8 @@ export default {
|
||||
all: '全部',
|
||||
category: '分类',
|
||||
uncategorized: '未分类',
|
||||
tag: '标签',
|
||||
untagged: '无标签',
|
||||
others: '其他',
|
||||
sites: '站点',
|
||||
files: '文件',
|
||||
@@ -64,6 +67,71 @@ export default {
|
||||
action: '操作',
|
||||
search_engine: '搜索引擎',
|
||||
|
||||
preferences: {
|
||||
change_applied: '配置已保存',
|
||||
downloads: '下载',
|
||||
adding_torrent: '添加 torrent 时',
|
||||
create_subfolder_enabled: '为多个文件的 Torrent 创建子目录',
|
||||
start_paused_enabled: '不要自动开始下载',
|
||||
auto_delete_mode: '完成后删除 .torrent 文件',
|
||||
preallocate_all: '为所有文件预分配磁盘空间',
|
||||
incomplete_files_ext: '为不完整的文件添加扩展名 .!qB',
|
||||
saving_management: '保存管理',
|
||||
auto_tmm_enabled: '默认 Torrent 管理模式',
|
||||
torrent_changed_tmm_enabled: '当 Torrent 分类修改时',
|
||||
save_path_changed_tmm_enabled: '当默认保存路径修改时',
|
||||
category_changed_tmm_enabled: '当分类保存路径修改时',
|
||||
auto_mode: '自动',
|
||||
manual_mode: '手动',
|
||||
switch_torrent_mode_to_manual: '切换受影响的 Torrent 至手动模式',
|
||||
move_affected_torrent: '重新定位受影响的 Torrent',
|
||||
save_path: '默认保存路径',
|
||||
temp_path: '保存未完成的 torrent 到',
|
||||
export_dir: '复制 .torrent 文件到',
|
||||
export_dir_fin: '复制下载完成的 .torrent 文件到',
|
||||
|
||||
speed: '速度',
|
||||
global_rate_limits: '全局速度限制',
|
||||
alternate_rate_limits: '备用速度限制',
|
||||
alternate_schedule_enable_time: '设置备用速度限制的启用时间',
|
||||
apply_speed_limit: '设置速度限制',
|
||||
dl_limit: '下载 (KiB/s)',
|
||||
up_limit: '上传 (KiB/s)',
|
||||
zero_for_unlimited: '0 为无限制',
|
||||
schedule_from: '从',
|
||||
schedule_to: '到',
|
||||
scheduler_days: '时间',
|
||||
limit_utp_rate: '对 µTP 协议进行速度限制',
|
||||
limit_tcp_overhead: '对传送总开销进行速度限制',
|
||||
limit_lan_peers: '对本地网络用户进行速度限制',
|
||||
|
||||
connection: '连接',
|
||||
bittorrent: 'BitTorrent',
|
||||
|
||||
rss_processing_enabled: '启用自动刷新',
|
||||
rss_auto_downloading_enabled: '启用自动下载种子',
|
||||
rss_refresh_interval: '订阅刷新间隔',
|
||||
|
||||
webui: 'Web UI',
|
||||
data_update_interval: '数据更新频率(ms)',
|
||||
webui_remote_control: 'Web 用户界面(远程控制)',
|
||||
ip_address: 'IP 地址',
|
||||
ip_port: '端口',
|
||||
enable_upnp: '使用我的路由器的 UPnP / NAT-PMP 功能来转发端口',
|
||||
authentication: '验证',
|
||||
web_ui_username: '用户名',
|
||||
web_ui_password: '密码',
|
||||
bypass_local_auth: '对本地主机上的客户端跳过身份验证',
|
||||
bypass_auth_subnet_whitelist: '对 IP 子网白名单中的客户端跳过身份验证',
|
||||
web_ui_session_timeout: '会话超时',
|
||||
web_ui_ban_duration: '禁止',
|
||||
web_ui_max_auth_fail_count: '连续失败后禁止客户端次数',
|
||||
web_ui_seconds: '秒',
|
||||
new_password: '更改当前的密码...',
|
||||
|
||||
display_speed_in_title: '在网页标题显示当前速度',
|
||||
},
|
||||
|
||||
title: {
|
||||
_: '标题',
|
||||
add_torrents: '添加种子',
|
||||
@@ -153,7 +221,7 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
state: {
|
||||
category_state: {
|
||||
_: '状态',
|
||||
|
||||
downloading: '下载',
|
||||
@@ -165,4 +233,26 @@ export default {
|
||||
inactive: '空闲',
|
||||
errored: '错误',
|
||||
},
|
||||
|
||||
torrent_state: {
|
||||
error: '错误',
|
||||
missingFiles: '文件丢失',
|
||||
uploading: '上传中',
|
||||
pausedUP: '完成',
|
||||
queuedUP: '排队上传',
|
||||
stalledUP: '上传',
|
||||
checkingUP: '上传校验',
|
||||
forcedUP: '强制上传',
|
||||
allocating: '分配空间',
|
||||
downloading: '下载中',
|
||||
metaDL: '获取信息',
|
||||
pausedDL: '暂停下载',
|
||||
queuedDL: '排队下载',
|
||||
stalledDL: '下载',
|
||||
checkingDL: '下载校验',
|
||||
forceDL: '强制下载',
|
||||
checkingResumeData: '快速校验',
|
||||
moving: '移动中',
|
||||
unknown: '未知',
|
||||
},
|
||||
}
|
||||
|
||||
252
src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
export default {
|
||||
lang: '繁體中文',
|
||||
auto: '自動',
|
||||
|
||||
close: '關閉',
|
||||
no: '否',
|
||||
yes: '是',
|
||||
cancel: '取消',
|
||||
ok: '確定',
|
||||
|
||||
start: '開始',
|
||||
stop: '停止',
|
||||
submit: '提交',
|
||||
edit: '編輯',
|
||||
delete: '刪除',
|
||||
todo: '待辦',
|
||||
resume: '恢復',
|
||||
pause: '暫停',
|
||||
force_start: '強制繼續',
|
||||
info: '資訊',
|
||||
reset: '重置',
|
||||
login: '登入',
|
||||
search: '搜索',
|
||||
refresh: '刷新',
|
||||
location: '位置',
|
||||
rename: '重新命名',
|
||||
trigger_application_shutdown: '退出qBittorrent',
|
||||
reannounce: '重新通告',
|
||||
recheck: '重新檢查',
|
||||
|
||||
username: '使用者名稱',
|
||||
password: '密碼',
|
||||
|
||||
name: '名稱',
|
||||
size: '大小',
|
||||
progress: '進度',
|
||||
status: '狀態',
|
||||
seeds: '做種',
|
||||
peers: '用戶',
|
||||
dl_speed: '下載速度',
|
||||
up_speed: '上傳速度',
|
||||
eta: '剩餘時間',
|
||||
ratio: '分享率',
|
||||
added_on: '添加時間',
|
||||
|
||||
settings: '設定',
|
||||
|
||||
logs: '日誌',
|
||||
light: '亮色',
|
||||
dark: '暗色',
|
||||
|
||||
all: '全部',
|
||||
category: '分類',
|
||||
uncategorized: '未分類',
|
||||
others: '其他',
|
||||
sites: '站點',
|
||||
files: '文件',
|
||||
less: '更少',
|
||||
more: '更多',
|
||||
feed: '訂閱',
|
||||
date: '日期',
|
||||
query: '查詢',
|
||||
plugin: '插件',
|
||||
action: '操作',
|
||||
search_engine: '搜尋引擎',
|
||||
|
||||
preferences: {
|
||||
change_applied: '設定已保存',
|
||||
downloads: '下載',
|
||||
adding_torrent: '添加 Torrent 時',
|
||||
create_subfolder_enabled: '為多個文件的 Torrent 創建子目錄',
|
||||
start_paused_enabled: '不要自動開始下載',
|
||||
auto_delete_mode: '完成後刪除 .torrent 文件',
|
||||
preallocate_all: '為所有文件預分配磁碟空間',
|
||||
incomplete_files_ext: '為不完整的文件添加副檔名 .!qB',
|
||||
saving_management: '保存管理',
|
||||
auto_tmm_enabled: '默認 Torrent 管理模式',
|
||||
torrent_changed_tmm_enabled: '當 Torrent 分類修改時',
|
||||
save_path_changed_tmm_enabled: '當默認保存路徑修改時',
|
||||
category_changed_tmm_enabled: '當分類保存路徑修改時',
|
||||
auto_mode: '自動',
|
||||
manual_mode: '手動',
|
||||
switch_torrent_mode_to_manual: '切換受影響的 Torrent 至手動模式',
|
||||
move_affected_torrent: '重新定位受影響的 Torrent',
|
||||
save_path: '默認保存路徑',
|
||||
temp_path: '保存未完成的 torrent 到',
|
||||
export_dir: '複製 .torrent 文件到',
|
||||
export_dir_fin: '複製下載完成的 .torrent 文件到',
|
||||
|
||||
speed: '速度',
|
||||
global_rate_limits: '全局速度限制',
|
||||
alternate_rate_limits: '備用速度限制',
|
||||
alternate_schedule_enable_time: '設定備用速度限制的啟用時間',
|
||||
apply_speed_limit: '設定速度限制',
|
||||
dl_limit: '下載 (KiB/s)',
|
||||
up_limit: '上傳 (KiB/s)',
|
||||
zero_for_unlimited: '0 為無限制',
|
||||
schedule_from: '從',
|
||||
schedule_to: '到',
|
||||
scheduler_days: '時間',
|
||||
limit_utp_rate: '對 µTP 協議進行速度限制',
|
||||
limit_tcp_overhead: '對傳送總開銷進行速度限制',
|
||||
limit_lan_peers: '對本地網路用戶進行速度限制',
|
||||
|
||||
connection: '連接',
|
||||
bittorrent: 'BitTorrent',
|
||||
|
||||
webui: 'Web UI',
|
||||
data_update_interval: '數據更新頻率(ms)',
|
||||
webui_remote_control: 'Web 用戶界面(遠端控制)',
|
||||
ip_address: 'IP 地址',
|
||||
ip_port: '埠',
|
||||
enable_upnp: '使用我的路由器的 UPnP / NAT-PMP 功能來轉發埠',
|
||||
authentication: '驗證',
|
||||
web_ui_username: '使用者名稱',
|
||||
web_ui_password: '密碼',
|
||||
bypass_local_auth: '對本地主機上的用戶端跳過身份驗證',
|
||||
bypass_auth_subnet_whitelist: '對 IP 子網白名單中的用戶端跳過身份驗證',
|
||||
web_ui_session_timeout: '會話超時',
|
||||
web_ui_ban_duration: '禁止',
|
||||
web_ui_max_auth_fail_count: '連續失敗後禁止用戶端次數',
|
||||
web_ui_seconds: '秒',
|
||||
new_password: '更改當前的密碼...',
|
||||
|
||||
display_speed_in_title: '在網頁標題顯示當前速度',
|
||||
},
|
||||
|
||||
title: {
|
||||
_: '標題',
|
||||
add_torrents: '添加種子',
|
||||
delete_torrents: '刪除種子',
|
||||
set_category: '設定分類',
|
||||
edit_tracker: '編輯 Tracker',
|
||||
set_location: '修改檔案位置',
|
||||
recheck_torrents: '重新檢查種子',
|
||||
},
|
||||
|
||||
label: {
|
||||
switch_to_old_ui: '切換到原版 UI',
|
||||
create_subfolder: '創建子文件夾',
|
||||
start_torrent: '開始種子',
|
||||
skip_hash_check: '跳過哈希校驗',
|
||||
in_sequential_order: '按順序下載',
|
||||
first_and_last_pieces_first: '先下載首尾文件塊',
|
||||
|
||||
also_delete_files: '同時刪除文件',
|
||||
|
||||
auto_tmm: '自動種子管理',
|
||||
|
||||
adding: '添加…',
|
||||
reloading: '刷新中…',
|
||||
deleting: '刪除中…',
|
||||
moving: '移動中…',
|
||||
moved: '已移動',
|
||||
next: '下一步',
|
||||
back: '返回',
|
||||
confirm: '確定',
|
||||
reannounced: '已重新通告',
|
||||
rechecking: '重新檢查中…',
|
||||
dht_nodes: '%{smart_count} 節點',
|
||||
},
|
||||
|
||||
msg: {
|
||||
'item_is_required': '%{item}不能為空',
|
||||
},
|
||||
|
||||
dialog: {
|
||||
trigger_exit_qb: {
|
||||
title: '退出 qBittorrent',
|
||||
text: '您確定要退出qBittorrent嗎?',
|
||||
},
|
||||
add_torrents: {
|
||||
placeholder: '將種子拖到這裡上傳,\n或者點擊右邊的附件圖示來選擇。',
|
||||
hint: '每行一個連結',
|
||||
},
|
||||
delete_torrents: {
|
||||
msg: '確定要刪除選中的種子嗎?',
|
||||
also_delete_same_name_torrents: '同時刪除 %{smart_count} 個同名的種子',
|
||||
},
|
||||
set_category: {
|
||||
move: '確定要移動選中的種子到分類 %{category} 嗎?',
|
||||
reset: '確定重置選中的種子的分類嗎?',
|
||||
also_move_same_name_torrents: '同時移動 %{smart_count} 個同名的種子',
|
||||
},
|
||||
switch_locale: {
|
||||
msg: '確定要切換语言為 %{lang} 嗎?\n這將會刷新頁面。',
|
||||
},
|
||||
recheck_torrents: {
|
||||
msg: '確定要重新檢查選中的種子嗎?',
|
||||
},
|
||||
rss: {
|
||||
add_feed: '添加訂閱',
|
||||
feed_url: '訂閱 URL',
|
||||
auto_refresh: '自動刷新',
|
||||
auto_download: '自動下載',
|
||||
delete_feeds: '確定要刪除選中的訂閱嗎?',
|
||||
date_format: '%{date}(%{duration} 之前)',
|
||||
},
|
||||
rss_rule: {
|
||||
add_rule: '添加規則',
|
||||
new_rule_name: '新規則的名稱',
|
||||
delete_rule: '確定要刪除選中的規則嗎?',
|
||||
title: 'RSS 自動下載',
|
||||
rule_settings: '規則設定',
|
||||
|
||||
use_regex: '使用正則',
|
||||
must_contain: '必須包含',
|
||||
must_not_contain: '必須排除',
|
||||
episode_filter: '劇集過濾',
|
||||
smart_episode: '使用智慧劇集過濾',
|
||||
assign_category: '分配分類',
|
||||
|
||||
apply_to_feeds: '應用到訂閱',
|
||||
},
|
||||
},
|
||||
|
||||
category_state: {
|
||||
_: '狀態',
|
||||
|
||||
downloading: '下載',
|
||||
seeding: '做種',
|
||||
completed: '完成',
|
||||
resumed: '恢復',
|
||||
paused: '暫停',
|
||||
active: '活動',
|
||||
inactive: '空閒',
|
||||
errored: '錯誤',
|
||||
},
|
||||
|
||||
torrent_state: {
|
||||
error: '錯誤',
|
||||
missingFiles: '文件遺失',
|
||||
uploading: '上傳中',
|
||||
pausedUP: '完成',
|
||||
queuedUP: '排隊上傳',
|
||||
stalledUP: '上傳',
|
||||
checkingUP: '上傳校驗',
|
||||
forcedUP: '強制上傳',
|
||||
allocating: '分配空間',
|
||||
downloading: '下載中',
|
||||
metaDL: '獲取資訊',
|
||||
pausedDL: '暫停下載',
|
||||
queuedDL: '排隊下載',
|
||||
stalledDL: '下載',
|
||||
checkingDL: '下載校驗',
|
||||
forceDL: '強制下載',
|
||||
checkingResumeData: '快速校驗',
|
||||
moving: '移動中',
|
||||
unknown: '未知',
|
||||
},
|
||||
}
|
||||
@@ -5,7 +5,17 @@ import i18n from '@/locale';
|
||||
Vue.use(Vuetify);
|
||||
|
||||
let locale = i18n.locale();
|
||||
locale = locale === 'zh-CN' ? 'zh-Hans' : locale.split('-', 1)[0];
|
||||
switch (locale) {
|
||||
case 'zh-CN':
|
||||
locale = 'zh-Hans';
|
||||
break;
|
||||
case 'zh-TW':
|
||||
locale = 'zh-Hant';
|
||||
break;
|
||||
default:
|
||||
locale = locale.split('-', 1)[0];
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { default: translation } = require('vuetify/src/locale/' + locale);
|
||||
|
||||
@@ -7,8 +7,10 @@ function registerProtocolHandler() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = location.origin + location.pathname;
|
||||
|
||||
try {
|
||||
navigator.registerProtocolHandler('magnet', location.origin + '#download=%s', document.title);
|
||||
navigator.registerProtocolHandler('magnet', baseUrl + '#download=%s', document.title);
|
||||
} catch (e) {
|
||||
log('Register protocol handler failed.', e);
|
||||
}
|
||||
|
||||
106
src/sites.ts
@@ -4,16 +4,16 @@ function getSiteIcon(name: string): string {
|
||||
|
||||
export interface SiteInfo {
|
||||
name: string;
|
||||
icon: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const sites: {[key: string]: SiteInfo} = {
|
||||
'tracker.m-team.cc': {
|
||||
'm-team.cc': {
|
||||
name: 'M-Team',
|
||||
icon: getSiteIcon('m-team'),
|
||||
},
|
||||
'tracker.keepfrds.com': {
|
||||
name: 'FRDS',
|
||||
'keepfrds.com': {
|
||||
name: 'PT@KEEPFRDS',
|
||||
icon: getSiteIcon('keepfrds'),
|
||||
},
|
||||
'springsunday.net': {
|
||||
@@ -30,12 +30,106 @@ const sites: {[key: string]: SiteInfo} = {
|
||||
},
|
||||
'hdhome.org': {
|
||||
name: 'HDHome',
|
||||
icon: getSiteIcon('hdhome'),
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'u2.dmhy.org': {
|
||||
'dmhy.org': {
|
||||
name: 'U2',
|
||||
icon: getSiteIcon('u2'),
|
||||
},
|
||||
'dmhy.best': {
|
||||
name: 'U2',
|
||||
icon: getSiteIcon('u2'),
|
||||
},
|
||||
'totheglory.im': {
|
||||
name: 'TTG',
|
||||
icon: getSiteIcon('totheglory'),
|
||||
},
|
||||
'oshen.win': {
|
||||
name: 'OshenPT',
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'soulvoice.club': {
|
||||
name: '铃音Club',
|
||||
icon: getSiteIcon('soulvoice'),
|
||||
},
|
||||
'ourbits.club': {
|
||||
name: 'OurBits',
|
||||
icon: getSiteIcon('ourbits'),
|
||||
},
|
||||
'btschool.club': {
|
||||
name: 'BTSCHOOL',
|
||||
},
|
||||
'ptsbao.club': {
|
||||
name: '烧包',
|
||||
icon: getSiteIcon('ptsbao'),
|
||||
},
|
||||
'pterclub.com': {
|
||||
name: 'PTer',
|
||||
icon: getSiteIcon('pterclub'),
|
||||
},
|
||||
'hdtime.org': {
|
||||
name: 'HDTime',
|
||||
icon: getSiteIcon('hdtime'),
|
||||
},
|
||||
'hddolby.com': {
|
||||
name: 'HD Dolby',
|
||||
},
|
||||
'lemonhd.org': {
|
||||
name: 'LemonHD',
|
||||
icon: getSiteIcon('lemonhd'),
|
||||
},
|
||||
'hares.top': {
|
||||
name: 'HaresClub',
|
||||
icon: getSiteIcon('hares'),
|
||||
},
|
||||
'pthome.net': {
|
||||
name: 'PTHOME',
|
||||
icon: getSiteIcon('pthome'),
|
||||
},
|
||||
'hdsky.me': {
|
||||
name: 'HDSky',
|
||||
icon: getSiteIcon('hdsky'),
|
||||
},
|
||||
'hdfans.org': {
|
||||
name: 'HDFans',
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'hdatmos.club': {
|
||||
name: 'HDAtmos',
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'hdzone.me': {
|
||||
name: 'HDZone',
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'open.cd': {
|
||||
name: 'OpenCD',
|
||||
icon: getSiteIcon('opencd'),
|
||||
},
|
||||
'1ptba.com': {
|
||||
name: '1PTBar',
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'pttime.org': {
|
||||
name: 'PTTime',
|
||||
icon: getSiteIcon('pttime'),
|
||||
},
|
||||
'beitai.pt': {
|
||||
name: '备胎',
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'kamept.com': {
|
||||
name: 'kamept',
|
||||
icon: getSiteIcon('kamept'),
|
||||
},
|
||||
'nicept.net': {
|
||||
name: 'NicePT',
|
||||
icon: getSiteIcon('nexusphp'),
|
||||
},
|
||||
'2xfree.org': {
|
||||
name: '2xfree',
|
||||
icon: getSiteIcon('2xfree'),
|
||||
},
|
||||
};
|
||||
|
||||
export default sites;
|
||||
|
||||
@@ -13,10 +13,10 @@ export interface Config {
|
||||
state: string | null;
|
||||
category: string | null;
|
||||
site: string | null;
|
||||
query: string | null;
|
||||
};
|
||||
locale: string | null;
|
||||
darkMode: string | null;
|
||||
displaySpeedInTitle: boolean | null;
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
@@ -29,10 +29,10 @@ const defaultConfig = {
|
||||
state: null,
|
||||
category: null,
|
||||
site: null,
|
||||
query: null,
|
||||
},
|
||||
locale: null,
|
||||
darkMode: null,
|
||||
displaySpeedInTitle: false,
|
||||
};
|
||||
|
||||
function saveConfig(obj: any) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cloneDeep, merge, map, groupBy, sortBy } from 'lodash';
|
||||
import { merge, map, groupBy, sortBy } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { computed, Ref } from '@vue/composition-api';
|
||||
@@ -11,6 +11,7 @@ import { AllStateTypes } from '../consts';
|
||||
import { torrentIsState } from '../utils';
|
||||
import searchEngineStore from './searchEngine';
|
||||
import { RootState } from './types';
|
||||
import stateMerge from '@/utils/vue-object-merge';
|
||||
import api from '@/Api';
|
||||
|
||||
Vue.use(Vuex);
|
||||
@@ -29,6 +30,7 @@ const store = new Vuex.Store<RootState>({
|
||||
preferences: null,
|
||||
pasteUrl: null,
|
||||
needAuth: false,
|
||||
query: null,
|
||||
},
|
||||
mutations: {
|
||||
/* eslint-disable no-param-reassign */
|
||||
@@ -39,20 +41,26 @@ const store = new Vuex.Store<RootState>({
|
||||
delete payload.full_update;
|
||||
state.mainData = payload;
|
||||
} else {
|
||||
const tmp: any = cloneDeep(state.mainData);
|
||||
const mainData = state.mainData!;
|
||||
if (payload.torrents_removed) {
|
||||
for (const hash of payload.torrents_removed) {
|
||||
delete tmp.torrents[hash];
|
||||
Vue.delete(mainData.torrents, hash);
|
||||
}
|
||||
delete payload.torrents_removed;
|
||||
}
|
||||
if (payload.categories_removed) {
|
||||
for (const key of payload.categories_removed) {
|
||||
delete tmp.categories[key];
|
||||
Vue.delete(mainData, key);
|
||||
}
|
||||
delete payload.categories_removed;
|
||||
}
|
||||
state.mainData = merge(tmp, payload);
|
||||
if (payload.tags_removed) {
|
||||
for (const key of payload.tags_removed) {
|
||||
Vue.delete(mainData, key);
|
||||
}
|
||||
delete payload.categories_removed;
|
||||
}
|
||||
stateMerge(mainData, payload);
|
||||
}
|
||||
},
|
||||
updatePreferences(state, payload) {
|
||||
@@ -65,6 +73,9 @@ const store = new Vuex.Store<RootState>({
|
||||
updateNeedAuth(state, payload) {
|
||||
state.needAuth = payload;
|
||||
},
|
||||
setQuery(state, payload) {
|
||||
state.query = payload;
|
||||
},
|
||||
/* eslint-enable no-param-reassign */
|
||||
},
|
||||
getters: {
|
||||
@@ -93,9 +104,38 @@ const store = new Vuex.Store<RootState>({
|
||||
(value, key) => merge({}, value, { key }));
|
||||
return sortBy(categories, 'name');
|
||||
},
|
||||
allTags(state) {
|
||||
if (!state.mainData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const finalTags: any[] = []
|
||||
for (const tag of state.mainData.tags) {
|
||||
finalTags.push({
|
||||
"key": tag,
|
||||
"name": tag,
|
||||
});
|
||||
}
|
||||
return sortBy(finalTags, 'name');
|
||||
},
|
||||
torrentGroupByCategory(state, getters) {
|
||||
return groupBy(getters.allTorrents, torrent => torrent.category);
|
||||
},
|
||||
torrentGroupByTag(state, getters) {
|
||||
const result: any = {}
|
||||
for (const torrent of getters.allTorrents) {
|
||||
const tags: any[] = torrent.tags.split(",");
|
||||
tags.forEach(tag => {
|
||||
let list: any[] = result[tag]
|
||||
if (!list) {
|
||||
list = []
|
||||
result[tag] = list;
|
||||
}
|
||||
list.push(torrent);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
torrentGroupBySite(state, getters) {
|
||||
return groupBy(getters.allTorrents, (torrent) => {
|
||||
if (!torrent.tracker) {
|
||||
@@ -131,9 +171,10 @@ const store = new Vuex.Store<RootState>({
|
||||
actions: {
|
||||
async updatePreferencesRequest({ dispatch }, preferences) {
|
||||
try {
|
||||
const response = await api.setPreferences(preferences);
|
||||
|
||||
dispatch("updatePreferencesRequestSuccess", response.data);
|
||||
await api.setPreferences(preferences);
|
||||
//setPreference api return a empty response. Need to update preference by another request.
|
||||
const preferenceRes = await api.getAppPreferences();
|
||||
dispatch("updatePreferencesRequestSuccess", preferenceRes.data);
|
||||
} catch {
|
||||
dispatch("updatePreferencesRequestFailure");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface RootState {
|
||||
preferences: any;
|
||||
pasteUrl: string | null;
|
||||
needAuth: boolean;
|
||||
query: string | null;
|
||||
}
|
||||
|
||||
export interface SearchEnginePage {
|
||||
@@ -25,6 +26,7 @@ export interface AddFormState {
|
||||
export interface TorrentFilter {
|
||||
state: string;
|
||||
category: string;
|
||||
tag: string;
|
||||
site: string;
|
||||
query: string;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ export interface SimpleCategory {
|
||||
savePath?: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ServerState {
|
||||
alltime_dl: number;
|
||||
alltime_ul: number;
|
||||
@@ -95,6 +100,7 @@ export interface ServerState {
|
||||
|
||||
export interface MainData {
|
||||
categories: Record<string, Category>;
|
||||
tags: [string];
|
||||
server_state: ServerState;
|
||||
torrents: Record<string, BaseTorrent>;
|
||||
}
|
||||
@@ -284,6 +290,9 @@ export interface Preferences {
|
||||
web_ui_port: number;
|
||||
web_ui_upnp: boolean;
|
||||
web_ui_username: string;
|
||||
web_ui_max_auth_fail_count: number;
|
||||
web_ui_ban_duration: number;
|
||||
web_ui_session_timeout: number;
|
||||
}
|
||||
|
||||
export interface SearchPlugin {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StateType } from './consts';
|
||||
import { Torrent } from './types';
|
||||
import { StateType } from '@/consts';
|
||||
import { Torrent } from '@/types';
|
||||
|
||||
const dlState = ['downloading', 'metaDL', 'stalledDL', 'checkingDL', 'pausedDL', 'queuedDL', 'forcedDL', 'allocating'];
|
||||
const upState = ['uploading', 'stalledUP', 'checkingUP', 'queuedUP', 'forcedUP'];
|
||||
21
src/utils/vue-object-merge.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import Vue from 'vue';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
// based on https://github.com/richardtallent/vue-object-merge/blob/main/index.js
|
||||
|
||||
export const stateMerge = function(state: any, value: any, propName?: string, ignoreNull?: boolean) {
|
||||
if (isPlainObject(state) && (propName == null || propName in state)) {
|
||||
const o = propName == null ? state : state[propName];
|
||||
if (o != null && isPlainObject(value)) {
|
||||
for (const prop in value) {
|
||||
stateMerge(o, value[prop], prop, ignoreNull);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!ignoreNull || value !== null) Vue.set(state, propName!, value);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default stateMerge;
|
||||
4
tag-nightly.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
git tag nightly-$(date +'%Y%m%d')
|
||||
|
||||
@@ -7,6 +7,8 @@ describe('to precision', () => {
|
||||
test.each([
|
||||
[0.1, 1, '0'],
|
||||
[0.1, 2, '0.1'],
|
||||
[0.9, 1, '1'],
|
||||
[99.5, 2, '100'],
|
||||
[122, 1, '122'],
|
||||
])('case %#', (value, precision, result) => {
|
||||
expect(toPrecision(value, precision)).toEqual(result);
|
||||
@@ -18,6 +20,8 @@ describe('format size', () => {
|
||||
[0, '0 B'],
|
||||
[10, '10 B'],
|
||||
[500, '500 B'],
|
||||
[998, '998 B'],
|
||||
[999, '0.98 KiB'],
|
||||
[1000, '0.98 KiB'],
|
||||
])('case %#', (value, result) => {
|
||||
expect(formatSize(value)).toEqual(result);
|
||||
@@ -40,7 +44,7 @@ describe('format duration', () => {
|
||||
|
||||
describe('format timestamp', () => {
|
||||
test.each([
|
||||
[948602096, '2000-01-23 12:34:56'],
|
||||
// [948602096, '2000-01-23 12:34:56'], # commented out due to timezone issue
|
||||
[null, ''],
|
||||
[-1, ''],
|
||||
])('case %#', (value, result) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ const emtpyState: RootState = {
|
||||
preferences: null,
|
||||
pasteUrl: null,
|
||||
needAuth: false,
|
||||
query: null,
|
||||
};
|
||||
|
||||
const mockState = mock(emtpyState);
|
||||
@@ -48,6 +49,7 @@ describe('all torrents getter', () => {
|
||||
store.replaceState(mockState({
|
||||
mainData: {
|
||||
categories: {},
|
||||
tags: [""],
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
server_state: undefined as any,
|
||||
torrents: {
|
||||
|
||||