feat: Add optional backend handling (#1547)

This commit is contained in:
Rémi Marseault 2024-04-15 12:25:00 +02:00 committed by GitHub
parent 4cbb45679c
commit d513e097bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 241 additions and 135 deletions

View file

@ -2,8 +2,7 @@
The sleekest looking WebUI for qBittorrent made with Vue.js!
[![Discord](https://img.shields.io/badge/Discord_server-join-blue?logo=discord&logoColor=white)](https://discord.gg/KDQP7fR467)
[![Wiki](https://img.shields.io/badge/Wiki-Click_me-blue)](https://github.com/VueTorrent/VueTorrent/wiki)
[![Discord](https://img.shields.io/discord/1170618192956243998?logo=discord)](https://discord.gg/KDQP7fR467)
![VueTorrent](https://cdn.jsdelivr.net/gh/VueTorrent/VueTorrent@master/VueTorrent-logo.png)
@ -82,6 +81,9 @@ Checkout the [wiki](https://github.com/VueTorrent/VueTorrent/wiki/Installation)!
- mobile friendly! (can be installed as a PWA)
- Configureable Dashboard: choose which torrent properties are shown for both busy and completed torrents
- Optimized for the latest version of qBittorrent
- Additional backend for improved experience, [see the repo for more info](https://github.com/VueTorrent/vuetorrent-backend)
- This is a work in progress, and is not required to use VueTorrent
- Stores server-side settings
## Important Information
@ -91,10 +93,18 @@ Everything that is compatible with qBittorrent will work regardless of the WebUI
## Contributing
Refer to the [Contributing Guidelines](https://github.com/VueTorrent/VueTorrent/blob/master/.github/CONTRIBUTING.md)
We gladly accept contributions!
Any help is appreciated, whether it's reporting bugs, suggesting enhancements, contributing code or localizing the app.
See the [Contributing Guidelines](https://github.com/VueTorrent/VueTorrent/blob/master/.github/CONTRIBUTING.md) for more information.
## Support
[![Discord](https://img.shields.io/discord/1170618192956243998?logo=discord)](https://discord.gg/KDQP7fR467)
[![Wiki](https://img.shields.io/badge/Wiki-blue)](https://github.com/VueTorrent/VueTorrent/wiki)
[![FAQ](https://img.shields.io/badge/FAQ-orange)](https://github.com/VueTorrent/VueTorrent/wiki/FAQ)
Open up an issue 😛
but before you do that:
@ -102,7 +112,11 @@ but before you do that:
- confirm you're on the latest version of VueTorrent
- confirm there is no other issue mentioning the same problem
<a href="https://www.buymeacoffee.com/wdaan"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=wdaan&button_colour=FFDD00&font_colour=000000&font_family=Arial&outline_colour=000000&coffee_colour=ffffff"></a>
## Funding
All donations are appreciated but purely optional.
Checkout the sponsor section of the repository.
## Contributors

View file

@ -16,3 +16,12 @@ services:
ports:
- '8080:8080'
restart: unless-stopped
backend:
image: ghcr.io/vuetorrent/vuetorrent-backend:latest
container_name: vuetorrent-backend
restart: unless-stopped
ports:
- '8081:3000'
volumes:
- ./docker/backend:/app/data

47
package-lock.json generated
View file

@ -21,7 +21,7 @@
"dayjs": "^1.11.9",
"lodash.debounce": "^4.0.8",
"pinia": "^2.1.6",
"pinia-plugin-persist": "^1.0.0",
"pinia-persistence-plugin": "^0.0.5",
"pixi.js": "^8.1.0",
"uuid": "^9.0.1",
"vite-plugin-vuetify": "^2.0.3",
@ -4058,47 +4058,12 @@
}
}
},
"node_modules/pinia-plugin-persist": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pinia-plugin-persist/-/pinia-plugin-persist-1.0.0.tgz",
"integrity": "sha512-M4hBBd8fz/GgNmUPaaUsC29y1M09lqbXrMAHcusVoU8xlQi1TqgkWnnhvMikZwr7Le/hVyMx8KUcumGGrR6GVw==",
"dependencies": {
"vue-demi": "^0.12.1"
},
"node_modules/pinia-persistence-plugin": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/pinia-persistence-plugin/-/pinia-persistence-plugin-0.0.5.tgz",
"integrity": "sha512-P+IRzlfn9WO17/Lb487bC4dwd4uUM9rxXyzVLbDDqGxfuUY5LK9pqX0FigFxLQZLViZliHPXBbnZhiBTyJqsPg==",
"peerDependencies": {
"@vue/composition-api": "^1.0.0",
"pinia": "^2.0.0",
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/pinia-plugin-persist/node_modules/vue-demi": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.5.tgz",
"integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
"pinia": "^2.0.0"
}
},
"node_modules/pinia/node_modules/vue-demi": {

View file

@ -28,7 +28,7 @@
"dayjs": "^1.11.9",
"lodash.debounce": "^4.0.8",
"pinia": "^2.1.6",
"pinia-plugin-persist": "^1.0.0",
"pinia-persistence-plugin": "^0.0.5",
"pixi.js": "^8.1.0",
"uuid": "^9.0.1",
"vite-plugin-vuetify": "^2.0.3",

View file

@ -4,9 +4,9 @@ import DnDZone from '@/components/DnDZone.vue'
import Navbar from '@/components/Navbar/Navbar.vue'
import { TitleOptions } from '@/constants/vuetorrent'
import { formatPercent, formatSpeed } from '@/helpers'
import { backend } from '@/services/backend'
import { useAddTorrentStore, useAppStore, useAuthStore, useDialogStore, useLogStore, useMaindataStore, usePreferenceStore, useTorrentStore, useVueTorrentStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { onBeforeMount, watch, watchEffect } from 'vue'
const addTorrentStore = useAddTorrentStore()
@ -42,6 +42,8 @@ const blockContextMenu = () => {
}
onBeforeMount(() => {
backend.init(vuetorrentStore.backendUrl)
backend.ping()
vuetorrentStore.updateTheme()
vuetorrentStore.setLanguage(language.value)
checkAuthentication()

View file

@ -99,6 +99,10 @@ const checkNewVersion = async () => {
toast.info(t('toast.new_version'))
}
function openBackendHelp() {
window.open('https://github.com/VueTorrent/vuetorrent-backend/wiki/Installation', '_blank', 'noreferrer')
}
onBeforeMount(() => {
appStore.fetchQbitVersion()
})
@ -166,7 +170,7 @@ onBeforeMount(() => {
</v-row>
</v-list-item>
<v-list-item>
<v-list-item class="mt-3">
<v-row>
<v-col cols="12" md="4">
<v-text-field v-model.number="vueTorrentStore.refreshInterval" type="number" hide-details suffix="ms" :label="t('settings.vuetorrent.general.refreshInterval')" />
@ -177,9 +181,7 @@ onBeforeMount(() => {
<v-col cols="12" md="4">
<v-text-field v-model.number="historyStore.historySize" type="number" hide-details :label="t('settings.vuetorrent.general.historySize')" />
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-select v-model="vueTorrentStore.language" flat hide-details :items="LOCALES" :label="t('settings.vuetorrent.general.language')" />
</v-col>
@ -188,13 +190,12 @@ onBeforeMount(() => {
v-model="paginationSize"
:messages="paginationSizeMessages"
flat
hide-details
:items="paginationSizes"
:return-object="false"
:label="t('settings.vuetorrent.general.paginationSize.label')" />
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="4">
<v-select v-model="vueTorrentStore.uiTitleType" flat hide-details :items="titleOptionsList" :label="t('settings.vuetorrent.general.vueTorrentTitle')" />
</v-col>
@ -211,6 +212,14 @@ onBeforeMount(() => {
<v-col cols="12" md="6">
<v-text-field v-model="vueTorrentStore.dateFormat" :placeholder="defaultDateFormat" hint="using Dayjs" :label="t('settings.vuetorrent.general.dateFormat')" />
</v-col>
<v-col cols="12" md="6">
<v-text-field v-model="vueTorrentStore.backendUrl"
:label="t('settings.vuetorrent.general.backendUrl')"
placeholder="https://YOUR-HOST:PORT/"
append-inner-icon="mdi-help-circle"
@click:appendInner="openBackendHelp" />
</v-col>
</v-row>
</v-list-item>

View file

@ -917,6 +917,7 @@
"title": "Settings",
"vuetorrent": {
"general": {
"backendUrl": "Backend URL",
"canvasRefreshThreshold": "Piece count to disable canvas auto-refresh",
"canvasRenderThreshold": "Piece count to disable canvas rendering",
"check_new": "Check for new version",

View file

@ -14,7 +14,8 @@ import VTorrentCardList from '@/components/Settings/VueTorrent/TorrentCard/List.
import VTorrentCardGrid from '@/components/Settings/VueTorrent/TorrentCard/Grid.vue'
import VTorrentCardTable from '@/components/Settings/VueTorrent/TorrentCard/Table.vue'
import WebUI from '@/components/Settings/WebUI.vue'
import { useDialogStore, usePreferenceStore } from '@/stores'
import { backend } from '@/services/backend'
import { useDialogStore, usePreferenceStore, useVueTorrentStore } from '@/stores'
import { onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@ -24,6 +25,7 @@ const router = useRouter()
const { t } = useI18n()
const dialogStore = useDialogStore()
const preferenceStore = usePreferenceStore()
const vuetorrentStore = useVueTorrentStore()
const tabs = [
{ text: t('settings.tabs.vuetorrent.title'), value: 'vuetorrent' },
@ -60,6 +62,10 @@ const saveSettings = async () => {
toast.success(t('settings.saveSuccess'))
await preferenceStore.fetchPreferences()
const oldInit = backend.isInitialized
backend.init(vuetorrentStore.backendUrl)
const newInit = backend.isInitialized
if (!preferenceStore.preferences!.alternative_webui_enabled) {
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations()
@ -70,7 +76,11 @@ const saveSettings = async () => {
location.hash = ''
location.reload()
} else {
goHome()
if (!oldInit && newInit) {
location.reload()
} else {
await backend.ping()
}
}
}

View file

@ -1,7 +1,15 @@
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
import { persistencePlugin } from 'pinia-persistence-plugin'
const pinia = createPinia()
pinia.use(piniaPersist)
pinia.use(
persistencePlugin({
assertStorage: () => {},
storeKeysPrefix: 'vuetorrent',
persistenceDefault: false,
ensureAsyncStorageUpdateOrder: true,
debug: import.meta.env.DEV
})
)
export default pinia

139
src/services/backend.ts Normal file
View file

@ -0,0 +1,139 @@
import i18n from '@/plugins/i18n'
import { StorageLikeAsync } from '@vueuse/core'
import axios, { AxiosInstance } from 'axios'
import { toast } from 'vue3-toastify'
class BackendProvider {
private axios: AxiosInstance
private _isInitialized: boolean = false
private up: boolean = true
private pingPromise: Promise<boolean> | null = null
constructor() {
this.axios = axios.create({
withCredentials: true,
headers: {
put: { 'Content-Type': 'application/json' }
}
})
}
get isInitialized() {
return this._isInitialized
}
init(baseURL: string) {
this.axios.defaults.baseURL = baseURL
this._isInitialized = !!baseURL
}
/**
* Ping the backend to check if it's up
* @returns true if backend is up, false otherwise
*/
async ping(): Promise<boolean> {
if (!this._isInitialized) return false
if (this.pingPromise) {
return this.pingPromise
}
this.pingPromise = this.axios
.get('/ping')
.then(
res => res.data === 'pong',
() => false
)
.then(ok => {
this.up = ok
this.pingPromise = null
if (!ok) {
// @ts-expect-error: TS2589: Type instantiation is excessively deep and possibly infinite.
toast.error(i18n.global.t('toast.backend_unreachable'), { delay: 1000, autoClose: 2500 })
}
return ok
})
return await this.pingPromise
}
private async waitForPing() {
if (this.pingPromise) {
await this.pingPromise
}
}
shouldDiscardCalls() {
return !this._isInitialized || !this.up
}
/**
* Get all values
*/
async getAll(): Promise<Record<string, string>> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return {}
return this.axios.get('/config').then(res => res.data)
}
/**
* Get a single value
* @param key
* @returns string or null if key doesn't exists
*/
async get(key: string): Promise<string | null> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return null
return this.axios.get(`/config/${key}`).then(
res => res.data[key],
() => null
)
}
/**
* Set a value
* @param key
* @param value
* @returns true if value was set, false otherwise
*/
async set(key: string, value: string): Promise<boolean> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return false
return this.axios.put(`/config/${key}`, { value }).then(
() => true,
() => false
)
}
/**
* Delete a value
* @param key
* @returns true if value was deleted, false otherwise
*/
async del(key: string): Promise<boolean> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return false
return this.axios.delete(`/config/${key}`).then(
() => true,
() => false
)
}
}
export const backend = new BackendProvider()
export const backendStorage: StorageLikeAsync = {
async getItem(key) {
return await backend.get(key)
},
async setItem(key, state) {
await backend.set(key, state)
},
async removeItem(key) {
await backend.del(key)
}
}

View file

@ -87,14 +87,9 @@ export const useAddTorrentStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: sessionStorage,
key: 'vuetorrent_addTorrents'
}
]
storageItems: [{ storage: sessionStorage }]
}
}
)

View file

@ -132,15 +132,9 @@ export const useDashboardStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: localStorage,
key: 'vuetorrent_dashboard',
paths: ['displayMode']
}
]
storageItems: [{ storage: localStorage, includePaths: ['displayMode'] }]
}
}
)

View file

@ -45,14 +45,9 @@ export const useHistoryStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: localStorage,
key: 'vuetorrent_history'
}
]
storageItems: [{ storage: localStorage }]
}
}
)

View file

@ -1,6 +1,6 @@
import { useVueTorrentStore } from './vuetorrent'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useVueTorrentStore } from './vuetorrent'
export const useNavbarStore = defineStore(
'navbar',
@ -45,14 +45,9 @@ export const useNavbarStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: sessionStorage,
key: 'vuetorrent_navbar'
}
]
storageItems: [{ storage: sessionStorage }]
}
}
)

View file

@ -26,14 +26,9 @@ export const usePreferenceStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: sessionStorage,
key: 'vuetorrent_preferences'
}
]
storageItems: [{ storage: sessionStorage }]
}
}
)

View file

@ -164,14 +164,9 @@ export const useRssStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: sessionStorage,
key: 'vuetorrent_rss'
}
]
storageItems: [{ storage: sessionStorage }]
}
}
)

