21 Commits

Author SHA1 Message Date
CzBiX
26a1228d17 Upgrade vue cli 2024-01-29 15:32:06 +08:00
CzBiX
66d4793fc0 Clean config 2024-01-29 14:39:07 +08:00
CzBiX
20930bfba9 Fix CI 2024-01-29 14:36:22 +08:00
Kevin Tan
b53f951b75 Remove service worker (#168) 2024-01-29 14:31:35 +08:00
CzBiX
396e98a29d Upgrade actions 2024-01-01 18:26:17 +08:00
Quan Dong
039c9a4b30 feat(in18): add i18n support in info dialog
Co-authored-by: tabris <tabrisdong@gmail.com>
2023-07-14 16:43:17 +08:00
CzBiX
222acd4ca3 upgrade actions 2023-05-13 19:51:04 +08:00
CzBiX
6093fd06a2 fix actions 2023-05-13 18:21:26 +08:00
CzBiX
d61085979f fix actions 2023-05-13 18:06:02 +08:00
CzBiX
c28c4f42f4 upgrade actions 2023-05-13 17:56:57 +08:00
CzBiX
81469468c2 enable rss for mobile layout, close #157 2023-05-13 17:47:51 +08:00
CzBiX
e6db6a86b2 support qb without tags feature, fix #151 2023-05-13 17:29:21 +08:00
CzBiX
16f84cdd74 upgrade yarn 2023-05-13 17:22:17 +08:00
CzBiX
49bf2f0694 fix torrents by tag, fix #158 2023-05-13 17:22:02 +08:00
jooleer
d9994f152c Added Dutch language (#155) 2023-04-23 17:45:14 +08:00
IITII
a28c31ef1f add/update some sites (#154)
* add some sites
---------

Co-authored-by: hxsf <i@hxsf.me>
Co-authored-by: CzBiX <CzBiX@users.noreply.github.com>
2023-04-16 21:46:46 +08:00
Ooggle
083b896056 update english translation (#150)
Add more consistent translation for english language
2022-12-03 15:23:01 +08:00
CzBiX
f9210b8ddb Update README.md 2022-10-07 16:55:13 +08:00
CzBiX
1cb51ac7c2 Update translation 2022-10-07 16:47:25 +08:00
CzBiX
f28cfa5c62 Tweak settings UI 2022-10-07 16:42:30 +08:00
CzBiX
9c2d613084 Do not persistent query
close #84
2022-10-07 16:11:13 +08:00
49 changed files with 14559 additions and 11593 deletions

View File

@@ -9,16 +9,14 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v1
- uses: actions/cache@v2
id: cache
uses: actions/setup-node@v4
with:
path: node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.cache.outputs.cache-hit != 'true'
node-version: '16'
- run: |
corepack enable
yarn install --frozen-lockfile
- name: Set env
run: echo "RELEASE_FILE=qb-web-${GITHUB_REF#refs/*/}.zip" >> $GITHUB_ENV

View File

@@ -6,16 +6,14 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v1
- uses: actions/cache@v2
id: cache
uses: actions/setup-node@v4
with:
path: node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.cache.outputs.cache-hit != 'true'
node-version: '16'
- run: |
corepack enable
yarn install --frozen-lockfile
- run: yarn run lint --no-fix --max-warnings 0
- run: yarn run test:unit

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -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

View 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()

View File

@@ -18,7 +18,6 @@
"debug": "^4.1.1",
"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",
@@ -31,24 +30,27 @@
"@types/debug": "^4.1.5",
"@types/jest": "^25.1.4",
"@types/lodash": "^4.14.149",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.6",
"@vue/cli-plugin-eslint": "~4.5.6",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.6",
"@vue/cli-plugin-typescript": "~4.5.6",
"@vue/cli-plugin-unit-jest": "~4.5.6",
"@vue/cli-plugin-vuex": "~4.5.6",
"@vue/cli-service": "~4.5.6",
"@vue/eslint-config-typescript": "^5.0.2",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-unit-jest": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^9.1.0",
"@vue/test-utils": "1.0.0-beta.29",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^6.2.2",
"@vue/vue2-jest": "^27.0.0-alpha.3",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"jest": "^27.1.0",
"lint-staged": "^10.1.1",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~3.9.3",
"ts-jest": "^27.0.4",
"typescript": "~4.5.5",
"vue-cli-plugin-vuetify": "^2.0.5",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.4.3"
@@ -60,5 +62,6 @@
"*.{js,vue,ts}": [
"vue-cli-service lint"
]
}
},
"packageManager": "yarn@3.5.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -60,7 +60,7 @@
</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';
@@ -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,
@@ -156,10 +164,10 @@ export default class Drawer extends Vue {
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) },
{ icon: 'mdi-rss-box', title: 'RSS', click: () => this.updateOptions('showRss', 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 },
]
@@ -239,9 +247,10 @@ export default class Drawer extends Vue {
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,

View File

@@ -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>

View File

@@ -348,6 +348,9 @@ function getStateInfo(state: string) {
filter(state, getters) {
return getters.config.filter;
},
query(state: any) {
return state.query;
},
}),
},
filters: {
@@ -421,6 +424,7 @@ export default class Torrents extends Vue {
torrentGroupBySite!: {[site: string]: Torrent[]}
torrentGroupByState!: {[state: string]: Torrent[]}
filter!: TorrentFilter
query!: string | null
updateConfig!: (_: ConfigPayload) => void
showSnackBar!: (_: SnackBarConfig) => void
@@ -454,8 +458,8 @@ export default class Torrents extends Vue {
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) ||

View File

@@ -15,16 +15,16 @@
<v-card-text>
<v-tabs v-model="tabSync">
<v-tab href="#general">
General
{{ $t("prop_tab_bar.general") }}
</v-tab>
<v-tab href="#trackers">
Trackers
{{ $t("prop_tab_bar.trackers") }}
</v-tab>
<v-tab href="#peers">
Peers
{{ $t("prop_tab_bar.peers") }}
</v-tab>
<v-tab href="#content">
Content
{{ $t("prop_tab_bar.content") }}
</v-tab>
</v-tabs>
<v-tabs-items

View File

@@ -50,6 +50,7 @@ import { formatSize } from '../../filters';
import BaseTorrentInfo from './baseTorrentInfo';
import Component from 'vue-class-component';
import { Prop } from 'vue-property-decorator';
import { tr } from '@/locale'
@Component({
filters: {
@@ -74,17 +75,17 @@ export default class Peers extends BaseTorrentInfo {
readonly hash!: string
headers = [
{ text: 'IP', value: 'ip' },
{ text: 'Connection', value: 'connection' },
{ text: 'Flags', value: 'flags' },
{ text: 'Client', value: 'client' },
{ text: 'Progress', value: 'progress' },
{ text: 'DL Speed', value: 'dl_speed' },
{ text: 'Downloaded', value: 'downloaded' },
{ text: 'UP Speed', value: 'up_speed' },
{ text: 'Uploaded', value: 'uploaded' },
{ text: 'Relevance', value: 'relevance' },
{ text: 'Files', value: 'files' },
{ text: tr('properties_widget.ip'), value: 'ip' },
{ text: tr('properties_widget.connection'), value: 'connection' },
{ text: tr('properties_widget.flags'), value: 'flags' },
{ text: tr('properties_widget.client'), value: 'client' },
{ text: tr('properties_widget.progress'), value: 'progress' },
{ text: tr('properties_widget.downloadSpeed'), value: 'dl_speed' },
{ text: tr('properties_widget.downloaded'), value: 'downloaded' },
{ text: tr('properties_widget.uploadSpeed'), value: 'up_speed' },
{ text: tr('properties_widget.uploaded'), value: 'uploaded' },
{ text: tr('properties_widget.relevance'), value: 'relevance' },
{ text: tr('properties_widget.files'), value: 'files' },
]
peersObj: any = null

View File

@@ -76,118 +76,119 @@
</v-btn>
</div>
<v-divider />
<div class="content">
<div class="content-inner">
<div
v-if="!rssNode"
class="loading"
>
<v-progress-circular indeterminate />
</div>
<template v-else>
<div class="rss-items">
<v-treeview
open-on-click
open-all
:items="rssTree"
item-key="path"
activatable
dense
@update:active="selectNode = $event[0]"
>
<template v-slot:prepend="row">
<v-progress-circular
v-if="isItemLoading(row)"
indeterminate
size="22"
width="2"
/>
<v-icon
v-else
v-text="getRowIcon(row)"
/>
</template>
<template v-slot:label="row">
{{ row.item.name }}
<template v-if="row.item.children">
({{ row.item.children.length }})
</template>
</template>
</v-treeview>
</div>
<v-divider vertical />
<div class="rss-details">
<div class="rss-info">
<p>
{{ $t('title._') }}:
<a
v-if="selectItem"
target="_blank"
:href="selectItem.url"
>{{ selectItem.title }}</a>
</p>
<p>{{ $t('date') }}: {{ (selectItem ? selectItem.lastBuildDate : null) | date }}</p>
</div>
<v-divider />
<div class="list-wrapper">
<v-list
v-if="selectItem"
dense
>
<v-list-item-group
v-model="selectArticle"
color="primary"
>
<v-list-item
v-for="article in sortArticles(selectItem.articles)"
:key="article.id"
:value="article"
>
<v-list-item-content>
<v-list-item-title>
<span
:title="article.title"
v-text="article.title"
/>
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn
icon
@click.stop="downloadTorrent(article)"
>
<v-icon>mdi-download</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list-item-group>
</v-list>
</div>
</div>
<v-divider vertical />
<div class="rss-desc">
<div class="rss-info">
<p>
{{ $t('title._') }}:
<a
v-if="selectArticle"
target="_blank"
:href="selectArticle.link"
>{{ selectArticle.title }}</a>
</p>
<p>{{ `${$t('category', 1)}: ${selectArticle ? selectArticle.category: ''}` }}</p>
<p>{{ $t('date') }}: {{ (selectArticle ? selectArticle.date: null) | date }}</p>
</div>
<v-divider />
<iframe
class="iframe"
sandbox="allow-same-origin"
v-if="selectArticle"
v-body="selectArticle.description"
/>
</div>
</template>
<div
class="content"
:class="{phone: $vuetify.breakpoint.smAndDown}"
>
<div
v-if="!rssNode"
class="loading"
>
<v-progress-circular indeterminate />
</div>
<template v-else>
<div class="rss-items">
<v-treeview
open-on-click
open-all
:items="rssTree"
item-key="path"
activatable
dense
@update:active="selectNode = $event[0]"
>
<template v-slot:prepend="row">
<v-progress-circular
v-if="isItemLoading(row)"
indeterminate
size="22"
width="2"
/>
<v-icon
v-else
v-text="getRowIcon(row)"
/>
</template>
<template v-slot:label="row">
{{ row.item.name }}
<template v-if="row.item.children">
({{ row.item.children.length }})
</template>
</template>
</v-treeview>
</div>
<v-divider :vertical="!phoneLayout" />
<div class="rss-details">
<div class="rss-info">
<p>
{{ $t('title._') }}:
<a
v-if="selectItem"
target="_blank"
:href="selectItem.url"
>{{ selectItem.title }}</a>
</p>
<p>{{ $t('date') }}: {{ (selectItem ? selectItem.lastBuildDate : null) | date }}</p>
</div>
<v-divider />
<div class="list-wrapper">
<v-list
v-if="selectItem"
dense
>
<v-list-item-group
v-model="selectArticle"
color="primary"
>
<v-list-item
v-for="article in sortArticles(selectItem.articles)"
:key="article.id"
:value="article"
>
<v-list-item-content>
<v-list-item-title>
<span
:title="article.title"
v-text="article.title"
/>
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn
icon
@click.stop="downloadTorrent(article)"
>
<v-icon>mdi-download</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list-item-group>
</v-list>
</div>
</div>
<v-divider :vertical="!phoneLayout" />
<div class="rss-desc">
<div class="rss-info">
<p>
{{ $t('title._') }}:
<a
v-if="selectArticle"
target="_blank"
:href="selectArticle.link"
>{{ selectArticle.title }}</a>
</p>
<p>{{ `${$t('category', 1)}: ${selectArticle ? selectArticle.category: ''}` }}</p>
<p>{{ $t('date') }}: {{ (selectArticle ? selectArticle.date: null) | date }}</p>
</div>
<v-divider />
<iframe
class="iframe"
sandbox="allow-same-origin"
v-if="selectArticle"
v-body="selectArticle.description"
/>
</div>
</template>
</div>
</v-card-text>
</v-card>
@@ -289,6 +290,10 @@ export default class RssDialog extends HasTask {
showSnackBar!: (_: SnackBarConfig) => void
closeSnackBar!: () => void
get phoneLayout() {
return this.$vuetify.breakpoint.smAndDown;
}
get rssTree() {
if (!this.rssNode) {
return [];
@@ -535,14 +540,10 @@ export default class RssDialog extends HasTask {
.content {
flex: 1;
position: relative;
display: flex;
.content-inner {
position: absolute;
width: 100%;
height: 100%;
display: flex;
&.phone {
flex-direction: column;
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="torrent-info">
<div class="progress">
<span>Progress:</span>
<span>{{ $t('properties_widget.progress') }}:</span>
<canvas
ref="canvas"
class="progress-inner"
@@ -9,7 +9,7 @@
<span>{{ torrent.progress | progress }}</span>
</div>
<fieldset>
<legend>Transfer</legend>
<legend>{{ $t('properties_widget.transfer') }}</legend>
<v-container
v-if="properties"
class="pa-1"
@@ -41,7 +41,7 @@
</v-container>
</fieldset>
<fieldset>
<legend>Information</legend>
<legend>{{ $t('properties_widget.information') }}</legend>
<v-container
v-if="properties"
class="pa-1"
@@ -85,6 +85,7 @@ import {Torrent, TorrentProperties} from '@/types'
import Component from 'vue-class-component'
import {Prop, Watch} from 'vue-property-decorator'
import BaseTorrentInfo from './baseTorrentInfo'
import { tr } from '@/locale'
interface Item {
label: string;
@@ -108,33 +109,33 @@ export default class TorrentInfo extends BaseTorrentInfo {
transfer: Item[] = [
{
label: 'Time active',
value: prop => formatDuration(prop.time_elapsed) + (prop.seeding_time ? ` (seeded ${formatDuration(prop.seeding_time)})` : ''),
label: tr('properties_widget.timeActive'),
value: prop => formatDuration(prop.time_elapsed) + (prop.seeding_time ? ` ($tr('properties_widget.seeded') ${formatDuration(prop.seeding_time)})` : ''),
},
{ label: 'ETA', value: prop => formatDuration(prop.eta, { dayLimit: 100 }) },
{ label: 'Connections', value: prop => `${prop.nb_connections} (${prop.nb_connections_limit} max)` },
{ label: 'Downloaded', value: prop => `${formatSize(prop.total_downloaded_session)}/${formatSize(prop.total_downloaded)}` },
{ label: 'Uploaded', value: prop => `${formatSize(prop.total_uploaded_session)}/${formatSize(prop.total_uploaded)}` },
{ label: 'Seeds', value: prop => `${prop.seeds} (${prop.seeds_total} total)` },
{ label: 'DL speed', value: prop => `${formatSize(prop.dl_speed)}/s` },
{ label: 'UP speed', value: prop => `${formatSize(prop.up_speed)}/s` },
{ label: 'Peers', value: prop => `${prop.peers} (${prop.peers_total} total)` },
{ label: 'Wasted', value: prop => formatSize(prop.total_wasted) },
{ label: 'Share ratio', value: prop => toPrecision(prop.share_ratio, 3) },
{ label: 'Reannounce', value: prop => formatDuration(prop.reannounce) },
{ label: 'Last seen', value: prop => formatTimestamp(prop.last_seen) },
{ label: tr('properties_widget.eta'), value: prop => formatDuration(prop.eta, { dayLimit: 100 }) },
{ label: tr('properties_widget.connections'), value: prop => `${prop.nb_connections} (${prop.nb_connections_limit} ${tr('properties_widget.max')})` },
{ label: tr('properties_widget.downloaded'), value: prop => `${formatSize(prop.total_downloaded_session)}/${formatSize(prop.total_downloaded)}` },
{ label: tr('properties_widget.uploaded'), value: prop => `${formatSize(prop.total_uploaded_session)}/${formatSize(prop.total_uploaded)}` },
{ label: tr('properties_widget.seeds'), value: prop => `${prop.seeds} (${prop.seeds_total} ${tr('properties_widget.total')})` },
{ label: tr('properties_widget.downloadSpeed'), value: prop => `${formatSize(prop.dl_speed)}/${tr('properties_widget.second')}` },
{ label: tr('properties_widget.uploadSpeed'), value: prop => `${formatSize(prop.up_speed)}/${tr('properties_widget.second')}` },
{ label: tr('properties_widget.peers'), value: prop => `${prop.peers} (${prop.peers_total} ${tr('properties_widget.total')})` },
{ label: tr('properties_widget.wasted'), value: prop => formatSize(prop.total_wasted) },
{ label: tr('properties_widget.shareRatio'), value: prop => toPrecision(prop.share_ratio, 3) },
{ label: tr('properties_widget.reannounce'), value: prop => formatDuration(prop.reannounce) },
{ label: tr('properties_widget.lastSeen'), value: prop => formatTimestamp(prop.last_seen) },
]
information: Item[] = [
{ label: 'Total size', value: prop => formatSize(prop.total_size) },
{ label: 'Pieces', value: prop => `${prop.pieces_num} x ${formatSize(prop.piece_size)} (have ${prop.pieces_have})` },
{ label: 'Created by', value: prop => prop.created_by },
{ label: 'Created on', value: prop => formatTimestamp(prop.creation_date) },
{ label: 'Added on', value: prop => formatTimestamp(prop.addition_date) },
{ label: 'Completed on', value: prop => formatTimestamp(prop.completion_date) },
{ label: 'Torrent hash', value: () => this.torrent.hash },
{ label: 'Save path', value: prop => prop.save_path },
{ label: 'Comment', value: prop => prop.comment },
{ label: tr('properties_widget.totalSize'), value: prop => formatSize(prop.total_size) },
{ label: tr('properties_widget.pieces'), value: prop => `${prop.pieces_num} x ${formatSize(prop.piece_size)} (${tr('properties_widget.have')} ${prop.pieces_have})` },
{ label: tr('properties_widget.createdBy'), value: prop => prop.created_by },
{ label: tr('properties_widget.createdOn'), value: prop => formatTimestamp(prop.creation_date) },
{ label: tr('properties_widget.addedOn'), value: prop => formatTimestamp(prop.addition_date) },
{ label: tr('properties_widget.completedOn'), value: prop => formatTimestamp(prop.completion_date) },
{ label: tr('properties_widget.torrentHash'), value: () => this.torrent.hash },
{ label: tr('properties_widget.savePath'), value: prop => prop.save_path },
{ label: tr('properties_widget.comment'), value: prop => prop.comment },
]
pieces: PieceState[] = []
canvas: CanvasRenderingContext2D | null = null

View File

@@ -25,16 +25,17 @@ import api from '../../Api';
import Component from 'vue-class-component';
import { Prop } from 'vue-property-decorator';
import BaseTorrentInfo from './baseTorrentInfo';
import { tr } from '@/locale'
@Component({
filters: {
formatTrackerStatus(status: number) {
const map = [
'Disabled',
'Not contacted',
'Working',
'Updating',
'Not working',
tr('properties_widget.disabled'),
tr('properties_widget.notContracted'),
tr('properties_widget.working'),
tr('properties_widget.updating'),
tr('properties_widget.notWorking'),
];
return map[status];
@@ -53,14 +54,14 @@ export default class Trackers extends BaseTorrentInfo {
readonly hash!: string
readonly headers = [
{ text: '#', value: 'tier' },
{ text: 'URL', value: 'url' },
{ text: 'Status', value: 'status' },
{ text: 'Peers', value: 'num_peers' },
{ text: 'Seeds', value: 'num_seeds' },
{ text: 'Leeches', value: 'num_leeches' },
{ text: 'Downloaded', value: 'num_downloaded' },
{ text: 'Message', value: 'msg' },
{ text: tr('properties_widget.tier'), value: 'tier' },
{ text: tr('properties_widget.url'), value: 'url' },
{ text: tr('properties_widget.status'), value: 'status' },
{ text: tr('properties_widget.numPeers'), value: 'num_peers' },
{ text: tr('properties_widget.numSeeds'), value: 'num_seeds' },
{ text: tr('properties_widget.numLeeches'), value: 'num_leeches' },
{ text: tr('properties_widget.numDownloaded'), value: 'num_downloaded' },
{ text: tr('properties_widget.msg'), value: 'msg' },
]
trackers = []

View File

@@ -46,6 +46,7 @@
>
<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])"
@@ -53,6 +54,7 @@
</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])"
@@ -60,6 +62,7 @@
</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])"
@@ -67,6 +70,7 @@
</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])"
@@ -74,6 +78,7 @@
</preference-row>
<preference-row i18n-key="save_path">
<v-text-field
dense
:value="preferences.save_path"
@change="changeSettings('save_path', $event)"
lazy
@@ -82,6 +87,7 @@
<preference-row i18n-key="temp_path">
<template v-slot:header>
<v-checkbox
dense
:value="preferences.temp_path_enabled"
@change="changeSettings('temp_path_enabled', $event)"
/>
@@ -91,6 +97,7 @@
:value="preferences.temp_path"
@change="changeSettings('temp_path', $event)"
lazy
dense
/>
</preference-row>
<preference-row

View File

@@ -3,18 +3,19 @@
align="center"
dense
>
<v-col cols="3">
<v-subheader v-text="$t('preferences.' + this.$props.i18nKey)" />
</v-col>
<v-col cols="4">
<slot />
</v-col>
<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>
@@ -28,3 +29,9 @@ export default class PreferenceRow extends Vue {
readonly i18nKey?: string
}
</script>
<style lang="scss" scoped>
.header {
height: 48px;
}
</style>

View File

@@ -1,61 +1,59 @@
<template>
<div>
<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-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-tabs v-model="tab">
<v-tab
v-for="item of tabList"
:key="item"
>
{{ $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-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>
</div>
{{ $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-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">
@@ -114,4 +112,7 @@ export default class SettingsDialog extends Vue {
@import "~@/assets/styles.scss";
@include dialog-title;
::v-deep .v-card__text {
}
</style>

View File

@@ -53,7 +53,7 @@ export default {
all: 'All',
category: 'Category |||| Categories',
uncategorized: 'Uncategorized',
tag: 'Tag',
tag: 'Tag |||| Tags',
untagged: 'Untagged',
others: 'Others',
sites: 'Sites',
@@ -182,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: {
@@ -260,4 +260,69 @@ export default {
moving: 'moving',
unknown: 'unknown',
},
prop_tab_bar: {
general: 'General',
trackers: 'Trackers',
peers: 'Peers',
// httpSource: 'HTTP Sources',
content: 'Content',
},
properties_widget: {
disabled: 'Disabled',
notContracted: 'Not contacted',
working: 'Working',
updating: 'Updating',
notWorking: 'Not working',
tier: '#',
url: 'URL',
status: 'Status',
numPeers: 'Peers',
numSeeds: 'Seeds',
numLeeches: 'Leeches',
numDownloaded: 'Downloaded',
msg: 'Message',
progress: 'Progress',
transfer: 'Transfer',
information: 'Information',
timeActive: 'Time active',
eta: 'ETA',
connections: 'Connections',
downloaded: 'Downloaded',
uploaded: 'Uploaded',
seeds: 'Seeds',
downloadSpeed: 'DL speed',
uploadSpeed: 'UP speed',
peers: 'Peers',
wasted: 'Wasted',
shareRatio: 'Share ratio',
reannounce: 'Reannounce',
lastSeen: 'Last seen',
totalSize: 'Total size',
pieces: 'Pieces',
CreatedBy: 'Created by',
CreatedOn: 'Created on',
addedOn: 'Added on',
completedOn: 'Completed on',
torrentHash: 'Torrent hash',
savePath: 'Save path',
comment: 'Comment',
ip: 'IP',
connection: 'Connection',
flags: 'Flags',
client: 'Client',
relevance: 'Relevance',
files: 'Files',
seeded: 'seeded',
second: 's',
total: 'Total',
max: 'max',
have: 'have',
},
}

View File

@@ -4,6 +4,7 @@ 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';
@@ -13,6 +14,7 @@ export const translations = {
'tr': langTr,
'zh-CN': langZhCn,
'zh-TW': langZhTw,
'nl': langNl,
}
export type LocaleKey = keyof typeof translations | null;

263
src/locale/nl.ts Normal file
View 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',
},
}

View File

@@ -18,6 +18,7 @@ export default {
resume: '恢复',
pause: '暂停',
force_start: '强制继续',
toggle_sequential: '切换顺序下载',
info: '信息',
reset: '重置',
login: '登录',
@@ -45,7 +46,6 @@ export default {
added_on: '添加时间',
settings: '设置',
logs: '日志',
light: '亮色',
dark: '暗色',
@@ -66,6 +66,9 @@ export default {
plugin: '插件',
action: '操作',
search_engine: '搜索引擎',
usage: '用法',
plugin_manager: '插件管理',
update_plugins: '更新插件',
preferences: {
change_applied: '配置已保存',
@@ -108,6 +111,7 @@ export default {
connection: '连接',
bittorrent: 'BitTorrent',
rss: 'RSS',
rss_processing_enabled: '启用自动刷新',
rss_auto_downloading_enabled: '启用自动下载种子',
rss_refresh_interval: '订阅刷新间隔',
@@ -165,6 +169,7 @@ export default {
reannounced: '已重新通告',
rechecking: '重新检查中…',
dht_nodes: '%{smart_count} 节点',
base_url: 'Base URL',
},
msg: {
@@ -255,4 +260,68 @@ export default {
moving: '移动中',
unknown: '未知',
},
prop_tab_bar: {
general: '普通',
trackers: 'Tracker',
peers: '用户',
// httpSource: 'HTTP 源',
content: '内容',
},
properties_widget: {
disabled: '禁用',
notContracted: '未联系',
working: '工作',
updating: '更新...',
notWorking: '未工作',
tier: '层级',
url: 'URL',
status: '状态',
numPeers: '用户',
numSeeds: '种子',
numLeeches: '下载',
numDownloaded: '下载次数',
msg: '消息',
progress: '进度',
transfer: '传输',
information: '信息',
timeActive: '活动时间',
eta: '剩余时间',
connections: '连接',
downloaded: '已下载',
uploaded: '已上传',
seeds: '种子',
downloadSpeed: '下载速度',
uploadSpeed: '上传速度',
peers: '用户',
wasted: '已丢弃',
shareRatio: '分享率',
reannounce: '下次汇报',
lastSeen: '最后完整可见',
totalSize: '总大小',
pieces: '区块',
createdBy: '创建',
createdOn: '创建于',
addedOn: '添加于',
completedOn: '完成于',
torrentHash: '种子哈希',
savePath: '保存路径',
comment: '注释',
ip: 'IP',
connection: '连接',
flags: '标志',
client: '客户端',
relevance: '文件关联',
files: '文件',
seeded: '已做种',
second: '秒',
total: '总计',
max: '最大',
have: '已完成',
},
}

View File

@@ -15,7 +15,6 @@ import App from './App.vue';
import 'roboto-fontface/css/roboto/roboto-fontface.css';
import '@mdi/font/css/materialdesignicons.css';
import './registerServiceWorker';
Vue.config.productionTip = false;

View File

@@ -1,32 +0,0 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB',
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
},
})
}

View File

@@ -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;

View File

@@ -13,7 +13,6 @@ export interface Config {
state: string | null;
category: string | null;
site: string | null;
query: string | null;
};
locale: string | null;
darkMode: string | null;
@@ -30,7 +29,6 @@ const defaultConfig = {
state: null,
category: null,
site: null,
query: null,
},
locale: null,
darkMode: null,

View File

@@ -13,6 +13,7 @@ import searchEngineStore from './searchEngine';
import { RootState } from './types';
import stateMerge from '@/utils/vue-object-merge';
import api from '@/Api';
import { Torrent } from '@/types'
Vue.use(Vuex);
@@ -30,6 +31,7 @@ const store = new Vuex.Store<RootState>({
preferences: null,
pasteUrl: null,
needAuth: false,
query: null,
},
mutations: {
/* eslint-disable no-param-reassign */
@@ -72,6 +74,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: {
@@ -106,7 +111,8 @@ const store = new Vuex.Store<RootState>({
}
const finalTags: any[] = []
for (const tag of state.mainData.tags) {
const tags = state.mainData.tags ?? [];
for (const tag of tags) {
finalTags.push({
"key": tag,
"name": tag,
@@ -118,11 +124,15 @@ const store = new Vuex.Store<RootState>({
return groupBy(getters.allTorrents, torrent => torrent.category);
},
torrentGroupByTag(state, getters) {
const result: any = {}
const result: Record<string, Torrent[]> = {}
for (const torrent of getters.allTorrents) {
const tags: any[] = torrent.tags.split(",");
if (!torrent.tags) {
continue;
}
const tags: string[] = torrent.tags.split(', ');
tags.forEach(tag => {
let list: any[] = result[tag]
let list: Torrent[] = result[tag]
if (!list) {
list = []
result[tag] = list;

View File

@@ -7,6 +7,7 @@ export interface RootState {
preferences: any;
pasteUrl: string | null;
needAuth: boolean;
query: string | null;
}
export interface SearchEnginePage {

View File

@@ -44,7 +44,7 @@ describe('format duration', () => {
describe('format timestamp', () => {
test.each([
// [948602096, '2000-01-23 12:34:56'], # comment for timezone issue
// [948602096, '2000-01-23 12:34:56'], # commented out due to timezone issue
[null, ''],
[-1, ''],
])('case %#', (value, result) => {

View File

@@ -14,6 +14,7 @@ const emtpyState: RootState = {
preferences: null,
pasteUrl: null,
needAuth: false,
query: null,
};
const mockState = mock(emtpyState);

View File

@@ -15,9 +15,6 @@ module.exports = {
maskIcon: null,
msTileImage: null,
},
workboxOptions: {
importWorkboxFrom: 'local',
},
},
devServer: {

24903
yarn.lock

File diff suppressed because it is too large Load Diff