mirror of
https://github.com/VueTorrent/VueTorrent.git
synced 2025-03-14 12:10:18 +03:00
update readme
This commit is contained in:
commit
280de2e651
42 changed files with 15477 additions and 12865 deletions
25
.eslintrc.js
Normal file
25
.eslintrc.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: ['plugin:vue/essential', 'eslint:recommended', '@vue/prettier'],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)'
|
||||
],
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
51
.github/workflows/dev-deploy.yml
vendored
51
.github/workflows/dev-deploy.yml
vendored
|
@ -1,23 +1,28 @@
|
|||
name: Build & Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
jobs:
|
||||
github-release-development:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- uses: montudor/action-zip@v0.1.0
|
||||
with:
|
||||
args: zip -qq -r ./release.zip ./vuetorrent
|
||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
automatic_release_tag: "dev-latest"
|
||||
prerelease: true
|
||||
title: "Development Build"
|
||||
files: release.zip
|
||||
name: Build & Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
jobs:
|
||||
github-release-development:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install & build
|
||||
uses: actions/setup-node@master
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- name: Zip it
|
||||
uses: montudor/action-zip@v0.1.0
|
||||
with:
|
||||
args: zip -qq -r ./release.zip ./vuetorrent
|
||||
- name: Get Version
|
||||
run: echo ::set-env name=VERSION::$(jq -r .version package.json)
|
||||
- name: Push release
|
||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
automatic_release_tag: 'dev-v${{ env.VERSION }}'
|
||||
prerelease: true
|
||||
title: 'Development Build v${{ env.VERSION }}'
|
||||
files: release.zip
|
||||
|
|
51
.github/workflows/prod-deploy copy.yml
vendored
51
.github/workflows/prod-deploy copy.yml
vendored
|
@ -1,23 +1,28 @@
|
|||
name: Build & Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
github-release-production:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- uses: montudor/action-zip@v0.1.0
|
||||
with:
|
||||
args: zip -qq -r ./release.zip ./vuetorrent
|
||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
automatic_release_tag: "prod-latest"
|
||||
prerelease: true
|
||||
title: "Stable Build"
|
||||
files: release.zip
|
||||
name: Build & Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
github-release-production:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install & build
|
||||
uses: actions/setup-node@master
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- name: Zip it
|
||||
uses: montudor/action-zip@v0.1.0
|
||||
with:
|
||||
args: zip -qq -r ./release.zip ./vuetorrent
|
||||
- name: Get Version
|
||||
run: echo ::set-env name=VERSION::$(jq -r .version package.json)
|
||||
- name: Push release
|
||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
automatic_release_tag: 'prod-v${{ env.VERSION }}'
|
||||
prerelease: true
|
||||
title: 'Stable Build v${{ env.VERSION }}'
|
||||
files: release.zip
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -19,3 +19,5 @@ yarn-error.log*
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
vuetorrent
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
30
README.md
30
README.md
|
@ -7,15 +7,11 @@ The sleekest looking WEBUI for qBittorrent made with Vuejs!
|
|||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://i.imgur.com/vPBcrK4.png"><img src="https://i.imgur.com/vPBcrK4.png" title="Desktop" alt="Desktop Screenshot" ></a>
|
||||
|
||||
<a href="https://imgur.com/xgwECT2.png"><img src="https://imgur.com/xgwECT2.png" title="Desktop" alt="Desktop Screenshot" ></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://i.imgur.com/SUOEyy9.png"><img src="https://i.imgur.com/SUOEyy9.png" title="Mobile" alt="Mobile Screenshot" width="320" height="540"></a>
|
||||
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
|
@ -32,20 +28,33 @@ The sleekest looking WEBUI for qBittorrent made with Vuejs!
|
|||
|
||||
- clone the repo
|
||||
|
||||
- npm install
|
||||
* npm install
|
||||
|
||||
- npm run serve
|
||||
|
||||
## Features
|
||||
|
||||
- viewing sessions stats ( down / upload speed, session uploaded / downloaded )
|
||||
- viewing sessions status ( down / upload speed, session uploaded / downloaded )
|
||||
|
||||
- adding / removing / pausing / resuming torrents
|
||||
|
||||
- sorting by every property shown!
|
||||
* sorting by every property shown!
|
||||
|
||||
* mobile friendly! (maybe not for thousands of torrents...)
|
||||
- mobile friendly! (maybe not for thousands of torrents...)
|
||||
|
||||
- works on QBittorrent V4.2 and later
|
||||
- torrent info / trackers / peers / content
|
||||
|
||||
* works on QBittorrent V4.2 and later
|
||||
|
||||
### Sorting/Filtring
|
||||
|
||||
example queries:
|
||||
|
||||
- s name asc => sort by name ascending
|
||||
- sort size desc => sort by size descending
|
||||
- f ubuntu => filter by torrent-name that contains 'ubuntu'
|
||||
- filter done => filter all completed torrents
|
||||
- filt busy => filter all downloading torrents
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -70,6 +79,5 @@ Reach out to me at one of the following places!
|
|||
## Credits
|
||||
|
||||
- Dashboard design heavily inspired by: '[Net Ninja - Vuetify](https://github.com/iamshaunjp/vuetify-playlist)'.
|
||||
Also check out The Net Ninja's Youtube Channel.
|
||||
|
||||
* This repo '[CzBiX qb-web ](https://github.com/CzBiX/qb-web)'
|
||||
|
|
24500
package-lock.json
generated
24500
package-lock.json
generated
File diff suppressed because it is too large
Load diff
101
package.json
101
package.json
|
@ -1,55 +1,54 @@
|
|||
{
|
||||
"name": "vuetorrent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"apexcharts": "^3.19.2",
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^3.6.4",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-apexcharts": "^1.5.3",
|
||||
"vue-observe-visibility": "^0.4.6",
|
||||
"vue-router": "^3.2.0",
|
||||
"vue-toastification": "^1.7.1",
|
||||
"vuetify": "^2.2.11",
|
||||
"vuex": "^3.4.0",
|
||||
"vuex-persist": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.3.0",
|
||||
"@vue/cli-plugin-eslint": "~4.3.0",
|
||||
"@vue/cli-service": "~4.3.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-cli-plugin-vuetify": "~2.0.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
"name": "vuetorrent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"format": "pretty-quick"
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"apexcharts": "^3.19.2",
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^3.6.4",
|
||||
"dayjs": "^1.8.27",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-apexcharts": "^1.5.3",
|
||||
"vue-async-computed": "^3.8.2",
|
||||
"vue-context": "^5.1.0",
|
||||
"vue-observe-visibility": "^0.4.6",
|
||||
"vue-router": "^3.2.0",
|
||||
"vue-toastification": "^1.7.1",
|
||||
"vue2-perfect-scrollbar": "^1.5.0",
|
||||
"vuetify": "^2.2.29",
|
||||
"vuex": "^3.4.0",
|
||||
"vuex-persist": "^2.2.0"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie <= 10"
|
||||
]
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.3.0",
|
||||
"@vue/cli-plugin-eslint": "~4.3.0",
|
||||
"@vue/cli-service": "~4.3.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"fibers": "^5.0.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"prettier": "^2.0.5",
|
||||
"pretty-quick": "^2.0.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-cli-plugin-vuetify": "~2.0.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie <= 10"
|
||||
]
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 22 KiB |
50
src/App.vue
50
src/App.vue
|
@ -1,49 +1,41 @@
|
|||
<template>
|
||||
<v-app class="background">
|
||||
<v-app :style="{ backgroundColor: background }">
|
||||
<AddModal />
|
||||
<div v-if="authenticated" class="background">
|
||||
<keep-alive><Navbar /></keep-alive>
|
||||
<v-content class="mx-4 mb-4">
|
||||
<router-view></router-view>
|
||||
</v-content>
|
||||
</div>
|
||||
<v-container v-else fill-height>
|
||||
<v-layout
|
||||
row
|
||||
wrap
|
||||
align-center
|
||||
class="justify-center"
|
||||
justify-center
|
||||
>
|
||||
<div style="margin: 0 auto;">
|
||||
<Login />
|
||||
</div>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<div class="background">
|
||||
<p class="grey--text caption text-center">
|
||||
Made by Daan Wijns
|
||||
</p>
|
||||
</div>
|
||||
<SettingsModal />
|
||||
<Navbar v-if="isAuthenticated" />
|
||||
<v-content fill-height fill-width>
|
||||
<router-view></router-view>
|
||||
</v-content>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import Navbar from './components/Navbar.vue'
|
||||
import Login from './components/Login.vue'
|
||||
import Navbar from '@/components/Navbar.vue'
|
||||
import { isAuthenticated } from '@/services/auth.js'
|
||||
|
||||
export default {
|
||||
components: { Navbar, Login },
|
||||
components: { Navbar },
|
||||
name: 'App',
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
async getAuth() {
|
||||
return await isAuthenticated()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['authenticated', 'rid', 'mainData', 'preferences']),
|
||||
...mapState(['rid', 'mainData', 'preferences']),
|
||||
...mapGetters(['getTheme']),
|
||||
theme() {
|
||||
return this.getTheme() ? 'dark' : 'light'
|
||||
},
|
||||
background() {
|
||||
return this.$vuetify.theme.themes[this.theme].background
|
||||
},
|
||||
isAuthenticated() {
|
||||
return this.getAuth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
18
src/assets/styles.scss
Normal file
18
src/assets/styles.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
@import '~vuetify/src/styles/styles.sass';
|
||||
|
||||
@mixin dialog-title {
|
||||
@include theme(v-card) using($material) {
|
||||
.v-card__title {
|
||||
background-color: map-get($material, 'app-bar');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dark-mode-value($light, $dark) {
|
||||
&.theme--light {
|
||||
@content ($light);
|
||||
}
|
||||
&.theme--dark {
|
||||
@content ($dark);
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
<template>
|
||||
<v-container class="grey lighten-4">
|
||||
<v-card max-width="400" flat>
|
||||
<v-container :class="`pa-3 project done`">
|
||||
<v-card-title class="justify-center">
|
||||
<h2>Login</h2>
|
||||
</v-card-title>
|
||||
<div class="mr-5 ml-5"></div>
|
||||
<v-card-text>
|
||||
<v-form class="px-3" ref="form">
|
||||
<v-text-field
|
||||
flat
|
||||
solo
|
||||
background-color="grey lighten-4"
|
||||
label="username"
|
||||
prepend-icon="person"
|
||||
v-model="username"
|
||||
:rules="inputRules"
|
||||
@keyup.enter.native="Login"
|
||||
autocomplete="current email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
flat
|
||||
solo
|
||||
background-color="grey lighten-4"
|
||||
type="password"
|
||||
label="password"
|
||||
prepend-icon="lock"
|
||||
v-model="password"
|
||||
:rules="inputRules"
|
||||
@keyup.enter.native="Login"
|
||||
autocomplete="current password"
|
||||
></v-text-field>
|
||||
<v-spacer></v-spacer>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
text
|
||||
@click="Login"
|
||||
class="blue_accent white--text mx-0 mt-3"
|
||||
>Login</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
inputRules: [v => v.length >= 1 || 'At least 1 character']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
Login() {
|
||||
this.$store.state.loading = true
|
||||
this.$store.dispatch('LOGIN', {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['loading'])
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,132 +1,142 @@
|
|||
<template>
|
||||
<v-dialog
|
||||
max-width="500px"
|
||||
v-model="dialog"
|
||||
>
|
||||
<v-card>
|
||||
<v-container :class="`pa-0 project done`">
|
||||
<v-card-title class="justify-center">
|
||||
<h2>Add a new Torrent</h2>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form v-model="valid" ref="form">
|
||||
<v-container>
|
||||
|
||||
<v-row no-gutters>
|
||||
<v-col ref="fileZone">
|
||||
<v-file-input
|
||||
v-model="files"
|
||||
color="deep-purple accent-4"
|
||||
counter
|
||||
label="File input"
|
||||
multiple
|
||||
placeholder="Select your files"
|
||||
prepend-icon="mdi-paperclip"
|
||||
outlined
|
||||
:show-size="1000"
|
||||
>
|
||||
<template v-slot:selection="{ index, text }">
|
||||
<v-chip
|
||||
v-if="index < 2"
|
||||
color="deep-purple accent-4"
|
||||
dark
|
||||
label
|
||||
small
|
||||
>
|
||||
{{ text }}
|
||||
</v-chip>
|
||||
<v-dialog max-width="500px" v-model="dialog">
|
||||
<v-card>
|
||||
<v-container :class="`pa-0 project done`">
|
||||
<v-card-title class="justify-center">
|
||||
<h2>Add a new Torrent</h2>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form v-model="valid" ref="form">
|
||||
<v-container>
|
||||
<v-row no-gutters>
|
||||
<v-col ref="fileZone">
|
||||
<v-file-input
|
||||
v-model="files"
|
||||
color="deep-purple accent-4"
|
||||
counter
|
||||
label="File input"
|
||||
multiple
|
||||
placeholder="Select your files"
|
||||
prepend-icon="mdi-paperclip"
|
||||
outlined
|
||||
:show-size="1000"
|
||||
>
|
||||
<template
|
||||
v-slot:selection="{ index, text }"
|
||||
>
|
||||
<v-chip
|
||||
v-if="index < 2"
|
||||
color="deep-purple accent-4"
|
||||
dark
|
||||
label
|
||||
small
|
||||
>
|
||||
{{ text }}
|
||||
</v-chip>
|
||||
|
||||
<span
|
||||
v-else-if="index === 2"
|
||||
class="overline grey--text text--darken-3 mx-2"
|
||||
>
|
||||
+{{ files.length - 2 }} File(s)
|
||||
</span>
|
||||
</template>
|
||||
</v-file-input>
|
||||
<v-text-field
|
||||
label="URL"
|
||||
prepend-icon="mdi-link"
|
||||
:rows="$vuetify.breakpoint.xsOnly ? 1 : 3"
|
||||
required
|
||||
:autofocus="!phoneLayout"
|
||||
v-model="url"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<span
|
||||
v-else-if="index === 2"
|
||||
class="overline grey--text text--darken-3 mx-2"
|
||||
>
|
||||
+{{ files.length - 2 }} File(s)
|
||||
</span>
|
||||
</template>
|
||||
</v-file-input>
|
||||
<v-text-field
|
||||
label="URL"
|
||||
prepend-icon="mdi-link"
|
||||
:rows="
|
||||
$vuetify.breakpoint.xsOnly ? 1 : 3
|
||||
"
|
||||
required
|
||||
:autofocus="!phoneLayout"
|
||||
v-model="url"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-text-field
|
||||
v-model="directory"
|
||||
label="Download Directory"
|
||||
prepend-icon="folder"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="directory"
|
||||
:placeholder="savepath"
|
||||
label="Download Directory"
|
||||
prepend-icon="folder"
|
||||
></v-text-field>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-form>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
text
|
||||
@click="submit"
|
||||
:disabled="!valid"
|
||||
class="blue_accent white--text mx-0 mt-3"
|
||||
>Add Torrent</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-form>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
text
|
||||
@click="submit"
|
||||
:disabled="!valid"
|
||||
class="blue_accent white--text mx-0 mt-3"
|
||||
>Add Torrent</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/mixins/Modal";
|
||||
import { mapGetters } from 'vuex'
|
||||
import Modal from '@/mixins/Modal'
|
||||
import qbit from '@/services/qbit'
|
||||
export default {
|
||||
name: "AddModal",
|
||||
mixins: [Modal],
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
directory: "",
|
||||
inputRules: [
|
||||
(v) =>
|
||||
v.indexOf("magnet") > -1 ||
|
||||
v.indexOf("http") > -1 ||
|
||||
this.validFile ||
|
||||
"Not a valid magnet link",
|
||||
],
|
||||
loading: false,
|
||||
url: null,
|
||||
valid: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
if(this.files.length || this.url){
|
||||
let torrents = []
|
||||
let params= { urls: null};
|
||||
if(this.files.length) torrents.push(...this.files)
|
||||
if(this.url) params.urls = this.url
|
||||
|
||||
qbit.addTorrents(params, torrents)
|
||||
name: 'AddModal',
|
||||
mixins: [Modal],
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
directory: '',
|
||||
inputRules: [
|
||||
v =>
|
||||
v.indexOf('magnet') > -1 ||
|
||||
v.indexOf('http') > -1 ||
|
||||
this.validFile ||
|
||||
'Not a valid magnet link'
|
||||
],
|
||||
loading: false,
|
||||
url: null,
|
||||
valid: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
if (this.files.length || this.url) {
|
||||
let torrents = []
|
||||
let params = { urls: null }
|
||||
if (this.files.length) torrents.push(...this.files)
|
||||
if (this.url) params.urls = this.url
|
||||
if (this.directory) params.savepath = this.directory
|
||||
|
||||
this.url = null
|
||||
qbit.addTorrents(params, torrents)
|
||||
|
||||
this.$store.commit('TOGGLE_MODAL', 'addmodal')
|
||||
}
|
||||
this.resetForm()
|
||||
|
||||
this.$store.commit('TOGGLE_MODAL', 'addmodal')
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.url = null
|
||||
;(this.files = []), (this.directory = null)
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
validFile() {
|
||||
return this.Files.length > 0;
|
||||
},
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
},
|
||||
};
|
||||
computed: {
|
||||
...mapGetters(['getSettings']),
|
||||
validFile() {
|
||||
return this.Files.length > 0
|
||||
},
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly
|
||||
},
|
||||
savepath() {
|
||||
return this.getSettings().savePath
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
99
src/components/Modals/SettingsModal.vue
Normal file
99
src/components/Modals/SettingsModal.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<v-dialog max-width="500px" v-model="dialog">
|
||||
<v-card>
|
||||
<v-container
|
||||
style="min-height: 300px;"
|
||||
:class="`pa-0 project done`"
|
||||
>
|
||||
<v-card-title class="justify-center">
|
||||
<h2>Settings</h2>
|
||||
</v-card-title>
|
||||
<v-form>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
text
|
||||
@click="switchOldUI"
|
||||
class="green_accent white--text mx-0 mt-3"
|
||||
>switch to old ui</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
<v-form class="px-6 mt-3">
|
||||
<v-container>
|
||||
<v-switch
|
||||
class="v-input--reverse v-input--expand"
|
||||
inset
|
||||
v-model="freeSpace"
|
||||
color="green_accent"
|
||||
>
|
||||
<template #label>
|
||||
<span class="grey--text">
|
||||
Show Free Space
|
||||
</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-container>
|
||||
<v-card-actions class="justify-center pb-5 project done">
|
||||
<v-btn
|
||||
text
|
||||
@click="save"
|
||||
class="green_accent white--text mx-0 mt-3"
|
||||
>Save</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Modal from '@/mixins/Modal'
|
||||
import qbit from '@/services/qbit'
|
||||
export default {
|
||||
name: 'SettingsModal',
|
||||
mixins: [Modal],
|
||||
methods: {
|
||||
async switchOldUI() {
|
||||
await qbit.switchToOldUi()
|
||||
|
||||
window.location.reload(true)
|
||||
},
|
||||
save() {
|
||||
this.$store.commit('TOGGLE_MODAL', 'settingsmodal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['webuiSettings']),
|
||||
freeSpace: {
|
||||
get() {
|
||||
return this.webuiSettings.showFreeSpace
|
||||
},
|
||||
set(val) {
|
||||
this.webuiSettings.showFreeSpace = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Reversed input variant
|
||||
::v-deep .v-input--reverse .v-input__slot {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
.v-application--is-ltr & {
|
||||
.v-input--selection-controls__input {
|
||||
margin-right: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.v-application--is-rtl & {
|
||||
.v-input--selection-controls__input {
|
||||
margin-left: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,274 +1,413 @@
|
|||
<template>
|
||||
<nav>
|
||||
<!--title-->
|
||||
<v-app-bar flat color="background">
|
||||
<v-btn
|
||||
@click="drawer = !drawer"
|
||||
text
|
||||
fab
|
||||
class="grey--text text--lighten-1"
|
||||
>
|
||||
<v-icon>menu</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title
|
||||
:class="[
|
||||
'grey--text',
|
||||
{ 'subheading ml-0': $vuetify.breakpoint.smAndDown }
|
||||
]"
|
||||
>
|
||||
<span class="font-weight-light">Vue</span>
|
||||
<span>Torrent</span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!--right corner functions-->
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
fab
|
||||
color="grey"
|
||||
class="mr-0 ml-0"
|
||||
@click="toggleModal('addmodal')"
|
||||
>
|
||||
<v-icon color="grey">add</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small fab text class="mr-0 ml-0" @click="removeTorrents">
|
||||
<v-icon color="grey">remove</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small fab text class="mr-0 ml-0" @click="resumeTorrents">
|
||||
<v-icon color="grey">play_arrow</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small fab text class="mr-0 ml-0" @click="pauseTorrents">
|
||||
<v-icon color="grey">pause</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<!--navigation drawer itself -->
|
||||
<v-navigation-drawer app v-model="drawer" class="primary" style="position:fixed;">
|
||||
<!--current download speeds -->
|
||||
<v-flex class="mt-3" v-if="stats">
|
||||
<div
|
||||
class="secondary_lighter--text text-uppercase caption ml-4"
|
||||
>
|
||||
current speed
|
||||
</div>
|
||||
<v-card color="secondary" flat class="mr-2 ml-2">
|
||||
<v-layout row wrap class="pa-3 project nav_download mx-auto">
|
||||
<v-icon color="download">keyboard_arrow_down</v-icon>
|
||||
<span class="download--text title">
|
||||
{{
|
||||
stats.dlspeed.substring(
|
||||
0,
|
||||
stats.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
stats.dlspeed.substring(
|
||||
stats.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
<v-icon class="pl-5" color="upload"
|
||||
>keyboard_arrow_up</v-icon
|
||||
>
|
||||
<span class="upload--text title">
|
||||
{{
|
||||
stats.upspeed.substring(
|
||||
0,
|
||||
stats.upspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
stats.upspeed.substring(
|
||||
stats.upspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
<!--speeds graph -->
|
||||
<div class="mt-4">
|
||||
<apexcharts
|
||||
ref="chart"
|
||||
type="line"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
></apexcharts>
|
||||
</div>
|
||||
<div class="mt-4"></div>
|
||||
<div
|
||||
class="secondary_lighter--text text-uppercase caption ml-4"
|
||||
>
|
||||
session stats
|
||||
</div>
|
||||
<v-card flat color="secondary" class="mr-2 ml-2">
|
||||
<v-layout row wrap class="pa-3 project nav_download mx-auto">
|
||||
<v-flex md6>
|
||||
<div style="font-size: 0.95em; margin-top: 6px;" class="download--text">Downloaded</div>
|
||||
</v-flex>
|
||||
<v-flex md5 class="ml-4">
|
||||
<span class="download--text title">
|
||||
{{
|
||||
stats.downloaded.substring(
|
||||
0,
|
||||
stats.downloaded.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
stats.downloaded.substring(
|
||||
stats.downloaded.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
<v-card flat color="secondary" class="ml-2 mr-2 mt-1">
|
||||
<v-layout row wrap class="pa-3 project nav_upload mx-auto">
|
||||
<v-flex md6>
|
||||
<div style="font-size: 0.95em; margin-top: 6px;" class="upload--text">Uploaded</div>
|
||||
</v-flex>
|
||||
<v-flex md5 class="ml-4">
|
||||
<span class="upload--text title">
|
||||
{{
|
||||
stats.uploaded.substring(
|
||||
0,
|
||||
stats.uploaded.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
stats.uploaded.substring(
|
||||
stats.uploaded.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
<v-flex style="position:fixed; bottom: 15px; right: 15px;" >
|
||||
<v-list>
|
||||
<v-list-item @click="toggleTheme" link>
|
||||
<v-icon v-if="theme === 'Light'" class="pr-2 white--text"
|
||||
>brightness_7</v-icon
|
||||
>
|
||||
<v-icon v-else class="pr-2 white--text">brightness_2</v-icon>
|
||||
<v-list-item-title class="white--text" style="font-size:15px">{{
|
||||
theme
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-flex>
|
||||
</v-navigation-drawer>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapState, mapGetters } from 'vuex'
|
||||
import { setInterval } from 'timers'
|
||||
import VueApexCharts from 'vue-apexcharts'
|
||||
import qbit from '@/services/qbit'
|
||||
|
||||
export default {
|
||||
components: { apexcharts: VueApexCharts },
|
||||
data() {
|
||||
return {
|
||||
drawer: false,
|
||||
paused: false,
|
||||
chartOptions: {
|
||||
chart: {
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
dynamicAnimation: {
|
||||
speed: 2000
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ['#00b3fa', '#64CEAA'],
|
||||
stroke: {
|
||||
show: true,
|
||||
curve: 'smooth',
|
||||
lineCap: 'round',
|
||||
width: 4
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shade: 'dark',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0.5,
|
||||
opacityFrom: 0.6,
|
||||
opacityTo: 0.5,
|
||||
stops: [0, 50, 100]
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'upload',
|
||||
type: 'area',
|
||||
data: this.$store.state.upload_data
|
||||
},
|
||||
{
|
||||
name: 'download',
|
||||
type: 'area',
|
||||
data: this.$store.state.download_data
|
||||
}
|
||||
],
|
||||
chartInterval: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['REFRESH_TORRENTS', 'CLEAR_INTERVALS']),
|
||||
pauseTorrents() {
|
||||
qbit.pauseTorrents(this.selected_torrents)
|
||||
},
|
||||
resumeTorrents(){
|
||||
qbit.resumeTorrents(this.selected_torrents)
|
||||
},
|
||||
removeTorrents() {
|
||||
qbit.deleteTorrents(this.selected_torrents, false)
|
||||
},
|
||||
updateChart() {
|
||||
this.$refs.chart.updateSeries(this.series, true)
|
||||
},
|
||||
toggleModal(name) {
|
||||
this.$store.commit('TOGGLE_MODAL', name)
|
||||
},
|
||||
toggleTheme() {
|
||||
this.$store.commit('TOGGLE_THEME')
|
||||
this.$vuetify.theme.dark = !this.$vuetify.theme.dark;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['stats', 'selected_torrents']),
|
||||
...mapGetters(['getTheme']),
|
||||
theme() {
|
||||
return this.getTheme() ? 'Dark' : 'Light'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.chartInterval = setInterval(async () => {
|
||||
this.updateChart()
|
||||
}, 2000)
|
||||
this.$vuetify.theme.dark = this.getTheme()
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.chartInterval)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.project.nav_upload {
|
||||
border-left: 4px solid #00b3fa;
|
||||
}
|
||||
.project.nav_download {
|
||||
border-left: 4px solid #64ceaa;
|
||||
}
|
||||
.allow-spacer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<nav>
|
||||
<!--title-->
|
||||
<v-app-bar app flat color="background">
|
||||
<v-app-bar-nav-icon
|
||||
@click.stop="drawer = !drawer"
|
||||
class="grey--text text--lighten-1"
|
||||
></v-app-bar-nav-icon>
|
||||
<v-toolbar-title
|
||||
:class="[
|
||||
'grey--text',
|
||||
{ 'subheading ml-0': $vuetify.breakpoint.smAndDown }
|
||||
]"
|
||||
>
|
||||
<div v-if="!$vuetify.breakpoint.xs">
|
||||
<span class="font-weight-light">Vue</span>
|
||||
<span>Torrent</span>
|
||||
</div>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!--right corner functions-->
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
fab
|
||||
color="grey"
|
||||
class="mr-0 ml-0"
|
||||
@click="toggleModal('addmodal')"
|
||||
>
|
||||
<v-icon color="grey">add</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small fab text class="mr-0 ml-0" @click="removeTorrents">
|
||||
<v-icon color="grey">remove</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small fab text class="mr-0 ml-0" @click="resumeTorrents">
|
||||
<v-icon color="grey">play_arrow</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small fab text class="mr-0 ml-0" @click="pauseTorrents">
|
||||
<v-icon color="grey">pause</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
small
|
||||
fab
|
||||
text
|
||||
class="mr-0 ml-0"
|
||||
@click="toggleModal('settingsmodal')"
|
||||
>
|
||||
<v-icon color="grey">settings</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<!--navigation drawer itself -->
|
||||
<v-navigation-drawer
|
||||
app
|
||||
v-model="drawer"
|
||||
class="primary"
|
||||
style="position: fixed;"
|
||||
disable-resize-watcher
|
||||
>
|
||||
<!--current download speeds -->
|
||||
<v-flex class="mt-3" v-if="status">
|
||||
<div
|
||||
class="secondary_lighter--text text-uppercase caption ml-4"
|
||||
>
|
||||
current speed
|
||||
</div>
|
||||
<v-card color="secondary" flat class="mr-2 ml-2">
|
||||
<v-layout
|
||||
row
|
||||
wrap
|
||||
class="pa-3 project nav_download mx-auto"
|
||||
>
|
||||
<v-icon color="download">keyboard_arrow_down</v-icon>
|
||||
<span class="download--text title">
|
||||
{{
|
||||
status.dlspeed.substring(
|
||||
0,
|
||||
status.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
status.dlspeed.substring(
|
||||
status.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
<v-icon class="pl-5" color="upload"
|
||||
>keyboard_arrow_up</v-icon
|
||||
>
|
||||
<span class="upload--text title">
|
||||
{{
|
||||
status.upspeed.substring(
|
||||
0,
|
||||
status.upspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
status.upspeed.substring(
|
||||
status.upspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
<!--speeds graph -->
|
||||
<div class="mt-4">
|
||||
<apexcharts
|
||||
ref="chart"
|
||||
type="line"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
></apexcharts>
|
||||
</div>
|
||||
<div class="mt-4"></div>
|
||||
<div
|
||||
class="secondary_lighter--text text-uppercase caption ml-4"
|
||||
>
|
||||
session status
|
||||
</div>
|
||||
<v-card flat color="secondary" class="mr-2 ml-2">
|
||||
<v-layout
|
||||
row
|
||||
wrap
|
||||
class="pa-3 project nav_download mx-auto"
|
||||
>
|
||||
<v-flex md6>
|
||||
<div
|
||||
style="font-size: 0.95em; margin-top: 6px;"
|
||||
class="download--text"
|
||||
>
|
||||
Downloaded
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex md5 class="ml-4">
|
||||
<span class="download--text title">
|
||||
{{
|
||||
status.downloaded.substring(
|
||||
0,
|
||||
status.downloaded.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
status.downloaded.substring(
|
||||
status.downloaded.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
<v-card flat color="secondary" class="ml-2 mr-2 mt-1">
|
||||
<v-layout row wrap class="pa-3 project nav_upload mx-auto">
|
||||
<v-flex md6>
|
||||
<div
|
||||
style="font-size: 0.95em; margin-top: 6px;"
|
||||
class="upload--text"
|
||||
>
|
||||
Uploaded
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex md5 class="ml-4">
|
||||
<span class="upload--text title">
|
||||
{{
|
||||
status.uploaded.substring(
|
||||
0,
|
||||
status.uploaded.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
status.uploaded.substring(
|
||||
status.uploaded.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
|
||||
<v-card
|
||||
v-if="webuiSettings.showFreeSpace"
|
||||
flat
|
||||
style="margin-top: 30px;"
|
||||
color="secondary"
|
||||
class="ml-2 mr-2"
|
||||
>
|
||||
<v-layout row wrap class="pa-3 project nav_upload mx-auto">
|
||||
<v-flex md6>
|
||||
<div
|
||||
style="font-size: 0.95em; margin-top: 6px;"
|
||||
class="upload--text"
|
||||
>
|
||||
Free Space
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex md5 class="ml-4">
|
||||
<span class="upload--text title">
|
||||
{{
|
||||
status.freeDiskSpace.substring(
|
||||
0,
|
||||
status.freeDiskSpace.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="font-weight-light caption">{{
|
||||
status.freeDiskSpace.substring(
|
||||
status.freeDiskSpace.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
<v-container>
|
||||
<v-row justify="space-between">
|
||||
<v-col>
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item v-on="on" @click="logout" link>
|
||||
<v-icon class="white--text"
|
||||
>exit_to_app</v-icon
|
||||
>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<span>Log out</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item
|
||||
v-on="on"
|
||||
@click="toggleSpeed"
|
||||
link
|
||||
>
|
||||
<v-icon
|
||||
:color="altSpeed ? 'download' : ''"
|
||||
class="white--text"
|
||||
>speed</v-icon
|
||||
>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<span>Alt speeds</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item
|
||||
v-on="on"
|
||||
@click="toggleTheme"
|
||||
link
|
||||
>
|
||||
<v-icon
|
||||
v-if="theme === 'Light'"
|
||||
class="white--text"
|
||||
>brightness_7</v-icon
|
||||
>
|
||||
<v-icon v-else class="pr-2 white--text"
|
||||
>brightness_2</v-icon
|
||||
>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<span>{{ theme }}</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-navigation-drawer>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapState, mapGetters } from 'vuex'
|
||||
import VueApexCharts from 'vue-apexcharts'
|
||||
import qbit from '@/services/qbit'
|
||||
|
||||
export default {
|
||||
components: { apexcharts: VueApexCharts },
|
||||
data() {
|
||||
return {
|
||||
drawer: false,
|
||||
paused: false,
|
||||
chartOptions: {
|
||||
chart: {
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
dynamicAnimation: {
|
||||
speed: 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ['#00b3fa', '#64CEAA'],
|
||||
stroke: {
|
||||
show: true,
|
||||
curve: 'smooth',
|
||||
lineCap: 'round',
|
||||
width: 4
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shade: 'dark',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0.5,
|
||||
opacityFrom: 0.6,
|
||||
opacityTo: 0.5,
|
||||
stops: [0, 50, 100]
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'light'
|
||||
}
|
||||
},
|
||||
chartInterval: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['REFRESH_TORRENTS', 'CLEAR_INTERVALS']),
|
||||
pauseTorrents() {
|
||||
qbit.pauseTorrents(this.selected_torrents)
|
||||
},
|
||||
resumeTorrents() {
|
||||
qbit.resumeTorrents(this.selected_torrents)
|
||||
},
|
||||
removeTorrents() {
|
||||
qbit.deleteTorrents(this.selected_torrents, false)
|
||||
},
|
||||
updateChart() {
|
||||
this.$refs.chart.updateSeries(this.series, true)
|
||||
},
|
||||
toggleModal(name) {
|
||||
this.$store.commit('TOGGLE_MODAL', name)
|
||||
},
|
||||
toggleTheme() {
|
||||
this.$store.commit('TOGGLE_THEME')
|
||||
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
|
||||
},
|
||||
logout() {
|
||||
this.$store.commit('LOGOUT')
|
||||
this.$router.push('/login')
|
||||
},
|
||||
toggleSpeed() {
|
||||
qbit.toggleSpeedLimitsMode()
|
||||
},
|
||||
setChartTooltipTheme(theme) {
|
||||
this.chartOptions.tooltip.theme = theme.toLowerCase()
|
||||
this.$refs.chart.updateOptions(this.chartOptions)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['status', 'selected_torrents']),
|
||||
...mapGetters(['getTheme', 'getStatus', 'getWebuiSettings']),
|
||||
theme() {
|
||||
return this.getTheme() ? 'Dark' : 'Light'
|
||||
},
|
||||
altSpeed() {
|
||||
return this.getStatus().altSpeed
|
||||
},
|
||||
series() {
|
||||
return [
|
||||
{
|
||||
name: 'upload',
|
||||
type: 'area',
|
||||
data: this.$store.state.upload_data
|
||||
},
|
||||
{
|
||||
name: 'download',
|
||||
type: 'area',
|
||||
data: this.$store.state.download_data
|
||||
}
|
||||
]
|
||||
},
|
||||
webuiSettings() {
|
||||
return this.getWebuiSettings()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$vuetify.theme.dark = this.getTheme()
|
||||
},
|
||||
mounted() {
|
||||
this.setChartTooltipTheme(this.theme)
|
||||
},
|
||||
watch: {
|
||||
theme(newValue) {
|
||||
this.setChartTooltipTheme(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.justify-space-between {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.project.nav_upload {
|
||||
border-left: 4px solid #00b3fa;
|
||||
}
|
||||
.project.nav_download {
|
||||
border-left: 4px solid #64ceaa;
|
||||
}
|
||||
.allow-spacer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,171 +1,209 @@
|
|||
<template>
|
||||
<v-card ripple
|
||||
flat class="pointer torrent"
|
||||
:class="
|
||||
containsTorrent(torrent.hash) ? 'torrent_selected' : ''
|
||||
"
|
||||
@click.native="selectTorrent(torrent.hash)">
|
||||
<v-layout row wrap :class="`pa-4 ml-0 project ${torrent.state}`">
|
||||
<v-flex xs12 sm2 md3>
|
||||
<div class="caption grey--text">Torrent title</div>
|
||||
<div>{{ torrent.name }}</div>
|
||||
</v-flex>
|
||||
<v-flex xs6 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Size</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.size.substring(
|
||||
0,
|
||||
torrent.size.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.size.substring(
|
||||
torrent.size.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Done</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.dloaded.substring(
|
||||
0,
|
||||
torrent.dloaded.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.dloaded.substring(
|
||||
torrent.dloaded.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs6 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Download</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.dlspeed.substring(
|
||||
0,
|
||||
torrent.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.dlspeed.substring(
|
||||
torrent.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Upload</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.upspeed.substring(
|
||||
0,
|
||||
torrent.upspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.upspeed.substring(
|
||||
torrent.upspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs6 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">ETA</div>
|
||||
<div>{{ torrent.eta }}</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Peers</div>
|
||||
<div>
|
||||
{{ torrent.num_leechs }}
|
||||
<span class="grey--text caption"
|
||||
>/{{ torrent.available_peers }}</span
|
||||
>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Seeds</div>
|
||||
<div>
|
||||
{{ torrent.num_seeds }}
|
||||
<span class="grey--text caption"
|
||||
>/{{ torrent.available_seeds }}</span
|
||||
>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs4 sm12 md1>
|
||||
<div class="right">
|
||||
<v-chip
|
||||
small
|
||||
:class="
|
||||
`${torrent.state} white--text my-2 caption`
|
||||
"
|
||||
>{{ torrent.state }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm12 md12>
|
||||
<v-progress-linear
|
||||
height="3"
|
||||
color="cyan darken-1"
|
||||
background-color="cyan lighten-3"
|
||||
:value="(torrent.dloaded / torrent.size) * 100"
|
||||
></v-progress-linear>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name:'Torrent',
|
||||
props: {
|
||||
torrent: Object
|
||||
},
|
||||
methods: {selectTorrent(hash) {
|
||||
if (this.containsTorrent(hash)) {
|
||||
this.$store.commit('SET_SELECTED', {type:"remove", hash})
|
||||
} else {
|
||||
this.$store.commit('SET_SELECTED', {type:"add", hash})
|
||||
}
|
||||
},
|
||||
containsTorrent(hash) {
|
||||
return this.$store.getters.containsTorrent(hash)
|
||||
},}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.project.done {
|
||||
border-left: 4px solid #3cd1c2;
|
||||
}
|
||||
.project.busy {
|
||||
border-left: 4px solid #ffaa2c;
|
||||
}
|
||||
.project.fail {
|
||||
border-left: 4px solid #f83e70;
|
||||
}
|
||||
.project.paused {
|
||||
border-left: 4px solid #cfd8dc;
|
||||
}
|
||||
.v-chip.done {
|
||||
background: #3cd1c2 !important;
|
||||
}
|
||||
.v-chip.busy {
|
||||
background: #ffaa2c !important;
|
||||
}
|
||||
.v-chip.fail {
|
||||
background: #f83e70 !important;
|
||||
}
|
||||
.v-chip.paused {
|
||||
background: #cfd8dc !important;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<v-card
|
||||
ripple
|
||||
flat
|
||||
class="pointer torrent"
|
||||
:class="containsTorrent(torrent.hash) ? 'torrent_selected' : ''"
|
||||
@click.native="selectTorrent(torrent.hash)"
|
||||
@dblclick.prevent="showInfo(torrent.hash)"
|
||||
>
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-layout
|
||||
@contextmenu.prevent="$refs.menu.open"
|
||||
v-on="on"
|
||||
row
|
||||
wrap
|
||||
:class="`pa-4 ml-0 project ${torrent.state}`"
|
||||
>
|
||||
<v-flex xs12 sm2 md3>
|
||||
<div class="caption grey--text">Torrent title</div>
|
||||
<div class="truncate">{{ torrent.name }}</div>
|
||||
</v-flex>
|
||||
<v-flex xs6 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Size</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.size.substring(
|
||||
0,
|
||||
torrent.size.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.size.substring(
|
||||
torrent.size.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Done</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.dloaded.substring(
|
||||
0,
|
||||
torrent.dloaded.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.dloaded.substring(
|
||||
torrent.dloaded.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs6 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Download</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.dlspeed.substring(
|
||||
0,
|
||||
torrent.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.dlspeed.substring(
|
||||
torrent.dlspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Upload</div>
|
||||
<div>
|
||||
{{
|
||||
torrent.upspeed.substring(
|
||||
0,
|
||||
torrent.upspeed.indexOf(' ')
|
||||
)
|
||||
}}
|
||||
<span class="caption grey--text">{{
|
||||
torrent.upspeed.substring(
|
||||
torrent.upspeed.indexOf(' ')
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs6 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">ETA</div>
|
||||
<div>{{ torrent.eta }}</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Peers</div>
|
||||
<div>
|
||||
{{ torrent.num_leechs }}
|
||||
<span class="grey--text caption"
|
||||
>/{{ torrent.available_peers }}</span
|
||||
>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs5 sm1 md1 class="mr-2">
|
||||
<div class="caption grey--text">Seeds</div>
|
||||
<div>
|
||||
{{ torrent.num_seeds }}
|
||||
<span class="grey--text caption"
|
||||
>/{{ torrent.available_seeds }}</span
|
||||
>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs4 sm12 md1>
|
||||
<div class="right">
|
||||
<v-chip
|
||||
small
|
||||
:class="`${torrent.state} white--text my-2 caption`"
|
||||
>{{ torrent.state }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</v-flex>
|
||||
<!-- labels -->
|
||||
<v-flex v-for="tag in torrent.tags" :key="tag" xs3 sm1 md1>
|
||||
<v-chip small class="download white--text my-2 caption">
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm12 md12>
|
||||
<v-progress-linear
|
||||
height="3"
|
||||
rounded
|
||||
color="cyan darken-1"
|
||||
background-color="cyan lighten-3"
|
||||
:value="torrent.progress"
|
||||
></v-progress-linear>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</template>
|
||||
<span>{{ torrent.name }}</span>
|
||||
</v-tooltip>
|
||||
<v-divider></v-divider>
|
||||
<vue-context ref="menu">
|
||||
<TorrentRightClickMenu :hash="torrent.hash" />
|
||||
</vue-context>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueContext } from 'vue-context'
|
||||
import TorrentRightClickMenu from '@/components/Torrent/TorrentRightClickMenu.vue'
|
||||
|
||||
export default {
|
||||
name: 'Torrent',
|
||||
components: {
|
||||
VueContext,
|
||||
TorrentRightClickMenu
|
||||
},
|
||||
props: {
|
||||
torrent: Object
|
||||
},
|
||||
methods: {
|
||||
selectTorrent(hash) {
|
||||
if (this.containsTorrent(hash)) {
|
||||
this.$store.commit('SET_SELECTED', { type: 'remove', hash })
|
||||
} else {
|
||||
this.$store.commit('SET_SELECTED', { type: 'add', hash })
|
||||
}
|
||||
},
|
||||
containsTorrent(hash) {
|
||||
return this.$store.getters.containsTorrent(hash)
|
||||
},
|
||||
showInfo(hash) {
|
||||
this.$store.commit('TOGGLE_MODAL', 'TorrentDetailModal')
|
||||
this.$store.commit('SET_SELECTED_TORRENT_DETAIL', hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.project.done {
|
||||
border-left: 4px solid #3cd1c2;
|
||||
}
|
||||
.project.busy {
|
||||
border-left: 4px solid #ffaa2c;
|
||||
}
|
||||
.project.fail {
|
||||
border-left: 4px solid #f83e70;
|
||||
}
|
||||
.project.paused {
|
||||
border-left: 4px solid #cfd8dc;
|
||||
}
|
||||
.v-chip.done {
|
||||
background: #3cd1c2 !important;
|
||||
}
|
||||
.v-chip.busy {
|
||||
background: #ffaa2c !important;
|
||||
}
|
||||
.v-chip.fail {
|
||||
background: #f83e70 !important;
|
||||
}
|
||||
.v-chip.paused {
|
||||
background: #cfd8dc !important;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
|
98
src/components/Torrent/TorrentRightClickMenu.vue
Normal file
98
src/components/Torrent/TorrentRightClickMenu.vue
Normal file
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<v-card
|
||||
elevation="20"
|
||||
width="200"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
overflow: show;
|
||||
"
|
||||
:dark="dark"
|
||||
>
|
||||
<v-list dense rounded>
|
||||
<v-list-item @click="showInfo" link>
|
||||
<v-icon>info</v-icon>
|
||||
<v-list-item-title class="ml-2" style="font-size: 15px;"
|
||||
>Show Info</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item @click="resume" link>
|
||||
<v-icon>play_arrow</v-icon>
|
||||
<v-list-item-title class="ml-2" style="font-size: 15px;"
|
||||
>Resume</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
<v-list-item @click="pause" link>
|
||||
<v-icon>pause</v-icon>
|
||||
<v-list-item-title class="ml-2" style="font-size: 15px;"
|
||||
>Pause</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item @click="reannounce" link>
|
||||
<v-icon>record_voice_over</v-icon>
|
||||
<v-list-item-title class="ml-2" style="font-size: 15px;"
|
||||
>reannounce</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item @click="deleteWithoutFiles" link>
|
||||
<v-icon color="red">delete</v-icon>
|
||||
<v-list-item-title
|
||||
class="ml-2"
|
||||
style="font-size: 15px; color: red;"
|
||||
>Delete</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteWithFiles" link>
|
||||
<v-icon color="red">delete</v-icon>
|
||||
<v-list-item-title
|
||||
class="ml-2"
|
||||
style="font-size: 15px; color: red;"
|
||||
>Delete with files</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import qbit from '@/services/qbit'
|
||||
export default {
|
||||
name: 'TorrentRightClickMenu',
|
||||
props: {
|
||||
hash: String
|
||||
},
|
||||
methods: {
|
||||
resume() {
|
||||
qbit.resumeTorrents([this.hash])
|
||||
},
|
||||
pause() {
|
||||
qbit.pauseTorrents([this.hash])
|
||||
},
|
||||
reannounce() {
|
||||
qbit.reannounceTorrents([this.hash])
|
||||
},
|
||||
deleteWithoutFiles() {
|
||||
qbit.deleteTorrents([this.hash], false)
|
||||
},
|
||||
deleteWithFiles() {
|
||||
qbit.deleteTorrents([this.hash], true)
|
||||
},
|
||||
showInfo() {
|
||||
this.$store.commit('TOGGLE_MODAL', 'TorrentDetailModal')
|
||||
this.$store.commit('SET_SELECTED_TORRENT_DETAIL', this.hash)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dark() {
|
||||
return this.$vuetify.dark
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
72
src/components/TorrentDetailModal/Content.vue
Normal file
72
src/components/TorrentDetailModal/Content.vue
Normal file
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<v-card flat>
|
||||
<perfect-scrollbar>
|
||||
<v-card-text style="max-height: 500px; min-height: 400px;">
|
||||
<v-treeview
|
||||
v-model="tree"
|
||||
:items="fileTree"
|
||||
activatable
|
||||
item-key="name"
|
||||
open-on-click
|
||||
>
|
||||
<template v-slot:prepend="{ item, open }">
|
||||
<v-icon v-if="!item.icon">
|
||||
{{ open ? 'mdi-folder-open' : 'mdi-folder' }}
|
||||
</v-icon>
|
||||
<v-icon v-else>{{ item.icon }}</v-icon>
|
||||
</template>
|
||||
<template v-slot:append="{ item }">
|
||||
<span v-if="!item.icon"
|
||||
>{{ item.children.length }} Files</span
|
||||
>
|
||||
<div v-else>
|
||||
<span>[{{ item.size }}]</span>
|
||||
<span class="ml-4">{{ item.progress }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</v-card-text>
|
||||
</perfect-scrollbar>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import qbit from '@/services/qbit'
|
||||
import { treeify } from '@/helpers'
|
||||
export default {
|
||||
name: 'Content',
|
||||
props: {
|
||||
hash: String,
|
||||
isActive: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tree: [],
|
||||
treeData: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fileTree() {
|
||||
if (this.treeData) {
|
||||
return treeify(this.treeData)
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getTorrentFiles() {
|
||||
const { data } = await qbit.getTorrentFiles(this.hash)
|
||||
this.treeData = data
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isActive(active) {
|
||||
if (active) {
|
||||
this.getTorrentFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
68
src/components/TorrentDetailModal/CreateNewTagDialog.vue
Normal file
68
src/components/TorrentDetailModal/CreateNewTagDialog.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-container
|
||||
style="min-height: 200px;"
|
||||
:class="`pa-0 project done`"
|
||||
>
|
||||
<v-card-title class="justify-center">
|
||||
<h2>Create New Tag</h2>
|
||||
</v-card-title>
|
||||
|
||||
<v-form class="px-6 mt-3">
|
||||
<v-container>
|
||||
<v-text-field
|
||||
class="mx-auto"
|
||||
style="max-width: 200px;"
|
||||
v-model="tagname"
|
||||
:rules="rules"
|
||||
:counter="10"
|
||||
label="Tag name"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-container>
|
||||
<v-card-actions class="justify-center pb-5 project done">
|
||||
<v-btn text @click="cancel" class="error white--text mt-3"
|
||||
>Cancel</v-btn
|
||||
>
|
||||
<v-btn
|
||||
text
|
||||
@click="create"
|
||||
class="green_accent white--text mt-3"
|
||||
>Save</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import qbit from '@/services/qbit'
|
||||
export default {
|
||||
name: 'createNewTagDialog',
|
||||
props: {
|
||||
dialog: Boolean
|
||||
},
|
||||
data: () => ({
|
||||
tagname: '',
|
||||
rules: [
|
||||
v => !!v || 'Tag is required',
|
||||
v => v.length <= 10 || 'Tag must be less than 10 characters'
|
||||
]
|
||||
}),
|
||||
methods: {
|
||||
create() {
|
||||
qbit.createTag(this.tagname)
|
||||
this.cancel()
|
||||
},
|
||||
cancel() {
|
||||
this.tagname = ''
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
59
src/components/TorrentDetailModal/DeleteTagDialog.vue
Normal file
59
src/components/TorrentDetailModal/DeleteTagDialog.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-container
|
||||
style="min-height: 200px;"
|
||||
:class="`pa-0 project done`"
|
||||
>
|
||||
<v-card-title class="justify-center">
|
||||
<h2>Delete Tag</h2>
|
||||
</v-card-title>
|
||||
|
||||
<v-list
|
||||
rounded
|
||||
v-if="tags"
|
||||
class="text-center mx-auto"
|
||||
style="max-width: 200px;"
|
||||
>
|
||||
<v-list-item
|
||||
@click="deleteTag(t)"
|
||||
v-for="(t, i) in tags"
|
||||
:key="i"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="t"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-container>
|
||||
<v-card-actions class="justify-center pb-5 project done">
|
||||
<v-btn text @click="cancel" class="error white--text mt-3"
|
||||
>Close</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import qbit from '@/services/qbit'
|
||||
export default {
|
||||
name: 'DeleteTagDialog',
|
||||
props: {
|
||||
dialog: Boolean,
|
||||
tags: Array
|
||||
},
|
||||
methods: {
|
||||
deleteTag(tag) {
|
||||
qbit.deleteTag(tag)
|
||||
this.cancel()
|
||||
},
|
||||
cancel() {
|
||||
this.tagname = ''
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
135
src/components/TorrentDetailModal/Info.vue
Normal file
135
src/components/TorrentDetailModal/Info.vue
Normal file
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text class="pa-0" style="font-size: 1.1em;">
|
||||
<v-simple-table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="grey--text">Torrent title</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin-top: 10px !important;">
|
||||
<td class="grey--text">hash</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.hash }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Size</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.size }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Done:</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.dloaded }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Download</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.dlspeed }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Upload</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.upspeed }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">ETA</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.eta }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Peers</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.num_leechs
|
||||
}}<span class="grey--text"
|
||||
>/{{ torrent.available_peers }}</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Seeds</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.num_seeds
|
||||
}}<span class="grey--text"
|
||||
>/{{ torrent.available_seeds }}</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Ratio</td>
|
||||
<td class="torrentmodaltext--text">
|
||||
{{ torrent.ratio }}%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Tags</td>
|
||||
<td v-if="torrent.tags">
|
||||
{{ torrent.tags.join(',') }}
|
||||
</td>
|
||||
<td v-else>None</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grey--text">Status</td>
|
||||
<v-chip
|
||||
small
|
||||
:class="`${torrent.state} white--text my-2 caption`"
|
||||
>{{ torrent.state }}</v-chip
|
||||
>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
|
||||
<v-flex class="pt-3 pb-4">
|
||||
<v-progress-linear
|
||||
height="5"
|
||||
stream
|
||||
rounded
|
||||
color="cyan darken-1"
|
||||
background-color="cyan lighten-3"
|
||||
:buffer-value="torrent.progress"
|
||||
></v-progress-linear>
|
||||
</v-flex>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
name: 'Info',
|
||||
props: {
|
||||
hash: String
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getTorrent']),
|
||||
torrent() {
|
||||
return this.getTorrent(this.hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .v-data-table thead th,
|
||||
::v-deep .v-data-table tbody td {
|
||||
padding: 0 !important;
|
||||
height: 3em;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-child {
|
||||
padding: 0 0 0 8px !important;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
149
src/components/TorrentDetailModal/Peers.vue
Normal file
149
src/components/TorrentDetailModal/Peers.vue
Normal file
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<perfect-scrollbar>
|
||||
<v-data-table
|
||||
v-if="peers"
|
||||
:headers="headers"
|
||||
:items="peers"
|
||||
:items-per-page="-1"
|
||||
:hide-default-footer="true"
|
||||
style="max-height: 500px; min-height: 400px;"
|
||||
>
|
||||
<template v-slot:item="row">
|
||||
<tr>
|
||||
<td class="ip">
|
||||
<template v-if="row.item.country_code">
|
||||
<img
|
||||
v-if="isWindows"
|
||||
class="country-flag"
|
||||
:title="row.item.country"
|
||||
:alt="codeToFlag(row.item.country_code).char"
|
||||
:src="codeToFlag(row.item.country_code).url"
|
||||
/>
|
||||
<template v-else>{{
|
||||
codeToFlag(row.item.country_code).char
|
||||
}}</template>
|
||||
</template>
|
||||
{{ row.item.ip }}
|
||||
<span class="grey--text">:{{ row.item.port }}</span>
|
||||
</td>
|
||||
<td>{{ row.item.connection }}</td>
|
||||
<td :title="row.item.flags_desc">{{ row.item.flags }}</td>
|
||||
<td>{{ row.item.client }}</td>
|
||||
<td>{{ row.item.progress | progress }}</td>
|
||||
<td>{{ row.item.dl_speed | networkSpeed }}</td>
|
||||
<td>{{ row.item.downloaded | networkSize }}</td>
|
||||
<td>{{ row.item.up_speed | networkSpeed }}</td>
|
||||
<td>{{ row.item.uploaded | networkSize }}</td>
|
||||
<td>{{ row.item.relevance | progress }}</td>
|
||||
<td>{{ row.item.files }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</perfect-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { map, merge, cloneDeep } from 'lodash'
|
||||
import qbit from '@/services/qbit'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { codeToFlag, isWindows } from '@/helpers'
|
||||
|
||||
export default {
|
||||
name: 'Peers',
|
||||
props: { hash: String, isActive: Boolean },
|
||||
data: () => ({
|
||||
headers: [
|
||||
{ text: 'IP', value: 'ip' },
|
||||
{ text: 'Connection', value: 'connection' },
|
||||
{ text: 'Flags', value: 'flags' },
|
||||
{ text: 'Client', value: 'client' },
|
||||
{ text: 'Progress', value: 'progress' },
|
||||
{ text: 'DL Speed', value: 'dl_speed' },
|
||||
{ text: 'Downloaded', value: 'downloaded' },
|
||||
{ text: 'UP Speed', value: 'up_speed' },
|
||||
{ text: 'Uploaded', value: 'uploaded' },
|
||||
{ text: 'Relevance', value: 'relevance' },
|
||||
{ text: 'Files', value: 'files' }
|
||||
],
|
||||
peersObj: null
|
||||
}),
|
||||
methods: {
|
||||
codeToFlag(val) {
|
||||
return codeToFlag(val)
|
||||
},
|
||||
isWindows() {
|
||||
return isWindows()
|
||||
},
|
||||
async getTorrentPeers() {
|
||||
const { data } = await qbit.getTorrentPeers(
|
||||
this.hash,
|
||||
this.rid || undefined
|
||||
)
|
||||
|
||||
this.rid = data.rid
|
||||
|
||||
if (data.full_update) {
|
||||
this.peersObj = data.peers
|
||||
} else {
|
||||
const tmp = cloneDeep(this.peersObj)
|
||||
if (data.peers_removed) {
|
||||
for (const key of data.peers_removed) {
|
||||
delete tmp[key]
|
||||
}
|
||||
}
|
||||
this.peersObj = merge(tmp, data.peers)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isActive(active) {
|
||||
if (active) {
|
||||
this.getTorrentPeers()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rid: {
|
||||
get() {
|
||||
return this.$store.state.rid
|
||||
},
|
||||
set(val) {
|
||||
this.$store.state.rid = val
|
||||
}
|
||||
},
|
||||
peers() {
|
||||
return map(this.peersObj, (value, key) => merge({}, value, { key }))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep .ip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::v-deep .ip .country-flag {
|
||||
width: 1.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
|
||||
::v-deep .v-data-table thead th,
|
||||
::v-deep .v-data-table tbody td {
|
||||
padding: 0 2px !important;
|
||||
height: auto;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-child {
|
||||
padding: 0 0 0 8px !important;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
105
src/components/TorrentDetailModal/Tags.vue
Normal file
105
src/components/TorrentDetailModal/Tags.vue
Normal file
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text
|
||||
class="mx-auto mt-5"
|
||||
style="font-size: 1.1em; max-height: 500px; min-height: 300px;"
|
||||
>
|
||||
<v-layout class="mx-auto" row wrap>
|
||||
<v-flex xs12 sm12>
|
||||
<h3>Available Tags:</h3>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm12 class="mt-3">
|
||||
<v-chip
|
||||
v-for="tag in availableTags"
|
||||
:key="tag"
|
||||
small
|
||||
class="download white--text caption mx-2"
|
||||
style="font-size: 0.95em !important;"
|
||||
@click="addTag(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout class="mx-auto mt-12" row wrap>
|
||||
<v-flex xs12 sm12>
|
||||
<h3>Current Tags:</h3>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm12 class="mt-3">
|
||||
<v-chip
|
||||
v-for="tag in torrent.tags"
|
||||
:key="tag"
|
||||
small
|
||||
close
|
||||
class="download white--text caption mx-2"
|
||||
style="font-size: 0.95em !important;"
|
||||
@click="deleteTag(tag)"
|
||||
@click:close="deleteTag(tag)"
|
||||
>{{ tag }}</v-chip
|
||||
>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center pb-5">
|
||||
<v-btn
|
||||
text
|
||||
class="error white--text mt-3"
|
||||
@click="DeleteDialog = true"
|
||||
>Delete</v-btn
|
||||
>
|
||||
<v-btn
|
||||
text
|
||||
class="green_accent white--text mt-3"
|
||||
@click="CreateNewDialog = true"
|
||||
>Create new</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
<CreateNewTagDialog
|
||||
:dialog="CreateNewDialog"
|
||||
@close="CreateNewDialog = false"
|
||||
/>
|
||||
<DeleteTagDialog
|
||||
:dialog="DeleteDialog"
|
||||
@close="DeleteDialog = false"
|
||||
:tags="availableTags"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { difference } from 'lodash'
|
||||
import { mapGetters } from 'vuex'
|
||||
import qbit from '@/services/qbit'
|
||||
import CreateNewTagDialog from './CreateNewTagDialog'
|
||||
import DeleteTagDialog from './DeleteTagDialog'
|
||||
export default {
|
||||
name: 'Tags',
|
||||
components: { CreateNewTagDialog, DeleteTagDialog },
|
||||
props: {
|
||||
hash: String
|
||||
},
|
||||
data: () => ({
|
||||
CreateNewDialog: false,
|
||||
DeleteDialog: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters(['getTorrent', 'getAvailableTags']),
|
||||
torrent() {
|
||||
return this.getTorrent(this.hash)
|
||||
},
|
||||
availableTags() {
|
||||
let availableTags = this.getAvailableTags()
|
||||
let currentTags = this.getTorrent(this.hash).tags
|
||||
return difference(availableTags, currentTags)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addTag(tag) {
|
||||
qbit.addTorrentTag(this.hash, tag)
|
||||
},
|
||||
deleteTag(tag) {
|
||||
qbit.removeTorrentTag(this.hash, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
106
src/components/TorrentDetailModal/TorrentDetailModal.vue
Normal file
106
src/components/TorrentDetailModal/TorrentDetailModal.vue
Normal file
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
scrollable
|
||||
:width="dialogWidth"
|
||||
:fullscreen="phoneLayout"
|
||||
>
|
||||
<v-card
|
||||
v-if="torrent"
|
||||
style="min-height: 400px; overflow: hidden !important;"
|
||||
>
|
||||
<div
|
||||
:class="`pa-0 project ${torrent.state}`"
|
||||
:style="{ height: phoneLayout ? '100vh' : '' }"
|
||||
>
|
||||
<v-card-title class="pb-0 justify-center primary">
|
||||
<h2 class="white--text">Torrent Detail</h2>
|
||||
</v-card-title>
|
||||
<v-tabs v-model="tab" background-color="primary" center-active>
|
||||
<v-tab href="#info">Info</v-tab>
|
||||
<v-tab href="#trackers">Trackers</v-tab>
|
||||
<v-tab href="#peers">Peers</v-tab>
|
||||
<v-tab href="#content">Content</v-tab>
|
||||
<v-tab href="#tags">Tags</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items v-model="tab" touchless>
|
||||
<v-tab-item value="info">
|
||||
<info :is-active="tab === 'info'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="peers">
|
||||
<Peers :is-active="tab === 'peers'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="trackers">
|
||||
<Trackers
|
||||
:is-active="tab === 'trackers'"
|
||||
:hash="hash"
|
||||
/>
|
||||
</v-tab-item>
|
||||
<v-tab-item value="content">
|
||||
<Content :is-active="tab === 'content'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="tags">
|
||||
<Tags :is-active="tab === 'tags'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</div>
|
||||
<v-fab-transition v-if="phoneLayout">
|
||||
<v-btn @click="close" color="red" dark absolute bottom right>
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
</v-fab-transition>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable vue/no-unused-components */
|
||||
import Modal from '@/mixins/Modal'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import Content from '@/components/TorrentDetailModal/Content'
|
||||
import Info from '@/components/TorrentDetailModal/Info'
|
||||
import Peers from '@/components/TorrentDetailModal/Peers'
|
||||
import Trackers from '@/components/TorrentDetailModal/Trackers'
|
||||
import Tags from '@/components/TorrentDetailModal/Tags'
|
||||
|
||||
export default {
|
||||
name: 'TorrentDetailModal',
|
||||
mixins: [Modal],
|
||||
components: { Content, Info, Peers, Trackers,Tags },
|
||||
data() {
|
||||
return {
|
||||
tab: null,
|
||||
items: [{ tab: 'Info' }, { tab: 'Content' }],
|
||||
peers: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$store.commit('TOGGLE_MODAL', 'TorrentDetailModal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getTorrent']),
|
||||
hash() {
|
||||
return this.$store.state.selectedDetailTorrent
|
||||
},
|
||||
torrent() {
|
||||
return this.getTorrent(this.hash)
|
||||
},
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly
|
||||
},
|
||||
dialogWidth() {
|
||||
return this.phoneLayout ? '100%' : '80%'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dialog(visible) {
|
||||
if (!visible) {
|
||||
this.tab = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
102
src/components/TorrentDetailModal/Trackers.vue
Normal file
102
src/components/TorrentDetailModal/Trackers.vue
Normal file
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<perfect-scrollbar>
|
||||
<v-data-table
|
||||
v-if="trackers"
|
||||
:headers="headers"
|
||||
:items="trackers"
|
||||
:hide-default-footer="true"
|
||||
style="max-height: 500px; min-height: 400px;"
|
||||
>
|
||||
<template v-slot:item="row">
|
||||
<tr>
|
||||
<td>{{ row.item.tier }}</td>
|
||||
<td>{{ row.item.url }}</td>
|
||||
<td>{{ row.item.status | formatTrackerStatus }}</td>
|
||||
<td>{{ row.item.num_peers | formatTrackerNum }}</td>
|
||||
<td>{{ row.item.num_seeds | formatTrackerNum }}</td>
|
||||
<td>{{ row.item.num_leeches | formatTrackerNum }}</td>
|
||||
<td>{{ row.item.num_downloaded | formatTrackerNum }}</td>
|
||||
<td>{{ row.item.msg }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</perfect-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import qbit from '@/services/qbit'
|
||||
export default {
|
||||
name: 'Trackers',
|
||||
props: { hash: String, isActive: Boolean },
|
||||
data: () => ({
|
||||
headers: [
|
||||
{ text: '#', value: 'tier' },
|
||||
{ text: 'URL', value: 'url' },
|
||||
{ text: 'Status', value: 'status' },
|
||||
{ text: 'Peers', value: 'num_peers' },
|
||||
{ text: 'Seeds', value: 'num_seeds' },
|
||||
{ text: 'Leeches', value: 'num_leeches' },
|
||||
{ text: 'Downloaded', value: 'num_downloaded' },
|
||||
{ text: 'Message', value: 'msg' }
|
||||
],
|
||||
tempTrackers: []
|
||||
}),
|
||||
methods: {
|
||||
async getTorrentTrackers() {
|
||||
const { data } = await qbit.getTorrentTrackers(this.hash)
|
||||
this.tempTrackers = data
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isActive(active) {
|
||||
if (active) {
|
||||
this.getTorrentTrackers()
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatTrackerStatus(status) {
|
||||
const map = [
|
||||
'Disabled',
|
||||
'Not contacted',
|
||||
'Working',
|
||||
'Updating',
|
||||
'Not working'
|
||||
]
|
||||
|
||||
return map[status]
|
||||
},
|
||||
formatTrackerNum(num) {
|
||||
if (num === -1) {
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
return num.toString()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
trackers() {
|
||||
return this.tempTrackers
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles.scss';
|
||||
|
||||
::v-deep .v-data-table thead th,
|
||||
::v-deep .v-data-table tbody td {
|
||||
padding: 0 2px !important;
|
||||
height: auto;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-child {
|
||||
padding: 0 0 0 8px !important;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
83
src/filters.js
Normal file
83
src/filters.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
import dayjs from 'dayjs'
|
||||
import Vue from 'vue'
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
export function toPrecision(value, precision) {
|
||||
if (value >= 10 ** precision) {
|
||||
return value.toString()
|
||||
}
|
||||
if (value >= 1) {
|
||||
return value.toPrecision(precision)
|
||||
}
|
||||
|
||||
return value.toFixed(precision - 1)
|
||||
}
|
||||
|
||||
export function formatSize(value) {
|
||||
const units = 'KMGTP'
|
||||
let index = -1
|
||||
|
||||
while (value >= 1000) {
|
||||
value /= 1024
|
||||
index++
|
||||
}
|
||||
|
||||
const unit = index < 0 ? 'B' : `${units[index]}iB`
|
||||
|
||||
if (index < 0) {
|
||||
return `${value} ${unit}`
|
||||
}
|
||||
return `${toPrecision(value, 3)} ${unit}`
|
||||
}
|
||||
|
||||
Vue.filter('formatSize', formatSize)
|
||||
Vue.filter('size', formatSize)
|
||||
|
||||
export function formatTimestamp(timestamp) {
|
||||
if (timestamp == null || timestamp === -1) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const m = dayjs.unix(timestamp)
|
||||
return m.format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
Vue.filter('formatTimestamp', formatTimestamp)
|
||||
|
||||
export function formatProgress(progress) {
|
||||
// eslint-disable-next-line
|
||||
progress *= 100
|
||||
return `${toPrecision(progress, 3)}%`
|
||||
}
|
||||
|
||||
Vue.filter('progress', formatProgress)
|
||||
|
||||
export function parseDate(str) {
|
||||
if (!str) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Date.parse(str) / 1000
|
||||
}
|
||||
|
||||
Vue.filter('parseDate', parseDate)
|
||||
|
||||
export function formatNetworkSpeed(speed) {
|
||||
if (speed === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${formatSize(speed)}/s`
|
||||
}
|
||||
|
||||
Vue.filter('networkSpeed', formatNetworkSpeed)
|
||||
|
||||
export function networkSize(size) {
|
||||
if (size === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return formatSize(size)
|
||||
}
|
||||
|
||||
Vue.filter('networkSize', networkSize)
|
98
src/helpers.js
Normal file
98
src/helpers.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
export function formatBytes(a, b) {
|
||||
if (a == 0) return '0 Bytes'
|
||||
const c = 1024
|
||||
const d = b || 2
|
||||
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const f = Math.floor(Math.log(a) / Math.log(c))
|
||||
return `${parseFloat((a / Math.pow(c, f)).toFixed(d))} ${e[f]}`
|
||||
}
|
||||
|
||||
export function getIconForFileType(type) {
|
||||
let types = {
|
||||
html: 'mdi-language-html5',
|
||||
js: 'mdi-nodejs',
|
||||
json: 'mdi-json',
|
||||
md: 'mdi-markdown',
|
||||
pdf: 'mdi-file-pdf',
|
||||
png: 'mdi-file-image',
|
||||
txt: 'mdi-file-document-outline',
|
||||
sub: 'mdi-file-document-outline',
|
||||
idx: 'mdi-file-document-outline',
|
||||
xls: 'mdi-file-excel',
|
||||
avi: 'movie',
|
||||
mp4: 'movie',
|
||||
mkv: 'movie'
|
||||
}
|
||||
|
||||
if (!types[type]) return 'insert_drive_file'
|
||||
|
||||
return types[type]
|
||||
}
|
||||
|
||||
export const isWindows = navigator.userAgent.includes('Windows')
|
||||
|
||||
export function codeToFlag(code) {
|
||||
const magicNumber = 0x1f1a5
|
||||
|
||||
// eslint-disable-next-line
|
||||
code = code.toUpperCase()
|
||||
const codePoints = [...code].map(c => magicNumber + c.charCodeAt(0))
|
||||
const char = String.fromCodePoint(...codePoints)
|
||||
const url =
|
||||
'https://cdn.jsdelivr.net/npm/twemoji/2/svg/' +
|
||||
`${codePoints[0].toString(16)}-${codePoints[1].toString(16)}.svg`
|
||||
|
||||
return {
|
||||
char,
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
export function treeify(paths) {
|
||||
let result = []
|
||||
let level = { result }
|
||||
|
||||
paths.forEach(path => {
|
||||
path.name.split('/').reduce((r, name, i, a) => {
|
||||
if (!r[name]) {
|
||||
r[name] = { result: [] }
|
||||
r.result.push(createFile(path, name, r[name].result))
|
||||
}
|
||||
|
||||
return r[name]
|
||||
}, level)
|
||||
})
|
||||
|
||||
//parse folders
|
||||
result = result.map(el => parseFolder(el))
|
||||
|
||||
function parseFolder(el) {
|
||||
if (el.children.length !== 0) {
|
||||
let folder = createFolder(el.name, el.children)
|
||||
folder.children = folder.children.map(el => parseFolder(el))
|
||||
return folder
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function createFile(data, name, children) {
|
||||
return {
|
||||
name: name,
|
||||
progress: Math.round(data.progress * 100),
|
||||
size: formatBytes(data.size),
|
||||
icon: getIconForFileType(name.split('.').pop()),
|
||||
children: children
|
||||
}
|
||||
}
|
||||
|
||||
function createFolder(name, children) {
|
||||
return {
|
||||
name: name,
|
||||
type: 'directory',
|
||||
children: children
|
||||
}
|
||||
}
|
31
src/main.js
31
src/main.js
|
@ -4,28 +4,39 @@ import '@/registerServiceWorker'
|
|||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import '@babel/polyfill'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import filters from '@/filters'
|
||||
|
||||
import VueObserveVisibility from 'vue-observe-visibility'
|
||||
|
||||
|
||||
Vue.use(VueObserveVisibility)
|
||||
|
||||
import Toast from 'vue-toastification'
|
||||
import 'vue-toastification/dist/index.css'
|
||||
import vuetify from './plugins/vuetify'
|
||||
Vue.use(Toast)
|
||||
Vue.use(Toast, {
|
||||
maxToasts: 5,
|
||||
timeout: 2000
|
||||
})
|
||||
|
||||
import PerfectScrollbar from 'vue2-perfect-scrollbar'
|
||||
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
|
||||
|
||||
Vue.use(PerfectScrollbar)
|
||||
|
||||
import AsyncComputed from 'vue-async-computed'
|
||||
|
||||
Vue.use(AsyncComputed)
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// register modals
|
||||
const files = require.context('@/components/Modals', true, /\.vue$/i)
|
||||
files.keys().map(key =>
|
||||
Vue.component(
|
||||
key
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('.')[0],
|
||||
files(key).default
|
||||
files
|
||||
.keys()
|
||||
.map(key =>
|
||||
Vue.component(key.split('/').pop().split('.')[0], files(key).default)
|
||||
)
|
||||
)
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters(['getModalState']),
|
||||
dialog: {
|
||||
get() {
|
||||
return this.getModalState(this.$options.name)
|
||||
},
|
||||
set() {
|
||||
this.$store.commit('TOGGLE_MODAL', this.$options.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters(['getModalState']),
|
||||
dialog: {
|
||||
get() {
|
||||
return this.getModalState(this.$options.name)
|
||||
},
|
||||
set() {
|
||||
this.$store.commit('TOGGLE_MODAL', this.$options.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
export default class Stat {
|
||||
constructor(data) {
|
||||
if (data != undefined && data != null) {
|
||||
this.status = data.connection_status
|
||||
this.downloaded = this.formatBytes(data.dl_info_data, 1)
|
||||
this.uploaded = this.formatBytes(data.up_info_data, 1)
|
||||
this.dlspeed = this.formatBytes(data.dl_info_speed, 1)
|
||||
this.upspeed = this.formatBytes(data.up_info_speed, 1)
|
||||
}
|
||||
}
|
||||
|
||||
formatBytes(a, b) {
|
||||
if (a == 0) return '0 Bytes'
|
||||
const c = 1024
|
||||
const d = b || 2
|
||||
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const f = Math.floor(Math.log(a) / Math.log(c))
|
||||
return `${parseFloat((a / Math.pow(c, f)).toFixed(d))} ${e[f]}`
|
||||
}
|
||||
}
|
||||
export default class Status {
|
||||
constructor(data, tags) {
|
||||
if (data != undefined && data != null) {
|
||||
this.status = data.connection_status
|
||||
this.downloaded = this.formatBytes(data.dl_info_data, 1)
|
||||
this.uploaded = this.formatBytes(data.up_info_data, 1)
|
||||
this.dlspeed = this.formatBytes(data.dl_info_speed, 1)
|
||||
this.upspeed = this.formatBytes(data.up_info_speed, 1)
|
||||
this.freeDiskSpace = this.formatBytes(data.free_space_on_disk)
|
||||
this.altSpeed = data.use_alt_speed_limits
|
||||
this.dlspeedRaw = Math.round(data.dl_info_speed / 1000)
|
||||
this.upspeedRaw = Math.round(data.up_info_speed / 1000)
|
||||
this.tags = tags
|
||||
}
|
||||
}
|
||||
|
||||
formatBytes(a, b) {
|
||||
if (a == 0) return '0 Bytes'
|
||||
const c = 1024
|
||||
const d = b || 2
|
||||
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const f = Math.floor(Math.log(a) / Math.log(c))
|
||||
return `${parseFloat((a / Math.pow(c, f)).toFixed(d))} ${e[f]}`
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
export default class Torrent {
|
||||
constructor(data) {
|
||||
this.id = data.id
|
||||
this.name = data.name
|
||||
this.size = this.formatBytes(data.size)
|
||||
this.birth = new Date(data.added_on * 1000).toLocaleString()
|
||||
|
@ -20,6 +19,10 @@ export default class Torrent {
|
|||
// available seeds
|
||||
this.available_seeds = data.num_complete
|
||||
this.available_peers = data.num_incomplete
|
||||
this.savePath = data.save_path
|
||||
this.progress = Math.round((data.downloaded / data.size) * 100)
|
||||
this.ratio = Math.round(data.ratio * 100)
|
||||
this.tags = data.tags.length > 0 ? data.tags.split(',') : null
|
||||
}
|
||||
|
||||
formatState(state) {
|
||||
|
|
|
@ -1,51 +1,50 @@
|
|||
import Vue from "vue";
|
||||
import Vuetify from "vuetify";
|
||||
import "vuetify/dist/vuetify.min.css";
|
||||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify'
|
||||
import 'vuetify/dist/vuetify.min.css'
|
||||
|
||||
import colors from "vuetify/lib/util/colors";
|
||||
import colors from 'vuetify/lib/util/colors'
|
||||
|
||||
Vue.use(Vuetify);
|
||||
Vue.use(Vuetify)
|
||||
|
||||
export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: "fa",
|
||||
},
|
||||
theme: {
|
||||
options: {
|
||||
customProperties: true,
|
||||
},
|
||||
dark: false,
|
||||
themes: {
|
||||
light: {
|
||||
primary: "#35495e",
|
||||
secondary: "#3e556d",
|
||||
secondary_lighter: "#56718c",
|
||||
blue_accent: "#3cd1c2",
|
||||
info: "#ffaa2c",
|
||||
error: "#f83e70",
|
||||
green_accent: "#3cd1c2",
|
||||
download: "#64CEAA",
|
||||
upload: "#00b3fa",
|
||||
torrent: '#fff',
|
||||
torrent_selected: colors.grey.lighten2,
|
||||
background: colors.grey.lighten4,
|
||||
search: colors.grey.darken1
|
||||
},
|
||||
dark: {
|
||||
primary: "#35495e",
|
||||
secondary: "#3e556d",
|
||||
secondary_lighter: "#56718c",
|
||||
blue_accent: "#3cd1c2",
|
||||
info: "#ffaa2c",
|
||||
error: "#f83e70",
|
||||
green_accent: "#3cd1c2",
|
||||
download: "#64CEAA",
|
||||
upload: "#00b3fa",
|
||||
torrent: colors.grey.darken3,
|
||||
torrent_selected: colors.grey,
|
||||
background: colors.grey.darken4,
|
||||
search: colors.grey.darken3
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
theme: {
|
||||
options: {
|
||||
customProperties: true
|
||||
},
|
||||
dark: false,
|
||||
themes: {
|
||||
light: {
|
||||
primary: '#35495e',
|
||||
secondary: '#3e556d',
|
||||
secondary_lighter: '#56718c',
|
||||
blue_accent: '#3cd1c2',
|
||||
info: '#ffaa2c',
|
||||
error: '#f83e70',
|
||||
green_accent: '#3cd1c2',
|
||||
download: '#64CEAA',
|
||||
upload: '#00b3fa',
|
||||
torrent: '#fff',
|
||||
torrent_selected: colors.grey.lighten2,
|
||||
background: colors.grey.lighten4,
|
||||
search: colors.grey.darken1,
|
||||
torrentmodaltext: colors.grey.darken4
|
||||
},
|
||||
dark: {
|
||||
primary: '#35495e',
|
||||
secondary: '#3e556d',
|
||||
secondary_lighter: '#56718c',
|
||||
blue_accent: '#3cd1c2',
|
||||
info: '#ffaa2c',
|
||||
error: '#f83e70',
|
||||
green_accent: '#3cd1c2',
|
||||
download: '#64CEAA',
|
||||
upload: '#00b3fa',
|
||||
torrent: colors.grey.darken3,
|
||||
torrent_selected: colors.grey,
|
||||
background: colors.grey.darken4,
|
||||
search: colors.grey.darken3,
|
||||
torrentmodaltext: colors.grey.lighten4
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Dashboard from './views/Dashboard.vue'
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import Login from '@/views/Login.vue'
|
||||
import { isAuthenticated } from '@/services/auth.js'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
export default new Router({
|
||||
const router = new Router({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes: [
|
||||
|
@ -12,6 +14,39 @@ export default new Router({
|
|||
path: '/',
|
||||
name: 'dashboard',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
meta: {
|
||||
public: true, // Allow access to even if not logged in
|
||||
onlyWhenLoggedOut: true
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const isPublic = to.matched.some(record => record.meta.public)
|
||||
const onlyWhenLoggedOut = to.matched.some(
|
||||
record => record.meta.onlyWhenLoggedOut
|
||||
)
|
||||
const authenticated = await isAuthenticated()
|
||||
|
||||
if (!isPublic && !authenticated) {
|
||||
return next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath } // Store the full path to redirect the user to after login
|
||||
})
|
||||
}
|
||||
|
||||
// Do not allow user to visit login page or register page if they are logged in
|
||||
if (authenticated && onlyWhenLoggedOut) {
|
||||
return next('/')
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
6
src/services/auth.js
Normal file
6
src/services/auth.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import qbit from '@/services/qbit'
|
||||
|
||||
export async function isAuthenticated() {
|
||||
const res = await qbit.login()
|
||||
return res === 'Ok.'
|
||||
}
|
|
@ -28,6 +28,10 @@ class Qbit {
|
|||
return data
|
||||
}
|
||||
|
||||
async logout() {
|
||||
this.axios.post('/auth/logout')
|
||||
}
|
||||
|
||||
getGlobalTransferInfo() {
|
||||
return this.axios.get('/transfer/info')
|
||||
}
|
||||
|
@ -49,7 +53,7 @@ class Qbit {
|
|||
let data
|
||||
if (torrents) {
|
||||
const formData = new FormData()
|
||||
if(params){
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
formData.append(key, value)
|
||||
}
|
||||
|
@ -108,6 +112,24 @@ class Qbit {
|
|||
return this.axios.post('/transfer/toggleSpeedLimitsMode')
|
||||
}
|
||||
|
||||
getTorrents(payload) {
|
||||
let params = {
|
||||
sort: payload.sort,
|
||||
reverse: payload.reverse,
|
||||
hashes: payload.hashes ? payload.hashes.join('|') : null,
|
||||
filter: payload.filter ? payload.filter : null
|
||||
}
|
||||
|
||||
//clean
|
||||
Object.keys(params).forEach(
|
||||
key => params[key] == null && delete params[key]
|
||||
)
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
|
||||
return this.axios.get(`/torrents/info?${data.toString()}`)
|
||||
}
|
||||
|
||||
deleteTorrents(hashes, deleteFiles) {
|
||||
return this.actionTorrents('delete', hashes, { deleteFiles })
|
||||
}
|
||||
|
@ -132,7 +154,7 @@ class Qbit {
|
|||
return this.actionTorrents('setCategory', hashes, { category })
|
||||
}
|
||||
|
||||
getTorrentTracker(hash) {
|
||||
getTorrentTrackers(hash) {
|
||||
const params = {
|
||||
hash
|
||||
}
|
||||
|
@ -191,75 +213,44 @@ class Qbit {
|
|||
})
|
||||
}
|
||||
|
||||
getRssItems() {
|
||||
const params = {
|
||||
withData: true
|
||||
}
|
||||
|
||||
return this.axios.get('/rss/items', {
|
||||
params
|
||||
})
|
||||
getAvailableTags() {
|
||||
return this.axios.get('/torrents/tags')
|
||||
}
|
||||
|
||||
addRssFeed(url, path = '') {
|
||||
removeTorrentTag(hash, tag) {
|
||||
const params = {
|
||||
url,
|
||||
path
|
||||
hashes: hash,
|
||||
tags: tag
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/addFeed', data)
|
||||
return this.axios.post('/torrents/removeTags', data)
|
||||
}
|
||||
|
||||
removeRssFeed(path) {
|
||||
addTorrentTag(hash, tag) {
|
||||
const params = {
|
||||
path
|
||||
hashes: hash,
|
||||
tags: tag
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/removeItem', data)
|
||||
return this.axios.post('/torrents/addTags ', data)
|
||||
}
|
||||
|
||||
refreshRssFeed(path) {
|
||||
createTag(tag) {
|
||||
const params = {
|
||||
itemPath: path
|
||||
tags: tag
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/refreshItem', data)
|
||||
return this.axios.post('/torrents/createTags ', data)
|
||||
}
|
||||
|
||||
moveRssFeed(path, newPath) {
|
||||
deleteTag(tag) {
|
||||
const params = {
|
||||
itemPath: path,
|
||||
destPath: newPath
|
||||
tags: tag
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/moveItem', data)
|
||||
}
|
||||
|
||||
getRssRules() {
|
||||
return this.axios.get('/rss/rules')
|
||||
}
|
||||
|
||||
setRssRule(name, def) {
|
||||
const params = {
|
||||
ruleName: name,
|
||||
ruleDef: JSON.stringify(def)
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/setRule', data)
|
||||
}
|
||||
|
||||
removeRssRule(name) {
|
||||
const params = {
|
||||
ruleName: name
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/removeRule', data)
|
||||
return this.axios.post('/torrents/deleteTags ', data)
|
||||
}
|
||||
|
||||
actionTorrents(action, hashes, extra) {
|
||||
|
|
23
src/store/actions.js
Normal file
23
src/store/actions.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import Vue from 'vue'
|
||||
import qbit from '../services/qbit'
|
||||
|
||||
export default {
|
||||
INIT_INTERVALS: async context => {
|
||||
context.state.intervals[0] = setInterval(() => {
|
||||
context.commit('updateMainData')
|
||||
}, 2000)
|
||||
},
|
||||
LOGIN: async (context, payload) => {
|
||||
const res = await qbit.login(payload)
|
||||
console.log(res)
|
||||
if (res === 'Ok.') {
|
||||
Vue.$toast.success('Successfully logged in!')
|
||||
context.commit('LOGIN', true)
|
||||
context.commit('updateMainData')
|
||||
context.commit('SET_SETTINGS')
|
||||
return true
|
||||
}
|
||||
Vue.$toast.error('Log in failed 😕')
|
||||
return false
|
||||
}
|
||||
}
|
11
src/store/getters.js
Normal file
11
src/store/getters.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default {
|
||||
containsTorrent: state => hash => state.selected_torrents.includes(hash),
|
||||
getTheme: state => () => state.webuiSettings.darkTheme,
|
||||
getModalState: state => name => state.modals[name.toLowerCase()],
|
||||
getSettings: state => () => state.settings,
|
||||
getStatus: state => () => state.status,
|
||||
getTorrent: state => hash =>
|
||||
state.torrents.filter(el => el.hash === hash)[0],
|
||||
getWebuiSettings: state => () => state.webuiSettings,
|
||||
getAvailableTags: state => () => state.status.tags
|
||||
}
|
|
@ -2,10 +2,6 @@ import Vue from 'vue'
|
|||
import Vuex from 'vuex'
|
||||
import VuexPersist from 'vuex-persist'
|
||||
|
||||
import Torrent from '../models/torrent'
|
||||
import Stat from '../models/sessionStat'
|
||||
import qbit from '../services/qbit'
|
||||
|
||||
const vuexPersist = new VuexPersist({
|
||||
key: 'vuetorrent',
|
||||
storage: window.localStorage
|
||||
|
@ -13,130 +9,49 @@ const vuexPersist = new VuexPersist({
|
|||
|
||||
Vue.use(Vuex)
|
||||
|
||||
import getters from './getters'
|
||||
import mutations from './mutations'
|
||||
import actions from './actions'
|
||||
|
||||
export default new Vuex.Store({
|
||||
plugins: [vuexPersist.plugin],
|
||||
state: {
|
||||
darkTheme: false,
|
||||
intervals: [],
|
||||
stats: null,
|
||||
status: null,
|
||||
upload_data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
download_data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
torrents: [],
|
||||
selected_torrents: [],
|
||||
authenticated: false,
|
||||
loading: false,
|
||||
sort_options: { sort: 'name', reverse: false },
|
||||
sort_options: {
|
||||
sort: 'name',
|
||||
reverse: false,
|
||||
hashes: [],
|
||||
filter: null
|
||||
},
|
||||
rid: 0,
|
||||
mainData: undefined,
|
||||
preferences: null,
|
||||
pasteUrl: null,
|
||||
modals: {
|
||||
addmodal: false,
|
||||
deletemodal: false,
|
||||
settingsmodal: false
|
||||
}
|
||||
settingsmodal: false,
|
||||
torrentdetailmodal: false
|
||||
},
|
||||
settings: {},
|
||||
webuiSettings: {
|
||||
darkTheme: false,
|
||||
showFreeSpace: true
|
||||
},
|
||||
selectedDetailTorrent: null
|
||||
},
|
||||
getters: {
|
||||
containsTorrent: state => hash =>
|
||||
state.selected_torrents.includes(hash),
|
||||
getTheme: state => () => state.darkTheme,
|
||||
getModalState: state => name => state.modals[name.toLowerCase()]
|
||||
...getters
|
||||
},
|
||||
|
||||
mutations: {
|
||||
REMOVE_INTERVALS: state => {
|
||||
state.intervals.forEach(el => clearInterval(el))
|
||||
},
|
||||
TOGGLE_MODAL(state, modal) {
|
||||
state.modals[modal.toLowerCase()] = !state.modals[
|
||||
modal.toLowerCase()
|
||||
]
|
||||
},
|
||||
SET_SELECTED: (state, payload) => {
|
||||
if(payload.type === 'add')
|
||||
state.selected_torrents.push(payload.hash)
|
||||
if(payload.type === 'remove')
|
||||
state.selected_torrents.splice(
|
||||
state.selected_torrents.indexOf(payload.hash),
|
||||
1
|
||||
)
|
||||
|
||||
},
|
||||
RESET_SELECTED: state => {
|
||||
state.selected_torrents = []
|
||||
},
|
||||
TOGGLE_THEME(state) {
|
||||
state.darkTheme = !state.darkTheme
|
||||
},
|
||||
REMOVE_TORRENTS: async state => {
|
||||
if (state.selected_torrents.length !== 0) {
|
||||
qbit.remove_torrents(state.selected_torrents)
|
||||
}
|
||||
},
|
||||
LOGIN: async (state, payload) => {
|
||||
const res = await qbit.login(payload)
|
||||
console.log(res)
|
||||
if (res === 'Ok.') {
|
||||
Vue.$toast.success('Successfully logged in!')
|
||||
state.authenticated = true
|
||||
}
|
||||
state.loading = false
|
||||
},
|
||||
updateMainData: async state => {
|
||||
const rid = state.rid ? state.rid : undefined
|
||||
const { data } = await qbit.getMainData(rid)
|
||||
|
||||
// torrents
|
||||
state.torrents = []
|
||||
for (const [key, value] of Object.entries(data.torrents)) {
|
||||
state.torrents.push(new Torrent({ hash: key, ...value }))
|
||||
}
|
||||
|
||||
// stats
|
||||
state.stats = new Stat(data.server_state)
|
||||
|
||||
// graph
|
||||
|
||||
state.download_data.splice(0, 1)
|
||||
if (state.stats.dlspeed.indexOf('KB' > -1)) {
|
||||
state.download_data.push(
|
||||
state.stats.dlspeed.substring(
|
||||
0,
|
||||
state.stats.dlspeed.indexOf(' ')
|
||||
) / 1000
|
||||
)
|
||||
} else {
|
||||
state.download_data.push(
|
||||
state.stats.dlspeed(0, state.stats.dlspeed.indexOf(' '))
|
||||
)
|
||||
}
|
||||
state.upload_data.splice(0, 1)
|
||||
if (state.stats.upspeed.indexOf('KB' > -1)) {
|
||||
state.upload_data.push(
|
||||
state.stats.upspeed.substring(
|
||||
0,
|
||||
state.stats.upspeed.indexOf(' ')
|
||||
) / 1000
|
||||
)
|
||||
} else {
|
||||
state.upload_data.push(
|
||||
state.stats.upspeed.substring(
|
||||
0,
|
||||
state.stats.upspeed.indexOf(' ')
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
...mutations
|
||||
},
|
||||
actions: {
|
||||
INIT_INTERVALS: async context => {
|
||||
context.state.intervals[0] = setInterval(() => {
|
||||
context.commit('updateMainData')
|
||||
}, 2000)
|
||||
},
|
||||
LOGIN: async (context, payload) => {
|
||||
context.commit('LOGIN', payload)
|
||||
context.commit('updateMainData')
|
||||
}
|
||||
...actions
|
||||
}
|
||||
})
|
||||
|
|
66
src/store/mutations.js
Normal file
66
src/store/mutations.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import Torrent from '../models/torrent'
|
||||
import Status from '../models/Status'
|
||||
import qbit from '../services/qbit'
|
||||
|
||||
export default {
|
||||
REMOVE_INTERVALS: state => {
|
||||
state.intervals.forEach(el => clearInterval(el))
|
||||
},
|
||||
TOGGLE_MODAL(state, modal) {
|
||||
state.modals[modal.toLowerCase()] = !state.modals[modal.toLowerCase()]
|
||||
},
|
||||
SET_SELECTED: (state, payload) => {
|
||||
if (payload.type === 'add') state.selected_torrents.push(payload.hash)
|
||||
if (payload.type === 'remove')
|
||||
state.selected_torrents.splice(
|
||||
state.selected_torrents.indexOf(payload.hash),
|
||||
1
|
||||
)
|
||||
},
|
||||
RESET_SELECTED: state => {
|
||||
state.selected_torrents = []
|
||||
},
|
||||
TOGGLE_THEME(state) {
|
||||
state.webuiSettings.darkTheme = !state.webuiSettings.darkTheme
|
||||
},
|
||||
LOGOUT: state => {
|
||||
qbit.logout()
|
||||
state.authenticated = false
|
||||
},
|
||||
LOGIN: async (state, payload) => {
|
||||
state.authenticated = payload
|
||||
},
|
||||
updateMainData: async state => {
|
||||
const rid = state.rid ? state.rid : undefined
|
||||
const res = await qbit.getMainData(rid)
|
||||
|
||||
// status
|
||||
state.status = new Status(res.data.server_state, res.data.tags)
|
||||
|
||||
// graph
|
||||
state.download_data.splice(0, 1)
|
||||
state.download_data.push(state.status.dlspeedRaw)
|
||||
state.upload_data.splice(0, 1)
|
||||
state.upload_data.push(state.status.upspeedRaw)
|
||||
|
||||
const { data } = await qbit.getTorrents(state.sort_options)
|
||||
// torrents
|
||||
state.torrents = []
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
state.torrents.push(new Torrent({ hash: key, ...value }))
|
||||
}
|
||||
},
|
||||
SET_SETTINGS: async state => {
|
||||
const { data } = await qbit.getAppPreferences()
|
||||
state.settings.savePath = data.save_path
|
||||
},
|
||||
SET_SELECTED_TORRENT_DETAIL: (state, hash) => {
|
||||
state.selectedDetailTorrent = hash
|
||||
},
|
||||
UPDATE_SORT_OPTIONS: (state, payload) => {
|
||||
state.sort_options.sort = payload.name
|
||||
state.sort_options.reverse = payload.reverse
|
||||
state.sort_options.hashes = payload.hashes ? payload.hashes : null
|
||||
state.sort_options.filter = payload.filter ? payload.filter : null
|
||||
}
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
<template>
|
||||
<div style="height: 89vh" color="background" @click.self="resetSelected">
|
||||
<h1 style="font-size: 1.1em !important" class="subtitle-1 grey--text">Dashboard</h1>
|
||||
<v-container color="background" class="my-4" @click.self="resetSelected">
|
||||
<div class="pl-5 pr-5" color="background" @click.self="resetSelected">
|
||||
<h1 style="font-size: 1.1em !important;" class="subtitle-1 grey--text">
|
||||
Dashboard
|
||||
</h1>
|
||||
<v-container
|
||||
color="background"
|
||||
class="my-4 pt-5 pa-0"
|
||||
@click.self="resetSelected"
|
||||
>
|
||||
<!-- justify-center here in layout to center!! -->
|
||||
<v-flex xs12 sm6 md3 @click.self="resetSelected">
|
||||
<v-text-field
|
||||
|
@ -10,7 +16,7 @@
|
|||
height="50"
|
||||
clearable
|
||||
solo
|
||||
hint="eg `size desc` + enter"
|
||||
hint="eg `s size desc` + enter"
|
||||
color="search"
|
||||
v-model="sort_input"
|
||||
@keyup.enter.native="sortBy"
|
||||
|
@ -20,26 +26,130 @@
|
|||
<p class="grey--text">No active Torrents!</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
|
||||
v-for="torrent in torrents"
|
||||
:key="torrent.hash"
|
||||
>
|
||||
<Torrent :torrent="torrent"/>
|
||||
|
||||
<div v-for="torrent in torrents" :key="torrent.hash">
|
||||
<Torrent :torrent="torrent" />
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
<TorrentDetailModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations} from 'vuex'
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import Torrent from '@/components/Torrent'
|
||||
import TorrentDetailModal from '@/components/TorrentDetailModal/TorrentDetailModal'
|
||||
|
||||
function getPropName(prop) {
|
||||
switch (prop) {
|
||||
case 'title':
|
||||
case 'name':
|
||||
case 'Name':
|
||||
case 'Title':
|
||||
return 'name'
|
||||
case 'size':
|
||||
case 'Size':
|
||||
return 'size'
|
||||
case 'dlspeed':
|
||||
case 'Dlspeed':
|
||||
case 'Download':
|
||||
case 'download':
|
||||
case 'downloadspeed':
|
||||
return 'dlspeed'
|
||||
case 'upspeed':
|
||||
case 'upload':
|
||||
case 'Upload':
|
||||
case 'Upspeed':
|
||||
case 'uploadspeed':
|
||||
return 'upspeed'
|
||||
case 'leechs':
|
||||
case 'leechers':
|
||||
case 'leech':
|
||||
case 'peers':
|
||||
case 'Leechs':
|
||||
case 'Leechers':
|
||||
case 'Leech':
|
||||
case 'Peers':
|
||||
return 'num_leechs'
|
||||
case 'seeds':
|
||||
case 'seeders':
|
||||
case 'Seeds':
|
||||
case 'Seeders':
|
||||
return 'num_seeds'
|
||||
case 'remaining':
|
||||
case 'time':
|
||||
case 'Time':
|
||||
case 'ETA':
|
||||
case 'eta':
|
||||
return 'eta'
|
||||
case 'done':
|
||||
case 'downloaded':
|
||||
case 'dloaded':
|
||||
case 'Done':
|
||||
case 'Downloaded':
|
||||
case 'Dloaded':
|
||||
return 'progress'
|
||||
case 'state':
|
||||
case 'status':
|
||||
case 'State':
|
||||
case 'Status':
|
||||
return 'state'
|
||||
default:
|
||||
return 'name'
|
||||
}
|
||||
}
|
||||
|
||||
function sortOrFilter(word) {
|
||||
switch (word) {
|
||||
case 'sort':
|
||||
case 's':
|
||||
case 'srt':
|
||||
return 'sort'
|
||||
case 'f':
|
||||
case 'filter':
|
||||
case 'filtr':
|
||||
case 'fltr':
|
||||
case 'filt':
|
||||
return 'filter'
|
||||
default:
|
||||
return 'sort'
|
||||
}
|
||||
}
|
||||
|
||||
function filterOption(word) {
|
||||
switch (word) {
|
||||
case 'Done':
|
||||
case 'done':
|
||||
case 'completed':
|
||||
case 'complete':
|
||||
return 'completed'
|
||||
case 'Busy':
|
||||
case 'busy':
|
||||
case 'downl':
|
||||
case 'download':
|
||||
case 'downloading':
|
||||
case 'act':
|
||||
case 'active':
|
||||
case 'resumed':
|
||||
return 'active'
|
||||
case 'fail':
|
||||
case 'failed':
|
||||
case 'faild':
|
||||
case 'stalled':
|
||||
case 'stalld':
|
||||
case 'stall':
|
||||
return 'stalled'
|
||||
case 'pause':
|
||||
case 'paused':
|
||||
return 'paused'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name:'Dashboard',
|
||||
components: {Torrent},
|
||||
name: 'Dashboard',
|
||||
components: { Torrent, TorrentDetailModal },
|
||||
data() {
|
||||
return {
|
||||
sort_input: ''
|
||||
|
@ -51,92 +161,81 @@ export default {
|
|||
methods: {
|
||||
...mapMutations(['SORT_TORRENTS']),
|
||||
sortBy() {
|
||||
let name
|
||||
let reverse
|
||||
// search if order was presented
|
||||
const index = this.sort_input.indexOf(' ')
|
||||
if (index > -1) {
|
||||
name = this.sort_input.substring(0, index)
|
||||
const temp = this.sort_input.substring(index)
|
||||
if (temp.indexOf('asc') > -1) {
|
||||
reverse = false
|
||||
} else if (temp.indexOf('desc') > -1) {
|
||||
reverse = true
|
||||
}
|
||||
} else {
|
||||
// no order so we assume input is propname
|
||||
name = this.sort_input
|
||||
reverse = false
|
||||
let parts = this.sort_input.split(' ')
|
||||
|
||||
if (parts.length === 0) {
|
||||
let name = 'name'
|
||||
let reverse = false
|
||||
return this.$store.commit('UPDATE_SORT_OPTIONS', {
|
||||
name,
|
||||
reverse
|
||||
})
|
||||
}
|
||||
// prop names
|
||||
switch (name) {
|
||||
case 'title':
|
||||
case 'name':
|
||||
case 'Name':
|
||||
case 'Title':
|
||||
name = 'name'
|
||||
break
|
||||
case 'size':
|
||||
case 'Size':
|
||||
name = 'size'
|
||||
break
|
||||
case 'dlspeed':
|
||||
case 'Dlspeed':
|
||||
case 'Download':
|
||||
case 'download':
|
||||
case 'downloadspeed':
|
||||
name = 'dlspeed'
|
||||
break
|
||||
case 'upspeed':
|
||||
case 'upload':
|
||||
case 'Upload':
|
||||
case 'Upspeed':
|
||||
case 'uploadspeed':
|
||||
name = 'upspeed'
|
||||
break
|
||||
case 'leechs':
|
||||
case 'leechers':
|
||||
case 'leech':
|
||||
case 'peers':
|
||||
case 'Leechs':
|
||||
case 'Leechers':
|
||||
case 'Leech':
|
||||
case 'Peers':
|
||||
name = 'num_leechs'
|
||||
break
|
||||
case 'seeds':
|
||||
case 'seeders':
|
||||
case 'Seeds':
|
||||
case 'Seeders':
|
||||
name = 'num_seeds'
|
||||
break
|
||||
case 'remaining':
|
||||
case 'time':
|
||||
case 'Time':
|
||||
case 'ETA':
|
||||
case 'eta':
|
||||
name = 'eta'
|
||||
break
|
||||
case 'done':
|
||||
case 'downloaded':
|
||||
case 'dloaded':
|
||||
case 'Done':
|
||||
case 'Downloaded':
|
||||
case 'Dloaded':
|
||||
name = 'downloaded'
|
||||
break
|
||||
case 'state':
|
||||
case 'status':
|
||||
case 'State':
|
||||
case 'Status':
|
||||
name = 'state'
|
||||
break
|
||||
default:
|
||||
name = 'name'
|
||||
break
|
||||
//basic sort
|
||||
if (parts.length === 1) {
|
||||
let name = getPropName(parts[0])
|
||||
let reverse = false
|
||||
|
||||
return this.$store.commit('UPDATE_SORT_OPTIONS', {
|
||||
name,
|
||||
reverse
|
||||
})
|
||||
}
|
||||
|
||||
this.$store.state.sort_options = { name, reverse }
|
||||
// could be sort OR filter
|
||||
if (parts.length === 2) {
|
||||
let type = sortOrFilter(parts[0])
|
||||
if (type === 'sort') {
|
||||
let name = getPropName(parts[1])
|
||||
let reverse = false
|
||||
|
||||
return this.$store.commit('UPDATE_SORT_OPTIONS', {
|
||||
name,
|
||||
reverse
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'filter') {
|
||||
let ftype = filterOption(parts[1])
|
||||
//filter state
|
||||
if (ftype) {
|
||||
let name = 'name'
|
||||
let reverse = false
|
||||
return this.$store.commit('UPDATE_SORT_OPTIONS', {
|
||||
name,
|
||||
reverse,
|
||||
filter: ftype
|
||||
})
|
||||
}
|
||||
|
||||
//filter name
|
||||
let filtered = this.torrents.filter(t =>
|
||||
t.name.toLowerCase().includes(parts[1].toLowerCase())
|
||||
)
|
||||
let name = 'name'
|
||||
let reverse = false
|
||||
let hashes = filtered.map(t => t.hash)
|
||||
return this.$store.commit('UPDATE_SORT_OPTIONS', {
|
||||
name,
|
||||
reverse,
|
||||
hashes
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//sort with asc/desc
|
||||
if (parts.length === 3) {
|
||||
let type = sortOrFilter(parts[0])
|
||||
if (type === 'sort') {
|
||||
let name = getPropName(parts[1])
|
||||
let reverse = parts[2] === 'desc'
|
||||
|
||||
return this.$store.commit('UPDATE_SORT_OPTIONS', {
|
||||
name,
|
||||
reverse
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
resetSelected() {
|
||||
this.$store.commit('RESET_SELECTED')
|
||||
|
@ -150,5 +249,3 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
|
79
src/views/Login.vue
Normal file
79
src/views/Login.vue
Normal file
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<v-layout row wrap align-center class="justify-center">
|
||||
<div style="margin: 100px auto;">
|
||||
<v-container class="grey lighten-4 pa-0">
|
||||
<v-card max-width="400" flat>
|
||||
<v-container :class="`pa-3 project done`">
|
||||
<v-card-title class="justify-center">
|
||||
<h2>Login</h2>
|
||||
</v-card-title>
|
||||
<div class="mr-5 ml-5"></div>
|
||||
<v-card-text>
|
||||
<v-form class="px-3" ref="form">
|
||||
<v-text-field
|
||||
flat
|
||||
solo
|
||||
background-color="torrent"
|
||||
label="username"
|
||||
prepend-icon="person"
|
||||
v-model="username"
|
||||
:rules="inputRules"
|
||||
@keyup.enter.native="Login"
|
||||
autocomplete="current email"
|
||||
name="username"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
flat
|
||||
solo
|
||||
background-color="torrent"
|
||||
type="password"
|
||||
label="password"
|
||||
prepend-icon="lock"
|
||||
v-model="password"
|
||||
:rules="inputRules"
|
||||
@keyup.enter.native="Login"
|
||||
autocomplete="current password"
|
||||
name="password"
|
||||
></v-text-field>
|
||||
<v-spacer></v-spacer>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
text
|
||||
@click="Login"
|
||||
class="blue_accent white--text mx-0 mt-3"
|
||||
>Login</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
inputRules: [v => v.length >= 1 || 'At least 1 character']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async Login() {
|
||||
const authenticated = await this.$store.dispatch('LOGIN', {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
})
|
||||
|
||||
if (authenticated) {
|
||||
this.$router.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = {
|
||||
outputDir: 'vuetorrent/public',
|
||||
publicPath: './',
|
||||
|
||||
transpileDependencies: ['vuetify'],
|
||||
devServer: {
|
||||
port: 8000,
|
||||
proxy: {
|
||||
|
|
Loading…
Add table
Reference in a new issue