mirror of
https://github.com/CzBiX/qb-web.git
synced 2026-02-03 10:34:23 +08:00
Compare commits
1 Commits
nightly-20
...
dark-color
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54a563d187 |
@@ -1,17 +0,0 @@
|
||||
# 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>"
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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
|
||||
@@ -16,14 +16,12 @@ module.exports = {
|
||||
'no-console': isProdEnv ? 'error' : 'warn',
|
||||
'no-debugger': isProdEnv ? 'error' : 'warn',
|
||||
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
|
||||
'vue/singleline-html-element-content-newline': ['warn', {
|
||||
ignores: ['pre', 'textarea', 'span', 'v-icon'],
|
||||
}],
|
||||
}]
|
||||
},
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe the bug
|
||||
|
||||
_description of what the bug is, and the conditions when it occurs._
|
||||
|
||||
## How to reproduce
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Expected behavior
|
||||
|
||||
_description of what you expected to happen._
|
||||
|
||||
## Official behavior
|
||||
|
||||
_description of what happened in the official web ui._
|
||||
|
||||
## Versions
|
||||
|
||||
- qBittorrent: [e.g. 4.2.5]
|
||||
- qb-web: [e.g. 20201023]
|
||||
|
||||
## Additional notes
|
||||
|
||||
_Add any other notes about the problem here._
|
||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@@ -1,62 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- nightly-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
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'
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_FILE=qb-web-${GITHUB_REF#refs/*/}.zip" >> $GITHUB_ENV
|
||||
|
||||
- name: Pack Release
|
||||
run: |
|
||||
yarn run build
|
||||
# see https://github.com/qbittorrent/qBittorrent/pull/10485, fixed in qb v4.2.0
|
||||
cp dist/public/{index,login}.html
|
||||
cp INSTALL.md dist
|
||||
zip -r $RELEASE_FILE dist
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
cd dist/public
|
||||
git init
|
||||
git config user.name ${{ github.actor }}
|
||||
git config user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git remote add origin https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git
|
||||
git checkout -b gh-pages
|
||||
git add --all
|
||||
git commit -m "Publish"
|
||||
git push origin gh-pages -f
|
||||
|
||||
- id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
prerelease: true
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ${{ env.RELEASE_FILE }}
|
||||
asset_name: ${{ env.RELEASE_FILE }}
|
||||
asset_content_type: application/zip
|
||||
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
@@ -1,21 +0,0 @@
|
||||
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
3
.gitignore
vendored
@@ -20,6 +20,3 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/.devcontainer/downloads
|
||||
/.devcontainer/qbittorrent
|
||||
38
.travis.yml
Normal file
38
.travis.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
language: node_js
|
||||
node_js: lts/*
|
||||
|
||||
branches:
|
||||
except: /^nightly-.*$/
|
||||
|
||||
before_install:
|
||||
- export TZ=Asia/Shanghai COMMIT_ID=${TRAVIS_COMMIT::6}
|
||||
|
||||
script:
|
||||
- yarn run lint
|
||||
- yarn run test:unit
|
||||
- yarn run build
|
||||
|
||||
before_deploy:
|
||||
# Add tag if missing
|
||||
- if [ -z "$TRAVIS_TAG" ]; then
|
||||
export TRAVIS_TAG=nightly-$COMMIT_ID RELEASE_TITLE=$(date +'%Y%m%d-%H%M%S') &&
|
||||
export RELEASE_FILE=qb-web-$RELEASE_TITLE.zip &&
|
||||
git tag $TRAVIS_TAG &&
|
||||
echo tagged as $TRAVIS_TAG;
|
||||
fi
|
||||
# Pack for release
|
||||
# see https://github.com/qbittorrent/qBittorrent/pull/10485, fixed in qb v4.2.0
|
||||
- cp dist/public/{index,login}.html
|
||||
- cp INSTALL.md dist
|
||||
- zip -r $RELEASE_FILE dist
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_OAUTH_TOKEN
|
||||
file: $RELEASE_FILE
|
||||
skip_cleanup: true
|
||||
overwrite: true
|
||||
prerelease: true
|
||||
name: $RELEASE_TITLE
|
||||
on:
|
||||
branch: master
|
||||
24
DEV.md
24
DEV.md
@@ -1,24 +0,0 @@
|
||||
# qb-web
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
674
LICENSE
674
LICENSE
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
21
README.md
21
README.md
@@ -1,19 +1,24 @@
|
||||
# qb-web
|
||||
## Info
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://github.com/CzBiX/qb-web/releases/latest)
|
||||
[](https://gitter.im/qb-web/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://github.com/CzBiX/qb-web/actions)
|
||||
[](https://travis-ci.com/CzBiX/qb-web)
|
||||
|
||||
## Features
|
||||
Keywords: SPA, RSS, Search, Responsive Design, Modern Design, i18n
|
||||
Keywords: SPA, RSS, Responsive Design, Modern Design, i18n
|
||||
|
||||
Languages: English, 中文, Русский, Türkçe
|
||||
Languages: English, 中文
|
||||
|
||||
[TODO](https://github.com/CzBiX/qb-web/projects/2)
|
||||
TODO:
|
||||
- [ ] Settings UI
|
||||
|
||||
## How to use
|
||||
see: [Wiki](https://github.com/CzBiX/qb-web/wiki/How-to-use)
|
||||
## Download
|
||||
|
||||
[Releases](https://github.com/CzBiX/qb-web/releases/latest)
|
||||
|
||||
## Install
|
||||
|
||||
see: [INSTALL.md](./INSTALL.md)
|
||||
|
||||
## Wiki
|
||||
|
||||
|
||||
45
package.json
45
package.json
@@ -11,48 +11,51 @@
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.0.45",
|
||||
"@types/node-polyglot": "^0.4.34",
|
||||
"@vue/composition-api": "^1.0.5",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.6.5",
|
||||
"@vue/composition-api": "^0.5.0",
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^3.6.4",
|
||||
"dayjs": "^1.8.23",
|
||||
"debug": "^4.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.17.15",
|
||||
"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": "^9.1.2",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.5.8",
|
||||
"vuex": "^3.4.0"
|
||||
"vue-property-decorator": "^8.4.1",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuetify": "^2.2.20",
|
||||
"vuex": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^2.25.0",
|
||||
"@typescript-eslint/parser": "^2.25.0",
|
||||
"@vue/cli-plugin-babel": "^4.2.3",
|
||||
"@vue/cli-plugin-eslint": "^4.2.3",
|
||||
"@vue/cli-plugin-router": "^4.2.3",
|
||||
"@vue/cli-plugin-typescript": "^4.2.3",
|
||||
"@vue/cli-plugin-unit-jest": "^4.2.3",
|
||||
"@vue/cli-plugin-vuex": "^4.2.3",
|
||||
"@vue/cli-service": "^4.2.3",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"lint-staged": "^10.1.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass": "^1.26.3",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "~3.9.3",
|
||||
"typescript": "^3.8.3",
|
||||
"vue-cli-plugin-vuetify": "^2.0.5",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.4.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"babel-jest": "^25.0.0",
|
||||
"ts-jest": "^25.0.0",
|
||||
"jest": "^25.0.0"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
|
||||
BIN
public/favicon.ico
Executable file
BIN
public/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 775 B |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -5,8 +5,7 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="referrer" content="same-origin">
|
||||
<meta name="description" content="qBittorrent Web UI" />
|
||||
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>qBittorrent Web UI</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"icons": [
|
||||
{
|
||||
"src": "./img/icons/favicon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
124
src/Api.ts
124
src/Api.ts
@@ -1,45 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import Axios, { AxiosInstance, AxiosPromise, AxiosResponse } from 'axios';
|
||||
import {
|
||||
RssNode,
|
||||
RssRule,
|
||||
SearchPlugin,
|
||||
ApiCategory,
|
||||
SearchTaskResponse,
|
||||
Preferences,
|
||||
MainData,
|
||||
} from '@/types'
|
||||
|
||||
const apiEndpoint = 'api/v2';
|
||||
import Axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { RssNode, RssRule } from '@/types';
|
||||
|
||||
class Api {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axios = Axios.create({
|
||||
baseURL: apiEndpoint,
|
||||
withCredentials: true,
|
||||
baseURL: 'api/v2',
|
||||
});
|
||||
|
||||
this.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
private normalizeBaseUrl(baseUrl?: string) {
|
||||
if (!baseUrl) {
|
||||
return apiEndpoint;
|
||||
}
|
||||
|
||||
if (!baseUrl.endsWith('/')) {
|
||||
baseUrl += '/';
|
||||
}
|
||||
|
||||
return baseUrl + apiEndpoint;
|
||||
}
|
||||
|
||||
public changeBaseUrl(baseUrl: string) {
|
||||
this.axios.defaults.baseURL = this.normalizeBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
public getAppVersion() {
|
||||
return this.axios.get('/app/version');
|
||||
}
|
||||
@@ -48,13 +21,12 @@ class Api {
|
||||
return this.axios.get('/app/webapiVersion');
|
||||
}
|
||||
|
||||
public login(params: any, baseUrl?: string) {
|
||||
public login(params: any) {
|
||||
const data = new URLSearchParams(params);
|
||||
return this.axios.post('/auth/login', data, {
|
||||
validateStatus(status) {
|
||||
return status === 200 || status === 403;
|
||||
},
|
||||
baseURL: this.normalizeBaseUrl(baseUrl),
|
||||
}).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
@@ -62,15 +34,11 @@ class Api {
|
||||
return this.axios.get('/transfer/info');
|
||||
}
|
||||
|
||||
public getAppPreferences(): AxiosPromise<Preferences> {
|
||||
public getAppPreferences() {
|
||||
return this.axios.get('/app/preferences');
|
||||
}
|
||||
|
||||
public shutdownApplication() {
|
||||
return this.axios.post('/app/shutdown');
|
||||
}
|
||||
|
||||
public getMainData(rid?: number): AxiosPromise<MainData> {
|
||||
public getMainData(rid?: number) {
|
||||
const params = {
|
||||
rid,
|
||||
};
|
||||
@@ -153,14 +121,6 @@ class Api {
|
||||
return this.actionTorrents('resume', hashes);
|
||||
}
|
||||
|
||||
public setForceStartTorrents(hashes: string[]) {
|
||||
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);
|
||||
}
|
||||
@@ -174,7 +134,13 @@ class Api {
|
||||
}
|
||||
|
||||
public getTorrentTracker(hash: string) {
|
||||
return this.actionTorrent('trackers', hash);
|
||||
const params = {
|
||||
hash,
|
||||
};
|
||||
|
||||
return this.axios.get('/torrents/trackers', {
|
||||
params,
|
||||
}).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public getTorrentPeers(hash: string, rid?: number) {
|
||||
@@ -189,7 +155,7 @@ class Api {
|
||||
}
|
||||
|
||||
public editTracker(hash: string, origUrl: string, newUrl: string) {
|
||||
return this.actionTorrent('editTracker', hash, { origUrl, newUrl });
|
||||
return this.actionTorrents('editTracker', [hash], { origUrl, newUrl });
|
||||
}
|
||||
|
||||
public setTorrentLocation(hashes: string[], location: string) {
|
||||
@@ -274,7 +240,7 @@ class Api {
|
||||
return this.axios.post('/rss/moveItem', data).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public getRssRules(): Promise<{ [key: string]: RssRule }> {
|
||||
public getRssRules(): Promise<{[key: string]: RssRule}> {
|
||||
return this.axios.get('/rss/rules').then(Api.handleResponse);
|
||||
}
|
||||
|
||||
@@ -297,63 +263,6 @@ class Api {
|
||||
return this.axios.post('/rss/removeRule', data).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
// Search page
|
||||
|
||||
public updateSearchPlugins(): Promise<SearchPlugin[]> {
|
||||
return this.axios.post('/search/updatePlugins').then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public getSearchPlugins(): Promise<SearchPlugin[]> {
|
||||
return this.axios.get('/search/plugins').then(Api.handleResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see getSearchCategories
|
||||
* When there are no categories available/set
|
||||
* @returns a Promise<{}> instead of Promise<[]>.
|
||||
*/
|
||||
public getSearchCategories(): Promise<ApiCategory> {
|
||||
return this.axios.get('/torrents/categories').then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public startSearch(pattern: string | null, pluginName: string | null, categoryName: string | null): Promise<{ id: number }> {
|
||||
const body = new URLSearchParams(
|
||||
{
|
||||
pattern: pattern || '',
|
||||
category: categoryName || 'all',
|
||||
plugins: pluginName || 'all',
|
||||
});
|
||||
return this.axios.post('/search/start', body).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public stopSearch(id: number) {
|
||||
const body = new URLSearchParams({ id: id.toString() });
|
||||
return this.axios.post('/search/stop', body).then(Api.handleResponse)
|
||||
}
|
||||
|
||||
public getSearchResults(id: number): Promise<SearchTaskResponse> {
|
||||
|
||||
return this.axios.get(`/search/results?id=${id}`).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public enablePlugin(plugin: SearchPlugin, enable: boolean) {
|
||||
const body = new URLSearchParams({
|
||||
names: plugin.name,
|
||||
enable: JSON.stringify(enable),
|
||||
});
|
||||
|
||||
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('|'),
|
||||
@@ -368,5 +277,4 @@ class Api {
|
||||
}
|
||||
}
|
||||
|
||||
const api = new Api();
|
||||
export default api;
|
||||
export default new Api();
|
||||
|
||||
86
src/App.vue
86
src/App.vue
@@ -15,13 +15,14 @@
|
||||
</v-navigation-drawer>
|
||||
<main-toolbar v-model="drawer" />
|
||||
|
||||
<v-main>
|
||||
<v-content>
|
||||
<torrents />
|
||||
</v-main>
|
||||
</v-content>
|
||||
|
||||
<add-form v-if="preferences" />
|
||||
<login-form
|
||||
v-if="needAuth"
|
||||
v-model="needAuth"
|
||||
/>
|
||||
<logs-dialog
|
||||
v-if="drawerOptions.showLogs"
|
||||
@@ -32,15 +33,7 @@
|
||||
v-model="drawerOptions.showRss"
|
||||
@download-torrent="setPasteUrl({url: $event})"
|
||||
/>
|
||||
<SearchDialog
|
||||
v-if="drawerOptions.showSearch"
|
||||
v-model="drawerOptions.showSearch"
|
||||
/>
|
||||
|
||||
<SettingsDialog
|
||||
v-if="drawerOptions.showSettings"
|
||||
v-model="drawerOptions.showSettings"
|
||||
/>
|
||||
<v-footer
|
||||
app
|
||||
class="elevation-4"
|
||||
@@ -71,18 +64,12 @@ import Torrents from './components/Torrents.vue';
|
||||
import AppFooter from './components/Footer.vue';
|
||||
import LogsDialog from './components/dialogs/LogsDialog.vue';
|
||||
import RssDialog from './components/dialogs/RssDialog.vue';
|
||||
import SearchDialog from './components/dialogs/searchDialog/SearchDialog.vue';
|
||||
import SettingsDialog from './components/dialogs/settingsDialog/SettingsDialog.vue';
|
||||
import DrawerFooter from './components/drawer/DrawerFooter.vue';
|
||||
|
||||
|
||||
import api from './Api';
|
||||
import Component from 'vue-class-component';
|
||||
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;
|
||||
|
||||
@@ -98,16 +85,13 @@ let appWrapEl: HTMLElement;
|
||||
GlobalDialog,
|
||||
GlobalSnackBar,
|
||||
RssDialog,
|
||||
SearchDialog,
|
||||
DrawerFooter,
|
||||
SettingsDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'mainData',
|
||||
'rid',
|
||||
'preferences',
|
||||
'needAuth',
|
||||
]),
|
||||
...mapGetters(['config']),
|
||||
},
|
||||
@@ -116,30 +100,26 @@ let appWrapEl: HTMLElement;
|
||||
'updateMainData',
|
||||
'updatePreferences',
|
||||
'setPasteUrl',
|
||||
'updateNeedAuth',
|
||||
]),
|
||||
},
|
||||
}
|
||||
})
|
||||
export default class App extends Vue {
|
||||
drawer = !this.phoneLayout
|
||||
needAuth = false
|
||||
drawer = true
|
||||
drawerOptions = {
|
||||
showLogs: false,
|
||||
showRss: false,
|
||||
showSettings: false,
|
||||
}
|
||||
task = 0
|
||||
mql?: MediaQueryList
|
||||
|
||||
mainData!: MainData
|
||||
rid!: number
|
||||
preferences!: any
|
||||
config!: Config
|
||||
needAuth!: boolean
|
||||
config!: any
|
||||
|
||||
updateMainData!: (_: any) => void
|
||||
updatePreferences!: (_: any) => void
|
||||
setPasteUrl!: (_: any) => void
|
||||
updateNeedAuth!: (_: boolean) => void
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
@@ -172,22 +152,13 @@ export default class App extends Vue {
|
||||
}
|
||||
|
||||
async getInitData() {
|
||||
const href = location.href;
|
||||
if (!this.config.baseUrl) {
|
||||
if (href.includes("czbix.github.io") || href.includes("localhost")) {
|
||||
this.updateNeedAuth(true);
|
||||
return;
|
||||
} else {
|
||||
Api.changeBaseUrl(href);
|
||||
}
|
||||
} else {
|
||||
Api.changeBaseUrl(this.config.baseUrl);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.getMainData();
|
||||
} catch (e) {
|
||||
this.updateNeedAuth(true);
|
||||
if (e.response.status === 403) {
|
||||
this.needAuth = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,18 +177,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -240,26 +200,6 @@ export default class App extends Vue {
|
||||
this.getInitData();
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('config.darkMode', {immediate: true})
|
||||
onDarkMode(mode: any) {
|
||||
const { theme } = this.$vuetify;
|
||||
|
||||
if (mode != null) {
|
||||
if (this.mql) {
|
||||
this.mql.removeListener(null)
|
||||
this.mql = undefined
|
||||
}
|
||||
theme.dark = mode
|
||||
return
|
||||
}
|
||||
|
||||
this.mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.mql.addListener((e: MediaQueryListEvent) => {
|
||||
theme.dark = e.matches
|
||||
})
|
||||
theme.dark = this.mql.matches
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -273,4 +213,4 @@ export default class App extends Vue {
|
||||
html {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
let buildInfo = process.env.GIT_TAG
|
||||
let buildInfo = process.env.COMMIT_ID
|
||||
|
||||
if (!buildInfo) {
|
||||
buildInfo = 'dev'
|
||||
@@ -8,6 +8,4 @@ if (!buildInfo) {
|
||||
console.log(`%c qb-web Build %c ${buildInfo} `,
|
||||
'background-color: #555; color: #fff; border-radius: 3px 0 0 3px;',
|
||||
'background-color: #1976d2; color: #fff; border-radius: 0 3px 3px 0;',
|
||||
)
|
||||
|
||||
export default buildInfo
|
||||
)
|
||||
@@ -7,14 +7,14 @@
|
||||
fixed
|
||||
right
|
||||
small
|
||||
@click="openAddForm"
|
||||
@click="dialog = !dialog"
|
||||
class="btn-add"
|
||||
:class="{'with-footer': $vuetify.breakpoint.smAndUp}"
|
||||
>
|
||||
<v-icon>mdi-link-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-dialog
|
||||
v-model="state.isOpen"
|
||||
v-model="dialog"
|
||||
eager
|
||||
persistent
|
||||
scrollable
|
||||
@@ -23,7 +23,7 @@
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<v-icon class="mr-2">mdi-link-plus</v-icon>
|
||||
<span>{{ state.downloadItem && state.downloadItem.title || $t('title.add_torrents') }}</span>
|
||||
<span>{{ $t('title.add_torrents') }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="pb-0">
|
||||
<v-form
|
||||
@@ -54,7 +54,6 @@
|
||||
required
|
||||
:autofocus="!phoneLayout"
|
||||
:value="params.urls"
|
||||
:readonly="state.downloadItem !== null"
|
||||
@input="setParams('urls', $event)"
|
||||
@click:append-outer="selectFiles"
|
||||
/>
|
||||
@@ -96,8 +95,7 @@
|
||||
hide-no-data
|
||||
:items="categoryItems"
|
||||
:value="params.category"
|
||||
:return-object="false"
|
||||
@input="setParams('category', $event)"
|
||||
@input="setParams('category', $event ? $event.value : null)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
@@ -180,7 +178,7 @@
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="closeAddForm"
|
||||
@click="dialog = false"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</v-btn>
|
||||
@@ -202,13 +200,12 @@
|
||||
<script lang="ts">
|
||||
import { isNil } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
import api from '../Api';
|
||||
import Component from 'vue-class-component';
|
||||
import { Watch } from 'vue-property-decorator';
|
||||
import { Preferences, Category } from '../types';
|
||||
import { AddFormState } from '@/store/types';
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
const defaultParams = {
|
||||
@@ -229,20 +226,14 @@ const defaultParams = {
|
||||
...mapState({
|
||||
pasteUrl: 'pasteUrl',
|
||||
prefs: 'preferences',
|
||||
state: 'addForm',
|
||||
}),
|
||||
...mapGetters({
|
||||
allCategories: 'allCategories',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'closeAddForm',
|
||||
'openAddForm',
|
||||
...mapGetters([
|
||||
'allCategories',
|
||||
]),
|
||||
},
|
||||
})
|
||||
export default class AddForm extends Vue {
|
||||
dialog = false
|
||||
valid = false
|
||||
files: FileList | [] = []
|
||||
defaultParams = defaultParams
|
||||
@@ -251,7 +242,6 @@ export default class AddForm extends Vue {
|
||||
submitting = false
|
||||
showMore = false
|
||||
|
||||
state!: AddFormState;
|
||||
pasteUrl!: string | null
|
||||
prefs!: Preferences
|
||||
allCategories!: Category[]
|
||||
@@ -261,10 +251,7 @@ export default class AddForm extends Vue {
|
||||
file: any;
|
||||
fileZone: HTMLElement;
|
||||
}
|
||||
|
||||
openAddForm!: () => void;
|
||||
closeAddForm!: () => void;
|
||||
|
||||
|
||||
get params() {
|
||||
return Object.assign({}, defaultParams, this.userParams);
|
||||
}
|
||||
@@ -278,11 +265,7 @@ export default class AddForm extends Vue {
|
||||
if (this.params.autoTMM && this.params.category) {
|
||||
const category = this.allCategories.find(c => {
|
||||
return c.key === this.params.category;
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return this.params.category;
|
||||
}
|
||||
})!;
|
||||
|
||||
return category.savePath || category.name
|
||||
}
|
||||
@@ -302,13 +285,6 @@ export default class AddForm extends Vue {
|
||||
this.$refs.fileZone.addEventListener('drop', this.onDrop, true);
|
||||
}
|
||||
|
||||
@Watch('state', {deep: true})
|
||||
onStateUpdate(state: AddFormState) {
|
||||
if (state.downloadItem) {
|
||||
this.setParams('urls', state.downloadItem.url);
|
||||
}
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
this.$refs.fileZone.removeEventListener('drop', this.onDrop, true);
|
||||
}
|
||||
@@ -352,7 +328,7 @@ export default class AddForm extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeAddForm();
|
||||
this.dialog = false;
|
||||
|
||||
Vue.delete(this.userParams, 'urls');
|
||||
this.files = [];
|
||||
@@ -382,9 +358,9 @@ export default class AddForm extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.isOpen) {
|
||||
if (!this.dialog) {
|
||||
Vue.set(this.userParams, 'urls', v);
|
||||
this.openAddForm();
|
||||
this.dialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +372,7 @@ export default class AddForm extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -65,53 +65,53 @@ import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import { Torrent, Category, Tag } from '@/types';
|
||||
import { Torrent, Category } 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('category_state.downloading'),
|
||||
title: tr('state.downloading'),
|
||||
state: StateType.Downloading,
|
||||
icon: 'download',
|
||||
},
|
||||
{
|
||||
title: tr('category_state.seeding'),
|
||||
title: tr('state.seeding'),
|
||||
state: StateType.Seeding,
|
||||
icon: 'upload',
|
||||
},
|
||||
{
|
||||
title: tr('category_state.completed'),
|
||||
title: tr('state.completed'),
|
||||
state: StateType.Completed,
|
||||
icon: 'check',
|
||||
},
|
||||
{
|
||||
title: tr('category_state.resumed'),
|
||||
title: tr('state.resumed'),
|
||||
state: StateType.Resumed,
|
||||
icon: 'play',
|
||||
},
|
||||
{
|
||||
title: tr('category_state.paused'),
|
||||
title: tr('state.paused'),
|
||||
state: StateType.Paused,
|
||||
icon: 'pause',
|
||||
},
|
||||
{
|
||||
title: tr('category_state.active'),
|
||||
title: tr('state.active'),
|
||||
state: StateType.Active,
|
||||
icon: 'filter',
|
||||
},
|
||||
{
|
||||
title: tr('category_state.inactive'),
|
||||
title: tr('state.inactive'),
|
||||
state: StateType.Inactive,
|
||||
icon: 'filter-outline',
|
||||
},
|
||||
{
|
||||
title: tr('category_state.errored'),
|
||||
title: tr('state.errored'),
|
||||
state: StateType.Errored,
|
||||
icon: 'alert',
|
||||
},
|
||||
@@ -121,7 +121,7 @@ interface MenuItem {
|
||||
icon: string;
|
||||
'icon-alt'?: string;
|
||||
title: string;
|
||||
model?: boolean | null;
|
||||
model?: boolean;
|
||||
select?: string;
|
||||
click?: () => void;
|
||||
children?: MenuChildrenItem[];
|
||||
@@ -141,9 +141,7 @@ interface MenuChildrenItem extends MenuItem {
|
||||
'isDataReady',
|
||||
'allTorrents',
|
||||
'allCategories',
|
||||
'allTags',
|
||||
'torrentGroupByCategory',
|
||||
'torrentGroupByTag',
|
||||
'torrentGroupBySite',
|
||||
'torrentGroupByState',
|
||||
]),
|
||||
@@ -153,34 +151,32 @@ export default class Drawer extends Vue {
|
||||
@Prop()
|
||||
readonly value: any
|
||||
|
||||
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) },
|
||||
basicItems: MenuItem[] = [
|
||||
{ icon: 'mdi-cog-box', title: tr('settings'), click: () => alert(tr('todo')) },
|
||||
]
|
||||
|
||||
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 },
|
||||
endItems: MenuItem[] = [
|
||||
{ icon: 'mdi-delta', title: tr('logs'), click: () => this.updateOptions('showLogs', true) },
|
||||
]
|
||||
|
||||
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() {
|
||||
if (this.phoneLayout) {
|
||||
return;
|
||||
if (this.phoneLayout) {
|
||||
return
|
||||
}
|
||||
|
||||
this.endItems = this.endItems.concat(this.pcItems)
|
||||
this.endItems = this.endItems.concat([
|
||||
{ icon: 'mdi-rss-box', title: 'RSS', click: () => this.updateOptions('showRss', true) },
|
||||
{ icon: 'mdi-history', title: tr('label.switch_to_old_ui'), click: this.switchUi },
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
}
|
||||
@@ -218,24 +214,6 @@ 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'));
|
||||
@@ -251,7 +229,7 @@ export default class Drawer extends Vue {
|
||||
|
||||
get items() {
|
||||
if (!this.isDataReady) {
|
||||
return this.endItems
|
||||
return this.basicItems.concat(this.endItems);
|
||||
}
|
||||
|
||||
const filterGroups: MenuItem[] = [];
|
||||
@@ -260,8 +238,8 @@ export default class Drawer extends Vue {
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('category_state._'),
|
||||
model: null,
|
||||
title: tr('state._'),
|
||||
model: false,
|
||||
select: 'state',
|
||||
children: [
|
||||
{
|
||||
@@ -275,7 +253,7 @@ export default class Drawer extends Vue {
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('category', 0),
|
||||
model: null,
|
||||
model: !this.$vuetify.breakpoint.xsOnly,
|
||||
select: 'category',
|
||||
children: [
|
||||
{
|
||||
@@ -285,25 +263,11 @@ 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',
|
||||
title: tr('sites'),
|
||||
model: null,
|
||||
model: false,
|
||||
select: 'site',
|
||||
children: [
|
||||
{
|
||||
@@ -313,7 +277,7 @@ export default class Drawer extends Vue {
|
||||
],
|
||||
});
|
||||
|
||||
return ([] as MenuItem[]).concat([{filterGroups}] as any, this.endItems);
|
||||
return this.basicItems.concat([{filterGroups}] as any, this.endItems);
|
||||
}
|
||||
|
||||
async switchUi() {
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
<span>
|
||||
API version: {{ app.apiVersion }}
|
||||
</span>
|
||||
<br>
|
||||
<span>
|
||||
qb-web version: {{ buildInfo }}
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<v-divider
|
||||
@@ -39,24 +35,10 @@
|
||||
class="mx-2"
|
||||
v-if="!phoneLayout"
|
||||
/>
|
||||
<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>
|
||||
<div class="icon-label">
|
||||
<v-icon>mdi-nas</v-icon>
|
||||
{{ info.free_space_on_disk | formatSize }}
|
||||
</div>
|
||||
<v-divider
|
||||
vertical
|
||||
class="mx-2"
|
||||
@@ -83,8 +65,8 @@
|
||||
v-if="!phoneLayout"
|
||||
class="icon-label"
|
||||
>
|
||||
<v-icon>mdi-lan</v-icon>
|
||||
{{ $t('label.dht_nodes', info.dht_nodes) }}
|
||||
<v-icon>mdi-access-point-network</v-icon>
|
||||
{{ info.dht_nodes }} nodes
|
||||
</div>
|
||||
<v-divider
|
||||
vertical
|
||||
@@ -200,19 +182,18 @@ import { sumBy } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
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({
|
||||
filters: {
|
||||
connectionIcon(status: string) {
|
||||
const statusMap: any = {
|
||||
connected: 'check-network',
|
||||
firewalled: 'minus-network',
|
||||
disconnected: 'close-network',
|
||||
connected: 'server-network',
|
||||
firewalled: 'server-network-off',
|
||||
disconnected: 'security-network',
|
||||
};
|
||||
return statusMap[status];
|
||||
},
|
||||
@@ -244,8 +225,6 @@ export default class Footer extends Vue {
|
||||
app: any = null
|
||||
speedLimited = false
|
||||
|
||||
buildInfo = buildInfo
|
||||
|
||||
info!: ServerState
|
||||
isDataReady!: boolean
|
||||
allTorrents!: Torrent[]
|
||||
@@ -328,7 +307,7 @@ export default class Footer extends Vue {
|
||||
align-items: center;
|
||||
|
||||
.v-icon {
|
||||
//margin-right: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,23 +63,21 @@ const BUTTONS = {
|
||||
],
|
||||
};
|
||||
|
||||
const DefaultDialogWidth = '25%'
|
||||
const DefaultConfig = {
|
||||
dialog: {
|
||||
width: '25%',
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
setup(_: any, ctx: any) {
|
||||
setup() {
|
||||
const mutations = useMutations(['closeDialog']);
|
||||
const { config: userConfig } = useState(['config'], 'dialog');
|
||||
const config = computed(() => {
|
||||
if (!userConfig.value) {
|
||||
return null;
|
||||
}
|
||||
const o = Object.assign({dialog: {}}, userConfig.value) as DialogConfig;
|
||||
|
||||
if (!('width' in o.dialog)) {
|
||||
o.dialog.width = ctx.root.$vuetify.breakpoint.smAndDown ? null : DefaultDialogWidth
|
||||
}
|
||||
|
||||
return o
|
||||
return Object.assign({}, DefaultConfig, userConfig.value) as DialogConfig;
|
||||
});
|
||||
const value = ref<boolean>();
|
||||
const input = ref<string>();
|
||||
@@ -117,7 +115,7 @@ export default {
|
||||
}
|
||||
|
||||
clickBtn(null);
|
||||
});
|
||||
}, { lazy: true });
|
||||
|
||||
const btns = computed(() => {
|
||||
const c = config.value;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:value="true"
|
||||
v-model="value"
|
||||
persistent
|
||||
width="25em"
|
||||
>
|
||||
@@ -21,17 +21,11 @@
|
||||
@keyup.enter.capture="submit"
|
||||
v-bind="{ [`grid-list-${$vuetify.breakpoint.name}`]: true }"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="baseUrl"
|
||||
prepend-icon="mdi-network"
|
||||
:label="$t('label.base_url')"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="params.username"
|
||||
prepend-icon="mdi-account"
|
||||
:label="$t('username')"
|
||||
:rules="[v => !!v || $t('msg.item_is_required', { item: $t('username') })]"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
@@ -42,6 +36,7 @@
|
||||
@click:append="showPassword = !showPassword"
|
||||
:label="$t('password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:rules="[v => !!v || $t('msg.item_is_required', { item: $t('password') })]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -68,65 +63,56 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs } from '@vue/composition-api';
|
||||
import Vue from 'vue';
|
||||
|
||||
import api from '@/Api';
|
||||
import { useStore } from '@/store';
|
||||
import { tr } from '@/locale';
|
||||
import api from '../Api';
|
||||
|
||||
export default defineComponent({
|
||||
setup(_, { emit }) {
|
||||
const store = useStore();
|
||||
const data = reactive({
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tr,
|
||||
valid: false,
|
||||
submitting: false,
|
||||
showPassword: false,
|
||||
loginError: null,
|
||||
baseUrl: store.getters.config.baseUrl || location.href,
|
||||
params: {
|
||||
username: '',
|
||||
password: '',
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
form: null,
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
const submit = async () => {
|
||||
if (data.submitting) {
|
||||
methods: {
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(data.form as any).validate()) {
|
||||
if (!(this.$refs.form as any).validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.submitting = true;
|
||||
this.submitting = true;
|
||||
let data;
|
||||
try {
|
||||
const resp = await api.login(data.params, data.baseUrl);
|
||||
data = await api.login(this.params);
|
||||
|
||||
if (resp === 'Ok.') {
|
||||
api.changeBaseUrl(data.baseUrl);
|
||||
|
||||
store.commit('updateConfig', {
|
||||
key: 'baseUrl',
|
||||
value: data.baseUrl,
|
||||
});
|
||||
store.commit('updateNeedAuth', false);
|
||||
|
||||
emit('input', false);
|
||||
if (data === 'Ok.') {
|
||||
this.$emit('input', false);
|
||||
return;
|
||||
}
|
||||
|
||||
data.loginError = resp;
|
||||
this.loginError = data;
|
||||
} catch (e) {
|
||||
data.loginError = e.message;
|
||||
this.loginError = e.message;
|
||||
}
|
||||
|
||||
data.submitting = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(data),
|
||||
submit,
|
||||
}
|
||||
this.submitting = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
>
|
||||
<img
|
||||
class="icon"
|
||||
src="img/icons/favicon-192x192.png"
|
||||
src="favicon.ico"
|
||||
>
|
||||
<span class="title hidden-sm-and-down ml-3 mr-5">
|
||||
qBittorrent Web UI
|
||||
|
||||
@@ -33,16 +33,6 @@
|
||||
>
|
||||
<v-icon>mdi-pause</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
@click="forceStartTorrents"
|
||||
:title="$t('force_start')"
|
||||
:disabled="!hasSelected"
|
||||
>
|
||||
<v-icon>mdi-play-speed</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-divider
|
||||
vertical
|
||||
inset
|
||||
@@ -102,19 +92,11 @@
|
||||
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>
|
||||
@@ -136,7 +118,7 @@
|
||||
icon
|
||||
@click="recheckTorrents"
|
||||
:title="$t('recheck')"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length == 0"
|
||||
>
|
||||
<v-icon>mdi-backup-restore</v-icon>
|
||||
</v-btn>
|
||||
@@ -188,15 +170,15 @@
|
||||
<v-progress-linear
|
||||
height="1.4em"
|
||||
:value="row.item.progress * 100"
|
||||
:color="row.item.state | stateColor(true, row.item.seq_dl)"
|
||||
:color="row.item.state | stateColor(true)"
|
||||
class="text-center ma-0"
|
||||
>
|
||||
<span :class="getProgressColorClass(row.item.progress)">
|
||||
<span :class="row.item.progress | progressColorClass">
|
||||
{{ row.item.progress | progress }}
|
||||
</span>
|
||||
</v-progress-linear>
|
||||
</td>
|
||||
<td>{{ $t('torrent_state.' + row.item.state) }}</td>
|
||||
<td>{{ 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>
|
||||
@@ -245,10 +227,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, Tag } from '@/types'
|
||||
import { Torrent, Category } from '../types'
|
||||
import { Watch } from 'vue-property-decorator'
|
||||
|
||||
function getStateInfo(state: string) {
|
||||
@@ -337,10 +319,8 @@ function getStateInfo(state: string) {
|
||||
...mapGetters([
|
||||
'isDataReady',
|
||||
'allTorrents',
|
||||
'allTags',
|
||||
'allCategories',
|
||||
'torrentGroupByCategory',
|
||||
'torrentGroupByTag',
|
||||
'torrentGroupBySite',
|
||||
'torrentGroupByState',
|
||||
]),
|
||||
@@ -351,6 +331,10 @@ function getStateInfo(state: string) {
|
||||
}),
|
||||
},
|
||||
filters: {
|
||||
progressColorClass(progress: number) {
|
||||
const color = progress >= 0.5 ? 'white' : 'black';
|
||||
return `${color}--text`;
|
||||
},
|
||||
formatNetworkSpeed(speed: number) {
|
||||
if (speed === 0) {
|
||||
return null;
|
||||
@@ -362,14 +346,11 @@ function getStateInfo(state: string) {
|
||||
const item = getStateInfo(state);
|
||||
return `mdi-${item.icon}`;
|
||||
},
|
||||
stateColor(state: string, isProgress?: boolean, isSeqDL?: boolean) {
|
||||
stateColor(state: string, isProgress?: boolean) {
|
||||
const item = getStateInfo(state);
|
||||
if (!isProgress) {
|
||||
return item.color;
|
||||
}
|
||||
if (isSeqDL) {
|
||||
return '#e33371' // icon.color.secondary;
|
||||
}
|
||||
|
||||
return item.color || '#0008';
|
||||
},
|
||||
@@ -415,9 +396,7 @@ 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
|
||||
@@ -448,9 +427,6 @@ 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]);
|
||||
}
|
||||
@@ -472,12 +448,6 @@ export default class Torrents extends Vue {
|
||||
=== Math.min(this.torrents.length, this.pageOptions.rowsPerPage);
|
||||
}
|
||||
|
||||
getProgressColorClass(progress: number) {
|
||||
const color = (progress >= 0.5 || (this as any).$vuetify.theme.dark)
|
||||
? 'white' : 'black';
|
||||
return `${color}--text`;
|
||||
}
|
||||
|
||||
created() {
|
||||
this.pageOptions = this.$store.getters.config.pageOptions;
|
||||
}
|
||||
@@ -494,13 +464,6 @@ export default class Torrents extends Vue {
|
||||
await api.resumeTorrents(this.selectedHashes);
|
||||
}
|
||||
|
||||
async forceStartTorrents() {
|
||||
await api.setForceStartTorrents(this.selectedHashes);
|
||||
}
|
||||
|
||||
async toggleSequentialTorrents() {
|
||||
await api.toggleSequentialTorrents(this.selectedHashes);
|
||||
}
|
||||
async pauseTorrents() {
|
||||
await api.pauseTorrents(this.selectedHashes);
|
||||
}
|
||||
@@ -509,16 +472,25 @@ export default class Torrents extends Vue {
|
||||
if (!this.hasSelected) {
|
||||
this.selectedRows = this.allTorrents;
|
||||
}
|
||||
const v = await this.asyncShowDialog({
|
||||
title: 'Reannounce Torrents',
|
||||
text: 'Are you sure want to reannounce torrents?',
|
||||
type: DialogType.OkCancel,
|
||||
});
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.reannounceTorrents(this.selectedHashes);
|
||||
|
||||
this.showSnackBar({text: tr('label.reannounced')});
|
||||
this.showSnackBar({text: 'Reannounced'});
|
||||
}
|
||||
|
||||
async recheckTorrents() {
|
||||
const v = await this.asyncShowDialog({
|
||||
title: tr('title.recheck_torrents'),
|
||||
text: tr('dialog.recheck_torrents.msg'),
|
||||
title: 'Recheck Torrents',
|
||||
text: 'Are you sure want to recheck torrents?',
|
||||
type: DialogType.OkCancel,
|
||||
});
|
||||
|
||||
@@ -527,7 +499,7 @@ export default class Torrents extends Vue {
|
||||
}
|
||||
await api.recheckTorrents(this.selectedHashes);
|
||||
|
||||
this.showSnackBar({text: tr('label.rechecking')});
|
||||
this.showSnackBar({text: 'Rechecking'});
|
||||
}
|
||||
|
||||
async setTorrentLocation() {
|
||||
@@ -600,7 +572,7 @@ export default class Torrents extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
@@ -670,7 +642,7 @@ export default class Torrents extends Vue {
|
||||
max-width: 32em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&::v-deep .v-data-footer {
|
||||
margin-right: 4em;
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export default class ConfirmDeleteDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export default class ConfirmSetCategoryDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
text
|
||||
@click="back"
|
||||
v-if="step < 3"
|
||||
v-text="step == 1 ? $t('cancel') : $t('back')"
|
||||
v-text="step == 1 ? 'Cancel' : 'Back'"
|
||||
>
|
||||
Back
|
||||
</v-btn>
|
||||
@@ -93,7 +93,7 @@
|
||||
color="warning"
|
||||
:disabled="!canNext"
|
||||
:loading="submitting"
|
||||
v-text="[null, $t('next'), $t('confirm'), $t('close')][step]"
|
||||
v-text="[null, 'Next', 'Confirm', 'Close'][step]"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -210,7 +210,7 @@ export default class EditTrackerDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<v-icon class="mr-2">
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
<span v-text="$t('info')" />
|
||||
<span>Info</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-tabs v-model="tabSync">
|
||||
@@ -90,8 +90,9 @@
|
||||
<v-btn
|
||||
text
|
||||
@click="closeDialog"
|
||||
v-text="$t('close')"
|
||||
/>
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -145,7 +146,7 @@ export default class InfoDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<v-icon class="mr-2">mdi-delta</v-icon>
|
||||
<span v-text="$t('logs')" />
|
||||
<span>Logs</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-progress-linear
|
||||
@@ -35,8 +35,9 @@
|
||||
<v-btn
|
||||
text
|
||||
@click="closeDialog"
|
||||
v-text="$t('close')"
|
||||
/>
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -63,7 +64,7 @@ import { Prop, Emit } from 'vue-property-decorator';
|
||||
const map: any = {
|
||||
1: null,
|
||||
2: 'info--text',
|
||||
4: 'warning--text',
|
||||
4: 'warn--text',
|
||||
8: 'error--text',
|
||||
};
|
||||
return map[type];
|
||||
@@ -112,7 +113,7 @@ export default class LogsDialog extends HasTask {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -497,7 +497,7 @@ export default class RssDialog extends HasTask {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ export default class RssRulesDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
@import '~@/styles/styles.scss';
|
||||
|
||||
@include dialog-title;
|
||||
|
||||
|
||||
@@ -8,16 +8,7 @@
|
||||
@input="selectChanged"
|
||||
>
|
||||
<template v-slot:prepend="row">
|
||||
<v-progress-circular
|
||||
v-if="inChanging.includes(row.item.id)"
|
||||
size="24"
|
||||
width="2"
|
||||
indeterminate
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
v-text="getRowIcon(row)"
|
||||
/>
|
||||
<v-icon v-text="getRowIcon(row)" />
|
||||
</template>
|
||||
<template v-slot:append="row">
|
||||
<span>
|
||||
@@ -47,7 +38,6 @@ enum EFilePriority {
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface File {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
progress: number;
|
||||
@@ -82,27 +72,26 @@ export default class TorrentContent extends BaseTorrentInfo {
|
||||
|
||||
files: File[] = []
|
||||
folderIndex!: number
|
||||
inChanging: number[] = []
|
||||
|
||||
get fileTree(): TreeItem[] {
|
||||
return this.buildTree(this.files, 0);
|
||||
}
|
||||
|
||||
get selected(): number[] {
|
||||
return this.files.filter((item) => {
|
||||
return item.priority !== EFilePriority.notDownload;
|
||||
}).map(item => item.id);
|
||||
const list: number[] = [];
|
||||
|
||||
this.files.forEach((item, index) => {
|
||||
if(item.priority !== EFilePriority.notDownload) {
|
||||
list.push(index);
|
||||
}
|
||||
})
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
async getFiles() {
|
||||
const files = await api.getTorrentFiles(this.hash) as File[]
|
||||
files.forEach((v, i) => v.id = i)
|
||||
files.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
this.files = files
|
||||
this.files = await api.getTorrentFiles(this.hash);
|
||||
this.folderIndex = 0
|
||||
|
||||
this.inChanging = [];
|
||||
}
|
||||
|
||||
getRowIcon(row: any) {
|
||||
@@ -113,15 +102,13 @@ export default class TorrentContent extends BaseTorrentInfo {
|
||||
return row.open ? 'mdi-folder-open' : 'mdi-folder';
|
||||
}
|
||||
|
||||
async selectChanged(items: Array<number>) {
|
||||
selectChanged(items: Array<number>) {
|
||||
const previous = this.selected;
|
||||
const diff = xor(previous, items);
|
||||
|
||||
if(diff.length == 0) return;
|
||||
|
||||
this.inChanging.push(...diff);
|
||||
|
||||
await api.setTorrentFilePriority(this.hash, diff, items.length > previous.length ?
|
||||
api.setTorrentFilePriority(this.hash, diff, items.length > previous.length ?
|
||||
EFilePriority.normal : EFilePriority.notDownload);
|
||||
}
|
||||
|
||||
@@ -135,6 +122,10 @@ export default class TorrentContent extends BaseTorrentInfo {
|
||||
return name.substring(start, index);
|
||||
}
|
||||
|
||||
getFileIndex(item: File): number {
|
||||
return this.files.findIndex(value => value.name === item.name);
|
||||
}
|
||||
|
||||
buildTree(files: Array<File>, start: number): TreeItem[] {
|
||||
if (!files.length) {
|
||||
return [];
|
||||
@@ -148,7 +139,7 @@ export default class TorrentContent extends BaseTorrentInfo {
|
||||
if(folder === UNWANTED_FILE) {
|
||||
for (const item of values) {
|
||||
result.push({
|
||||
id: item.id,
|
||||
id: this.getFileIndex(item),
|
||||
name: item.name.substring(start + folder.length + 1),
|
||||
item,
|
||||
size: item.size,
|
||||
@@ -173,7 +164,7 @@ export default class TorrentContent extends BaseTorrentInfo {
|
||||
|
||||
for (const item of values) {
|
||||
result.push({
|
||||
id: item.id,
|
||||
id: this.getFileIndex(item),
|
||||
name: item.name.substring(start),
|
||||
item,
|
||||
size: item.size,
|
||||
|
||||
@@ -76,15 +76,17 @@
|
||||
</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 {Torrent, TorrentProperties} from '@/types'
|
||||
import Component from 'vue-class-component'
|
||||
import {Prop, Watch} from 'vue-property-decorator'
|
||||
import BaseTorrentInfo from './baseTorrentInfo'
|
||||
import { TorrentProperties, Torrent } from '@/types'
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Watch } from 'vue-property-decorator';
|
||||
import BaseTorrentInfo from './baseTorrentInfo';
|
||||
|
||||
interface Item {
|
||||
label: string;
|
||||
@@ -151,7 +153,8 @@ export default class TorrentInfo extends BaseTorrentInfo {
|
||||
el.height = clientHeight;
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
return el.getContext('2d')!;
|
||||
const ctx = el.getContext('2d')!;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
fetchInfo() {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="searchEngineState.isPluginManagerOpen"
|
||||
max-width="20rem"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title> <v-icon>mdi-toy-brick</v-icon> {{ $t("plugin_manager") }} </v-card-title>
|
||||
<v-card-text>
|
||||
<v-switch
|
||||
v-for="(plugin, key) in searchEngineState.searchPlugins"
|
||||
:key="key"
|
||||
:input-value="plugin.enabled"
|
||||
:label="plugin.fullName"
|
||||
@change="togglePluginAvailability(plugin)"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="updatePlugins()"
|
||||
>
|
||||
{{ $t("update_plugins") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { SearchEnginePage } from "@/store/types";
|
||||
import { SearchPlugin } from "@/types";
|
||||
import Vue from "vue";
|
||||
import Component from "vue-class-component";
|
||||
import { mapActions, mapState } from "vuex";
|
||||
|
||||
@Component({
|
||||
computed: {
|
||||
...mapState({
|
||||
searchEngineState: "searchEngine",
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
togglePluginAvailabilityAction: "togglePluginAvailability",
|
||||
updatePluginsRequest: "updatePluginsRequest",
|
||||
}),
|
||||
},
|
||||
})
|
||||
export default class PluginsManager extends Vue {
|
||||
searchEngineState!: SearchEnginePage;
|
||||
togglePluginAvailabilityAction!: (_: any) => void;
|
||||
updatePluginsRequest!: () => void;
|
||||
|
||||
togglePluginAvailability(plugin: SearchPlugin) {
|
||||
this.togglePluginAvailabilityAction(plugin);
|
||||
}
|
||||
|
||||
updatePlugins() {
|
||||
this.updatePluginsRequest();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,210 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-dialog
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
scrollable
|
||||
fullscreen
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<v-icon class="mr-2">mdi-card-search-outline</v-icon>
|
||||
<span v-text="$t('search')" />
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
@click="closeDialog"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<SearchDialogForm
|
||||
:loading="loading"
|
||||
@triggerSearch="triggerSearch"
|
||||
@stopSearch="stopSearch"
|
||||
/>
|
||||
|
||||
<v-data-table
|
||||
:headers="grid.headers"
|
||||
:items="grid.searchItems"
|
||||
:items-per-page="10"
|
||||
:loading="loading"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template #[`item.fileName`]="{ item }">
|
||||
<a
|
||||
:href="item.descrLink"
|
||||
target="_blank"
|
||||
v-text="item.fileName"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:[`item.fileSize`]="{ item }">
|
||||
{{ item.fileSize | formatSize }}
|
||||
</template>
|
||||
<template v-slot:[`item.actions`]="{ item }">
|
||||
<v-icon @click="downloadTorrent(item)">mdi-download</v-icon>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn @click="openPluginManager">
|
||||
<v-icon>mdi-cog</v-icon> {{ $t("plugin_manager") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<PluginManager />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import api from "@/Api";
|
||||
import HasTask from "@/mixins/hasTask";
|
||||
import { Component, Prop, Emit } from "vue-property-decorator";
|
||||
import { SearchTaskTorrent } from "@/types";
|
||||
import { mapActions, mapGetters, mapMutations } from "vuex";
|
||||
import { tr } from "@/locale";
|
||||
import SearchDialogForm from "./SearchDialogForm.vue";
|
||||
import PluginManager from "./PluginsManager.vue";
|
||||
|
||||
interface GridConfig {
|
||||
searchItems: SearchTaskTorrent[];
|
||||
downloadItem: SearchTaskTorrent | null;
|
||||
headers: { [key: string]: any }[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
SearchDialogForm,
|
||||
PluginManager,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
allCategories: "allCategories",
|
||||
preferences: "preferences",
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["openAddForm", "setPasteUrl", "addFormDownloadItem", "openPluginManager"]),
|
||||
...mapActions({
|
||||
loadSearchPlugins: 'fetchSearchPlugins',
|
||||
}),
|
||||
},
|
||||
})
|
||||
export default class SearchDialog extends HasTask {
|
||||
private _searchId = 0;
|
||||
|
||||
@Prop(Boolean)
|
||||
readonly value!: boolean;
|
||||
|
||||
grid: GridConfig = {
|
||||
searchItems: [],
|
||||
downloadItem: {
|
||||
descrLink: "",
|
||||
fileName: "",
|
||||
fileSize: 0,
|
||||
fileUrl: "",
|
||||
nbLeechers: 0,
|
||||
nbSeeders: 0,
|
||||
siteUrl: "",
|
||||
},
|
||||
headers: [
|
||||
{ text: tr("name"), value: "fileName" },
|
||||
{ text: tr("size"), value: "fileSize" },
|
||||
{ text: tr("seeds"), value: "nbSeeders" },
|
||||
{ text: tr("peers"), value: "nbLeechers" },
|
||||
{ text: tr("search_engine"), value: "siteUrl" },
|
||||
{ text: tr("action", 2), value: "actions", sortable: false },
|
||||
],
|
||||
};
|
||||
|
||||
loading = false;
|
||||
|
||||
setPasteUrl!: (_: any) => void;
|
||||
openAddForm!: () => void;
|
||||
addFormDownloadItem!: (_: any) => void;
|
||||
loadSearchPlugins!: () => void;
|
||||
openPluginManager!: () => void;
|
||||
|
||||
mounted() {
|
||||
this.loadSearchPlugins(); // load the plugins so they are available in the entire module
|
||||
}
|
||||
|
||||
async downloadTorrent(item: SearchTaskTorrent) {
|
||||
this.addFormDownloadItem({
|
||||
downloadItem: {
|
||||
title: item.fileName,
|
||||
url: item.fileUrl,
|
||||
},
|
||||
});
|
||||
this.openAddForm();
|
||||
}
|
||||
|
||||
async stopSearch() {
|
||||
this.cancelTask();
|
||||
await this._stopSearch(this._searchId);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@Emit("input")
|
||||
closeDialog() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async triggerSearch(searchForm: any) { // TODO: find a good way to type the form.
|
||||
this.grid.searchItems = []; // Clear the table
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const response = await this._startSearch(searchForm);
|
||||
this._searchId = response.id;
|
||||
|
||||
this.setTaskAndRun(this.task(response.id));
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
private async _startSearch(searchForm: any): Promise<{ id: number }> {
|
||||
const result = await api.startSearch(
|
||||
searchForm.pattern,
|
||||
searchForm.plugins,
|
||||
searchForm.category,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _stopSearch(id: number) {
|
||||
await api.stopSearch(id);
|
||||
this._searchId = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does request until the plugins return data
|
||||
*/
|
||||
private task(responseId: number): CallableFunction {
|
||||
return async () => {
|
||||
const response = await api.getSearchResults(responseId);
|
||||
const isStopped = response.status === "Stopped";
|
||||
|
||||
const items = this.grid.searchItems
|
||||
items.splice(items.length, 0, ...response.results.slice(items.length))
|
||||
|
||||
if (isStopped) {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
return isStopped;
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/assets/styles.scss";
|
||||
|
||||
@include dialog-title;
|
||||
</style>
|
||||
@@ -1,256 +0,0 @@
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="searchForm.valid"
|
||||
>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col class="col-12 col-sm-6 col-md-9">
|
||||
<v-text-field
|
||||
v-model="searchForm.pattern"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
@keypress.enter="$refs.searchButton.click"
|
||||
:label="$t('search')"
|
||||
:rules="[v => !!v || $t('msg.item_is_required', { item: $t('query') })]"
|
||||
clearable
|
||||
/>
|
||||
<v-btn
|
||||
ref="searchButton"
|
||||
:disabled="!searchForm.valid"
|
||||
:color="loading ? 'warning' : 'primary'"
|
||||
@click="loading ? stopSearch() : triggerSearch()"
|
||||
>
|
||||
{{ loading ? $t("stop") : $t("search") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col class="col__plugins">
|
||||
<v-btn
|
||||
:loading="searchPlugins === undefined"
|
||||
:disabled="searchPlugins === null"
|
||||
type="button"
|
||||
class="btn"
|
||||
@click="plugginSelectorOpen = true"
|
||||
>
|
||||
{{ $t("plugin", 2) }}
|
||||
</v-btn>
|
||||
<v-dialog
|
||||
v-if="!this.$vuetify.breakpoint.mobile"
|
||||
v-model="plugginSelectorOpen"
|
||||
max-width="20rem"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ $t("plugin", 1) }} {{ $t("usage") }}
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
@click="toggleSelectAll"
|
||||
:color="searchForm.plugins.length > 0 ? 'primary' : ''"
|
||||
>
|
||||
{{ $t("all") }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<v-checkbox
|
||||
v-for="(plugin, key) in availablePlugins"
|
||||
:key="key"
|
||||
v-model="searchForm.plugins"
|
||||
:label="plugin.fullName"
|
||||
:value="plugin"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-bottom-sheet
|
||||
scrollable
|
||||
inset
|
||||
v-model="plugginSelectorOpen"
|
||||
v-if="this.$vuetify.breakpoint.mobile"
|
||||
>
|
||||
<v-sheet class="text-center">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ $t("plugin", 1) }} {{ $t("usage") }}
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
@click="toggleSelectAll"
|
||||
:color="searchForm.plugins.length > 0 ? 'primary' : ''"
|
||||
>
|
||||
{{ $t("all") }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<v-checkbox
|
||||
v-for="(plugin, key) in availablePlugins"
|
||||
:key="key"
|
||||
v-model="searchForm.plugins"
|
||||
:label="plugin.fullName"
|
||||
:value="plugin"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-sheet>
|
||||
</v-bottom-sheet>
|
||||
</v-col>
|
||||
<v-col align-self="center">
|
||||
<v-autocomplete
|
||||
v-model="searchForm.category"
|
||||
:items="availableCategories"
|
||||
item-text="name"
|
||||
item-value="key"
|
||||
:label="$t('category', 1)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { Component, Emit, Prop, Watch } from "vue-property-decorator";
|
||||
import { SearchPlugin } from "@/types";
|
||||
import { tr } from "@/locale";
|
||||
import { intersection } from "lodash";
|
||||
import { mapGetters } from "vuex";
|
||||
import { SearchEnginePage } from '@/store/types';
|
||||
|
||||
const ALL_KEY = "all";
|
||||
|
||||
const ALL_CATEGORY: Category = {
|
||||
key: ALL_KEY,
|
||||
name: tr("all"),
|
||||
};
|
||||
|
||||
interface Category {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SearchForm {
|
||||
valid: boolean;
|
||||
category: string;
|
||||
pattern: string;
|
||||
plugins: SearchPlugin[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
computed: {
|
||||
...mapGetters({
|
||||
searchPlugins: "allSearchPlugins",
|
||||
}),
|
||||
},
|
||||
})
|
||||
export default class SearchDialogForm extends Vue {
|
||||
searchEngineState!: SearchEnginePage;
|
||||
searchPlugins!: SearchPlugin[];
|
||||
|
||||
@Prop(Boolean)
|
||||
readonly loading: boolean = false;
|
||||
|
||||
plugginSelectorOpen = false;
|
||||
|
||||
availablePlugins: SearchPlugin[] = [];
|
||||
|
||||
searchForm: SearchForm = {
|
||||
valid: false,
|
||||
category: ALL_KEY,
|
||||
pattern: "",
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
get hasSelectAllPlugins() {
|
||||
return this.searchForm.plugins.length === this.availablePlugins?.length;
|
||||
}
|
||||
|
||||
get availableCategories() {
|
||||
if (this.hasSelectAllPlugins) {
|
||||
return [ALL_CATEGORY];
|
||||
}
|
||||
|
||||
const result: Category[] = [ALL_CATEGORY, { divider: true } as any];
|
||||
|
||||
const categories = intersection(
|
||||
...this.searchForm.plugins.map(p => p.supportedCategories),
|
||||
).map(c => ({ key: c, name: c }));
|
||||
result.push(...categories);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.searchForm.plugins = this.hasSelectAllPlugins ? [] : this.availablePlugins.slice();
|
||||
}
|
||||
|
||||
@Watch("searchPlugins")
|
||||
searchPluginsUpdated(plugins: SearchPlugin[] | undefined | null) {
|
||||
if (!plugins) {
|
||||
this.availablePlugins = [];
|
||||
} else {
|
||||
this.availablePlugins = this.searchPlugins.filter(x => x.enabled);
|
||||
this.toggleSelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Emit("triggerSearch")
|
||||
triggerSearch(): SearchForm | void {
|
||||
if (!this.searchForm.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const plugins = this.hasSelectAllPlugins
|
||||
? ALL_KEY
|
||||
: this.searchForm.plugins.map(p => p.name).join("|");
|
||||
|
||||
const searchForm = Object.assign({}, this.searchForm, {
|
||||
plugins,
|
||||
});
|
||||
|
||||
return searchForm;
|
||||
}
|
||||
|
||||
@Emit("stopSearch")
|
||||
stopSearch() {
|
||||
//
|
||||
}
|
||||
|
||||
@Watch("searchForm.plugins")
|
||||
onPluginChanged() {
|
||||
if (!this.availableCategories.find(c => c.key === this.searchForm.category)) {
|
||||
this.searchForm.category = ALL_KEY;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/assets/styles.scss";
|
||||
.v-form {
|
||||
.col__plugins {
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> * {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-bottom-sheet {
|
||||
.v-card__text {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<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
|
||||
:items="torrentMode"
|
||||
:value="preferences.auto_tmm_enabled ? torrentMode[0] : torrentMode[1]"
|
||||
@change="changeSettings('auto_tmm_enabled', $event == torrentMode[0])"
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="torrent_changed_tmm_enabled">
|
||||
<v-select
|
||||
: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
|
||||
: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
|
||||
: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
|
||||
:value="preferences.save_path"
|
||||
@change="changeSettings('save_path', $event)"
|
||||
lazy
|
||||
/>
|
||||
</preference-row>
|
||||
<preference-row i18n-key="temp_path">
|
||||
<template v-slot:header>
|
||||
<v-checkbox
|
||||
: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
|
||||
/>
|
||||
</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 PreferenceRow from './PreferenceRow.vue'
|
||||
import { tr } from '@/locale'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
PreferenceRow,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
preferences: 'allPreferences',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
updatePreferencesRequest: 'updatePreferencesRequest',
|
||||
}),
|
||||
},
|
||||
})
|
||||
export default class DownloadSettings extends Vue {
|
||||
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
|
||||
|
||||
changeSettings(property: string, value: string | boolean) {
|
||||
this.updatePreferencesRequest({[property]: value})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/assets/styles.scss";
|
||||
|
||||
h4 {
|
||||
margin-top: 8px;
|
||||
padding-left: 4px
|
||||
}
|
||||
|
||||
.v-input--switch {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@include dialog-title;
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<v-row
|
||||
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"
|
||||
>
|
||||
<slot name="header" />
|
||||
</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>
|
||||
@@ -1,61 +0,0 @@
|
||||
<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,117 +0,0 @@
|
||||
<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-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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
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 SettingsDialog extends Vue {
|
||||
@Prop(Boolean)
|
||||
readonly value!: boolean
|
||||
preferences!: Preferences
|
||||
config!: Config
|
||||
|
||||
preferenceUpdated = false
|
||||
tabList = ['downloads', 'speed', 'rss', 'webui']
|
||||
tab = 'download'
|
||||
|
||||
@Watch('preferences')
|
||||
@Watch('config')
|
||||
async onPreferenceUpdate() {
|
||||
this.preferenceUpdated = true
|
||||
await timeout(3000)
|
||||
this.preferenceUpdated = false
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/assets/styles.scss";
|
||||
|
||||
@include dialog-title;
|
||||
</style>
|
||||
@@ -1,157 +0,0 @@
|
||||
<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>
|
||||
@@ -1,172 +0,0 @@
|
||||
<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>
|
||||
@@ -50,37 +50,11 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-menu>
|
||||
<template #activator="{ on }">
|
||||
<v-btn
|
||||
icon
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon v-text="darkModeIcon" />
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item-group
|
||||
v-model="currentDarkMode"
|
||||
color="primary"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="item in darkModes"
|
||||
:key="item[0]"
|
||||
:value="item[0]"
|
||||
>
|
||||
<v-list-item-title>{{ item[1] }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn
|
||||
icon
|
||||
@click="triggerApplicationShutdown"
|
||||
:title="$t('trigger_application_shutdown')"
|
||||
@click="toggleDarkMode"
|
||||
>
|
||||
<v-icon>mdi-power-plug-off</v-icon>
|
||||
<v-icon v-text="darkModeIcon" />
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +65,6 @@ import Vue from 'vue'
|
||||
import Component from 'vue-class-component';
|
||||
import { mapMutations, mapActions } from 'vuex';
|
||||
import { Watch } from 'vue-property-decorator';
|
||||
import api from '../../Api';
|
||||
|
||||
import { tr, translations, defaultLocale, LocaleKey } from '@/locale';
|
||||
import { DialogType, DialogConfig, SnackBarConfig, ConfigPayload } from '@/store/types';
|
||||
@@ -100,7 +73,6 @@ import AppFooter from '@/components/Footer.vue';
|
||||
const AUTO_KEY = 'auto';
|
||||
|
||||
type AllLocaleKey = NonNullable<LocaleKey> | typeof AUTO_KEY;
|
||||
type DarkModeKey = true | false | null;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -118,29 +90,16 @@ type DarkModeKey = true | false | null;
|
||||
})
|
||||
export default class DrawerFooter extends Vue {
|
||||
locales = this.buildLocales()
|
||||
currentLocale = this.$store.getters.config.locale || AUTO_KEY
|
||||
currentDarkMode = this.$store.getters.config.darkMode || AUTO_KEY
|
||||
currentLocale = this.$store.getters.config.locale || AUTO_KEY;
|
||||
oldLocale = this.currentLocale
|
||||
showInfo = false
|
||||
|
||||
darkModes = [
|
||||
[false, tr('light')],
|
||||
[true, tr('dark')],
|
||||
[AUTO_KEY, tr('auto')],
|
||||
]
|
||||
|
||||
asyncShowDialog!: (_: DialogConfig) => Promise<string | undefined>
|
||||
showSnackBar!: (_: SnackBarConfig) => void
|
||||
updateConfig!: (_: ConfigPayload) => void
|
||||
|
||||
get darkModeIcon() {
|
||||
if (this.currentDarkMode == true) {
|
||||
return 'mdi-brightness-4'
|
||||
} else if (this.currentDarkMode == false) {
|
||||
return 'mdi-brightness-7'
|
||||
} else {
|
||||
return 'mdi-brightness-auto'
|
||||
}
|
||||
return this.$vuetify.theme.dark ? 'mdi-brightness-4' : 'mdi-brightness-7';
|
||||
}
|
||||
|
||||
get phoneLayout() {
|
||||
@@ -160,12 +119,11 @@ export default class DrawerFooter extends Vue {
|
||||
text: tr('auto'),
|
||||
value: 'auto',
|
||||
},
|
||||
...locales,
|
||||
...locales
|
||||
]
|
||||
}
|
||||
|
||||
@Watch('currentLocale')
|
||||
async onCurrentLocaleChanged(locale: AllLocaleKey) {
|
||||
async switchLocale(locale: AllLocaleKey) {
|
||||
if (locale === this.oldLocale) {
|
||||
return;
|
||||
}
|
||||
@@ -193,25 +151,19 @@ export default class DrawerFooter extends Vue {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
@Watch('currentDarkMode')
|
||||
onDarkModeChanged(mode: DarkModeKey | typeof AUTO_KEY) {
|
||||
toggleDarkMode() {
|
||||
const { theme } = this.$vuetify;
|
||||
theme.dark = !theme.dark;
|
||||
|
||||
this.updateConfig({
|
||||
key: 'darkMode',
|
||||
value: mode == AUTO_KEY ? null : mode,
|
||||
value: theme.dark,
|
||||
});
|
||||
}
|
||||
|
||||
async triggerApplicationShutdown() {
|
||||
const v = await this.asyncShowDialog({
|
||||
title: tr('dialog.trigger_exit_qb.title'),
|
||||
text: tr('dialog.trigger_exit_qb.text'),
|
||||
type: DialogType.OkCancel,
|
||||
});
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
await api.shutdownApplication();
|
||||
@Watch('currentLocale')
|
||||
onCurrentLocaleChanged(v: AllLocaleKey) {
|
||||
this.switchLocale(v)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -225,4 +177,4 @@ export default class DrawerFooter extends Vue {
|
||||
.footer {
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -68,9 +68,6 @@ export default class FilterGroup extends Vue {
|
||||
} else {
|
||||
this.select(null);
|
||||
}
|
||||
if (this.model == null) {
|
||||
this.model = this.selected != null;
|
||||
}
|
||||
}
|
||||
|
||||
select(key: string | null) {
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface Group {
|
||||
title: string;
|
||||
icon: string;
|
||||
children: Child[];
|
||||
model: boolean | null;
|
||||
model: boolean;
|
||||
select: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,37 +3,29 @@ import Vue from 'vue';
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
export function toPrecision(value: number, precision: number) {
|
||||
const limit = 10 ** precision;
|
||||
if (value >= limit) {
|
||||
if (value >= (10 ** precision)) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value >= 1) {
|
||||
if (value >= limit - 1) {
|
||||
return limit.toString();
|
||||
}
|
||||
|
||||
} if (value >= 1) {
|
||||
return value.toPrecision(precision);
|
||||
}
|
||||
|
||||
return value.toFixed(precision - 1);
|
||||
}
|
||||
|
||||
export function formatSize(value: number): string {
|
||||
export function formatSize(value: number) {
|
||||
const units = 'KMGTP';
|
||||
let index = value ? Math.floor(Math.log2(value) / 10) : 0;
|
||||
let index = -1;
|
||||
|
||||
value = value / (1024 ** index);
|
||||
if (value >= 999) {
|
||||
while (value >= 1000) {
|
||||
value /= 1024;
|
||||
index++;
|
||||
}
|
||||
|
||||
const unit = index === 0 ? 'B' : `${units[index - 1]}iB`;
|
||||
const unit = index < 0 ? 'B' : `${units[index]}iB`;
|
||||
|
||||
if (index === 0) {
|
||||
if (index < 0) {
|
||||
return `${value} ${unit}`;
|
||||
}
|
||||
|
||||
return `${toPrecision(value, 3)} ${unit}`;
|
||||
}
|
||||
|
||||
@@ -137,4 +129,4 @@ export function parseDate(str: string) {
|
||||
return Date.parse(str) / 1000
|
||||
}
|
||||
|
||||
Vue.filter('parseDate', parseDate)
|
||||
Vue.filter('parseDate', parseDate)
|
||||
128
src/locale/en.ts
128
src/locale/en.ts
@@ -9,16 +9,12 @@ export default {
|
||||
cancel: 'Cancel',
|
||||
ok: 'OK',
|
||||
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
submit: 'Submit',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
todo: 'To Do',
|
||||
resume: 'Resume',
|
||||
pause: 'Pause',
|
||||
force_start: 'Force Start',
|
||||
toggle_sequential: 'Toggle Sequential Download',
|
||||
info: 'Info',
|
||||
reset: 'Reset',
|
||||
login: 'Login',
|
||||
@@ -26,7 +22,7 @@ export default {
|
||||
refresh: 'Refresh',
|
||||
location: 'Location',
|
||||
rename: 'Rename',
|
||||
trigger_application_shutdown: 'Exit qBittorrent',
|
||||
|
||||
reannounce: 'Reannounce',
|
||||
recheck: 'Recheck',
|
||||
|
||||
@@ -35,7 +31,7 @@ export default {
|
||||
|
||||
name: 'Name',
|
||||
size: 'Size',
|
||||
progress: 'Progress',
|
||||
progress: 'progress',
|
||||
status: 'Status',
|
||||
seeds: 'Seeds',
|
||||
peers: 'Peers',
|
||||
@@ -47,14 +43,10 @@ export default {
|
||||
|
||||
settings: 'Settings',
|
||||
logs: 'Logs',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
|
||||
all: 'All',
|
||||
category: 'Category |||| Categories',
|
||||
uncategorized: 'Uncategorized',
|
||||
tag: 'Tag',
|
||||
untagged: 'Untagged',
|
||||
others: 'Others',
|
||||
sites: 'Sites',
|
||||
files: 'Files',
|
||||
@@ -62,79 +54,6 @@ export default {
|
||||
more: 'More',
|
||||
feed: 'Feed',
|
||||
date: 'Date',
|
||||
query: 'Query',
|
||||
plugin: 'Plugin |||| Plugins',
|
||||
action: 'Action |||| Actions',
|
||||
search_engine: 'Search engine',
|
||||
usage: 'usage',
|
||||
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',
|
||||
@@ -143,7 +62,6 @@ export default {
|
||||
set_category: 'Set Category',
|
||||
edit_tracker: 'Edit Tracker',
|
||||
set_location: 'Set Location',
|
||||
recheck_torrents: 'Recheck Torrents',
|
||||
},
|
||||
|
||||
label: {
|
||||
@@ -163,13 +81,6 @@ export default {
|
||||
deleting: 'Deleting…',
|
||||
moving: 'Moving…',
|
||||
moved: 'Moved',
|
||||
next: 'Next',
|
||||
back: 'Back',
|
||||
confirm: 'Confirm',
|
||||
reannounced: 'Reannounced',
|
||||
rechecking: 'Rechecking…',
|
||||
dht_nodes: '%{smart_count} node |||| %{smart_count} nodes',
|
||||
base_url: 'Base URL',
|
||||
},
|
||||
|
||||
msg: {
|
||||
@@ -177,10 +88,6 @@ export default {
|
||||
},
|
||||
|
||||
dialog: {
|
||||
trigger_exit_qb: {
|
||||
title: 'Exit qBittorrent',
|
||||
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.',
|
||||
hint: 'One link per line',
|
||||
@@ -197,9 +104,6 @@ export default {
|
||||
switch_locale: {
|
||||
msg: 'Are you sure to switch language to %{lang}?\nThis action will reload page.',
|
||||
},
|
||||
recheck_torrents: {
|
||||
msg: 'Are you sure want to recheck torrents?',
|
||||
},
|
||||
rss: {
|
||||
add_feed: 'Add Feed',
|
||||
feed_url: 'Feed URL',
|
||||
@@ -226,7 +130,7 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
category_state: {
|
||||
state: {
|
||||
_: 'State',
|
||||
|
||||
downloading: 'Downloading',
|
||||
@@ -237,27 +141,5 @@ export default {
|
||||
active: 'Active',
|
||||
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',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,18 @@
|
||||
import Polyglot from 'node-polyglot';
|
||||
import langEn from './en';
|
||||
import langRu from './ru';
|
||||
import langTr from './tr';
|
||||
import langZhCn from './zh-CN';
|
||||
import langZhTw from './zh-TW';
|
||||
import en from './en';
|
||||
import zhCn from './zh-CN';
|
||||
|
||||
import { loadConfig } from '@/store/config';
|
||||
|
||||
export const translations = {
|
||||
en: langEn,
|
||||
'ru': langRu,
|
||||
'tr': langTr,
|
||||
'zh-CN': langZhCn,
|
||||
'zh-TW': langZhTw,
|
||||
en: en,
|
||||
'zh-CN': zhCn,
|
||||
}
|
||||
|
||||
export type LocaleKey = keyof typeof translations | null;
|
||||
|
||||
const polyglot = new Polyglot({
|
||||
phrases: translations.en,
|
||||
phrases: translations.en
|
||||
});
|
||||
|
||||
function matchLocale() {
|
||||
@@ -36,7 +30,7 @@ function matchLocale() {
|
||||
export const defaultLocale = matchLocale()
|
||||
|
||||
function updateLocale() {
|
||||
let locale = loadConfig()['locale'] as LocaleKey;
|
||||
let locale: LocaleKey | undefined = loadConfig()['locale'];
|
||||
|
||||
if (!locale) {
|
||||
locale = defaultLocale;
|
||||
|
||||
251
src/locale/ru.ts
251
src/locale/ru.ts
@@ -1,251 +0,0 @@
|
||||
/* 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: 'Feed',
|
||||
date: 'Дата',
|
||||
query: 'Запрос',
|
||||
plugin: 'Плагин |||| Плагины',
|
||||
action: 'Действие |||| Действия',
|
||||
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: {
|
||||
_: 'Заголовок',
|
||||
add_torrents: 'Добавить торрент',
|
||||
delete_torrents: 'Удалить торрент',
|
||||
set_category: 'Установить категорию',
|
||||
edit_tracker: 'Изменить трекер',
|
||||
set_location: 'Установить расположение',
|
||||
recheck_torrents: 'Перепроверить торренты',
|
||||
},
|
||||
|
||||
label: {
|
||||
switch_to_old_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} узел |||| Узлов: %{smart_count}',
|
||||
base_url: 'Базовый URL',
|
||||
},
|
||||
|
||||
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',
|
||||
feed_url: 'Ссылка Feed',
|
||||
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: 'Применить правило к Feed',
|
||||
},
|
||||
},
|
||||
|
||||
category_state: {
|
||||
_: 'Статистика',
|
||||
|
||||
downloading: 'Скачивается',
|
||||
seeding: 'Раздаётся',
|
||||
completed: 'Завершено',
|
||||
resumed: 'Возобновлён',
|
||||
paused: 'Приостановлен',
|
||||
active: 'Активный',
|
||||
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',
|
||||
},
|
||||
}
|
||||
171
src/locale/tr.ts
171
src/locale/tr.ts
@@ -1,171 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
export default {
|
||||
lang: 'Türkçe',
|
||||
auto: 'Otomatik',
|
||||
|
||||
close: 'Kapat',
|
||||
no: 'Hayır',
|
||||
yes: 'Evet',
|
||||
cancel: 'İptal',
|
||||
ok: 'Tamam',
|
||||
|
||||
start: 'Başlat',
|
||||
stop: 'Durdur',
|
||||
submit: 'Tamam',
|
||||
edit: 'Düzenle',
|
||||
delete: 'Sil',
|
||||
todo: 'Yapılacak',
|
||||
resume: 'Devam Et',
|
||||
pause: 'Duraklat',
|
||||
force_start: 'Zorla Başlat',
|
||||
info: 'Bilgi',
|
||||
reset: 'Sıfırla',
|
||||
login: 'Oturum Aç',
|
||||
search: 'Ara',
|
||||
refresh: 'Yenile',
|
||||
location: 'Kaydetme yeri',
|
||||
rename: 'Yeniden adlandır',
|
||||
trigger_application_shutdown: 'qBittorrent\'ten çık',
|
||||
reannounce: 'Yeniden Duyur',
|
||||
recheck: 'Yeniden Denetle',
|
||||
|
||||
username: 'Kullanıcı Adı',
|
||||
password: 'Parola',
|
||||
|
||||
name: 'Adı',
|
||||
size: 'Boyut',
|
||||
progress: 'İlerleme',
|
||||
status: 'Durum',
|
||||
seeds: 'Gönderim',
|
||||
peers: 'Kişi',
|
||||
dl_speed: 'İnd. Hızı',
|
||||
up_speed: 'Gön. Hızı',
|
||||
eta: 'TBS',
|
||||
ratio: 'Oran',
|
||||
added_on: 'Eklenme',
|
||||
|
||||
settings: 'Ayarlar',
|
||||
logs: 'Günlükler',
|
||||
light: 'Aydınlık',
|
||||
dark: 'Karanlık',
|
||||
|
||||
all: 'Tümü',
|
||||
category: 'Kategori |||| Kategoriler',
|
||||
uncategorized: 'Kategorilenmemiş',
|
||||
others: 'Diğerleri',
|
||||
sites: 'Siteler',
|
||||
files: 'Dosyalar',
|
||||
less: 'Daha Az',
|
||||
more: 'Daha Çok',
|
||||
feed: 'Bildirim',
|
||||
date: 'Tarih',
|
||||
query: 'Sorgu',
|
||||
plugin: 'Eklenti |||| Eklentiler',
|
||||
action: 'Eylem |||| Eylemler',
|
||||
search_engine: 'Arama motoru',
|
||||
usage: 'Kullanım',
|
||||
plugin_manager: 'Eklenti yöneticisi',
|
||||
|
||||
title: {
|
||||
_: 'Başlık',
|
||||
add_torrents: 'Torrent ekle',
|
||||
delete_torrents: 'Torrent\'leri sil',
|
||||
set_category: 'Kategtori ayarla',
|
||||
edit_tracker: 'İzleyicileri Düzenle',
|
||||
set_location: 'Yeri ayarla...',
|
||||
recheck_torrents: 'Torrent\'leri yeniden denetle',
|
||||
},
|
||||
|
||||
label: {
|
||||
switch_to_old_ui: 'Resmi Web Arayüzü\'ne geç',
|
||||
create_subfolder: 'Alt klasör oluştur',
|
||||
start_torrent: 'Torrent\'i başlat',
|
||||
skip_hash_check: 'Adresleme denetimini atla',
|
||||
in_sequential_order: 'Sıralı düzende indir',
|
||||
first_and_last_pieces_first: 'Önce ilk ve son parçaları indir',
|
||||
|
||||
also_delete_files: 'Aynı zamanda sabit diskteki dosyaları da sil',
|
||||
|
||||
auto_tmm: 'Otomatik Torrent Yönetimi',
|
||||
|
||||
adding: 'Ekleniyor…',
|
||||
reloading: 'Yeniden yükleniyor…',
|
||||
deleting: 'Siliniyor…',
|
||||
moving: 'Taşınıyor…',
|
||||
moved: 'Taşındı.',
|
||||
next: 'İleri',
|
||||
back: 'Geri',
|
||||
confirm: 'Onayla',
|
||||
reannounced: 'Yeniden duyuruldu.',
|
||||
rechecking: 'Yeniden denetleniyor…',
|
||||
dht_nodes: '%{smart_count} düğüm |||| %{smart_count} düğüm',
|
||||
base_url: 'Ana makine URL',
|
||||
},
|
||||
|
||||
msg: {
|
||||
item_is_required: '%{item} gerekli!',
|
||||
},
|
||||
|
||||
dialog: {
|
||||
trigger_exit_qb: {
|
||||
title: 'qBittorrent\'ten Çık',
|
||||
text: 'qBittorrent uygulamasından çıkmak istediğinize emin misiniz?',
|
||||
},
|
||||
add_torrents: {
|
||||
placeholder: 'Torrentleri yüklemek için\nlinkleri buraya girin\nveya sağdaki ataç butonuna tıklayıp seçim yapın.',
|
||||
hint: 'Her satıra sadece bir bağlantı',
|
||||
},
|
||||
delete_torrents: {
|
||||
msg: 'Seçilen torrent\'leri aktarım listesinden silmek istediğinize emin misiniz?',
|
||||
also_delete_same_name_torrents: 'Aynı zamanda, aynı isimli bir torrenti de sil. |||| Aynı zamanda, aynı isimli %{smart_count} torrentleri de sil.',
|
||||
},
|
||||
set_category: {
|
||||
move: 'Seçilmiş torrentlerin kategorilerini, %{category} olarak değiştirmek istediğinize emin misiniz?',
|
||||
reset: 'Seçilmiş torrentlerin kategorilerini sıfırlamak istediğinize emin misiniz?',
|
||||
also_move_same_name_torrents: 'Aynı zamanda, aynı isimli bir torrenti de taşı. |||| Aynı zamanda, aynı isimli %{smart_count} torrentleri de taşı.',
|
||||
},
|
||||
switch_locale: {
|
||||
msg: 'Dili %{lang} olarak değiştirmek istediğinize emin misiniz?\nBu eylem sayfayı yeniden yükleyecek.',
|
||||
},
|
||||
recheck_torrents: {
|
||||
msg: 'Torrentleri yeniden denetlemek istediğinize emin misiniz?',
|
||||
},
|
||||
rss: {
|
||||
add_feed: 'Bildirim ekle',
|
||||
feed_url: 'Bildirim URL\'si',
|
||||
auto_refresh: 'Otomatik yenile',
|
||||
auto_download: 'Otomatik indir',
|
||||
delete_feeds: 'Seçilmiş bildirimlerin silmek istediğinize emin misiniz?',
|
||||
date_format: '%{date} (%{duration} önce)',
|
||||
},
|
||||
rss_rule: {
|
||||
add_rule: 'Yeni kural ekle',
|
||||
new_rule_name: 'Yeni kural adı',
|
||||
delete_rule: 'Seçilmiş kuralları silmek istediğinize emin misiniz?',
|
||||
title: 'RSS indirici',
|
||||
rule_settings: 'Kural ayarları',
|
||||
|
||||
use_regex: 'Regex kullan',
|
||||
must_contain: 'İçermeli',
|
||||
must_not_contain: 'İçermemeli',
|
||||
episode_filter: 'Bölüm süzgeci',
|
||||
smart_episode: 'Akıllı bölüm süzgeci kullan',
|
||||
assign_category: 'Kategori ata',
|
||||
|
||||
apply_to_feeds: 'Kuralı bildirimlere uygula',
|
||||
},
|
||||
},
|
||||
|
||||
state: {
|
||||
_: 'Durum',
|
||||
|
||||
downloading: 'İndiriliyor',
|
||||
seeding: 'Gönderiliyor',
|
||||
completed: 'Tamamlandı',
|
||||
resumed: 'Devam Edildi',
|
||||
paused: 'Duraklatıldı',
|
||||
active: 'Etkin',
|
||||
inactive: 'Etkin Değil',
|
||||
errored: 'Hata Oldu',
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
export default {
|
||||
lang: '简体中文',
|
||||
lang: '中文',
|
||||
auto: '自动',
|
||||
|
||||
close: '关闭',
|
||||
@@ -9,15 +9,12 @@ export default {
|
||||
cancel: '取消',
|
||||
ok: '确定',
|
||||
|
||||
start: '开始',
|
||||
stop: '停止',
|
||||
submit: '提交',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
todo: '待办',
|
||||
resume: '恢复',
|
||||
pause: '暂停',
|
||||
force_start: '强制继续',
|
||||
info: '信息',
|
||||
reset: '重置',
|
||||
login: '登录',
|
||||
@@ -25,7 +22,7 @@ export default {
|
||||
refresh: '刷新',
|
||||
location: '位置',
|
||||
rename: '重命名',
|
||||
trigger_application_shutdown: '退出qBittorrent',
|
||||
|
||||
reannounce: '重新通告',
|
||||
recheck: '重新检查',
|
||||
|
||||
@@ -45,16 +42,11 @@ export default {
|
||||
added_on: '添加时间',
|
||||
|
||||
settings: '设置',
|
||||
|
||||
logs: '日志',
|
||||
light: '亮色',
|
||||
dark: '暗色',
|
||||
|
||||
all: '全部',
|
||||
category: '分类',
|
||||
uncategorized: '未分类',
|
||||
tag: '标签',
|
||||
untagged: '无标签',
|
||||
others: '其他',
|
||||
sites: '站点',
|
||||
files: '文件',
|
||||
@@ -62,75 +54,6 @@ export default {
|
||||
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',
|
||||
|
||||
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: {
|
||||
_: '标题',
|
||||
@@ -139,7 +62,6 @@ export default {
|
||||
set_category: '设置分类',
|
||||
edit_tracker: '编辑 Tracker',
|
||||
set_location: '修改文件位置',
|
||||
recheck_torrents: '重新检查种子',
|
||||
},
|
||||
|
||||
label: {
|
||||
@@ -159,12 +81,6 @@ export default {
|
||||
deleting: '删除中…',
|
||||
moving: '移动中…',
|
||||
moved: '已移动',
|
||||
next: '下一步',
|
||||
back: '返回',
|
||||
confirm: '确定',
|
||||
reannounced: '已重新通告',
|
||||
rechecking: '重新检查中…',
|
||||
dht_nodes: '%{smart_count} 节点',
|
||||
},
|
||||
|
||||
msg: {
|
||||
@@ -172,10 +88,6 @@ export default {
|
||||
},
|
||||
|
||||
dialog: {
|
||||
trigger_exit_qb: {
|
||||
title: '退出 qBittorrent',
|
||||
text: '您确定要退出qBittorrent吗?',
|
||||
},
|
||||
add_torrents: {
|
||||
placeholder: '将种子拖到这里上传,\n或者点击右边的附件图标来选择。',
|
||||
hint: '每行一个链接',
|
||||
@@ -192,9 +104,6 @@ export default {
|
||||
switch_locale: {
|
||||
msg: '确定要切换语言为 %{lang} 吗?\n这将会刷新页面。',
|
||||
},
|
||||
recheck_torrents: {
|
||||
msg: '确定要重新检查选中的种子吗?',
|
||||
},
|
||||
rss: {
|
||||
add_feed: '添加订阅',
|
||||
feed_url: '订阅 URL',
|
||||
@@ -221,7 +130,7 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
category_state: {
|
||||
state: {
|
||||
_: '状态',
|
||||
|
||||
downloading: '下载',
|
||||
@@ -232,27 +141,5 @@ export default {
|
||||
active: '活动',
|
||||
inactive: '空闲',
|
||||
errored: '错误',
|
||||
},
|
||||
|
||||
torrent_state: {
|
||||
error: '错误',
|
||||
missingFiles: '文件丢失',
|
||||
uploading: '上传中',
|
||||
pausedUP: '完成',
|
||||
queuedUP: '排队上传',
|
||||
stalledUP: '上传',
|
||||
checkingUP: '上传校验',
|
||||
forcedUP: '强制上传',
|
||||
allocating: '分配空间',
|
||||
downloading: '下载中',
|
||||
metaDL: '获取信息',
|
||||
pausedDL: '暂停下载',
|
||||
queuedDL: '排队下载',
|
||||
stalledDL: '下载',
|
||||
checkingDL: '下载校验',
|
||||
forceDL: '强制下载',
|
||||
checkingResumeData: '快速校验',
|
||||
moving: '移动中',
|
||||
unknown: '未知',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
/* 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: '未知',
|
||||
},
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import Vue from 'vue';
|
||||
import Vuetify from 'vuetify/lib';
|
||||
import i18n from '@/locale';
|
||||
import { loadConfig } from '@/store/config';
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
let locale = i18n.locale();
|
||||
switch (locale) {
|
||||
case 'zh-CN':
|
||||
locale = 'zh-Hans';
|
||||
break;
|
||||
case 'zh-TW':
|
||||
locale = 'zh-Hant';
|
||||
break;
|
||||
default:
|
||||
locale = locale.split('-', 1)[0];
|
||||
break;
|
||||
}
|
||||
locale = locale === 'zh-CN' ? 'zh-Hans' : locale.split('-', 1)[0];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { default: translation } = require('vuetify/src/locale/' + locale);
|
||||
const darkMode = !!loadConfig()['darkMode'];
|
||||
|
||||
export default new Vuetify({
|
||||
lang: {
|
||||
@@ -28,4 +20,7 @@ export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: 'mdi',
|
||||
},
|
||||
theme: {
|
||||
dark: darkMode,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,10 +7,8 @@ function registerProtocolHandler() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = location.origin + location.pathname;
|
||||
|
||||
try {
|
||||
navigator.registerProtocolHandler('magnet', baseUrl + '#download=%s', document.title);
|
||||
navigator.registerProtocolHandler('magnet', location.origin + '#url=%s', document.title);
|
||||
} catch (e) {
|
||||
log('Register protocol handler failed.', e);
|
||||
}
|
||||
@@ -22,14 +20,14 @@ function checkDownloadUrl() {
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(location.hash.substring(1));
|
||||
const url = params.get('download')
|
||||
const url = params.get('url')
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
params.delete('download');
|
||||
params.delete('url');
|
||||
location.hash = '#' + params.toString()
|
||||
return url
|
||||
}
|
||||
|
||||
export { registerProtocolHandler, checkDownloadUrl };
|
||||
export { registerProtocolHandler, checkDownloadUrl };
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Module } from 'vuex';
|
||||
import { AddFormState } from './types';
|
||||
|
||||
export const addFormStore: Module<AddFormState, any> = {
|
||||
state() {
|
||||
return {
|
||||
isOpen: false,
|
||||
downloadItem: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
isOpen(state) {
|
||||
return state.isOpen;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
openAddForm(state) {
|
||||
state.isOpen = true;
|
||||
},
|
||||
closeAddForm(state) {
|
||||
state.isOpen = false;
|
||||
state.downloadItem = null;
|
||||
},
|
||||
addFormDownloadItem(state, payload) {
|
||||
const { downloadItem } = payload;
|
||||
state.downloadItem = downloadItem;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -5,23 +5,7 @@ import { ConfigState, ConfigPayload } from './types';
|
||||
|
||||
const configKey = 'qb-config';
|
||||
|
||||
export interface Config {
|
||||
baseUrl: string | null;
|
||||
updateInterval: number;
|
||||
pageOptions: any;
|
||||
filter: {
|
||||
state: string | null;
|
||||
category: string | null;
|
||||
site: string | null;
|
||||
query: string | null;
|
||||
};
|
||||
locale: string | null;
|
||||
darkMode: string | null;
|
||||
displaySpeedInTitle: boolean | null;
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
baseUrl: null,
|
||||
updateInterval: 2000,
|
||||
pageOptions: {
|
||||
itemsPerPage: 50,
|
||||
@@ -34,9 +18,10 @@ const defaultConfig = {
|
||||
},
|
||||
locale: null,
|
||||
darkMode: null,
|
||||
displaySpeedInTitle: false,
|
||||
};
|
||||
|
||||
export type Config = typeof defaultConfig
|
||||
|
||||
function saveConfig(obj: any) {
|
||||
localStorage.setItem(configKey, JSON.stringify(obj));
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@ export const dialogStore: Module<DialogState, any> = {
|
||||
commit('showDialog', options);
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { merge, map, groupBy, sortBy } from 'lodash';
|
||||
import { cloneDeep, merge, map, groupBy, sortBy } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { computed, Ref } from '@vue/composition-api';
|
||||
@@ -6,13 +6,9 @@ import { computed, Ref } from '@vue/composition-api';
|
||||
import { configStore } from './config';
|
||||
import { dialogStore } from './dialog';
|
||||
import { snackBarStore } from './snackBar';
|
||||
import { addFormStore } from './addForm';
|
||||
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);
|
||||
|
||||
@@ -21,15 +17,12 @@ const store = new Vuex.Store<RootState>({
|
||||
config: configStore,
|
||||
dialog: dialogStore,
|
||||
snackBar: snackBarStore,
|
||||
addForm: addFormStore,
|
||||
searchEngine: searchEngineStore,
|
||||
},
|
||||
state: {
|
||||
rid: 0,
|
||||
mainData: undefined,
|
||||
preferences: null,
|
||||
pasteUrl: null,
|
||||
needAuth: false,
|
||||
},
|
||||
mutations: {
|
||||
/* eslint-disable no-param-reassign */
|
||||
@@ -40,26 +33,20 @@ const store = new Vuex.Store<RootState>({
|
||||
delete payload.full_update;
|
||||
state.mainData = payload;
|
||||
} else {
|
||||
const mainData = state.mainData!;
|
||||
const tmp: any = cloneDeep(state.mainData);
|
||||
if (payload.torrents_removed) {
|
||||
for (const hash of payload.torrents_removed) {
|
||||
Vue.delete(mainData.torrents, hash);
|
||||
delete tmp.torrents[hash];
|
||||
}
|
||||
delete payload.torrents_removed;
|
||||
}
|
||||
if (payload.categories_removed) {
|
||||
for (const key of payload.categories_removed) {
|
||||
Vue.delete(mainData, key);
|
||||
delete tmp.categories[key];
|
||||
}
|
||||
delete payload.categories_removed;
|
||||
}
|
||||
if (payload.tags_removed) {
|
||||
for (const key of payload.tags_removed) {
|
||||
Vue.delete(mainData, key);
|
||||
}
|
||||
delete payload.categories_removed;
|
||||
}
|
||||
stateMerge(mainData, payload);
|
||||
state.mainData = merge(tmp, payload);
|
||||
}
|
||||
},
|
||||
updatePreferences(state, payload) {
|
||||
@@ -69,18 +56,9 @@ const store = new Vuex.Store<RootState>({
|
||||
const { url } = payload;
|
||||
state.pasteUrl = url;
|
||||
},
|
||||
updateNeedAuth(state, payload) {
|
||||
state.needAuth = payload;
|
||||
},
|
||||
/* eslint-enable no-param-reassign */
|
||||
},
|
||||
getters: {
|
||||
allPreferences(state) {
|
||||
return state.preferences;
|
||||
},
|
||||
savePath(state) {
|
||||
return state.preferences['save_path'];
|
||||
},
|
||||
isDataReady(state) {
|
||||
return !!state.mainData;
|
||||
},
|
||||
@@ -100,38 +78,9 @@ 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) {
|
||||
@@ -164,24 +113,6 @@ const store = new Vuex.Store<RootState>({
|
||||
return result;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async updatePreferencesRequest({ dispatch }, preferences) {
|
||||
try {
|
||||
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");
|
||||
}
|
||||
},
|
||||
updatePreferencesRequestSuccess({ commit }, preferences) {
|
||||
commit("updatePreferences", preferences);
|
||||
},
|
||||
updatePreferencesRequestFailure() {
|
||||
alert('Preferences failed to update');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Module } from "vuex";
|
||||
import { SearchPlugin } from "@/types";
|
||||
import { SearchEnginePage } from "./types";
|
||||
import api from "@/Api";
|
||||
|
||||
export default {
|
||||
state: {
|
||||
searchPlugins: [],
|
||||
isPluginManagerOpen: false,
|
||||
},
|
||||
mutations: {
|
||||
setSearchPlugins(state, plugins: SearchPlugin[] | undefined | null) {
|
||||
state.searchPlugins = plugins;
|
||||
},
|
||||
openPluginManager(state) {
|
||||
state.isPluginManagerOpen = true;
|
||||
},
|
||||
closePluginManager(state) {
|
||||
state.isPluginManagerOpen = false;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
allSearchPlugins(state): SearchPlugin[] | undefined | null {
|
||||
return state.searchPlugins;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchSearchPlugins({ dispatch }) {
|
||||
// semantic helper
|
||||
dispatch("getSearchPluginsRequest");
|
||||
},
|
||||
async getSearchPluginsRequest({ dispatch }) {
|
||||
try {
|
||||
const searchPlugins = await api.getSearchPlugins();
|
||||
|
||||
dispatch("getSearchPluginRequestSuccess", searchPlugins);
|
||||
} catch {
|
||||
dispatch("getSearchPluginsRequestFailure");
|
||||
}
|
||||
},
|
||||
getSearchPluginRequestSuccess({ commit }, searchPlugins) {
|
||||
commit("setSearchPlugins", undefined);
|
||||
|
||||
commit("setSearchPlugins", searchPlugins);
|
||||
},
|
||||
getSearchPluginRequestFailure({ commit }) {
|
||||
commit("setSearchPlugins", null);
|
||||
},
|
||||
togglePluginAvailability({ dispatch }, plugin) {
|
||||
dispatch("togglePluginEnableRequest", plugin);
|
||||
},
|
||||
async togglePluginEnableRequest({ dispatch }, plugin: SearchPlugin) {
|
||||
try {
|
||||
await api.enablePlugin(plugin, !plugin.enabled); // switch plugin enable state
|
||||
|
||||
dispatch("enablePluginRequestSuccess", plugin);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
},
|
||||
enablePluginRequestSuccess({ dispatch }) {
|
||||
dispatch('fetchSearchPlugins'); // refresh the plugins
|
||||
},
|
||||
async updatePluginsRequest({ dispatch }) {
|
||||
try {
|
||||
await api.updateSearchPlugins();
|
||||
|
||||
dispatch("updatePluginsRequestSuccess");
|
||||
} catch {
|
||||
dispatch("updatePluginsRequestFailure");
|
||||
}
|
||||
},
|
||||
async updatePluginsRequestSuccess({ dispatch }) {
|
||||
await dispatch('getSearchPluginsRequest');
|
||||
},
|
||||
updatePluginsRequestFailure() {
|
||||
// Do nothing
|
||||
},
|
||||
},
|
||||
} as Module<SearchEnginePage, any>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MainData, SearchPlugin } from '@/types';
|
||||
import { MainData } from '@/types';
|
||||
import { Config } from './config';
|
||||
|
||||
export interface RootState {
|
||||
@@ -6,26 +6,11 @@ export interface RootState {
|
||||
mainData?: MainData;
|
||||
preferences: any;
|
||||
pasteUrl: string | null;
|
||||
needAuth: boolean;
|
||||
}
|
||||
|
||||
export interface SearchEnginePage {
|
||||
searchPlugins: SearchPlugin[] | null | undefined;
|
||||
isPluginManagerOpen: boolean;
|
||||
}
|
||||
|
||||
export interface AddFormState {
|
||||
isOpen: boolean;
|
||||
downloadItem: {
|
||||
title: string;
|
||||
url: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface TorrentFilter {
|
||||
state: string;
|
||||
category: string;
|
||||
tag: string;
|
||||
site: string;
|
||||
query: string;
|
||||
}
|
||||
@@ -48,7 +33,9 @@ export enum DialogType {
|
||||
}
|
||||
|
||||
export interface DialogConfig {
|
||||
dialog?: any;
|
||||
dialog?: {
|
||||
width?: string;
|
||||
};
|
||||
|
||||
title?: string;
|
||||
text: string;
|
||||
|
||||
5
src/styles/variables.scss
Normal file
5
src/styles/variables.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
$material-dark: (
|
||||
'text': (
|
||||
'primary': #dddddd,
|
||||
),
|
||||
);
|
||||
56
src/types.ts
56
src/types.ts
@@ -54,23 +54,6 @@ export interface Category {
|
||||
savePath?: string;
|
||||
}
|
||||
|
||||
export interface ApiCategory {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
savePath?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SimpleCategory {
|
||||
name: string | null;
|
||||
savePath?: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ServerState {
|
||||
alltime_dl: number;
|
||||
alltime_ul: number;
|
||||
@@ -99,10 +82,9 @@ export interface ServerState {
|
||||
}
|
||||
|
||||
export interface MainData {
|
||||
categories: Record<string, Category>;
|
||||
tags: [string];
|
||||
server_state: ServerState;
|
||||
torrents: Record<string, BaseTorrent>;
|
||||
categories: Record<string, Category>;
|
||||
server_state: ServerState;
|
||||
torrents: Record<string, BaseTorrent>;
|
||||
}
|
||||
|
||||
export interface RssTorrent {
|
||||
@@ -262,7 +244,7 @@ export interface Preferences {
|
||||
rss_refresh_interval: number;
|
||||
save_path: string;
|
||||
save_path_changed_tmm_enabled: boolean;
|
||||
scan_dirs: { [key: string]: string | number };
|
||||
scan_dirs: {[key: string]: string | number};
|
||||
schedule_from_hour: number;
|
||||
schedule_from_min: number;
|
||||
schedule_to_hour: number;
|
||||
@@ -290,32 +272,4 @@ 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 {
|
||||
enabled: boolean;
|
||||
fullName: string;
|
||||
name: string;
|
||||
supportedCategories: string[];
|
||||
url: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface SearchTaskTorrent {
|
||||
descrLink: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileUrl: string;
|
||||
nbLeechers: number;
|
||||
nbSeeders: number;
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
export interface SearchTaskResponse {
|
||||
results: SearchTaskTorrent[];
|
||||
status: 'Running' | 'Stopped';
|
||||
total: number;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
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'];
|
||||
const completeState = ['uploading', 'stalledUP', 'checkingUP', 'pausedUP', 'queuedUP', 'forcedUP'];
|
||||
const activeState = ['metaDL', 'downloading', 'forcedDL', 'uploading', 'forcedUP', 'moving'];
|
||||
const dlState = ['downloading', 'metaDL', 'stalledDL', 'checkingDL', 'pausedDL', 'queuedDL', 'forceDL', 'allocating'];
|
||||
const upState = ['uploading', 'stalledUP', 'checkingUP', 'queuedUP', 'forceUP'];
|
||||
const completeState = ['uploading', 'stalledUP', 'checkingUP', 'pausedUP', 'queuedUP', 'forceUP'];
|
||||
const activeState = ['metaDL', 'downloading', 'forceDL', 'uploading', 'forcedUP', 'moving'];
|
||||
const errorState = ['error', 'missingFiles'];
|
||||
|
||||
export function torrentIsState(type: StateType, state: string) {
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
git tag nightly-$(date +'%Y%m%d')
|
||||
|
||||
@@ -7,8 +7,6 @@ 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);
|
||||
@@ -20,8 +18,6 @@ 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);
|
||||
@@ -44,7 +40,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'],
|
||||
[null, ''],
|
||||
[-1, ''],
|
||||
])('case %#', (value, result) => {
|
||||
|
||||
@@ -13,7 +13,6 @@ const emtpyState: RootState = {
|
||||
mainData: undefined,
|
||||
preferences: null,
|
||||
pasteUrl: null,
|
||||
needAuth: false,
|
||||
};
|
||||
|
||||
const mockState = mock(emtpyState);
|
||||
@@ -48,7 +47,6 @@ describe('all torrents getter', () => {
|
||||
store.replaceState(mockState({
|
||||
mainData: {
|
||||
categories: {},
|
||||
tags: [""],
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
server_state: undefined as any,
|
||||
torrents: {
|
||||
|
||||
@@ -2,24 +2,6 @@ module.exports = {
|
||||
outputDir: 'dist/public',
|
||||
publicPath: './',
|
||||
|
||||
pwa: {
|
||||
// name: "qb-web",
|
||||
themeColor: "#4d8ad5",
|
||||
msTileColor: "#4d8ad5",
|
||||
appleMobileWebAppCapable: 'yes',
|
||||
|
||||
iconPaths: {
|
||||
favicon32: 'img/icons/favicon-32x32.png',
|
||||
favicon16: 'img/icons/favicon-16x16.png',
|
||||
appleTouchIcon: 'img/icons/apple-touch-icon.png',
|
||||
maskIcon: null,
|
||||
msTileImage: null,
|
||||
},
|
||||
workboxOptions: {
|
||||
importWorkboxFrom: 'local',
|
||||
},
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port: 8000,
|
||||
proxy: {
|
||||
@@ -34,10 +16,10 @@ module.exports = {
|
||||
let arg = args[0]
|
||||
arg = {
|
||||
...arg,
|
||||
'process.env.GIT_TAG': JSON.stringify(process.env.GIT_TAG),
|
||||
'process.env.COMMIT_ID': JSON.stringify(process.env.COMMIT_ID)
|
||||
}
|
||||
|
||||
return [arg]
|
||||
})
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user