View file

@ -90,14 +90,9 @@ export const useSearchEngineStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: sessionStorage,
key: 'vuetorrent_searchEngine'
}
]
storageItems: [{ storage: sessionStorage }]
}
}
)

View file

@ -187,27 +187,9 @@ export const useTorrentStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: localStorage,
key: 'vuetorrent_torrents',
paths: [
'isTextFilterActive',
'textFilter',
'isStatusFilterActive',
'statusFilter',
'isCategoryFilterActive',
'categoryFilter',
'isTagFilterActive',
'tagFilter',
'isTrackerFilterActive',
'trackerFilter',
'sortOptions'
]
}
]
storageItems: [{ storage: localStorage, excludePaths: ['torrents'] }]
}
}
)

View file

@ -1,5 +1,6 @@
import { DashboardProperty, defaultDateFormat, PropertyData, propsData, propsMetadata, TitleOptions, TorrentProperty, VuetorrentTheme } from '@/constants/vuetorrent'
import { Theme } from '@/plugins/vuetify'
import { backendStorage } from '@/services/backend'
import { useMediaQuery } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
@ -10,6 +11,8 @@ import { useTheme } from 'vuetify'
export const useVueTorrentStore = defineStore(
'vuetorrent',
() => {
const backendUrl = ref('')
const language = ref('en')
const vuetorrentTheme = ref<VuetorrentTheme>(VuetorrentTheme.SYSTEM)
const showFreeSpace = ref(true)
@ -215,6 +218,7 @@ export const useVueTorrentStore = defineStore(
}
return {
backendUrl,
vuetorrentTheme,
dateFormat,
deleteWithFiles,
@ -267,6 +271,7 @@ export const useVueTorrentStore = defineStore(
toggleDoneGridProperty,
toggleTableProperty,
$reset: () => {
backendUrl.value = ''
language.value = 'en'
vuetorrentTheme.value = VuetorrentTheme.SYSTEM
showFreeSpace.value = true
@ -305,13 +310,11 @@ export const useVueTorrentStore = defineStore(
}
},
{
persist: {
persistence: {
enabled: true,
strategies: [
{
storage: localStorage,
key: 'vuetorrent_webuiSettings'
}
storageItems: [
{ storage: localStorage, key: 'webuiSettings' },
{ storage: backendStorage, key: 'webuiSettings', excludePaths: ['backendUrl']}
]
}
}

View file

@ -24,7 +24,7 @@
"paths": {
"@/*": ["./src/*"]
},
"types": ["node", "vue3-toastify/global", "pinia-plugin-persist", "vitest/globals"]
"types": ["node", "vue3-toastify/global", "vitest/globals"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]

View file

@ -21,7 +21,7 @@ export default defineConfig(({ mode }) => {
output: {
manualChunks: {
// apexcharts: ['apexcharts', 'vue3-apexcharts'],
vue: ['vue', 'vue-router', 'vue-i18n', 'vue3-toastify', 'vuedraggable', 'pinia', 'pinia-plugin-persist'],
vue: ['vue', 'vue-router', 'vue-i18n', 'vue3-toastify', 'vuedraggable', 'pinia', 'pinia-persistence-plugin'],
vuetify: ['vuetify']
}
}