diff --git a/README.md b/README.md index 7af0f008..5e1555da 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ The sleekest looking WebUI for qBittorrent made with Vue.js! -[](https://discord.gg/KDQP7fR467) -[](https://github.com/VueTorrent/VueTorrent/wiki) +[](https://discord.gg/KDQP7fR467)  @@ -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 +[](https://discord.gg/KDQP7fR467) +[](https://github.com/VueTorrent/VueTorrent/wiki) +[](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 diff --git a/docker-compose.yml b/docker-compose.yml index e48d5ec1..ebd07898 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/package-lock.json b/package-lock.json index c719094c..0435688a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 131f2b06..92232b07 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.vue b/src/App.vue index 8b6e3466..b4b78ea0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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() diff --git a/src/components/Settings/VueTorrent/General.vue b/src/components/Settings/VueTorrent/General.vue index 1132d059..86293523 100644 --- a/src/components/Settings/VueTorrent/General.vue +++ b/src/components/Settings/VueTorrent/General.vue @@ -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> diff --git a/src/locales/en.json b/src/locales/en.json index 18ed23c5..5b77572f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index 5774fd1b..5b26bef3 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -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() + } } } diff --git a/src/plugins/pinia.ts b/src/plugins/pinia.ts index bc2376b8..35ff559f 100644 --- a/src/plugins/pinia.ts +++ b/src/plugins/pinia.ts @@ -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 diff --git a/src/services/backend.ts b/src/services/backend.ts new file mode 100644 index 00000000..bb9cd821 --- /dev/null +++ b/src/services/backend.ts @@ -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) + } +} diff --git a/src/stores/addTorrents.ts b/src/stores/addTorrents.ts index 17c22cfb..bc23037e 100644 --- a/src/stores/addTorrents.ts +++ b/src/stores/addTorrents.ts @@ -87,14 +87,9 @@ export const useAddTorrentStore = defineStore( } }, { - persist: { + persistence: { enabled: true, - strategies: [ - { - storage: sessionStorage, - key: 'vuetorrent_addTorrents' - } - ] + storageItems: [{ storage: sessionStorage }] } } ) diff --git a/src/stores/dashboard.ts b/src/stores/dashboard.ts index 54d56499..bf8ac049 100644 --- a/src/stores/dashboard.ts +++ b/src/stores/dashboard.ts @@ -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'] }] } } ) diff --git a/src/stores/history.ts b/src/stores/history.ts index 23788977..51605717 100644 --- a/src/stores/history.ts +++ b/src/stores/history.ts @@ -45,14 +45,9 @@ export const useHistoryStore = defineStore( } }, { - persist: { + persistence: { enabled: true, - strategies: [ - { - storage: localStorage, - key: 'vuetorrent_history' - } - ] + storageItems: [{ storage: localStorage }] } } ) diff --git a/src/stores/navbar.ts b/src/stores/navbar.ts index 92cb4434..687ba7a0 100644 --- a/src/stores/navbar.ts +++ b/src/stores/navbar.ts @@ -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 }] } } ) diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 7f7e45b0..72990f47 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -26,14 +26,9 @@ export const usePreferenceStore = defineStore( } }, { - persist: { + persistence: { enabled: true, - strategies: [ - { - storage: sessionStorage, - key: 'vuetorrent_preferences' - } - ] + storageItems: [{ storage: sessionStorage }] } } ) diff --git a/src/stores/rss.ts b/src/stores/rss.ts index 76ca4297..490ae329 100644 --- a/src/stores/rss.ts +++ b/src/stores/rss.ts @@ -164,14 +164,9 @@ export const useRssStore = defineStore( } }, { - persist: { + persistence: { enabled: true, - strategies: [ - { - storage: sessionStorage, - key: 'vuetorrent_rss' - } - ] + storageItems: [{ storage: sessionStorage }] } } ) diff --git a/src/stores/searchEngine.ts b/src/stores/searchEngine.ts index 001d032f..ffd2ad11 100644 --- a/src/stores/searchEngine.ts +++ b/src/stores/searchEngine.ts @@ -90,14 +90,9 @@ export const useSearchEngineStore = defineStore( } }, { - persist: { + persistence: { enabled: true, - strategies: [ - { - storage: sessionStorage, - key: 'vuetorrent_searchEngine' - } - ] + storageItems: [{ storage: sessionStorage }] } } ) diff --git a/src/stores/torrents.ts b/src/stores/torrents.ts index 5291b1dc..f0db3f4f 100644 --- a/src/stores/torrents.ts +++ b/src/stores/torrents.ts @@ -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'] }] } } ) diff --git a/src/stores/vuetorrent.ts b/src/stores/vuetorrent.ts index 14675a7b..1034ec26 100644 --- a/src/stores/vuetorrent.ts +++ b/src/stores/vuetorrent.ts @@ -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']} ] } } diff --git a/tsconfig.json b/tsconfig.json index a5a03dd1..fb7975f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" }] diff --git a/vite.config.ts b/vite.config.ts index 77496101..10f96bad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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'] } }