From e09e8a0300551b813432dfba5d867be3f6709694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Marseault?= <22910497+Larsluph@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:10:59 +0100 Subject: [PATCH] perf: full Typescript migration + restructure (#612) @Larsluph --- index.html | 2 +- package-lock.json | 44 +- package.json | 8 +- .../{DocumentTitle.js => DocumentTitle.ts} | 12 +- src/actions/{Graph.js => Graph.ts} | 4 +- src/actions/ServerStatus.js | 10 - src/actions/ServerStatus.ts | 12 + src/actions/Tags.js | 22 - src/actions/Tags.ts | 23 + src/actions/{Torrents.js => Torrents.ts} | 9 +- src/actions/{Trackers.js => Trackers.ts} | 5 +- src/actions/{index.js => index.ts} | 0 src/components/Modals/Rss/FeedForm.vue | 2 +- .../TorrentDetail/Tabs/Trackers.vue | 2 +- src/enums/qbit/AppPreferences.ts | 67 +++ src/enums/qbit/ConnectionStatus.ts | 5 + src/enums/qbit/LogType.ts | 6 + src/enums/qbit/Priority.ts | 6 + src/enums/qbit/TorrentState.ts | 40 ++ src/enums/qbit/TrackerStatus.ts | 12 + src/enums/qbit/index.ts | 8 + src/enums/vuetorrent/TitleOptions.ts | 5 + src/enums/vuetorrent/TorrentState.ts | 12 + src/enums/vuetorrent/index.ts | 4 + src/{filters.js => filters.ts} | 50 +- src/global.d.ts | 4 + src/helpers.js | 2 +- src/lang/index.ts | 28 +- src/{main.js => main.ts} | 6 +- src/mixins/FullScreenModal.js | 17 - src/mixins/FullScreenModal.ts | 18 + src/mixins/General.js | 24 - src/mixins/General.ts | 27 + src/mixins/Modal.js | 36 -- src/mixins/Modal.ts | 35 ++ src/mixins/SettingsTab.js | 28 - src/mixins/SettingsTab.ts | 33 ++ src/mixins/Tab.js | 13 - src/mixins/Tab.ts | 14 + src/mixins/TorrentDashboardItem.js | 16 - src/mixins/TorrentDashboardItem.ts | 22 + src/mixins/TorrentSelect.js | 19 - src/mixins/TorrentSelect.ts | 20 + src/mixins/{index.js => index.ts} | 0 src/models/Status.js | 28 - src/models/Status.ts | 47 ++ src/models/{Torrent.js => Torrent.ts} | 105 ++-- src/models/index.ts | 4 + src/plugins/{vuetify.js => vuetify.ts} | 40 +- src/{router.js => router.ts} | 0 src/services/{auth.js => auth.ts} | 0 src/services/qbit.ts | 483 +++++++++--------- src/store/actions.js | 34 -- src/store/actions.ts | 37 ++ src/store/getters.js | 29 -- src/store/getters.ts | 30 ++ src/store/{index.js => index.ts} | 67 ++- src/store/mutations.js | 84 --- src/store/mutations.ts | 86 ++++ src/types/mappers/FeedRuleMapper.ts | 39 ++ src/types/mappers/TrackerMapper.ts | 32 ++ src/types/mappers/index.ts | 9 + src/types/qbit/models/AppPreferences.ts | 309 +++++++++++ src/types/qbit/models/Category.ts | 4 + src/types/qbit/models/Feed.ts | 4 + src/types/qbit/models/FeedRule.ts | 29 ++ src/types/qbit/models/Peer.ts | 18 + src/types/qbit/models/SearchJob.ts | 4 + src/types/qbit/models/SearchPlugin.ts | 16 + src/types/qbit/models/SearchResult.ts | 16 + src/types/qbit/models/SearchStatus.ts | 8 + src/types/qbit/models/ServerState.ts | 28 + src/types/qbit/models/Torrent.ts | 92 ++++ src/types/qbit/models/TorrentFile.ts | 20 + src/types/qbit/models/TorrentProperties.ts | 68 +++ src/types/qbit/models/Tracker.ts | 20 + src/types/qbit/models/index.ts | 34 ++ src/types/qbit/payloads/AddTorrentPayload.ts | 36 ++ .../qbit/payloads/AppPreferencesPayload.ts | 3 + src/types/qbit/payloads/BasePayload.ts | 1 + src/types/qbit/payloads/LoginPayload.ts | 8 + src/types/qbit/payloads/PeerLogPayload.ts | 7 + src/types/qbit/payloads/index.ts | 7 + src/types/qbit/responses/MainDataResponse.ts | 17 + src/types/qbit/responses/PeerLogResponse.ts | 14 + .../qbit/responses/SearchResultsResponse.ts | 10 + .../qbit/responses/TorrentPeersResponse.ts | 8 + src/types/qbit/responses/index.ts | 6 + src/types/vuetorrent/Category.ts | 4 + src/types/vuetorrent/ModalTemplate.ts | 5 + src/types/vuetorrent/SortOptions.ts | 13 + src/types/vuetorrent/StoreState.ts | 42 ++ src/types/vuetorrent/Tracker.ts | 5 + src/types/vuetorrent/TreeObjects.ts | 17 + src/types/vuetorrent/WebUISettings.ts | 31 ++ src/types/vuetorrent/index.ts | 15 + src/types/vuetorrent/rss/Feed.ts | 5 + src/types/vuetorrent/rss/FeedRule.ts | 16 + src/types/vuetorrent/search/SearchResult.ts | 9 + src/types/vuetorrent/search/SearchStatus.ts | 9 + src/views/Login.vue | 2 +- tests/unit/filters.spec.js | 3 + tests/unit/helpers.spec.js | 2 +- tsconfig.json | 1 + vite.config.js | 44 +- 105 files changed, 2091 insertions(+), 805 deletions(-) rename src/actions/{DocumentTitle.js => DocumentTitle.ts} (80%) rename src/actions/{Graph.js => Graph.ts} (79%) delete mode 100644 src/actions/ServerStatus.js create mode 100644 src/actions/ServerStatus.ts delete mode 100644 src/actions/Tags.js create mode 100644 src/actions/Tags.ts rename src/actions/{Torrents.js => Torrents.ts} (71%) rename src/actions/{Trackers.js => Trackers.ts} (74%) rename src/actions/{index.js => index.ts} (100%) create mode 100644 src/enums/qbit/AppPreferences.ts create mode 100644 src/enums/qbit/ConnectionStatus.ts create mode 100644 src/enums/qbit/LogType.ts create mode 100644 src/enums/qbit/Priority.ts create mode 100644 src/enums/qbit/TorrentState.ts create mode 100644 src/enums/qbit/TrackerStatus.ts create mode 100644 src/enums/qbit/index.ts create mode 100644 src/enums/vuetorrent/TitleOptions.ts create mode 100644 src/enums/vuetorrent/TorrentState.ts create mode 100644 src/enums/vuetorrent/index.ts rename src/{filters.js => filters.ts} (58%) create mode 100644 src/global.d.ts rename src/{main.js => main.ts} (88%) delete mode 100644 src/mixins/FullScreenModal.js create mode 100644 src/mixins/FullScreenModal.ts delete mode 100644 src/mixins/General.js create mode 100644 src/mixins/General.ts delete mode 100644 src/mixins/Modal.js create mode 100644 src/mixins/Modal.ts delete mode 100644 src/mixins/SettingsTab.js create mode 100644 src/mixins/SettingsTab.ts delete mode 100644 src/mixins/Tab.js create mode 100644 src/mixins/Tab.ts delete mode 100644 src/mixins/TorrentDashboardItem.js create mode 100644 src/mixins/TorrentDashboardItem.ts delete mode 100644 src/mixins/TorrentSelect.js create mode 100644 src/mixins/TorrentSelect.ts rename src/mixins/{index.js => index.ts} (100%) delete mode 100644 src/models/Status.js create mode 100644 src/models/Status.ts rename src/models/{Torrent.js => Torrent.ts} (59%) create mode 100644 src/models/index.ts rename src/plugins/{vuetify.js => vuetify.ts} (54%) rename src/{router.js => router.ts} (100%) rename src/services/{auth.js => auth.ts} (100%) delete mode 100644 src/store/actions.js create mode 100644 src/store/actions.ts delete mode 100644 src/store/getters.js create mode 100644 src/store/getters.ts rename src/store/{index.js => index.ts} (81%) delete mode 100644 src/store/mutations.js create mode 100644 src/store/mutations.ts create mode 100644 src/types/mappers/FeedRuleMapper.ts create mode 100644 src/types/mappers/TrackerMapper.ts create mode 100644 src/types/mappers/index.ts create mode 100644 src/types/qbit/models/AppPreferences.ts create mode 100644 src/types/qbit/models/Category.ts create mode 100644 src/types/qbit/models/Feed.ts create mode 100644 src/types/qbit/models/FeedRule.ts create mode 100644 src/types/qbit/models/Peer.ts create mode 100644 src/types/qbit/models/SearchJob.ts create mode 100644 src/types/qbit/models/SearchPlugin.ts create mode 100644 src/types/qbit/models/SearchResult.ts create mode 100644 src/types/qbit/models/SearchStatus.ts create mode 100644 src/types/qbit/models/ServerState.ts create mode 100644 src/types/qbit/models/Torrent.ts create mode 100644 src/types/qbit/models/TorrentFile.ts create mode 100644 src/types/qbit/models/TorrentProperties.ts create mode 100644 src/types/qbit/models/Tracker.ts create mode 100644 src/types/qbit/models/index.ts create mode 100644 src/types/qbit/payloads/AddTorrentPayload.ts create mode 100644 src/types/qbit/payloads/AppPreferencesPayload.ts create mode 100644 src/types/qbit/payloads/BasePayload.ts create mode 100644 src/types/qbit/payloads/LoginPayload.ts create mode 100644 src/types/qbit/payloads/PeerLogPayload.ts create mode 100644 src/types/qbit/payloads/index.ts create mode 100644 src/types/qbit/responses/MainDataResponse.ts create mode 100644 src/types/qbit/responses/PeerLogResponse.ts create mode 100644 src/types/qbit/responses/SearchResultsResponse.ts create mode 100644 src/types/qbit/responses/TorrentPeersResponse.ts create mode 100644 src/types/qbit/responses/index.ts create mode 100644 src/types/vuetorrent/Category.ts create mode 100644 src/types/vuetorrent/ModalTemplate.ts create mode 100644 src/types/vuetorrent/SortOptions.ts create mode 100644 src/types/vuetorrent/StoreState.ts create mode 100644 src/types/vuetorrent/Tracker.ts create mode 100644 src/types/vuetorrent/TreeObjects.ts create mode 100644 src/types/vuetorrent/WebUISettings.ts create mode 100644 src/types/vuetorrent/index.ts create mode 100644 src/types/vuetorrent/rss/Feed.ts create mode 100644 src/types/vuetorrent/rss/FeedRule.ts create mode 100644 src/types/vuetorrent/search/SearchResult.ts create mode 100644 src/types/vuetorrent/search/SearchStatus.ts diff --git a/index.html b/index.html index 8d518ee7..62ea0c83 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,6 @@ <strong>We're sorry but Vuetorrent doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> - <script type="module" src="/src/main.js"></script> + <script type="module" src="/src/main.ts"></script> </body> </html> diff --git a/package-lock.json b/package-lock.json index c07f4eaf..7849e749 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "vue": "^2.7.14", "vue-apexcharts": "^1.6.2", "vue-i18n": "^8", + "vue-property-decorator": "^9.1.2", "vue-router": "^3.6.5", "vue-toastification": "^1.7.11", "vuedraggable": "^2.24.3", @@ -30,6 +31,8 @@ "@faker-js/faker": "^7.6.0", "@mdi/js": "^7", "@types/jsdom": "^20.0.1", + "@types/lodash": "^4.14.191", + "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5", "@typescript-eslint/parser": "^5", "@vitejs/plugin-vue2": "^2", @@ -46,8 +49,9 @@ "vite": "^3", "vite-plugin-eslint": "^1", "vite-plugin-pwa": "^0.13", - "vitest": "^0.25", - "vue-template-compiler": "^2" + "vitest": "^0.25.8", + "vue-template-compiler": "^2", + "vue-typed-mixins": "^0.2.0" } }, "node_modules/@ampproject/remapping": { @@ -2260,6 +2264,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "dev": true + }, "node_modules/@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", @@ -2293,6 +2303,12 @@ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", "dev": true }, + "node_modules/@types/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz", @@ -7244,6 +7260,15 @@ "apexcharts": "^3.26.0" } }, + "node_modules/vue-class-component": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz", + "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==", + "peer": true, + "peerDependencies": { + "vue": "^2.0.0" + } + }, "node_modules/vue-eslint-parser": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz", @@ -7295,6 +7320,15 @@ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.28.2.tgz", "integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA==" }, + "node_modules/vue-property-decorator": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz", + "integrity": "sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==", + "peerDependencies": { + "vue": "*", + "vue-class-component": "*" + } + }, "node_modules/vue-router": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", @@ -7318,6 +7352,12 @@ "vue": "^2.0.0" } }, + "node_modules/vue-typed-mixins": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/vue-typed-mixins/-/vue-typed-mixins-0.2.0.tgz", + "integrity": "sha512-0OxuinandPWv3nm5k/reYkuKtX3jjPZ40Sy9roJz0ih8PUzmI7zSRiXFEJ62LsyRegw9Tqy+qMkajk7ipKP8Vg==", + "dev": true + }, "node_modules/vuedraggable": { "version": "2.24.3", "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz", diff --git a/package.json b/package.json index 9afa1bc5..7d50c485 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "vue": "^2.7.14", "vue-apexcharts": "^1.6.2", "vue-i18n": "^8", + "vue-property-decorator": "^9.1.2", "vue-router": "^3.6.5", "vue-toastification": "^1.7.11", "vuedraggable": "^2.24.3", @@ -33,6 +34,8 @@ "@faker-js/faker": "^7.6.0", "@mdi/js": "^7", "@types/jsdom": "^20.0.1", + "@types/lodash": "^4.14.191", + "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5", "@typescript-eslint/parser": "^5", "@vitejs/plugin-vue2": "^2", @@ -49,8 +52,9 @@ "vite": "^3", "vite-plugin-eslint": "^1", "vite-plugin-pwa": "^0.13", - "vitest": "^0.25", - "vue-template-compiler": "^2" + "vitest": "^0.25.8", + "vue-template-compiler": "^2", + "vue-typed-mixins": "^0.2.0" }, "browserslist": [ "> 1%", diff --git a/src/actions/DocumentTitle.js b/src/actions/DocumentTitle.ts similarity index 80% rename from src/actions/DocumentTitle.js rename to src/actions/DocumentTitle.ts index 81d5bae4..11ac4ff2 100644 --- a/src/actions/DocumentTitle.js +++ b/src/actions/DocumentTitle.ts @@ -1,24 +1,24 @@ import { formatBytes } from '@/helpers' -import store from '../store' +import store from '@/store' export class DocumentTitle { - static setDefault() { + private static setDefault() { this.set('qBittorrent') } - static setGlobalSpeed() { + private static setGlobalSpeed() { const status = store.getters.getStatus() this.set(`[D: ${formatBytes(status.dlspeed)}/s, U: ${formatBytes(status.upspeed)}/s] VueTorrent`) } - static setFirstTorrentStatus() { + private static setFirstTorrentStatus() { const torrents = store.getters.getTorrents() if (!torrents && !torrents.length) return const torrent = torrents[0] this.set(`[D: ${formatBytes(torrent.dlspeed)}/s, U: ${formatBytes(torrent.upspeed)}/s] ${torrent.progress}%`) } - static update() { + public static update() { const mode = store.getters.getWebuiSettings().title switch (mode) { case 'Default': @@ -32,7 +32,7 @@ export class DocumentTitle { } } - static set(title) { + private static set(title: string) { document.title = title } } diff --git a/src/actions/Graph.js b/src/actions/Graph.ts similarity index 79% rename from src/actions/Graph.js rename to src/actions/Graph.ts index 7ffc3f81..f28f8975 100644 --- a/src/actions/Graph.js +++ b/src/actions/Graph.ts @@ -1,7 +1,7 @@ -import store from '../store' +import store from '@/store' export class Graph { - static update() { + public static shiftValues() { const state = store.state state.download_data.shift() state.download_data.push(state.status.dlspeedRaw || 0) diff --git a/src/actions/ServerStatus.js b/src/actions/ServerStatus.js deleted file mode 100644 index af58b889..00000000 --- a/src/actions/ServerStatus.js +++ /dev/null @@ -1,10 +0,0 @@ -import store from '../store' -import Status from '@/models/Status' - -export class ServerStatus { - static update(response) { - if (response.server_state) { - store.state.status = new Status(response.server_state) - } - } -} diff --git a/src/actions/ServerStatus.ts b/src/actions/ServerStatus.ts new file mode 100644 index 00000000..1ddbd659 --- /dev/null +++ b/src/actions/ServerStatus.ts @@ -0,0 +1,12 @@ +import store from '@/store' +import {Status} from '@/models' +import type {ServerState} from "@/types/qbit/models"; +import type {Optional} from "@/global"; + +export class ServerStatus { + static update(server_state: Optional<ServerState>) { + if (server_state) { + store.state.status = new Status(server_state) + } + } +} diff --git a/src/actions/Tags.js b/src/actions/Tags.js deleted file mode 100644 index c4bd0266..00000000 --- a/src/actions/Tags.js +++ /dev/null @@ -1,22 +0,0 @@ -import store from '../store' -import { ArrayHelper } from '@/helpers' - -export class Tags { - static update(response) { - if (response?.full_update === true) { - store.state.tags = response.tags - - return - } - - if (response.tags_removed) { - store.state.tags = ArrayHelper.remove(store.state.tags, response.tags_removed) - - return - } - - if (response.tags) { - store.state.tags = ArrayHelper.concat(store.state.tags, response.tags) - } - } -} diff --git a/src/actions/Tags.ts b/src/actions/Tags.ts new file mode 100644 index 00000000..21e19b9d --- /dev/null +++ b/src/actions/Tags.ts @@ -0,0 +1,23 @@ +import store from '@/store' +import {ArrayHelper} from '@/helpers' +import type {MainDataResponse} from "@/types/qbit/responses"; + +export class Tags { + static update(response: MainDataResponse) { + if (response?.fullUpdate === true) { + store.state.tags = response.tags || [] + + return + } + + if (response.tags_removed) { + store.state.tags = ArrayHelper.remove(store.state.tags, ...response.tags_removed) + + return + } + + if (response.tags) { + store.state.tags = ArrayHelper.concat(store.state.tags, response.tags) + } + } +} diff --git a/src/actions/Torrents.js b/src/actions/Torrents.ts similarity index 71% rename from src/actions/Torrents.js rename to src/actions/Torrents.ts index 305601bb..ec1d7c97 100644 --- a/src/actions/Torrents.js +++ b/src/actions/Torrents.ts @@ -1,11 +1,12 @@ -import store from '../store' +import store from '@/store' import { Hostname } from '@/helpers' -import Torrent from '@/models/Torrent' +import {Torrent as VtTorrent} from '@/models' +import type {Torrent as QbitTorrent} from '@/types/qbit/models' import { isProduction } from '@/utils' import { generateMultiple } from '@/utils/faker' export class Torrents { - static update(data) { + static update(data: QbitTorrent[]) { if (store.state.webuiSettings.showTrackerFilter) { // don't calculate trackers when disabled @@ -15,7 +16,7 @@ export class Torrents { } // update torrents - store.state.torrents = data.map(t => new Torrent(t, store.state.webuiSettings.dateFormat)) + store.state.torrents = data.map(t => new VtTorrent(t, store.state.webuiSettings.dateFormat)) // load fake torrents if enabled if (isProduction()) return diff --git a/src/actions/Trackers.js b/src/actions/Trackers.ts similarity index 74% rename from src/actions/Trackers.js rename to src/actions/Trackers.ts index da73b95f..9a579ea5 100644 --- a/src/actions/Trackers.js +++ b/src/actions/Trackers.ts @@ -1,8 +1,9 @@ -import store from '../store' +import store from '@/store' import { Hostname } from '@/helpers' +import type {Torrent} from "@/types/qbit/models"; export class Trackers { - static update(data) { + static update(data: Torrent[]) { if (store.state.webuiSettings.showTrackerFilter) { store.state.trackers = data .map(t => t.tracker) diff --git a/src/actions/index.js b/src/actions/index.ts similarity index 100% rename from src/actions/index.js rename to src/actions/index.ts diff --git a/src/components/Modals/Rss/FeedForm.vue b/src/components/Modals/Rss/FeedForm.vue index 3f785069..4a9d32a8 100644 --- a/src/components/Modals/Rss/FeedForm.vue +++ b/src/components/Modals/Rss/FeedForm.vue @@ -70,7 +70,7 @@ export default { this.dialog = false }, edit() { - qbit.editfeed(this.feed) + qbit.editFeed(this.feed) Vue.$toast.success(this.$t('toast.feedSaved')) this.cancel() } diff --git a/src/components/TorrentDetail/Tabs/Trackers.vue b/src/components/TorrentDetail/Tabs/Trackers.vue index 60da114b..51cec608 100644 --- a/src/components/TorrentDetail/Tabs/Trackers.vue +++ b/src/components/TorrentDetail/Tabs/Trackers.vue @@ -110,7 +110,7 @@ export default { async addTrackers() { if (!this.newTrackers.length) return (this.trackerDialog = false) - qbit.addTorrenTrackers(this.hash, this.newTrackers) + qbit.addTorrentTrackers(this.hash, this.newTrackers) this.newTrackers = '' await this.getTorrentTrackers() this.trackerDialog = false diff --git a/src/enums/qbit/AppPreferences.ts b/src/enums/qbit/AppPreferences.ts new file mode 100644 index 00000000..27d8bb62 --- /dev/null +++ b/src/enums/qbit/AppPreferences.ts @@ -0,0 +1,67 @@ +export enum BitTorrentProtocol { + TCP_uTP, + TCP, + uTP +} + +export enum DynDnsService { + USE_DYNDNS, + USE_NOIP +} + +export enum Encryption { + PREFER_ENCRYPTION, + FORCE_ON, + FORCE_OFF +} + +export enum MaxRatioAction { + PAUSE_TORRENT, + REMOVE_TORRENT, + REMOVE_TORRENT_AND_FILES, + ENABLE_SUPERSEEDING +} + +export enum ProxyType { + DISABLED = -1, + HTTP_WITHOUT_AUTH = 1, + SOCKS5_WITHOUT_AUTH, + HTTP_WITH_AUTH, + SOCKS5_WITH_AUTH, + SOCKS4_WITHOUT_AUTH +} + +enum ScanDirsEnum { + MONITORED_FOLDER, + DEFAULT_SAVE_PATH +} +export type ScanDirs = ScanDirsEnum | string + +export enum SchedulerDays { + EVERY_DAY, + EVERY_WEEKDAY, + EVERY_WEEKEND, + EVERY_MONDAY, + EVERY_TUESDAY, + EVERY_WEDNESDAY, + EVERY_THURSDAY, + EVERY_FRIDAY, + EVERY_SATURDAY, + EVERY_SUNDAY +} + +export enum UploadChokingAlgorithm { + ROUND_ROBIN, + FASTEST_UPLOAD, + ANTI_LEECH +} + +export enum UploadSlotsBehavior { + FIXED_SLOTS, + UPLOAD_RATE_BASED +} + +export enum UtpTcpMixedMode { + PREFER_TCP, + PEER_PROPORTIONAL +} diff --git a/src/enums/qbit/ConnectionStatus.ts b/src/enums/qbit/ConnectionStatus.ts new file mode 100644 index 00000000..eec2200c --- /dev/null +++ b/src/enums/qbit/ConnectionStatus.ts @@ -0,0 +1,5 @@ +export enum ConnectionStatus { + CONNECTED = 'connected', + FIREWALLED = 'firewalled', + DISCONNECTED = 'disconnected' +} diff --git a/src/enums/qbit/LogType.ts b/src/enums/qbit/LogType.ts new file mode 100644 index 00000000..42c29650 --- /dev/null +++ b/src/enums/qbit/LogType.ts @@ -0,0 +1,6 @@ +export enum LogType { + NORMAL = 1, + INFO = 2, + WARNING = 4, + CRITICAL = 8 +} diff --git a/src/enums/qbit/Priority.ts b/src/enums/qbit/Priority.ts new file mode 100644 index 00000000..7a84795c --- /dev/null +++ b/src/enums/qbit/Priority.ts @@ -0,0 +1,6 @@ +export enum Priority { + DO_NOT_DOWNLOAD = 0, + NORMAL = 1, + HIGH = 6, + MAXIMAL = 7 +} diff --git a/src/enums/qbit/TorrentState.ts b/src/enums/qbit/TorrentState.ts new file mode 100644 index 00000000..6074bce3 --- /dev/null +++ b/src/enums/qbit/TorrentState.ts @@ -0,0 +1,40 @@ +export enum TorrentState { + /** Some error occurred, applies to paused torrents */ + ERROR = 'error', + /** Torrent data files is missing */ + MISSING_FILES = 'missingFiles', + /** Torrent is being seeded and data is being transferred */ + UPLOADING = 'uploading', + /** Torrent is paused and has finished downloading */ + PAUSED_UP = 'pausedUP', + /** Queuing is enabled and torrent is queued for upload */ + QUEUED_UP = 'queuedUP', + /** Torrent is being seeded, but no connection were made */ + STALLED_UP = 'stalledUP', + /** Torrent has finished downloading and is being checked */ + CHECKING_UP = 'checkingUP', + /** Torrent is forced to uploading and ignore queue limit */ + FORCED_UP = 'forcedUP', + /** Torrent is allocating disk space for download */ + ALLOCATING = 'allocating', + /** Torrent is being downloaded and data is being transferred */ + DOWNLOADING = 'downloading', + /** Torrent has just started downloading and is fetching metadata */ + META_DL = 'metaDL', + /** Torrent is paused and has NOT finished downloading */ + PAUSED_DL = 'pausedDL', + /** Queuing is enabled and torrent is queued for download */ + QUEUED_DL = 'queuedDL', + /** Torrent is being downloaded, but no connection were made */ + STALLED_DL = 'stalledDL', + /** Same as checkingUP, but torrent has NOT finished downloading */ + CHECKING_DL = 'checkingDL', + /** Torrent is forced to downloading to ignore queue limit */ + FORCED_DL = 'forcedDL', + /** Checking resume data on qBt startup */ + CHECKING_RESUME_DATA = 'checkingResumeData', + /** Torrent is moving to another location */ + MOVING = 'moving', + /** Unknown status */ + UNKNOWN = 'unknown' +} diff --git a/src/enums/qbit/TrackerStatus.ts b/src/enums/qbit/TrackerStatus.ts new file mode 100644 index 00000000..57a85b0f --- /dev/null +++ b/src/enums/qbit/TrackerStatus.ts @@ -0,0 +1,12 @@ +export enum TrackerStatus { + /** Tracker is disabled (used for DHT, PeX, and LSD) */ + DISABLED, + /** Tracker has not been contacted yet */ + NOT_YET_CONTACTED, + /** Tracker has been contacted and is working */ + WORKING, + /** Tracker is updating */ + UPDATING, + /** Tracker has been contacted, but it is not working (or doesn't send proper replies) */ + NOT_WORKING +} diff --git a/src/enums/qbit/index.ts b/src/enums/qbit/index.ts new file mode 100644 index 00000000..20c76d34 --- /dev/null +++ b/src/enums/qbit/index.ts @@ -0,0 +1,8 @@ +import * as AppPreferences from './AppPreferences' +import { LogType } from './LogType' +import { Priority } from './Priority' +import { TrackerStatus } from './TrackerStatus' +import { ConnectionStatus } from './ConnectionStatus' +import {TorrentState} from './TorrentState' + +export { AppPreferences, ConnectionStatus, LogType, Priority, TrackerStatus, TorrentState } diff --git a/src/enums/vuetorrent/TitleOptions.ts b/src/enums/vuetorrent/TitleOptions.ts new file mode 100644 index 00000000..4d45df46 --- /dev/null +++ b/src/enums/vuetorrent/TitleOptions.ts @@ -0,0 +1,5 @@ +export enum TitleOptions { + DEFAULT = 'Default', + GLOBAL_SPEED = 'Global Speed', + FIRST_TORRENT_STATUS = 'First Torrent Status' +} \ No newline at end of file diff --git a/src/enums/vuetorrent/TorrentState.ts b/src/enums/vuetorrent/TorrentState.ts new file mode 100644 index 00000000..e3334c63 --- /dev/null +++ b/src/enums/vuetorrent/TorrentState.ts @@ -0,0 +1,12 @@ +export enum TorrentState { + DOWNLOADING = 'Downloading', + SEEDING = 'Seeding', + PAUSED = 'Paused', + STALLED = 'Stalled', + METADATA = 'Metadata', + DONE = 'Done', + QUEUED = 'Queued', + CHECKING = 'Checking', + MOVING = 'Moving', + FAIL = 'Fail' +} \ No newline at end of file diff --git a/src/enums/vuetorrent/index.ts b/src/enums/vuetorrent/index.ts new file mode 100644 index 00000000..8b684557 --- /dev/null +++ b/src/enums/vuetorrent/index.ts @@ -0,0 +1,4 @@ +import {TorrentState} from "./TorrentState" +import {TitleOptions} from "./TitleOptions" + +export {TorrentState, TitleOptions} \ No newline at end of file diff --git a/src/filters.js b/src/filters.ts similarity index 58% rename from src/filters.js rename to src/filters.ts index c6a56121..611d2a91 100644 --- a/src/filters.js +++ b/src/filters.ts @@ -1,7 +1,6 @@ import Vue from 'vue' -/* eslint-disable no-param-reassign */ -export function toPrecision(value, precision) { +export function toPrecision(value: number, precision: number): string { if (value >= 10 ** precision) { return value.toString() } @@ -12,7 +11,7 @@ export function toPrecision(value, precision) { return value.toFixed(precision - 1) } -export function formatSize(value) { +export function formatSize(value: number): string { const units = 'KMGTP' let index = -1 @@ -33,7 +32,7 @@ export function formatSize(value) { Vue.filter('formatSize', formatSize) Vue.filter('size', formatSize) -export function formatProgress(progress) { +export function formatProgress(progress: number): string { progress *= 100 return `${toPrecision(progress, 3)}%` @@ -41,17 +40,7 @@ export function formatProgress(progress) { 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) { +export function formatNetworkSpeed(speed: number): string|null { if (speed === 0) { return null } @@ -61,7 +50,7 @@ export function formatNetworkSpeed(speed) { Vue.filter('networkSpeed', formatNetworkSpeed) -export function networkSize(size) { +export function networkSize(size: number) { if (size === 0) { return null } @@ -71,42 +60,41 @@ export function networkSize(size) { Vue.filter('networkSize', networkSize) -export function getDataUnit(a, b) { - if (a === -1) return null - if (!a) return 'B' +export function getDataUnit(data: number) { + if (data === -1) return null + if (!data) return 'B' const c = 1024 const e = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - const f = Math.floor(Math.log(a) / Math.log(c)) + const f = Math.floor(Math.log(data) / Math.log(c)) return `${e[f]}` } Vue.filter('getDataUnit', getDataUnit) -export function getDataValue(a, b) { - if (a === -1) return 'None' - if (!a) return '0' +export function getDataValue(data: number, precision: number = 2) { + if (data === -1) return 'None' + if (!data) return '0' const c = 1024 - const d = b || 2 - const f = Math.floor(Math.log(a) / Math.log(c)) + const f = Math.floor(Math.log(data) / Math.log(c)) - return `${parseFloat((a / Math.pow(c, f)).toFixed(d))}` + return `${parseFloat((data / Math.pow(c, f)).toFixed(precision))}` } Vue.filter('getDataValue', getDataValue) -export function titleCase(str) { - if (!str) return +export function titleCase(str: string): string { + if (str.length == 0) return str return str .split(' ') - .map(w => w[0].toUpperCase() + w.substr(1).toLowerCase()) + .map(w => w[0] && w[0].toUpperCase() + w.substring(1).toLowerCase()) .join(' ') } Vue.filter('titleCase', titleCase) -export function limitToValue(value) { +export function limitToValue(value: number): string { if (value === -2) { return 'global' } @@ -114,7 +102,7 @@ export function limitToValue(value) { return 'unlimited' } - return value + return value.toString() } Vue.filter('limitToValue', limitToValue) diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..99b90c4c --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,4 @@ +export type Optional<T> = T | null | undefined + +export type $TSFixMe = any +export type $TSFixMeFunction = (...args: any[]) => any diff --git a/src/helpers.js b/src/helpers.js index f5a0bc5b..2e54f3c8 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -195,4 +195,4 @@ export class Hostname { return '' } } -} \ No newline at end of file +} diff --git a/src/lang/index.ts b/src/lang/index.ts index 96ceefc9..fa818753 100644 --- a/src/lang/index.ts +++ b/src/lang/index.ts @@ -14,20 +14,20 @@ import vi from './vi.json' import zh_hans from './zh-hans.json' import zh_hant from './zh-hant.json' -export const messages = { - [Locales.EN]: en, - [Locales.ES]: es, - [Locales.FR]: fr, - [Locales.ID]: id, - [Locales.IT]: it, - [Locales.JA]: ja, - [Locales.NL]: nl, - [Locales.PT_BR]: pt_br, - [Locales.RU]: ru, - [Locales.UK]: uk, - [Locales.VI]: vi, - [Locales.ZH_HANS]: zh_hans, - [Locales.ZH_HANT]: zh_hant +export const messages: Record<Locales, any> = { + [Locales.EN]: en, + [Locales.ES]: es, + [Locales.FR]: fr, + [Locales.ID]: id, + [Locales.IT]: it, + [Locales.JA]: ja, + [Locales.NL]: nl, + [Locales.PT_BR]: pt_br, + [Locales.RU]: ru, + [Locales.UK]: uk, + [Locales.VI]: vi, + [Locales.ZH_HANS]: zh_hans, + [Locales.ZH_HANT]: zh_hant } export const defaultLocale = Locales.EN diff --git a/src/main.js b/src/main.ts similarity index 88% rename from src/main.js rename to src/main.ts index 5fe11f5f..86d216f4 100644 --- a/src/main.js +++ b/src/main.ts @@ -16,11 +16,7 @@ Vue.use(toast, config) // register modals const components = import.meta.glob('./components/Modals/**/*.vue') Object.entries(components).forEach(([path, definition]) => { - const componentName = path - .split('/') - .pop() - .replace(/\.\w+$/, '') - + const componentName = (path.split('/').pop() as string).replace(/\.\w+$/, '') Vue.component(componentName, definition) }) diff --git a/src/mixins/FullScreenModal.js b/src/mixins/FullScreenModal.js deleted file mode 100644 index b36f6283..00000000 --- a/src/mixins/FullScreenModal.js +++ /dev/null @@ -1,17 +0,0 @@ -export default { - computed: { - phoneLayout() { - return this.$vuetify.breakpoint.xsOnly - }, - dialogWidth() { - return this.phoneLayout ? '100%' : '80%' - } - }, - watch: { - dialog(visible) { - if (!visible) { - this.tab = null - } - } - } -} diff --git a/src/mixins/FullScreenModal.ts b/src/mixins/FullScreenModal.ts new file mode 100644 index 00000000..ba511084 --- /dev/null +++ b/src/mixins/FullScreenModal.ts @@ -0,0 +1,18 @@ +import {Component, Vue, Watch} from "vue-property-decorator"; + +@Component +export default class FullScreenModal extends Vue { + tab!: string|null + + get phoneLayout() { + return this.$vuetify.breakpoint.xsOnly + } + get dialogWidth() { + return this.phoneLayout ? '100%' : '80%' + } + + @Watch('dialog') + onDialogChanged(visible: boolean) { + if (!visible) this.tab = null + } +} diff --git a/src/mixins/General.js b/src/mixins/General.js deleted file mode 100644 index 8058dc8b..00000000 --- a/src/mixins/General.js +++ /dev/null @@ -1,24 +0,0 @@ -import { v4 as uuidv4 } from 'uuid' -import { mapGetters } from 'vuex' -export default { - computed: { - ...mapGetters(['getTheme']), - theme() { - return this.getTheme() - }, - isMobile() { - return this.$vuetify.breakpoint.smAndDown - } - }, - methods: { - createModal(name, props) { - const component = { - component: name, - props, - guid: uuidv4() - } - - this.$store.commit('ADD_MODAL', component) - } - } -} diff --git a/src/mixins/General.ts b/src/mixins/General.ts new file mode 100644 index 00000000..8a212d2e --- /dev/null +++ b/src/mixins/General.ts @@ -0,0 +1,27 @@ +import { v4 as uuidv4 } from 'uuid' +import { mapGetters } from 'vuex' +import {Component, Vue} from "vue-property-decorator"; + +@Component({ + computed: mapGetters(['getTheme']) +}) +export default class General extends Vue { + getTheme!: () => string + + get theme() { + return this.getTheme() + } + get isMobile() { + return this.$vuetify.breakpoint.smAndDown + } + + createModal(name: string, props: any) { + const component = { + component: name, + props, + guid: uuidv4() + } + + this.$store.commit('ADD_MODAL', component) + } +} diff --git a/src/mixins/Modal.js b/src/mixins/Modal.js deleted file mode 100644 index 2fc425b0..00000000 --- a/src/mixins/Modal.js +++ /dev/null @@ -1,36 +0,0 @@ -import { mapGetters } from 'vuex' -export default { - props: ['guid'], - data() { - return { - hndlDialog: true - } - }, - computed: { - ...mapGetters(['getModalState']), - dialog: { - get() { - return this.hndlDialog - }, - set(val) { - this.hndlDialog = val - if (!val) this.deleteModal() - } - } - }, - - methods: { - deleteModal() { - //this.hndlDialog = false - setTimeout( - function () { - this.$store.commit('DELETE_MODAL', this.guid) - }.bind(this), - 300 - ) - } - }, - beforeDestroy() { - this.deleteModal() - } -} diff --git a/src/mixins/Modal.ts b/src/mixins/Modal.ts new file mode 100644 index 00000000..e69c7ac0 --- /dev/null +++ b/src/mixins/Modal.ts @@ -0,0 +1,35 @@ +import { mapGetters } from 'vuex' +import {Component, Prop, Vue} from "vue-property-decorator"; + +@Component({ + computed: mapGetters(['getModalState']) +}) +export default class Modal extends Vue { + //data + hndlDialog = true + + //props + @Prop() guid!: string + + // mapGetters + getModalState!: () => any + + // computed + get dialog() { + return this.hndlDialog + } + set dialog(val) { + this.hndlDialog = val + if (!val) this.deleteModal() + } + + // methods + deleteModal() { + //this.hndlDialog = false + setTimeout(() => { this.$store.commit('DELETE_MODAL', this.guid) }, 300) + } + + beforeDestroy() { + this.deleteModal() + } +} diff --git a/src/mixins/SettingsTab.js b/src/mixins/SettingsTab.js deleted file mode 100644 index 8cd8beb6..00000000 --- a/src/mixins/SettingsTab.js +++ /dev/null @@ -1,28 +0,0 @@ -import { mapGetters } from 'vuex' -import qbit from '@/services/qbit' -import Vue from 'vue' -export default { - computed: { - ...mapGetters(['getSettings']), - settings() { - return this.getSettings() - } - }, - methods: { - async saveSettings() { - qbit.setPreferences(this.getSettings()).then(() => { - Vue.$toast.success(this.$t('toast.settingsSaved')) - }) - await this.$store.dispatch('FETCH_SETTINGS') - await this.$store.commit('SET_LANGUAGE') - this.close() - if (!this.settings.alternative_webui_enabled) { - navigator.serviceWorker.getRegistrations().then(function (registrations) { - for (const registration of registrations) { - registration.unregister() - } - }) - } - } - } -} diff --git a/src/mixins/SettingsTab.ts b/src/mixins/SettingsTab.ts new file mode 100644 index 00000000..316c6d61 --- /dev/null +++ b/src/mixins/SettingsTab.ts @@ -0,0 +1,33 @@ +import { mapGetters } from 'vuex' +import qbit from '@/services/qbit' +import {Component, Vue} from "vue-property-decorator"; +import type {AppPreferences} from "@/types/qbit/models"; + +@Component({ + computed: mapGetters(['getSettings']) +}) +export default class SettingsTab extends Vue { + getSettings!: () => AppPreferences + + get settings() { + return this.getSettings() + } + + close!: () => void + + async saveSettings() { + qbit.setPreferences(this.getSettings()).then(() => { + Vue.$toast.success(this.$t('toast.settingsSaved').toString()) + }) + await this.$store.dispatch('FETCH_SETTINGS') + await this.$store.commit('SET_LANGUAGE') + this.close() + if (!this.settings.alternative_webui_enabled) { + navigator.serviceWorker.getRegistrations().then(function (registrations) { + for (const registration of registrations) { + registration.unregister() + } + }) + } + } +} diff --git a/src/mixins/Tab.js b/src/mixins/Tab.js deleted file mode 100644 index 6dcb1db9..00000000 --- a/src/mixins/Tab.js +++ /dev/null @@ -1,13 +0,0 @@ -export default { - props: { - hash: String, - isActive: Boolean - }, - watch: { - isActive(active) { - if (active) { - this.activeMethod() - } - } - } -} diff --git a/src/mixins/Tab.ts b/src/mixins/Tab.ts new file mode 100644 index 00000000..266e3e51 --- /dev/null +++ b/src/mixins/Tab.ts @@ -0,0 +1,14 @@ +import {Component, Prop, Vue, Watch} from "vue-property-decorator"; + +@Component +export default class Tab extends Vue { + @Prop() hash!: string + @Prop() isActive!: boolean + + activeMethod!: () => void + + @Watch('isActive') + isActiveChanged(active: boolean) { + if (active) this.activeMethod() + } +} diff --git a/src/mixins/TorrentDashboardItem.js b/src/mixins/TorrentDashboardItem.js deleted file mode 100644 index da4ad55d..00000000 --- a/src/mixins/TorrentDashboardItem.js +++ /dev/null @@ -1,16 +0,0 @@ -import { mapGetters } from 'vuex' - -export default { - computed: { - ...mapGetters(['getTheme']), - phoneLayout() { - return this.$vuetify.breakpoint.xsOnly - }, - theme() { - return this.getTheme() - }, - state() { - return this.torrent.state.toLowerCase() - } - } -} diff --git a/src/mixins/TorrentDashboardItem.ts b/src/mixins/TorrentDashboardItem.ts new file mode 100644 index 00000000..ba577c4c --- /dev/null +++ b/src/mixins/TorrentDashboardItem.ts @@ -0,0 +1,22 @@ +import { mapGetters } from 'vuex' +import {Component, Prop, Vue} from "vue-property-decorator"; +import type {Torrent} from "@/models"; + +@Component({ + computed: mapGetters(['getTheme']) +}) +export default class TorrentDashboardItem extends Vue { + getTheme!: () => string + + @Prop() torrent!: Torrent + + get phoneLayout() { + return this.$vuetify.breakpoint.xsOnly + } + get theme() { + return this.getTheme() + } + get state() { + return this.torrent.state.toLowerCase() + } +} diff --git a/src/mixins/TorrentSelect.js b/src/mixins/TorrentSelect.js deleted file mode 100644 index 641a066e..00000000 --- a/src/mixins/TorrentSelect.js +++ /dev/null @@ -1,19 +0,0 @@ -export default { - methods: { - isAlreadySelected(hash) { - return this.$store.getters.containsTorrent(hash) - }, - selectTorrent(hash) { - if (!this.$store.state.selectMode) this.$store.state.selectMode = true - if (this.isAlreadySelected(hash)) { - this.$store.commit('SET_SELECTED', { type: 'remove', hash }) - } else { - this.$store.commit('SET_SELECTED', { type: 'add', hash }) - } - }, - selectUntil(hash, index) { - if (!this.$store.state.selectMode) return - this.$store.commit('SET_SELECTED', { type: 'until', hash, index }) - } - } -} diff --git a/src/mixins/TorrentSelect.ts b/src/mixins/TorrentSelect.ts new file mode 100644 index 00000000..157586a0 --- /dev/null +++ b/src/mixins/TorrentSelect.ts @@ -0,0 +1,20 @@ +import {Component, Vue} from "vue-property-decorator"; + +@Component +export default class TorrentSelect extends Vue { + isAlreadySelected(hash: string) { + return this.$store.getters.containsTorrent(hash) + } + selectTorrent(hash: string) { + if (!this.$store.state.selectMode) this.$store.state.selectMode = true + if (this.isAlreadySelected(hash)) { + this.$store.commit('SET_SELECTED', { type: 'remove', hash }) + } else { + this.$store.commit('SET_SELECTED', { type: 'add', hash }) + } + } + selectUntil(hash: string, index: number) { + if (!this.$store.state.selectMode) return + this.$store.commit('SET_SELECTED', { type: 'until', hash, index }) + } +} diff --git a/src/mixins/index.js b/src/mixins/index.ts similarity index 100% rename from src/mixins/index.js rename to src/mixins/index.ts diff --git a/src/models/Status.js b/src/models/Status.js deleted file mode 100644 index c1dd3e3b..00000000 --- a/src/models/Status.js +++ /dev/null @@ -1,28 +0,0 @@ -import store from '../store' - -export default class Status { - constructor({ connection_status, dl_info_data, up_info_data, alltime_dl, alltime_ul, dl_info_speed, up_info_speed, free_space_on_disk, use_alt_speed_limits }) { - const previous = store.state.status - - this.status = connection_status || previous.status - this.sessionDownloaded = dl_info_data || previous.sessionDownloaded - this.sessionUploaded = up_info_data || previous.sessionUploaded - this.alltimeDownloaded = alltime_dl || previous.alltimeDownloaded - this.alltimeUploaded = alltime_ul || previous.alltimeUploaded - this.dlspeed = dl_info_speed || 0 - this.upspeed = up_info_speed || 0 - this.freeDiskSpace = free_space_on_disk || previous.freeDiskSpace - this.altSpeed = use_alt_speed_limits !== undefined ? use_alt_speed_limits : previous.altSpeed - this.dlspeedRaw = this.formatSpeed(dl_info_speed) || 0 - this.upspeedRaw = this.formatSpeed(up_info_speed) || 0 - Object.freeze(this) - } - - formatSpeed(value) { - if (!value) { - return 0 - } - - return Math.round(value) - } -} diff --git a/src/models/Status.ts b/src/models/Status.ts new file mode 100644 index 00000000..41444216 --- /dev/null +++ b/src/models/Status.ts @@ -0,0 +1,47 @@ +import store from '@/store' +import type { ServerState } from '@/types/qbit/models' +import { ConnectionStatus } from '@/enums/qbit' + +export default class Status { + alltimeDownloaded: number = 0 + alltimeUploaded: number = 0 + altSpeed: boolean = false + dlspeed: number = 0 + dlspeedRaw: number = 0 + freeDiskSpace: number = 0 + sessionDownloaded: number = 0 + sessionUploaded: number = 0 + status: ConnectionStatus = ConnectionStatus.DISCONNECTED + upspeed: number = 0 + upspeedRaw: number = 0 + + constructor(in_state?: ServerState) { + if (!in_state) { + return +} + +const previous = store.state.status + +this.alltimeDownloaded = in_state.alltime_dl || previous.alltimeDownloaded +this.alltimeUploaded = in_state.alltime_ul || previous.alltimeUploaded +this.altSpeed = in_state.use_alt_speed_limits !== undefined ? in_state.use_alt_speed_limits : previous.altSpeed +this.dlspeed = in_state.dl_info_speed || 0 +this.dlspeedRaw = this.formatSpeed(in_state.dl_info_speed) || 0 +this.freeDiskSpace = in_state.free_space_on_disk || previous.freeDiskSpace +this.sessionDownloaded = in_state.dl_info_data || previous.sessionDownloaded +this.sessionUploaded = in_state.up_info_data || previous.sessionUploaded +this.status = in_state.connection_status || previous.status +this.upspeed = in_state.up_info_speed || 0 +this.upspeedRaw = this.formatSpeed(in_state.up_info_speed) || 0 + +Object.freeze(this) +} + +formatSpeed(value: number): number { + if (!value) { + return 0 + } + + return Math.round(value) +} +} diff --git a/src/models/Torrent.js b/src/models/Torrent.ts similarity index 59% rename from src/models/Torrent.js rename to src/models/Torrent.ts index 7d41efb9..ab3f5b87 100644 --- a/src/models/Torrent.js +++ b/src/models/Torrent.ts @@ -1,6 +1,9 @@ import dayjs from 'dayjs' import duration from 'dayjs/plugin/duration' import relativeTime from 'dayjs/plugin/relativeTime' +import {TorrentState as QbitTorrentState} from "@/enums/qbit"; +import {TorrentState as VtTorrentState} from "@/enums/vuetorrent"; +import type {Torrent as QbitTorrent} from '@/types/qbit/models' dayjs.extend(duration) dayjs.extend(relativeTime) @@ -9,7 +12,45 @@ const durationFormat = 'D[d] H[h] m[m] s[s]' export default class Torrent { static computedValues = ['globalSpeed', 'globalVolume'] - constructor(data, format = 'DD/MM/YYYY, HH:mm:ss') { + + name: string + size: number + added_on: string + completed_on: string + dlspeed: number + dloaded: number + upspeed: number + uploaded: number + uploaded_session: number + eta: string + num_leechs: number + num_seeds: number + state: VtTorrentState + hash: string + available_seeds: number + available_peers: number + savePath: string + progress: number + ratio: number + tags: string[] | null + category: string + tracker: string + f_l_piece_prio: boolean + seq_dl: boolean + auto_tmm: boolean + dl_limit: number + up_limit: number + ratio_limit: number + ratio_time_limit: number + availability: number + forced: boolean + magnet: string + time_active: string + seeding_time: string | null + last_activity: string + globalSpeed: number + globalVolume: number + constructor(data: QbitTorrent, format = 'DD/MM/YYYY, HH:mm:ss') { this.name = data.name this.size = data.size this.added_on = dayjs(data.added_on * 1000).format(format) @@ -22,7 +63,6 @@ export default class Torrent { this.eta = this.formatEta(data.eta) this.num_leechs = data.num_leechs this.num_seeds = data.num_seeds - this.path = data.path === undefined ? '/downloads' : data.path this.state = this.formatState(data.state) this.hash = data.hash this.available_seeds = data.num_complete @@ -54,42 +94,41 @@ export default class Torrent { Object.freeze(this) } - formatState(state) { + formatState(state: QbitTorrentState): VtTorrentState { switch (state) { - case 'forcedDL': - case 'downloading': - return 'Downloading' - case 'metaDL': - return 'Metadata' - case 'forcedUP': - case 'uploading': - case 'stalledUP': - return 'Seeding' - case 'pausedDL': - return 'Paused' - case 'pausedUP': - return 'Done' - case 'queuedDL': - case 'queuedUP': - return 'Queued' - case 'allocating': - case 'checkingDL': - case 'checkingUP': - case 'checkingResumeData': - return 'Checking' - case 'moving': - return 'Moving' - case 'unknown': - case 'missingFiles': - return 'Fail' - case 'stalledDL': - return 'Stalled' + case QbitTorrentState.FORCED_DL: + case QbitTorrentState.DOWNLOADING: + return VtTorrentState.DOWNLOADING + case QbitTorrentState.META_DL: + return VtTorrentState.METADATA + case QbitTorrentState.FORCED_UP: + case QbitTorrentState.UPLOADING: + case QbitTorrentState.STALLED_UP: + return VtTorrentState.SEEDING + case QbitTorrentState.PAUSED_DL: + return VtTorrentState.PAUSED + case QbitTorrentState.PAUSED_UP: + return VtTorrentState.DONE + case QbitTorrentState.QUEUED_DL: + case QbitTorrentState.QUEUED_UP: + return VtTorrentState.QUEUED + case QbitTorrentState.ALLOCATING: + case QbitTorrentState.CHECKING_DL: + case QbitTorrentState.CHECKING_UP: + case QbitTorrentState.CHECKING_RESUME_DATA: + return VtTorrentState.CHECKING + case QbitTorrentState.MOVING: + return VtTorrentState.MOVING + case QbitTorrentState.UNKNOWN: + case QbitTorrentState.STALLED_DL: + return VtTorrentState.STALLED + case QbitTorrentState.MISSING_FILES: default: - return 'Fail' + return VtTorrentState.FAIL } } - formatEta(value) { + formatEta(value: number): string { const options = { dayLimit: 100 } const minute = 60 const hour = minute * 60 diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 00000000..0e967edb --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,4 @@ +import Status from './Status' +import Torrent from './Torrent' + +export {Status, Torrent} diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.ts similarity index 54% rename from src/plugins/vuetify.js rename to src/plugins/vuetify.ts index 5d528eec..bc036246 100644 --- a/src/plugins/vuetify.js +++ b/src/plugins/vuetify.ts @@ -1,4 +1,5 @@ import Vue from 'vue' +import type { Framework } from 'vuetify' import Vuetify from 'vuetify/lib' import { Ripple } from 'vuetify/lib/directives' @@ -46,45 +47,22 @@ export default new Vuetify({ background: colors.grey.lighten4, selected: colors.grey.lighten2, red: colors.red.accent2, - primary: '#35495e', - secondary: '#3e556d', - download: '#64CEAA', - upload: '#00b3fa', - // Torrent status colors - 'torrent-done': '#16573e', - 'torrent-downloading': '#5bb974', - 'torrent-fail': '#f83e70', - 'torrent-paused': '#9CA3AF', - 'torrent-queued': '#2e5eaa', - 'torrent-seeding': '#4ecde6', - 'torrent-checking': '#ff7043', - 'torrent-stalled': '#4ADE80', - 'torrent-metadata': '#7e57c2', - 'torrent-moving': '#ffaa2c', ...variables }, dark: { accent: '#64CEAA', - background: colors.black, + background: colors.shades.black, selected: colors.grey.darken1, red: colors.red.accent3, - primary: '#35495e', - secondary: '#3e556d', - download: '#64CEAA', - upload: '#00b3fa', - // Torrent status colors - 'torrent-done': '#16573e', - 'torrent-downloading': '#5bb974', - 'torrent-fail': '#f83e70', - 'torrent-paused': '#9CA3AF', - 'torrent-queued': '#2e5eaa', - 'torrent-seeding': '#4ecde6', - 'torrent-checking': '#ff7043', - 'torrent-stalled': '#4ADE80', - 'torrent-metadata': '#7e57c2', - 'torrent-moving': '#ffaa2c', ...variables } } } }) + +declare module 'vue/types/vue' { + // this.$vuetify inside Vue components + interface Vue { + $vuetify: Framework + } +} \ No newline at end of file diff --git a/src/router.js b/src/router.ts similarity index 100% rename from src/router.js rename to src/router.ts diff --git a/src/services/auth.js b/src/services/auth.ts similarity index 100% rename from src/services/auth.js rename to src/services/auth.ts diff --git a/src/services/qbit.ts b/src/services/qbit.ts index fdca77ca..0c6b1d47 100644 --- a/src/services/qbit.ts +++ b/src/services/qbit.ts @@ -1,4 +1,25 @@ -import axios, { AxiosInstance } from 'axios' +import axios from 'axios' +import type { AxiosInstance } from 'axios' +import type { + ApplicationVersion, + AppPreferences, + Category, + Feed, + FeedRule, + SearchJob, + SearchPlugin, + SearchStatus, + TorrentFile, + TorrentProperties, + Tracker, + Torrent +} from '@/types/qbit/models' +import type { MainDataResponse, SearchResultsResponse, TorrentPeersResponse } from '@/types/qbit/responses' +import type { AddTorrentPayload, AppPreferencesPayload, LoginPayload } from '@/types/qbit/payloads' +import type { SortOptions } from '@/types/vuetorrent' +import type { Priority } from '@/enums/qbit' + +type Parameters = Record<string, any> export class QBitApi { private axios: AxiosInstance @@ -11,92 +32,63 @@ export class QBitApi { this.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded' } - execute(method, action, params) { - if (method === 'post') { - const data = new URLSearchParams(params) - - return this.axios.post(action, data).then(res => res.data) - } + async execute(action: string, params?: Parameters): Promise<any> { + const data = new URLSearchParams(params) + return this.axios.post(action, data).then(res => res.data) } /** Begin General functions * */ - getAppVersion(): Promise<string> { + async getAppVersion(): Promise<ApplicationVersion> { return this.axios - .get('/app/version') - .then(res => res.data) - .then(version => (version.includes('v') ? version.substring(1) : version)) + .get('/app/version') + .then(res => res.data) + .then(version => (version.includes('v') ? version.substring(1) : version)) } - getApiVersion() { - return this.axios.get('/app/webapiVersion') - } - - async login(params) { + async login(params: LoginPayload): Promise<string> { const payload = new URLSearchParams(params) - const { data } = await this.axios - .post('/auth/login', payload, { - validateStatus(status) { - return status === 200 || status === 403 - } - }) - .catch(err => console.log(err)) + const res = await this.axios + .post('/auth/login', payload, { + validateStatus: (status: number) => status === 200 || status === 403 + }) + .catch(err => console.log(err)) - return data + return res?.data } - async getAuthenticationStatus() { + async getAuthenticationStatus(): Promise<boolean> { return this.axios - .get('/app/version') - .then(() => true) - .catch(() => false) + .get('/app/version') + .then(() => true) + .catch(() => false) } - async logout() { - this.axios.post('/auth/logout') + async logout(): Promise<void> { + await this.axios.post('/auth/logout') } - getGlobalTransferInfo() { - return this.axios.get('/transfer/info') + async getAppPreferences(): Promise<AppPreferences> { + return this.axios.get('/app/preferences').then(r => r.data) } - getAppPreferences() { - return this.axios.get('/app/preferences') - } - - setPreferences(params) { - const data = new URLSearchParams({ + async setPreferences(params: AppPreferencesPayload): Promise<void> { + const data = { json: JSON.stringify(params) - }) + } - return this.axios.post('/app/setPreferences', data) + await this.execute('/app/setPreferences', data) } - getMainData(rid) { + async getMainData(rid?: number): Promise<MainDataResponse> { return this.axios.get('/sync/maindata', { params: { rid } }).then(res => res.data) } - switchToOldUi() { - return this.setPreferences({ - alternative_webui_enabled: false - }) + async toggleSpeedLimitsMode(): Promise<void> { + await this.execute('/transfer/toggleSpeedLimitsMode') } - toggleSpeedLimitsMode() { - return this.axios.post('/transfer/toggleSpeedLimitsMode') - } - - /** Begin Torrent functions * */ - - // Get - - getLogs(lastId) { - return this.axios.get('/log/main', { - last_known_id: lastId - }) - } - - getTorrents(payload) { - const params = { + async getTorrents(payload: SortOptions): Promise<Torrent[]> { + const params: Parameters = { sort: !payload.isCustomSortEnabled ? payload.sort : null, reverse: !payload.isCustomSortEnabled ? payload.reverse : null, hashes: payload.hashes.length > 0 ? payload.hashes.join('|') : null, @@ -110,104 +102,111 @@ export class QBitApi { const data = new URLSearchParams(params) - return this.axios.get(`/torrents/info?${data.toString()}`) + return this.axios.get(`/torrents/info?${data.toString()}`).then(r => r.data) } - getTorrentTrackers(hash) { - return this.axios.get('/torrents/trackers', { - params: { hash } - }) + async getTorrentTrackers(hash: string): Promise<Tracker[]> { + return this.axios + .get('/torrents/trackers', { + params: { hash } + }) + .then(r => r.data) } - getTorrentPeers(hash, rid) { + async getTorrentPeers(hash: string, rid?: number): Promise<TorrentPeersResponse> { return this.axios.get('/sync/torrentPeers', { params: { hash, rid } }) } - setTorrentName(hash, name) { - return this.execute('post', '/torrents/rename', { hash, name }) + async setTorrentName(hash: string, name: string): Promise<void> { + await this.execute('/torrents/rename', { hash, name }) } - getTorrentPieceStates(hash) { - return this.axios.get('/torrents/pieceStates', { - params: { hash } - }) + async getTorrentPieceStates(hash: string): Promise<number[]> { + return this.axios + .get('/torrents/pieceStates', { + params: { hash } + }) + .then(res => res.data) } - getTorrentFiles(hash) { - return this.axios.get('/torrents/files', { - params: { hash } - }) + async getTorrentFiles(hash: string, indexes?: number[]): Promise<TorrentFile[]> { + return this.axios + .get('/torrents/files', { + params: { hash, indexes: indexes?.join('|') } + }) + .then(res => res.data) } - getAvailableTags() { + async getAvailableTags(): Promise<string[]> { return this.axios.get('/torrents/tags').then(res => res.data) } - getTorrentProperties(hash) { + async getTorrentProperties(hash: string): Promise<TorrentProperties> { return this.axios - .get('/torrents/properties', { - params: { hash } - }) - .then(res => res.data) + .get('/torrents/properties', { + params: { hash } + }) + .then(res => res.data) } // RSS - createFeed(feed) { - return this.execute('post', '/rss/addFeed', { - url: feed.url, - path: feed.url + async createFeed(url: string, path?: string): Promise<void> { + await this.execute('/rss/addFeed', { + url: url, + path: path }) } - createRule(ruleName, defs) { - return this.execute('post', '/rss/setRule', { + async createRule(ruleName: string, ruleDef: FeedRule) { + return this.execute('/rss/setRule', { ruleName: ruleName, - ruleDef: JSON.stringify(defs) + ruleDef: JSON.stringify(ruleDef, ['enabled', 'mustContain', 'mustNotContain', 'useRegex', 'affectedFeeds']) }) } - getFeeds() { - return this.axios - .get('/rss/items') - .then(res => res.data) - .then(data => - Object.entries(data).map(feed => { - return { name: feed[0], ...feed[1] } - }) - ) + async getFeeds(): Promise<Record<string, Feed>> { + return this.axios.get('/rss/items').then(res => res.data) } - getRules() { - return this.axios - .get('/rss/rules') - .then(res => res.data) - .then(data => - Object.entries(data).map(rule => { - return { name: rule[0], ...rule[1] } - }) - ) + async getRules(): Promise<Record<string, FeedRule>> { + return this.axios.get('/rss/rules').then(res => res.data) } - deleteRule(ruleName) { - return this.execute('post', 'rss/removeRule', { + async editFeed(itemPath: string, destPath: string): Promise<void> { + await this.execute('/rss/moveItem', { + itemPath, + destPath + }) + } + + async editRule(ruleName: string, newRuleName: string): Promise<void> { + await this.execute('/rss/renameRule', { + ruleName, + newRuleName + }) + } + + async deleteRule(ruleName: string): Promise<void> { + await this.execute('rss/removeRule', { ruleName }) } - deleteFeed(name) { - return this.execute('post', 'rss/removeItem', { + async deleteFeed(name: string): Promise<void> { + await this.execute('rss/removeItem', { path: name }) } // Post - addTorrents(params, torrents) { + async addTorrents(params: AddTorrentPayload, torrents: File[]): Promise<void> { let data if (torrents) { + // torrent files const formData = new FormData() if (params) { for (const [key, value] of Object.entries(params)) { @@ -221,283 +220,269 @@ export class QBitApi { data = formData } else { - data = new URLSearchParams(params) + // magnet links + data = new URLSearchParams(params as Parameters) } - return this.axios.post('/torrents/add', data) + await this.axios.post('/torrents/add', data) } - setTorrentFilePriority(hash, idList, priority) { + async setTorrentFilePriority(hash: string, idList: number[], priority: Priority): Promise<void> { const params = { hash, id: idList.join('|'), priority } - return this.execute('post', '/torrents/filePrio', params) + await this.execute('/torrents/filePrio', params) } - deleteTorrents(hashes, deleteFiles) { + async deleteTorrents(hashes: string[], deleteFiles: boolean): Promise<void> { if (!hashes.length) return - return this.torrentAction('delete', hashes, { deleteFiles }) + await this.torrentAction('delete', hashes, { deleteFiles }) } - pauseTorrents(hashes) { - return this.torrentAction('pause', hashes) + async pauseTorrents(hashes: string[]): Promise<void> { + await this.torrentAction('pause', hashes) } - resumeTorrents(hashes) { - return this.torrentAction('resume', hashes) + async resumeTorrents(hashes: string[]): Promise<void> { + await this.torrentAction('resume', hashes) } - forceStartTorrents(hashes) { - return this.torrentAction('setForceStart', hashes, { value: true }) + async forceStartTorrents(hashes: string[]): Promise<void> { + await this.torrentAction('setForceStart', hashes, { value: true }) } - toggleSequentialDownload(hashes) { - return this.torrentAction('toggleSequentialDownload', hashes) + async toggleSequentialDownload(hashes: string[]): Promise<void> { + await this.torrentAction('toggleSequentialDownload', hashes) } - toggleFirstLastPiecePriority(hashes) { - return this.torrentAction('toggleFirstLastPiecePrio', hashes) + async toggleFirstLastPiecePriority(hashes: string[]): Promise<void> { + await this.torrentAction('toggleFirstLastPiecePrio', hashes) } - setAutoTMM(hashes, enable) { - return this.torrentAction('setAutoManagement', hashes, { enable }) + async setAutoTMM(hashes: string[], enable: boolean): Promise<void> { + await this.torrentAction('setAutoManagement', hashes, { enable }) } - setDownloadLimit(hashes, limit) { - return this.torrentAction('setDownloadLimit', hashes, { limit }) + async setDownloadLimit(hashes: string[], limit: number): Promise<void> { + await this.torrentAction('setDownloadLimit', hashes, { limit }) } - setUploadLimit(hashes, limit) { - return this.torrentAction('setUploadLimit', hashes, { limit }) + async setUploadLimit(hashes: string[], limit: number): Promise<void> { + await this.torrentAction('setUploadLimit', hashes, { limit }) } - async getGlobalDownloadLimit() { - const { data } = await this.axios.get('/transfer/downloadLimit') - - return data + /** + * @return current global download speed limit in bytes/second; this value will be zero if no limit is applied. + */ + async getGlobalDownloadLimit(): Promise<number> { + return this.axios.get('/transfer/downloadLimit').then(res => res.data) } - async getGlobalUploadLimit() { - const { data } = await this.axios.get('/transfer/uploadLimit') - - return data + /** + * @return current global upload speed limit in bytes/second; this value will be zero if no limit is applied. + */ + async getGlobalUploadLimit(): Promise<number> { + return this.axios.get('/transfer/uploadLimit').then(res => res.data) } - setGlobalDownloadLimit(limit) { - const formData = new FormData() - formData.append('limit', limit) + /** + * @param limit - The global download speed limit to set in bytes/second + */ + async setGlobalDownloadLimit(limit: number): Promise<void> { + const data = { + limit + } - return this.axios.post('/transfer/setDownloadLimit', formData) + await this.execute('/transfer/setDownloadLimit', data) } - setGlobalUploadLimit(limit) { - const formData = new FormData() - formData.append('limit', limit) + /** + * @param limit - The global upload speed limit to set in bytes/second + */ + async setGlobalUploadLimit(limit: number): Promise<void> { + const data = { + limit + } - return this.axios.post('/transfer/setUploadLimit', formData) + await this.execute('/transfer/setUploadLimit', data) } - setShareLimit(hashes, ratioLimit, seedingTimeLimit) { - return this.torrentAction('setShareLimits', hashes, { + async setShareLimit(hashes: string[], ratioLimit: number, seedingTimeLimit: number): Promise<void> { + await this.torrentAction('setShareLimits', hashes, { ratioLimit, seedingTimeLimit }) } - reannounceTorrents(hashes) { - return this.torrentAction('reannounce', hashes) + async reannounceTorrents(hashes: string[]): Promise<void> { + await this.torrentAction('reannounce', hashes) } - recheckTorrents(hashes) { - return this.torrentAction('recheck', hashes) + async recheckTorrents(hashes: string[]): Promise<void> { + await this.torrentAction('recheck', hashes) } - setTorrentsCategory(hashes, category) { - return this.torrentAction('setCategory', hashes, { category }) + async setTorrentLocation(hashes: string[], location: string): Promise<void> { + await this.torrentAction('setLocation', hashes, { location }) } - editTracker(hash, origUrl, newUrl) { - return this.torrentAction('editTracker', [hash], { origUrl, newUrl }) + async addTorrentTrackers(hash: string, trackers: string): Promise<void> { + await this.torrentAction('addTrackers', [hash], { urls: trackers }) } - setTorrentLocation(hashes, location) { - return this.torrentAction('setLocation', hashes, { location }) + async removeTorrentTrackers(hash: string, trackers: string[]): Promise<void> { + await this.torrentAction('removeTrackers', [hash], { urls: trackers.join('|') }) } - addTorrenTrackers(hash, trackers) { - const params = { - hash, - urls: trackers - } - - return this.execute('post', '/torrents/addTrackers', params) + async addTorrentPeers(hashes: string[], peers: string[]): Promise<void> { + await this.torrentAction('addPeers', hashes, { peers: peers.join('|') }) } - removeTorrentTrackers(hash, trackers) { - const params = { - hash, - urls: trackers.join('|') - } - - return this.execute('post', '/torrents/removeTrackers', params) - } - - addTorrentPeers(hashes: Array<string>, peers: Array<string>) { - const params = { - hashes: hashes.join('|'), - peers: peers.join('|') - } - - return this.execute('post', '/torrents/addPeers', params) - } - - banPeers(peers: Array<string>) { + async banPeers(peers: string[]): Promise<void> { const params = { peers: peers.join('|') } - return this.execute('post', '/transfer/banPeers', params) + await this.execute('/transfer/banPeers', params) } - torrentAction(action, hashes, extra) { + async torrentAction(action: string, hashes: string[], extra?: Record<string, any>): Promise<any> { const params = { hashes: hashes.length ? hashes.join('|') : 'all', ...extra } - return this.execute('post', `/torrents/${action}`, params) + return this.execute(`/torrents/${action}`, params) } - renameFile(hash, oldPath, newPath) { + async renameFile(hash: string, oldPath: string, newPath: string): Promise<void> { const params = { hash, oldPath, newPath } - return this.execute('post', '/torrents/renameFile', params) + await this.execute('/torrents/renameFile', params) } - renameFolder(hash, oldPath, newPath) { + async renameFolder(hash: string, oldPath: string, newPath: string): Promise<void> { const params = { hash, oldPath, newPath } - return this.execute('post', '/torrents/renameFolder', params) + await this.execute('/torrents/renameFolder', params) } /** Torrent Priority **/ - setTorrentPriority(hashes, priority) { - if (['increasePrio', 'decreasePrio', 'topPrio', 'bottomPrio'].includes(priority)) { - return this.execute('post', `/torrents/${priority}`, { - hashes: hashes.join('|') - }) - } + async setTorrentPriority(hashes: string[], priority: 'increasePrio' | 'decreasePrio' | 'topPrio' | 'bottomPrio'): Promise<void> { + await this.execute(`/torrents/${priority}`, { + hashes: hashes.join('|') + }) } /** Begin Torrent Tags **/ - removeTorrentTag(hashes, tag) { - return this.execute('post', '/torrents/removeTags', { - hashes: hashes.join('|'), - tags: tag + async removeTorrentTag(hashes: string[], tags: string[]): Promise<void> { + await this.torrentAction('removeTags', hashes, { tags: tags.join('|') }) + } + + async addTorrentTag(hashes: string[], tags: string[]): Promise<void> { + await this.torrentAction('addTags', hashes, { tags: tags.join('|') }) + } + + async createTag(tags: string[]): Promise<void> { + await this.execute('/torrents/createTags', { + tags: tags.join(',') }) } - addTorrentTag(hashes, tag) { - return this.execute('post', '/torrents/addTags ', { - hashes: hashes.join('|'), - tags: tag - }) - } - - createTag(tag) { - return this.execute('post', '/torrents/createTags ', { - tags: tag - }) - } - - deleteTag(tag) { - return this.execute('post', '/torrents/deleteTags', { - tags: tag + async deleteTag(tags: string[]): Promise<void> { + await this.execute('/torrents/deleteTags', { + tags: tags.join(',') }) } /** Begin Categories **/ - getCategories() { - return this.axios.get('/torrents/categories').then(res => res.data) + async getCategories(): Promise<Category[]> { + return this.axios + .get('/torrents/categories') + .then(res => res.data) + .then(data => Object.values(data)) } - deleteCategory(categories) { - return this.execute('post', '/torrents/removeCategories', { - categories + async deleteCategory(categories: string[]): Promise<void> { + await this.execute('/torrents/removeCategories', { + categories: categories.join('\n') }) } - createCategory(cat) { - return this.execute('post', '/torrents/createCategory', { + async createCategory(cat: Category): Promise<void> { + await this.execute('/torrents/createCategory', { category: cat.name, savePath: cat.savePath }) } - setCategory(hashes, category) { - return this.torrentAction('setCategory', hashes, { category }) + async setCategory(hashes: string[], category: string): Promise<void> { + await this.torrentAction('setCategory', hashes, { category }) } - editCategory(cat) { + async editCategory(cat: Category): Promise<void> { const params = { category: cat.name, savePath: cat.savePath } - return this.execute('post', '/torrents/editCategory', params) + await this.execute('/torrents/editCategory', params) } /** Search **/ - getSearchPlugins() { + async getSearchPlugins(): Promise<SearchPlugin[]> { return this.axios.get('/search/plugins').then(res => res.data) } - updateSearchPlugins() { - return this.execute('post', '/search/updatePlugins') + async updateSearchPlugins(): Promise<void> { + await this.execute('/search/updatePlugins') } - enableSearchPlugin(plugins, enable) { + async enableSearchPlugin(pluginNames: string[], enable: boolean): Promise<void> { const params = { - names: plugins.join('|'), + names: pluginNames.join('|'), enable } - return this.execute('post', '/search/enablePlugin', params) + await this.execute('/search/enablePlugin', params) } - startSearch(pattern, plugins) { + async startSearch(pattern: string, plugins: string[]): Promise<SearchJob> { const params = { pattern, - plugins: Array.isArray(plugins) ? plugins.join('|') : 'all', + plugins: plugins.length ? plugins.join('|') : 'enabled', category: 'all' } - return this.execute('post', '/search/start', params) + return this.execute('/search/start', params) } - stopSearch(id) { - return this.execute('post', '/search/stop', { id }) + async stopSearch(id: number): Promise<void> { + await this.execute('/search/stop', { id }) } - getSearchStatus(id) { - return this.execute('post', '/search/status', { id }) + async getSearchStatus(id?: number): Promise<SearchStatus[]> { + const params = id !== undefined ? { id } : undefined + return this.execute('/search/status', params) } - getSearchResults(id) { - return this.execute('post', '/search/results', { - id + async getSearchResults(id: number, limit?: number, offset?: number): Promise<SearchResultsResponse> { + return this.execute('/search/results', { + id, + limit, + offset }) } } diff --git a/src/store/actions.js b/src/store/actions.js deleted file mode 100644 index b0290d2f..00000000 --- a/src/store/actions.js +++ /dev/null @@ -1,34 +0,0 @@ -import Vue from 'vue' -import qbit from '../services/qbit' -import { i18n } from '@/plugins/i18n' - -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(i18n.t('toast.loginSuccess')) - context.commit('LOGIN', true) - context.commit('updateMainData') - await context.dispatch('FETCH_SETTINGS') - context.commit('FETCH_CATEGORIES') - context.commit('FETCH_TAGS') - - return true - } - Vue.$toast.error(i18n.t('toast.loginFailed')) - - return false - }, - FETCH_SETTINGS: async context => { - const { data } = await qbit.getAppPreferences() - context.commit('FETCH_SETTINGS', data) - - return data - } -} diff --git a/src/store/actions.ts b/src/store/actions.ts new file mode 100644 index 00000000..71d9c0b1 --- /dev/null +++ b/src/store/actions.ts @@ -0,0 +1,37 @@ +import Vue from 'vue' +import qbit from '@/services/qbit' +import { i18n } from '@/plugins/i18n' +import type { StoreState } from '@/types/vuetorrent' +import type { Store } from 'vuex' +import type { LoginPayload } from '@/types/qbit/payloads' + +export default { + INIT_INTERVALS: async (store: Store<StoreState>) => { + store.state.intervals[0] = setInterval(() => { + store.commit('updateMainData') + }, 2000) + }, + LOGIN: async (store: Store<StoreState>, payload: LoginPayload) => { + const res = await qbit.login(payload) + console.log(res) + if (res === 'Ok.') { + Vue.$toast.success(i18n.t('toast.loginSuccess').toString()) + store.commit('LOGIN', true) + store.commit('updateMainData') + await store.dispatch('FETCH_SETTINGS') + store.commit('FETCH_CATEGORIES') + store.commit('FETCH_TAGS') + + return true + } + Vue.$toast.error(i18n.t('toast.loginFailed').toString()) + + return false + }, + FETCH_SETTINGS: async (store: Store<StoreState>) => { + const data = await qbit.getAppPreferences() + store.commit('FETCH_SETTINGS', data) + + return data + } +} diff --git a/src/store/getters.js b/src/store/getters.js deleted file mode 100644 index 12604d5b..00000000 --- a/src/store/getters.js +++ /dev/null @@ -1,29 +0,0 @@ -import { i18n } from '@/plugins/i18n' - -export default { - getAppVersion: state => () => state.version, - containsTorrent: state => hash => state.selected_torrents.includes(hash), - isDarkMode: state => () => state.webuiSettings.darkTheme, - getTheme: state => () => state.webuiSettings.darkTheme ? 'dark' : 'light', - getModalState: state => guid => state.modals.filter(m => m.guid === guid)[0], - 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.tags, - getCategories: state => () => state.categories, - getFeeds: state => () => state.rss.feeds, - getRules: state => () => state.rss.rules, - getModals: state => () => state.modals, - getTorrents: state => () => state.torrents, - getTrackers: state => () => state.trackers, - getAuthenticated: state => () => state.authenticated, - getTorrentCountString: state => () => { - if (state.selected_torrents && state.selected_torrents.length) { - return `${state.selected_torrents.length} ${i18n.t('of')} ${i18n.tc('navbar.torrentsCount', state.filteredTorrentsCount)}` - } - - return i18n.tc('navbar.torrentsCount', state.filteredTorrentsCount) - }, - getSearchPlugins: state => () => state.searchPlugins -} diff --git a/src/store/getters.ts b/src/store/getters.ts new file mode 100644 index 00000000..eb7ea3cb --- /dev/null +++ b/src/store/getters.ts @@ -0,0 +1,30 @@ +import { i18n } from '@/plugins/i18n' +import type { StoreState } from '@/types/vuetorrent' + +export default { + getAppVersion: (state: StoreState) => () => state.version, + containsTorrent: (state: StoreState) => (hash: string) => state.selected_torrents.includes(hash), + isDarkMode: (state: StoreState) => () => state.webuiSettings.darkTheme, + getTheme: (state: StoreState) => () => state.webuiSettings.darkTheme ? 'dark' : 'light', + getModalState: (state: StoreState) => (guid: string) => state.modals.filter(m => m.guid === guid)[0], + getSettings: (state: StoreState) => () => state.settings, + getStatus: (state: StoreState) => () => state.status, + getTorrent: (state: StoreState) => (hash: string) => state.torrents.filter(el => el.hash === hash)[0], + getWebuiSettings: (state: StoreState) => () => state.webuiSettings, + getAvailableTags: (state: StoreState) => () => state.tags, + getCategories: (state: StoreState) => () => state.categories, + getFeeds: (state: StoreState) => () => state.rss.feeds, + getRules: (state: StoreState) => () => state.rss.rules, + getModals: (state: StoreState) => () => state.modals, + getTorrents: (state: StoreState) => () => state.torrents, + getTrackers: (state: StoreState) => () => state.trackers, + getAuthenticated: (state: StoreState) => () => state.authenticated, + getTorrentCountString: (state: StoreState) => () => { + if (state.selected_torrents && state.selected_torrents.length) { + return `${state.selected_torrents.length} ${i18n.t('of')} ${i18n.tc('navbar.torrentsCount', state.filteredTorrentsCount)}` + } + + return i18n.tc('navbar.torrentsCount', state.filteredTorrentsCount) + }, + getSearchPlugins: (state: StoreState) => () => state.searchPlugins +} diff --git a/src/store/index.js b/src/store/index.ts similarity index 81% rename from src/store/index.js rename to src/store/index.ts index a974d611..b91d8258 100644 --- a/src/store/index.js +++ b/src/store/index.ts @@ -4,8 +4,11 @@ import VuexPersist from 'vuex-persist' import actions from './actions' import getters from './getters' import mutations from './mutations' +import type { StoreState } from '@/types/vuetorrent' +import { Status } from '@/models' +import {TitleOptions} from "@/enums/vuetorrent"; -const vuexPersist = new VuexPersist({ +const vuexPersist = new VuexPersist<StoreState>({ key: 'vuetorrent', storage: window.localStorage, reducer: state => ({ @@ -17,6 +20,7 @@ const vuexPersist = new VuexPersist({ Vue.use(Vuex) +// noinspection DuplicatedCode const propertiesTemplate = [ { name: 'Size', active: true }, { name: 'Progress', active: true }, @@ -40,32 +44,29 @@ const propertiesTemplate = [ { name: 'GlobalVolume', active: false } ] -export default new Vuex.Store({ +export default new Vuex.Store<StoreState>({ plugins: [vuexPersist.plugin], state: { - version: 0, + authenticated: false, + categories: [], + dashboard: { + currentPage: 1, + searchFilter: '' + }, + download_data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + filteredTorrentsCount: 0, intervals: [], + latestSelectedTorrent: -1, + modals: [], + rid: 0, rss: { feeds: [], rules: [] }, - status: { - status: '', - downloaded: '', - uploaded: '', - dlspeed: '', - upspeed: '', - freeDiskSpace: '', - altSpeed: '', - dlspeedRaw: '', - upspeedRaw: '', - tags: '' - }, - 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: [], + searchPlugins: [], + selectMode: false, selected_torrents: [], - authenticated: false, + settings: null, sort_options: { isCustomSortEnabled: false, sort: 'priority', @@ -76,10 +77,12 @@ export default new Vuex.Store({ tag: null, tracker: null }, - rid: 0, - pasteUrl: null, - modals: [], - settings: {}, + status: new Status(), + tags: [], + torrents: [], + trackers: [], + upload_data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + version: '', webuiSettings: { lang: 'en', darkTheme: false, @@ -91,27 +94,17 @@ export default new Vuex.Store({ showTrackerFilter: false, showSpeedInTitle: false, deleteWithFiles: false, - title: 'Default', + title: TitleOptions.DEFAULT, rightDrawer: false, topPagination: false, paginationSize: 15, dateFormat: 'DD/MM/YYYY, HH:mm:ss', openSideBarOnStart: true, - busyTorrentProperties: JSON.parse(JSON.stringify(propertiesTemplate)), - doneTorrentProperties: JSON.parse(JSON.stringify(propertiesTemplate)) - }, - categories: [], - trackers: [], - tags: [], - filteredTorrentsCount: 0, - latestSelectedTorrent: null, - selectMode: false, - searchPlugins: [], - dashboard: { - currentPage: 1, - searchFilter: '' + busyTorrentProperties: [...propertiesTemplate], + doneTorrentProperties: [...propertiesTemplate] } }, + // @ts-expect-error actions: { ...actions }, diff --git a/src/store/mutations.js b/src/store/mutations.js deleted file mode 100644 index 1267da03..00000000 --- a/src/store/mutations.js +++ /dev/null @@ -1,84 +0,0 @@ -import qbit from '../services/qbit' -import { DocumentTitle, Tags, Trackers, Torrents, Graph, ServerStatus } from '@/actions' -import { setLanguage } from '@/plugins/i18n' -import Torrent from "@/models/Torrent"; - -export default { - SET_APP_VERSION(state, version) { - state.version = version - }, - REMOVE_INTERVALS: state => { - state.intervals.forEach(el => clearInterval(el)) - }, - ADD_MODAL(state, modal) { - state.modals.push(modal) - }, - DELETE_MODAL(state, guid) { - state.modals = state.modals.filter(m => m.guid !== guid) - }, - SET_SELECTED: (state, { type, hash, index }) => { - if (type === 'add') { - state.selected_torrents.push(hash) - state.latestSelectedTorrent = state.torrents.map(t => t.hash).indexOf(hash) - } else if (type === 'remove') { - state.selected_torrents.splice(state.selected_torrents.indexOf(hash), 1) - } else if (type === 'until') { - let from - let until - if (state.latestSelectedTorrent > index) { - from = index - until = state.latestSelectedTorrent + 1 // include latest selected - } else { - from = state.latestSelectedTorrent - until = index + 1 - } - state.selected_torrents = state.torrents.map(t => t.hash).slice(from, until) - } - }, - RESET_SELECTED: state => { - state.selected_torrents = [] - }, - TOGGLE_THEME(state) { - state.webuiSettings.darkTheme = !state.webuiSettings.darkTheme - }, - LOGOUT: async state => { - await qbit.logout() - state.authenticated = false - }, - LOGIN: async (state, payload) => { - state.authenticated = payload - }, - updateMainData: async state => { - const response = await qbit.getMainData(state.rid || undefined) - state.rid = response.rid || undefined - - ServerStatus.update(response) - Tags.update(response) - Graph.update() - - // fetch torrent data - state.sort_options.isCustomSortEnabled = Torrent.computedValues.indexOf(state.sort_options.sort) !== -1 - const { data } = await qbit.getTorrents(state.sort_options) - - Trackers.update(data) - Torrents.update(data) - DocumentTitle.update() - }, - FETCH_SETTINGS: async (state, settings) => { - state.settings = settings - }, - UPDATE_SORT_OPTIONS: (state, { hashes = [], filter = null, category = null, tag = null, tracker = null }) => { - state.sort_options.hashes = hashes - state.sort_options.filter = filter - state.sort_options.category = category - state.sort_options.tag = tag - state.sort_options.tracker = tracker - }, - FETCH_CATEGORIES: async state => (state.categories = Object.values(await qbit.getCategories())), - FETCH_TAGS: async state => (state.tags = await qbit.getAvailableTags()), - FETCH_FEEDS: async state => (state.rss.feeds = await qbit.getFeeds()), - FETCH_RULES: async state => (state.rss.rules = await qbit.getRules()), - FETCH_SEARCH_PLUGINS: async state => (state.searchPlugins = await qbit.getSearchPlugins()), - SET_CURRENT_ITEM_COUNT: (state, count) => (state.filteredTorrentsCount = count), - SET_LANGUAGE: async state => setLanguage(state.webuiSettings.lang) -} diff --git a/src/store/mutations.ts b/src/store/mutations.ts new file mode 100644 index 00000000..6c94655d --- /dev/null +++ b/src/store/mutations.ts @@ -0,0 +1,86 @@ +import qbit from '../services/qbit' +import { DocumentTitle, Tags, Trackers, Torrents, Graph, ServerStatus } from '@/actions' +import { setLanguage } from '@/plugins/i18n' +import type { ModalTemplate, StoreState } from '@/types/vuetorrent' +import Torrent from '@/models/Torrent' +import type { AppPreferences } from '@/types/qbit/models' + +export default { + SET_APP_VERSION(state: StoreState, version: string) { + state.version = version + }, + REMOVE_INTERVALS: (state: StoreState) => { + state.intervals.forEach(el => clearInterval(el)) + }, + ADD_MODAL(state: StoreState, modal: ModalTemplate) { + state.modals.push(modal) + }, + DELETE_MODAL(state: StoreState, guid: string) { + state.modals = state.modals.filter(m => m.guid !== guid) + }, + SET_SELECTED: (state: StoreState, data: { type: string; hash: string; index: number }) => { + const { type, hash, index } = data + if (type === 'add') { + state.selected_torrents.push(hash) + state.latestSelectedTorrent = state.torrents.map(t => t.hash).indexOf(hash) + } else if (type === 'remove') { + state.selected_torrents.splice(state.selected_torrents.indexOf(hash), 1) + } else if (type === 'until') { + let from, until + if (state.latestSelectedTorrent > index) { + from = index + until = state.latestSelectedTorrent + 1 // include latest selected + } else { + from = state.latestSelectedTorrent + until = index + 1 + } + state.selected_torrents = state.torrents.map(t => t.hash).slice(from, until) + } + }, + RESET_SELECTED: (state: StoreState) => { + state.selected_torrents = [] + }, + TOGGLE_THEME(state: StoreState) { + state.webuiSettings.darkTheme = !state.webuiSettings.darkTheme + }, + LOGOUT: async (state: StoreState) => { + await qbit.logout() + state.authenticated = false + }, + LOGIN: async (state: StoreState, payload: boolean) => { + state.authenticated = payload + }, + updateMainData: async (state: StoreState) => { + const response = await qbit.getMainData(state.rid || undefined) + state.rid = response.rid || undefined + + ServerStatus.update(response.server_state) + Tags.update(response) + Graph.shiftValues() + + // fetch torrent data + state.sort_options.isCustomSortEnabled = Torrent.computedValues.indexOf(state.sort_options.sort) !== -1 + const data = await qbit.getTorrents(state.sort_options) + + Trackers.update(data) + Torrents.update(data) + DocumentTitle.update() + }, + FETCH_SETTINGS: async (state: StoreState, settings: AppPreferences) => { + state.settings = settings + }, + UPDATE_SORT_OPTIONS: (state: StoreState, { hashes = [], filter = null, category = null, tag = null, tracker = null }) => { + state.sort_options.hashes = hashes + state.sort_options.filter = filter + state.sort_options.category = category + state.sort_options.tag = tag + state.sort_options.tracker = tracker + }, + FETCH_CATEGORIES: async (state: StoreState) => (state.categories = Object.values(await qbit.getCategories())), + FETCH_TAGS: async (state: StoreState) => (state.tags = await qbit.getAvailableTags()), + FETCH_FEEDS: async (state: StoreState) => (state.rss.feeds = Object.entries(await qbit.getFeeds()).map(([key, value]) => ({ name: key, ...value }))), + FETCH_RULES: async (state: StoreState) => (state.rss.rules = Object.entries(await qbit.getRules()).map(([key, value]) => ({ name: key, ...value }))), + FETCH_SEARCH_PLUGINS: async (state: StoreState) => (state.searchPlugins = await qbit.getSearchPlugins()), + SET_CURRENT_ITEM_COUNT: (state: StoreState, count: number) => (state.filteredTorrentsCount = count), + SET_LANGUAGE: async (state: StoreState) => setLanguage(state.webuiSettings.lang) +} diff --git a/src/types/mappers/FeedRuleMapper.ts b/src/types/mappers/FeedRuleMapper.ts new file mode 100644 index 00000000..436adfe6 --- /dev/null +++ b/src/types/mappers/FeedRuleMapper.ts @@ -0,0 +1,39 @@ +import type {FeedRule as QbitFeedRule} from '@/types/qbit/models' +import type {FeedRule as VtFeedRule} from '@/types/vuetorrent' +import type {BaseMapper} from "@/types/mappers/index"; + +export default class FeedRuleMapper implements BaseMapper<QbitFeedRule, VtFeedRule> { + public toQbit(rule: VtFeedRule): QbitFeedRule { + return <QbitFeedRule>{ + addPaused: rule.addPaused, + affectedFeeds: rule.affectedFeeds, + assignedCategory: rule.assignedCategory, + enabled: rule.enabled, + episodeFilter: rule.episodeFilter, + ignoreDays: rule.ignoreDays, + lastMatch: rule.lastMatch, + mustContain: rule.mustContain, + mustNotContain: rule.mustNotContain, + savePath: rule.savePath, + smartFilter: rule.smartFilter, + useRegex: rule.useRegex + } + } + + public toVt(rule: QbitFeedRule): VtFeedRule { + return <VtFeedRule>{ + addPaused: rule.addPaused, + affectedFeeds: rule.affectedFeeds, + assignedCategory: rule.assignedCategory, + enabled: rule.enabled, + episodeFilter: rule.episodeFilter, + ignoreDays: rule.ignoreDays, + lastMatch: rule.lastMatch, + mustContain: rule.mustContain, + mustNotContain: rule.mustNotContain, + savePath: rule.savePath, + smartFilter: rule.smartFilter, + useRegex: rule.useRegex + } + } +} \ No newline at end of file diff --git a/src/types/mappers/TrackerMapper.ts b/src/types/mappers/TrackerMapper.ts new file mode 100644 index 00000000..b409c171 --- /dev/null +++ b/src/types/mappers/TrackerMapper.ts @@ -0,0 +1,32 @@ +import type {Tracker as QbitTracker} from '@/types/qbit/models' +import type {Tracker as VtTracker} from '@/types/vuetorrent' +import type {BaseMapper} from "@/types/mappers/index" + +export default class TrackerMapper implements BaseMapper<QbitTracker, VtTracker> { + public toVt(tracker: QbitTracker): VtTracker { + return { + isSelectable: tracker.tier >= 0, + msg: tracker.msg, + num_downloaded: tracker.num_downloaded, + num_leeches: tracker.num_leeches, + num_peers: tracker.num_peers, + num_seeds: tracker.num_seeds, + status: tracker.status, + tier: tracker.tier, + url: tracker.url + } + } + + toQbit(tracker: VtTracker): QbitTracker { + return { + msg: tracker.msg, + num_downloaded: tracker.num_downloaded, + num_leeches: tracker.num_leeches, + num_peers: tracker.num_peers, + num_seeds: tracker.num_seeds, + status: tracker.status, + tier: tracker.tier, + url: tracker.url + } + } +} \ No newline at end of file diff --git a/src/types/mappers/index.ts b/src/types/mappers/index.ts new file mode 100644 index 00000000..d326c808 --- /dev/null +++ b/src/types/mappers/index.ts @@ -0,0 +1,9 @@ +import FeedRuleMapper from "./FeedRuleMapper" +import TrackerMapper from "./TrackerMapper" + +export {FeedRuleMapper, TrackerMapper} + +export interface BaseMapper<QbitType, VtType> { + toVt(data: QbitType): VtType + toQbit(data: VtType): QbitType +} diff --git a/src/types/qbit/models/AppPreferences.ts b/src/types/qbit/models/AppPreferences.ts new file mode 100644 index 00000000..1ca6a689 --- /dev/null +++ b/src/types/qbit/models/AppPreferences.ts @@ -0,0 +1,309 @@ +import type { + BitTorrentProtocol, + DynDnsService, + Encryption, + MaxRatioAction, + ProxyType, + ScanDirs, + SchedulerDays, + UploadChokingAlgorithm, + UploadSlotsBehavior, + UtpTcpMixedMode +} from '@/enums/qbit/AppPreferences' + +export default interface AppPreferences { + /** List of trackers to add to new torrent */ + add_trackers: string + /** Enable automatic adding of trackers to new torrents */ + add_trackers_enabled: boolean + /** Alternative global download speed limit in KiB/s */ + alt_dl_limit: number + /** Alternative global upload speed limit in KiB/s */ + alt_up_limit: number + /** True if an alternative WebUI should be used */ + alternative_webui_enabled: boolean + /** File path to the alternative WebUI */ + alternative_webui_path: string + announce_ip: string + /** True always announce to all tiers */ + announce_to_all_tiers: boolean + /** True always announce to all trackers in a tier */ + announce_to_all_trackers: boolean + /** If true anonymous mode will be enabled; read more here; this option is only available in qBittorent built against libtorrent version 0.16.X and higher */ + anonymous_mode: boolean + /** Number of asynchronous I/O threads */ + async_io_threads: number + auto_delete_mode: number + /** True if Automatic Torrent Management is enabled by default */ + auto_tmm_enabled: boolean + /** True if external program should be run after torrent has finished downloading */ + autorun_enabled: boolean + /** Program path/name/arguments to run if autorun_enabled is enabled; path is separated by slashes; you can use %f and %n arguments, which will be expanded by qBittorent as path_to_torrent_file and torrent_name (from the GUI; not the .torrent file name) respectively */ + autorun_program: string + /** True if external program should be run after torrent has been added */ + autorun_on_torrent_added_enabled: boolean + /** Program path/name/arguments to run if autorun_on_torrent_added_enabled is enabled; path is separated by slashes; you can use %f and %n arguments, which will be expanded by qBittorent as path_to_torrent_file and torrent_name (from the GUI; not the .torrent file name) respectively */ + autorun_on_torrent_added_program: string + /** List of banned IPs */ + banned_IPs: string + /** Bittorrent Protocol to use (see list of possible values below) */ + bittorrent_protocol: BitTorrentProtocol + /** (White)list of ipv4/ipv6 subnets for which webui authentication should be bypassed; list entries are separated by commas */ + bypass_auth_subnet_whitelist: string + /** True if webui authentication should be bypassed for clients whose ip resides within (at least) one of the subnets on the whitelist */ + bypass_auth_subnet_whitelist_enabled: boolean + /** True if authentication challenge for loopback address (127.0.0.1) should be disabled */ + bypass_local_auth: boolean + /** True if torrent should be relocated when its Category's save path changes */ + category_changed_tmm_enabled: boolean + /** Outstanding memory when checking torrents in MiB */ + checking_memory_use: number + /** True if a subfolder should be created when adding a torrent */ + create_subfolder_enabled: boolean + /** IP Address to bind to. Empty String means All addresses */ + current_interface_address: string + /** Network Interface used */ + current_network_interface: string + /** True if DHT is enabled */ + dht: boolean + /** Disk cache used in MiB */ + disk_cache: number + /** Disk cache expiry interval in seconds */ + disk_cache_ttl: number + /** Global download speed limit in KiB/s; -1 means no limit is applied */ + dl_limit: number + /** If true torrents w/o any activity (stalled ones) will not be counted towards max_active_* limits; see dont_count_slow_torrents for more information */ + dont_count_slow_torrents: boolean + /** Your DDNS domain name */ + dyndns_domain: string + /** True if server DNS should be updated dynamically */ + dyndns_enabled: boolean + /** Password for DDNS service */ + dyndns_password: string + /** See list of possible values here below */ + dyndns_service: DynDnsService + /** Username for DDNS service */ + dyndns_username: string + /** Port used for embedded tracker */ + embedded_tracker_port: number + /** True enables coalesce reads & writes */ + enable_coalesce_read_write: boolean + /** True enables embedded tracker */ + enable_embedded_tracker: boolean + /** True allows multiple connections from the same IP address */ + enable_multi_connections_from_same_ip: boolean + /** True enables os cache */ + enable_os_cache: boolean + /** True if the advanced libtorrent option piece_extent_affinity is enabled */ + enable_piece_extent_affinity: boolean + /** True enables sending of upload piece suggestions */ + enable_upload_suggestions: boolean + /** See list of possible values here below */ + encryption: Encryption + /** Path to directory to copy .torrent files to. Slashes are used as path separators */ + export_dir: string + /** Path to directory to copy .torrent files of completed downloads to. Slashes are used as path separators */ + export_dir_fin: string + /** File pool size */ + file_pool_size: number + /** True if ".!qB" should be appended to incomplete files */ + incomplete_files_ext: boolean + /** True if external IP filter should be enabled */ + ip_filter_enabled: boolean + /** Path to IP filter file (.dat, .p2p, .p2b files are supported); path is separated by slashes */ + ip_filter_path: string + /** True if IP filters are applied to trackers */ + ip_filter_trackers: boolean + /** True if [du]l_limit should be applied to peers on the LAN */ + limit_lan_peers: boolean + /** True if [du]l_limit should be applied to estimated TCP overhead (service data: e.g. packet headers) */ + limit_tcp_overhead: boolean + /** True if [du]l_limit should be applied to uTP connections; this option is only available in qBittorent built against libtorrent version 0.16.X and higher */ + limit_utp_rate: boolean + /** Port for incoming connections */ + listen_port: number + /** Currently selected language (e.g. en_GB for English) */ + locale: string + /** True if LSD is enabled */ + lsd: boolean + /** True if smtp server requires authentication */ + mail_notification_auth_enabled: boolean + /** e-mail to send notifications to */ + mail_notification_email: string + /** True if e-mail notification should be enabled */ + mail_notification_enabled: boolean + /** Password for smtp authentication */ + mail_notification_password: string + /** e-mail where notifications should originate from */ + mail_notification_sender: string + /** smtp server for e-mail notifications */ + mail_notification_smtp: string + /** True if smtp server requires SSL connection */ + mail_notification_ssl_enabled: boolean + /** Username for smtp authentication */ + mail_notification_username: string + /** Maximum number of active simultaneous downloads */ + max_active_downloads: number + /** Maximum number of active simultaneous downloads and uploads */ + max_active_torrents: number + /** Maximum number of active simultaneous uploads */ + max_active_uploads: number + /** Maximum global number of simultaneous connections */ + max_connec: number + /** Maximum number of simultaneous connections per torrent */ + max_connec_per_torrent: number + /** Get the global share ratio limit */ + max_ratio: number + /** Action performed when a torrent reaches the maximum share ratio. See list of possible values here below. */ + max_ratio_act: MaxRatioAction + /** True if share ratio limit is enabled */ + max_ratio_enabled: boolean + /** Number of minutes to seed a torrent */ + max_seeding_time: number + /** True enables max seeding time */ + max_seeding_time_enabled: boolean + /** Maximum number of upload slots */ + max_uploads: number + /** Maximum number of upload slots per torrent */ + max_uploads_per_torrent: number + /** Maximal outgoing port (0: Disabled) */ + outgoing_ports_max: number + /** Minimal outgoing port (0: Disabled) */ + outgoing_ports_min: number + /** True if PeX is enabled */ + pex: boolean + /** True if disk space should be pre-allocated for all files */ + preallocate_all: boolean + /** True proxy requires authentication; doesn't apply to SOCKS4 proxies */ + proxy_auth_enabled: boolean + /** Proxy IP address or domain name */ + proxy_ip: string + /** Password for proxy authentication */ + proxy_password: string + /** True if peer and web seed connections should be proxified; this option will have any effect only in qBittorent built against libtorrent version 0.16.X and higher */ + proxy_peer_connections: boolean + /** Proxy port */ + proxy_port: number + /** True if proxy is only used for torrents */ + proxy_torrents_only: boolean + /** See list of possible values here below */ + proxy_type: ProxyType + /** Username for proxy authentication */ + proxy_username: string + /** True if torrent queuing is enabled */ + queueing_enabled: boolean + /** True if the port is randomly selected */ + random_port: boolean + /** True rechecks torrents on completion */ + recheck_completed_torrents: boolean + /** True resolves peer countries */ + resolve_peer_countries: boolean + /** Enable auto-downloading of torrents from the RSS feeds */ + rss_auto_downloading_enabled: boolean + /** For API ≥ v2.5.1: Enable downloading of repack/proper Episodes */ + rss_download_repack_proper_episodes: boolean + /** Max stored articles per RSS feed */ + rss_max_articles_per_feed: number + /** Enable processing of RSS feeds */ + rss_processing_enabled: boolean + /** RSS refresh interval */ + rss_refresh_interval: number + /** For API ≥ v2.5.1: List of RSS Smart Episode Filters */ + rss_smart_episode_filters: string + /** Default save path for torrents, separated by slashes */ + save_path: string + /** True if torrent should be relocated when the default save path changes */ + save_path_changed_tmm_enabled: boolean + /** Save resume data interval in min */ + save_resume_data_interval: number + /** Property: directory to watch for torrent files, value: where torrents loaded from this directory should be downloaded to (see list of possible values below). Slashes are used as path separators; multiple key/value pairs can be specified */ + scan_dirs: Record<string, ScanDirs> + /** Scheduler starting hour */ + schedule_from_hour: number + /** Scheduler starting minute */ + schedule_from_min: number + /** Scheduler ending hour */ + schedule_to_hour: number + /** Scheduler ending minute */ + schedule_to_min: number + /** Scheduler days. See possible values here below */ + scheduler_days: SchedulerDays + /** True if alternative limits should be applied according to schedule */ + scheduler_enabled: boolean + /** Send buffer low watermark in KiB */ + send_buffer_low_watermark: number + /** Send buffer watermark in KiB */ + send_buffer_watermark: number + /** Send buffer watermark factor in percent */ + send_buffer_watermark_factor: number + /** Download rate in KiB/s for a torrent to be considered "slow" */ + slow_torrent_dl_rate_threshold: number + /** Seconds a torrent should be inactive before considered "slow" */ + slow_torrent_inactive_timer: number + /** Upload rate in KiB/s for a torrent to be considered "slow" */ + slow_torrent_ul_rate_threshold: number + /** Socket backlog size */ + socket_backlog_size: number + /** For API < v2.0.1: SSL certificate contents (this is a not a path) */ + ssl_cert: string + /** For API < v2.0.1: SSL keyfile contents (this is a not a path) */ + ssl_key: string + /** True if torrents should be added in a Paused state */ + start_paused_enabled: boolean + /** Timeout in seconds for a stopped announce request to trackers */ + stop_tracker_timeout: number + /** Path for incomplete torrents, separated by slashes */ + temp_path: string + /** True if folder for incomplete torrents is enabled */ + temp_path_enabled: boolean + /** True if torrent should be relocated when its Category changes */ + torrent_changed_tmm_enabled: boolean + /** Global upload speed limit in KiB/s; -1 means no limit is applied */ + up_limit: number + /** Upload choking algorithm used (see list of possible values below) */ + upload_choking_algorithm: UploadChokingAlgorithm + /** Upload slots behavior used (see list of possible values below) */ + upload_slots_behavior: UploadSlotsBehavior + /** True if UPnP/NAT-PMP is enabled */ + upnp: boolean + /** UPnP lease duration (0: Permanent lease) */ + upnp_lease_duration: number + /** True if WebUI HTTPS access is enabled */ + use_https: boolean + /** μTP-TCP mixed mode algorithm (see list of possible values below) */ + utp_tcp_mixed_mode: UtpTcpMixedMode + /** IP address to use for the WebUI */ + web_ui_address: string + /** WebUI access ban duration in seconds */ + web_ui_ban_duration: number + /** True if WebUI clickjacking protection is enabled */ + web_ui_clickjacking_protection_enabled: boolean + /** True if WebUI CSRF protection is enabled */ + web_ui_csrf_protection_enabled: boolean + /** For API ≥ v2.5.1: List of custom http headers */ + web_ui_custom_http_headers: string + /** Comma-separated list of domains to accept when performing Host header validation */ + web_ui_domain_list: string + /** True if WebUI host header validation is enabled */ + web_ui_host_header_validation_enabled: boolean + /** For API ≥ v2.0.1: Path to SSL certificate */ + web_ui_https_cert_path: string + /** For API ≥ v2.0.1: Path to SSL keyfile */ + web_ui_https_key_path: string + /** Maximum number of authentication failures before WebUI access ban */ + web_ui_max_auth_fail_count: number + /** For API ≥ v2.3.0: Plaintext WebUI password, not readable, write-only. For API < v2.3.0: MD5 hash of WebUI password, hash is generated from the following string: username:Web UI Access:plain_text_web_ui_password */ + web_ui_password: string + /** WebUI port */ + web_ui_port: number + /** True if WebUI cookie Secure flag is enabled */ + web_ui_secure_cookie_enabled: boolean + /** Seconds until WebUI is automatically signed off */ + web_ui_session_timeout: number + /** True if UPnP is used for the WebUI port */ + web_ui_upnp: boolean + /** For API ≥ v2.5.1: Enable custom http headers */ + web_ui_use_custom_http_headers_enabled: boolean + /** WebUI username */ + web_ui_username: string +} diff --git a/src/types/qbit/models/Category.ts b/src/types/qbit/models/Category.ts new file mode 100644 index 00000000..e68371bd --- /dev/null +++ b/src/types/qbit/models/Category.ts @@ -0,0 +1,4 @@ +export default interface Category { + name: string + savePath: string +} diff --git a/src/types/qbit/models/Feed.ts b/src/types/qbit/models/Feed.ts new file mode 100644 index 00000000..360a8842 --- /dev/null +++ b/src/types/qbit/models/Feed.ts @@ -0,0 +1,4 @@ +export default interface Feed { + uid: string + url: string +} diff --git a/src/types/qbit/models/FeedRule.ts b/src/types/qbit/models/FeedRule.ts new file mode 100644 index 00000000..e486beb0 --- /dev/null +++ b/src/types/qbit/models/FeedRule.ts @@ -0,0 +1,29 @@ +export default interface FeedRule { + /** Add matched torrent in paused mode */ + addPaused: boolean + /** The feed URLs the rule applied to */ + affectedFeeds: string[] + /** Assign category to the torrent */ + assignedCategory: string + /** Whether the rule is enabled */ + enabled: boolean + /** Episode filter definition */ + episodeFilter: string + /** Ignore sunsequent rule matches */ + ignoreDays: number + /** The rule last match time */ + lastMatch: string + /** The substring that the torrent name must contain */ + mustContain: string + /** The substring that the torrent name must not contain */ + mustNotContain: string + /** The list of episode IDs already matched by smart filter */ + previouslyMatchedEpisodes?: unknown[] + /** Save torrent to the given directory */ + savePath: string + /** Enable smart episode filter */ + smartFilter: boolean + torrentContentLayout?: unknown + /** Enable regex mode in "mustContain" and "mustNotContain" */ + useRegex: boolean +} diff --git a/src/types/qbit/models/Peer.ts b/src/types/qbit/models/Peer.ts new file mode 100644 index 00000000..18b710bd --- /dev/null +++ b/src/types/qbit/models/Peer.ts @@ -0,0 +1,18 @@ +export default interface Peer { + client: string + connection: string + country: string + country_code: string + dl_speed: number + downloaded: number + files: string + flags: string + flags_desc: string + ip: string + peer_id_client: string + port: number + progress: number + relevance: number + up_speed: number + uploaded: number +} diff --git a/src/types/qbit/models/SearchJob.ts b/src/types/qbit/models/SearchJob.ts new file mode 100644 index 00000000..04785c4d --- /dev/null +++ b/src/types/qbit/models/SearchJob.ts @@ -0,0 +1,4 @@ +export default interface SearchJob { + /** ID of the search job */ + id: number +} diff --git a/src/types/qbit/models/SearchPlugin.ts b/src/types/qbit/models/SearchPlugin.ts new file mode 100644 index 00000000..faa8c649 --- /dev/null +++ b/src/types/qbit/models/SearchPlugin.ts @@ -0,0 +1,16 @@ +type PluginCategory = { id: string; name: string } + +export default interface SearchPlugin { + /** Whether the plugin is enabled */ + enabled: boolean + /** Full name of the plugin */ + fullName: string + /** Short name of the plugin */ + name: string + /** List of category objects */ + supportedCategories: PluginCategory[] + /** URL of the torrent site */ + url: string + /** Installed version of the plugin */ + version: string +} diff --git a/src/types/qbit/models/SearchResult.ts b/src/types/qbit/models/SearchResult.ts new file mode 100644 index 00000000..2db71109 --- /dev/null +++ b/src/types/qbit/models/SearchResult.ts @@ -0,0 +1,16 @@ +export default interface SearchResult { + /** URL of the torrent's description page */ + descrLink: string + /** Name of the file */ + fileName: string + /** Size of the file in Bytes */ + fileSize: number + /** Torrent download link (usually either .torrent file or magnet link) */ + fileUrl: string + /** Number of leechers */ + nbLeechers: number + /** Number of seeders */ + nbSeeders: number + /** URL of the torrent site */ + siteUrl: string +} diff --git a/src/types/qbit/models/SearchStatus.ts b/src/types/qbit/models/SearchStatus.ts new file mode 100644 index 00000000..aa9576fa --- /dev/null +++ b/src/types/qbit/models/SearchStatus.ts @@ -0,0 +1,8 @@ +export default interface SearchStatus { + /** ID of the search job */ + id: number + /** Current status of the search job (either Running or Stopped) */ + status: 'Running' | 'Stopped' + /** Total number of results. If the status is Running this number may contineu to increase */ + total: number +} diff --git a/src/types/qbit/models/ServerState.ts b/src/types/qbit/models/ServerState.ts new file mode 100644 index 00000000..13865a71 --- /dev/null +++ b/src/types/qbit/models/ServerState.ts @@ -0,0 +1,28 @@ +import type { ConnectionStatus } from '@/enums/qbit' + +export default interface ServerState { + alltime_dl: number + alltime_ul: number + average_time_queue: number + connection_status: ConnectionStatus + dht_nodes: number + dl_info_data: number + dl_info_speed: number + dl_rate_limit: number + free_space_on_disk: number + global_ratio: string + queued_io_jobs: number + queueing: boolean + read_cache_hits: string + read_cache_overload: string + refresh_interval: number + total_buffers_size: number + total_peer_connections: number + total_queued_size: number + total_wasted_session: number + up_info_data: number + up_info_speed: number + up_rate_limit: number + use_alt_speed_limits: boolean + write_cache_overload: string +} diff --git a/src/types/qbit/models/Torrent.ts b/src/types/qbit/models/Torrent.ts new file mode 100644 index 00000000..1cea520f --- /dev/null +++ b/src/types/qbit/models/Torrent.ts @@ -0,0 +1,92 @@ +import type {TorrentState} from "@/enums/qbit"; + +export default interface Torrent { + // Time (Unix Epoch) when the torrent was added to the client + added_on: number + // Amount of data left to download (bytes) + amount_left: number + // Whether this torrent is managed by Automatic Torrent Management + auto_tmm: boolean + // Percentage of file pieces currently available + availability: number + // Category of the torrent + category: string + // Amount of transfer data completed (bytes) + completed: number + // Time (Unix Epoch) when the torrent completed + completion_on: number + // Absolute path of torrent content (root path for multifile torrents, absolute file path for singlefile torrents) + content_path: string + // Torrent download speed limit (bytes/s). -1 if ulimited. + dl_limit: number + // Torrent download speed (bytes/s) + dlspeed: number + // Amount of data downloaded + downloaded: number + // Amount of data downloaded this session + downloaded_session: number + // Torrent ETA (seconds) + eta: number + // True if first last piece are prioritized + f_l_piece_prio: boolean + // True if force start is enabled for this torrent + force_start: boolean + // Torrent hash + hash: string + // Last time (Unix Epoch) when a chunk was downloaded/uploaded + last_activity: number + // Magnet URI corresponding to this torrent + magnet_uri: string + // Maximum share ratio until torrent is stopped from seeding/uploading + max_ratio: number + // Maximum seeding time (seconds) until torrent is stopped from seeding + max_seeding_time: number + // Torrent name + name: string + // Number of seeds in the swarm + num_complete: number + // Number of leechers in the swarm + num_incomplete: number + // Number of leechers connected to + num_leechs: number + // Number of seeds connected to + num_seeds: number + // Torrent priority. Returns -1 if queuing is disabled or torrent is in seed mode + priority: number + // Torrent progress (percentage/100) + progress: number + // Torrent share ratio. Max ratio value: 9999. + ratio: number + ratio_limit: number + // Path where this torrent's data is stored + save_path: string + // Torrent elapsed time while complete (seconds) + seeding_time: number + seeding_time_limit: number + // Time (Unix Epoch) when this torrent was last seen complete + seen_complete: number + // True if sequential download is enabled + seq_dl: boolean + // Total size (bytes) of files selected for download + size: number + // Torrent state. See table here below for the possible values + state: TorrentState + // True if super seeding is enabled + super_seeding: boolean + // Comma-concatenated tag list of the torrent + tags: string + // Total active time (seconds) + time_active: number + // Total size (bytes) of all file in this torrent (including unselected ones) + total_size: number + // The first tracker with working status. Returns empty string if no tracker is working. + tracker: string + // Torrent upload speed limit (bytes/s). -1 if unlimited. + up_limit: number + // Amount of data uploaded + uploaded: number + // Amount of data uploaded this session + uploaded_session: number + // Torrent upload speed (bytes/s) + upspeed: number +} diff --git a/src/types/qbit/models/TorrentFile.ts b/src/types/qbit/models/TorrentFile.ts new file mode 100644 index 00000000..fa0a27ce --- /dev/null +++ b/src/types/qbit/models/TorrentFile.ts @@ -0,0 +1,20 @@ +import type { Priority } from '@/enums/qbit' + +export default interface TorrentFile { + /** Percentage of file pieces currently available (percentage/100) */ + availability: number + /** File index */ + index: number + /** True if file is seeding/complete */ + is_seed: boolean + /** File name (including relative path) */ + name: string + /** The first number is the starting piece index and the second number is the ending piece index (inclusive) */ + piece_range: [number, number] + /** File priority. See possible values here below */ + priority: Priority + /** File progress (percentage/100) */ + progress: number + /** File size (bytes) */ + size: number +} diff --git a/src/types/qbit/models/TorrentProperties.ts b/src/types/qbit/models/TorrentProperties.ts new file mode 100644 index 00000000..05e70f5b --- /dev/null +++ b/src/types/qbit/models/TorrentProperties.ts @@ -0,0 +1,68 @@ +export default interface TorrentProperties { + /** When this torrent was added (unix timestamp) */ + addition_date: number + /** Torrent comment */ + comment: string + /** Torrent completion date (unix timestamp) */ + completion_date: number + /** Torrent creator */ + created_by: string + /** Torrent creation date (Unix timestamp) */ + creation_date: number + /** Torrent download limit (bytes/s) */ + dl_limit: number + /** Torrent download speed (bytes/second) */ + dl_speed: number + /** Torrent average download speed (bytes/second) */ + dl_speed_avg: number + /** Torrent ETA (seconds) */ + eta: number + /** Last seen complete date (unix timestamp) */ + last_seen: number + /** Torrent connection count */ + nb_connections: number + /** Torrent connection count limit */ + nb_connections_limit: number + /** Number of peers connected to */ + peers: number + /** Number of peers in the swarm */ + peers_total: number + /** Torrent piece size (bytes) */ + piece_size: number + /** Number of pieces owned */ + pieces_have: number + /** Number of pieces of the torrent */ + pieces_num: number + /** Number of seconds until the next announce */ + reannounce: number + /** Torrent save path */ + save_path: string + /** Torrent elapsed time while complete (seconds) */ + seeding_time: number + /** Number of seeds connected to */ + seeds: number + /** Number of seeds in the swarm */ + seeds_total: number + /** Torrent share ratio */ + share_ratio: number + /** Torrent elapsed time (seconds) */ + time_elapsed: number + /** Total data downloaded for torrent (bytes) */ + total_downloaded: number + /** Total data downloaded this session (bytes) */ + total_downloaded_session: number + /** Torrent total size (bytes) */ + total_size: number + /** Total data uploaded for torrent (bytes) */ + total_uploaded: number + /** Total data uploaded this session (bytes) */ + total_uploaded_session: number + /** Total data wasted for torrent (bytes) */ + total_wasted: number + /** Torrent upload limit (bytes/s) */ + up_limit: number + /** Torrent upload speed (bytes/second) */ + up_speed: number + /** Torrent average upload speed (bytes/second) */ + up_speed_avg: number +} diff --git a/src/types/qbit/models/Tracker.ts b/src/types/qbit/models/Tracker.ts new file mode 100644 index 00000000..9f163286 --- /dev/null +++ b/src/types/qbit/models/Tracker.ts @@ -0,0 +1,20 @@ +import type { TrackerStatus } from '@/enums/qbit' + +export default interface Tracker { + /** Tracker message (there is no way of knowing what this message is - it's up to tracker admins) */ + msg: string + /** Number of completed downlods for current torrent, as reported by the tracker */ + num_downloaded: number + /** Number of leeches for current torrent, as reported by the tracker */ + num_leeches: number + /** Number of peers for current torrent, as reported by the tracker */ + num_peers: number + /** Number of seeds for current torrent, asreported by the tracker */ + num_seeds: number + /** Tracker status. See the table below for possible values */ + status: TrackerStatus + /** Tracker priority tier. Lower tier trackers are tried before higher tiers. Tier numbers are valid when >= 0, < 0 is used as placeholder when tier does not exist for special entries (such as DHT). */ + tier: number + /** Tracker url */ + url: string +} diff --git a/src/types/qbit/models/index.ts b/src/types/qbit/models/index.ts new file mode 100644 index 00000000..32419128 --- /dev/null +++ b/src/types/qbit/models/index.ts @@ -0,0 +1,34 @@ +import type AppPreferences from './AppPreferences' +import type Category from './Category' +import type ServerState from './ServerState' +import type Torrent from './Torrent' +import type Tracker from './Tracker' +import type Peer from './Peer' +import type TorrentFile from './TorrentFile' +import type TorrentProperties from './TorrentProperties' +import type FeedRule from './FeedRule' +import type Feed from './Feed' +import type SearchPlugin from './SearchPlugin' +import type SearchJob from './SearchJob' +import type SearchStatus from './SearchStatus' +import type SearchResult from './SearchResult' + +type ApplicationVersion = string + +export type { + ApplicationVersion, + AppPreferences, + Category, + ServerState, + Tracker, + Torrent, + Peer, + TorrentFile, + TorrentProperties, + FeedRule, + Feed, + SearchPlugin, + SearchJob, + SearchStatus, + SearchResult +} diff --git a/src/types/qbit/payloads/AddTorrentPayload.ts b/src/types/qbit/payloads/AddTorrentPayload.ts new file mode 100644 index 00000000..63b6b393 --- /dev/null +++ b/src/types/qbit/payloads/AddTorrentPayload.ts @@ -0,0 +1,36 @@ +import type { BasePayload } from '.' + +export default interface AddTorrentPayload extends BasePayload { + /** Whether Automatic Torrent Management should be used */ + autoTMM?: boolean + /** Category for the torrent */ + category?: string + /** Cookie sent to download the .torrent file */ + cookie?: string + /** Set torrent download speed limit. Unit in bytes/second */ + dlLimit?: number + /** Prioritize download first last piece. Possible values are true, false (default) */ + firstLastPiecePrio?: boolean + /** Add torrents in the paused state. Possible values are true, false (default) */ + paused?: boolean + /** Set torrent share ratio limit */ + ratioLimit?: number + /** Rename torrent */ + rename?: string + /** Create the root folder. Possible values are true, false, unset (default) */ + root_folder?: boolean + /** Download folder */ + savepath?: string + /** Set torrent seeding time limit. Unit in minutes */ + seedingTimeLimit?: number + /** Enable sequential download. Possible values are true, false (default) */ + sequentialDownload?: boolean + /** Skip hash checking. Possible values are true, false (default) */ + skip_checking?: boolean + /** Tags for the torrent, split by ',' */ + tags?: string + /** Set torrent upload speed limit. Unit in bytes/second */ + upLimit?: number + /** URLs separated with newlines */ + urls?: string +} diff --git a/src/types/qbit/payloads/AppPreferencesPayload.ts b/src/types/qbit/payloads/AppPreferencesPayload.ts new file mode 100644 index 00000000..d26a218c --- /dev/null +++ b/src/types/qbit/payloads/AppPreferencesPayload.ts @@ -0,0 +1,3 @@ +import type { AppPreferences } from '@/types/qbit/models' + +export type AppPreferencesPayload = Partial<AppPreferences> diff --git a/src/types/qbit/payloads/BasePayload.ts b/src/types/qbit/payloads/BasePayload.ts new file mode 100644 index 00000000..ff7a7e8c --- /dev/null +++ b/src/types/qbit/payloads/BasePayload.ts @@ -0,0 +1 @@ +export default interface BasePayload extends Record<string, any> {} diff --git a/src/types/qbit/payloads/LoginPayload.ts b/src/types/qbit/payloads/LoginPayload.ts new file mode 100644 index 00000000..f4a9d0a5 --- /dev/null +++ b/src/types/qbit/payloads/LoginPayload.ts @@ -0,0 +1,8 @@ +import type { BasePayload } from '.' + +export default interface LoginPayload extends BasePayload { + /** Username used to access the WebUI */ + username: string + /** Password used to access the WebUI */ + password: string +} diff --git a/src/types/qbit/payloads/PeerLogPayload.ts b/src/types/qbit/payloads/PeerLogPayload.ts new file mode 100644 index 00000000..356cf6fe --- /dev/null +++ b/src/types/qbit/payloads/PeerLogPayload.ts @@ -0,0 +1,7 @@ +import type { Optional } from '@/global' +import type { BasePayload } from '.' + +export default interface PeerLogPayload extends BasePayload { + // Exclude messages with "message id" <= last_known_id (default: -1) + last_known_id: Optional<number> +} diff --git a/src/types/qbit/payloads/index.ts b/src/types/qbit/payloads/index.ts new file mode 100644 index 00000000..8f23a75e --- /dev/null +++ b/src/types/qbit/payloads/index.ts @@ -0,0 +1,7 @@ +import type LoginPayload from './LoginPayload' +import type AddTorrentPayload from './AddTorrentPayload' +import type PeerLogPayload from './PeerLogPayload' +import type { AppPreferencesPayload } from './AppPreferencesPayload' +import type BasePayload from './BasePayload' + +export { AppPreferencesPayload, LoginPayload, AddTorrentPayload, PeerLogPayload, BasePayload } diff --git a/src/types/qbit/responses/MainDataResponse.ts b/src/types/qbit/responses/MainDataResponse.ts new file mode 100644 index 00000000..2015b70e --- /dev/null +++ b/src/types/qbit/responses/MainDataResponse.ts @@ -0,0 +1,17 @@ +import type { Category, Torrent, ServerState } from '@/types/qbit/models' +import type { Optional } from '@/global' + +export default interface MainDataResponse { + // Response ID + rid: number + // Whether the response contains all the data or partial data + fullUpdate: Optional<boolean> + torrents: Optional<Record<string, Torrent>> + torrents_removed: Optional<string[]> + categories: Optional<Record<string, Category>> + categories_removed: Optional<Category[]> + tags: Optional<string[]> + tags_removed: Optional<string[]> + trackers: Optional<Record<string, string[]>> + server_state?: Optional<ServerState> +} diff --git a/src/types/qbit/responses/PeerLogResponse.ts b/src/types/qbit/responses/PeerLogResponse.ts new file mode 100644 index 00000000..015e5ab0 --- /dev/null +++ b/src/types/qbit/responses/PeerLogResponse.ts @@ -0,0 +1,14 @@ +interface PeerLog { + // ID of the peer + id: number + // IP of the peer + ip: string + // Milliseconds since epoch + timestamp: number + // Whether or not the peer was blocked + blocked: boolean + // Reason of the block + reason: string +} + +export type PeerLogResponse = PeerLog[] diff --git a/src/types/qbit/responses/SearchResultsResponse.ts b/src/types/qbit/responses/SearchResultsResponse.ts new file mode 100644 index 00000000..f09eee4d --- /dev/null +++ b/src/types/qbit/responses/SearchResultsResponse.ts @@ -0,0 +1,10 @@ +import type { SearchResult } from '@/types/qbit/models' + +export default interface SearchResultsResponse { + /** Array of result objects- see table below */ + results: SearchResult[] + /** Current status of the search job (either Running or Stopped) */ + status: 'Running' | 'Stopped' + /** Total number of results. If the status is Running this number may continue to increase */ + total: number +} diff --git a/src/types/qbit/responses/TorrentPeersResponse.ts b/src/types/qbit/responses/TorrentPeersResponse.ts new file mode 100644 index 00000000..1815d770 --- /dev/null +++ b/src/types/qbit/responses/TorrentPeersResponse.ts @@ -0,0 +1,8 @@ +import type { Peer } from '@/types/qbit/models' + +export default interface TorrentPeersResponse { + full_update?: boolean + rid: number + peers: Record<string, Peer> + show_flags: boolean +} diff --git a/src/types/qbit/responses/index.ts b/src/types/qbit/responses/index.ts new file mode 100644 index 00000000..57e9481d --- /dev/null +++ b/src/types/qbit/responses/index.ts @@ -0,0 +1,6 @@ +import type MainDataResponse from './MainDataResponse' +import type { PeerLogResponse } from './PeerLogResponse' +import type TorrentPeersResponse from './TorrentPeersResponse' +import type SearchResultsResponse from './SearchResultsResponse' + +export { MainDataResponse, PeerLogResponse, TorrentPeersResponse, SearchResultsResponse } diff --git a/src/types/vuetorrent/Category.ts b/src/types/vuetorrent/Category.ts new file mode 100644 index 00000000..5ca9b34e --- /dev/null +++ b/src/types/vuetorrent/Category.ts @@ -0,0 +1,4 @@ +export default interface Category { + name: string + savePath: string +} \ No newline at end of file diff --git a/src/types/vuetorrent/ModalTemplate.ts b/src/types/vuetorrent/ModalTemplate.ts new file mode 100644 index 00000000..e60764ca --- /dev/null +++ b/src/types/vuetorrent/ModalTemplate.ts @@ -0,0 +1,5 @@ +export default interface ModalTemplate { + component: string + props: Object + guid: string +} diff --git a/src/types/vuetorrent/SortOptions.ts b/src/types/vuetorrent/SortOptions.ts new file mode 100644 index 00000000..1c5608c3 --- /dev/null +++ b/src/types/vuetorrent/SortOptions.ts @@ -0,0 +1,13 @@ +import type { Optional } from '@/global' +import type {TorrentState} from "@/enums/vuetorrent" + +export default interface SortOptions { + isCustomSortEnabled: boolean + sort: string + reverse: boolean + hashes: string[] + filter: Optional<TorrentState> + category: Optional<string> + tag: Optional<string> + tracker: Optional<string> +} diff --git a/src/types/vuetorrent/StoreState.ts b/src/types/vuetorrent/StoreState.ts new file mode 100644 index 00000000..3c531b1d --- /dev/null +++ b/src/types/vuetorrent/StoreState.ts @@ -0,0 +1,42 @@ +import type Feed from './rss/Feed' +import type FeedRule from './rss/FeedRule' +import type Category from '../qbit/models/Category' +import type Torrent from '@/models/Torrent' +import type SortOptions from './SortOptions' +import type { AppPreferences } from '../qbit/models' +import type { Optional } from '@/global' +import type ModalTemplate from './ModalTemplate' +import type {Status} from '@/models' +import type WebUISettings from '@/types/vuetorrent/WebUISettings' +import type {SearchPlugin} from "@/types/qbit/models"; + +export default interface StoreState { + authenticated: boolean + categories: Category[] + dashboard: { + currentPage: number + searchFilter: string + } + download_data: number[] + filteredTorrentsCount: number + intervals: NodeJS.Timer[] + latestSelectedTorrent: number + modals: ModalTemplate[] + rid?: number + rss: { + feeds: Feed[] + rules: FeedRule[] + } + searchPlugins: SearchPlugin[] + selectMode: boolean + selected_torrents: string[] + settings: Optional<AppPreferences> + sort_options: SortOptions + status: Status + tags: string[] + torrents: Torrent[] + trackers: string[] + upload_data: number[] + version: string + webuiSettings: WebUISettings +} diff --git a/src/types/vuetorrent/Tracker.ts b/src/types/vuetorrent/Tracker.ts new file mode 100644 index 00000000..d19f90ac --- /dev/null +++ b/src/types/vuetorrent/Tracker.ts @@ -0,0 +1,5 @@ +import type {Tracker as QbitTracker} from '../qbit/models' + +export default interface Tracker extends QbitTracker { + isSelectable: boolean +} \ No newline at end of file diff --git a/src/types/vuetorrent/TreeObjects.ts b/src/types/vuetorrent/TreeObjects.ts new file mode 100644 index 00000000..acf4ec4a --- /dev/null +++ b/src/types/vuetorrent/TreeObjects.ts @@ -0,0 +1,17 @@ +export interface TreeNode { + name: string + fullName: string + children: TreeNode[] +} + +export interface TreeFile extends TreeNode { + id: number + progress: number + size: string + icon: string + priority: number +} + +export interface TreeFolder extends TreeNode { + type: 'directory' +} diff --git a/src/types/vuetorrent/WebUISettings.ts b/src/types/vuetorrent/WebUISettings.ts new file mode 100644 index 00000000..bd672875 --- /dev/null +++ b/src/types/vuetorrent/WebUISettings.ts @@ -0,0 +1,31 @@ +import type {TitleOptions} from "@/enums/vuetorrent" + +export interface TorrentProperty { + name: string + active: boolean +} + +export interface TorrentPropertyLocalized extends TorrentProperty { + label: string +} + +export default interface WebUISettings { + lang: string + darkTheme: boolean + showFreeSpace: boolean + showSpeedGraph: boolean + showSessionStat: boolean + showAlltimeStat: boolean + showCurrentSpeed: boolean + showTrackerFilter: boolean + showSpeedInTitle: boolean + deleteWithFiles: boolean + title: TitleOptions + rightDrawer: boolean + topPagination: boolean + paginationSize: number + dateFormat: string + openSideBarOnStart: boolean + busyTorrentProperties: TorrentProperty[] + doneTorrentProperties: TorrentProperty[] +} diff --git a/src/types/vuetorrent/index.ts b/src/types/vuetorrent/index.ts new file mode 100644 index 00000000..d079d9d9 --- /dev/null +++ b/src/types/vuetorrent/index.ts @@ -0,0 +1,15 @@ +import type Category from "./Category" +import type Feed from './rss/Feed' +import type FeedRule from './rss/FeedRule' +import type SearchStatus from "./search/SearchStatus" +import type SearchResult from "./search/SearchResult" +import type ModalTemplate from './ModalTemplate' +import type SortOptions from './SortOptions' +import type StoreState from './StoreState' +import type { TreeNode, TreeFile, TreeFolder } from './TreeObjects' +import type {TorrentProperty, TorrentPropertyLocalized} from "@/types/vuetorrent/WebUISettings" +import type Tracker from './Tracker' + +export type ComponentRule = (value: string) => boolean|string + +export { Category, Feed, FeedRule, SearchStatus, SearchResult, ModalTemplate, SortOptions, StoreState, TreeNode, TreeFile, TreeFolder, TorrentProperty, TorrentPropertyLocalized, Tracker } diff --git a/src/types/vuetorrent/rss/Feed.ts b/src/types/vuetorrent/rss/Feed.ts new file mode 100644 index 00000000..47e8326c --- /dev/null +++ b/src/types/vuetorrent/rss/Feed.ts @@ -0,0 +1,5 @@ +export default interface Feed { + name: string + uid?: string + url: string +} diff --git a/src/types/vuetorrent/rss/FeedRule.ts b/src/types/vuetorrent/rss/FeedRule.ts new file mode 100644 index 00000000..d9c77ab0 --- /dev/null +++ b/src/types/vuetorrent/rss/FeedRule.ts @@ -0,0 +1,16 @@ +export default interface FeedRule { + addPaused?: boolean + affectedFeeds?: string[] + assignedCategory?: string + enabled: boolean + episodeFilter?: string + ignoreDays?: number + lastMatch?: string + mustContain?: string + mustNotContain?: string + name: string + previouslyMatchedEpisodes?: unknown[] + savePath?: string + smartFilter?: boolean + useRegex?: boolean +} diff --git a/src/types/vuetorrent/search/SearchResult.ts b/src/types/vuetorrent/search/SearchResult.ts new file mode 100644 index 00000000..43871a97 --- /dev/null +++ b/src/types/vuetorrent/search/SearchResult.ts @@ -0,0 +1,9 @@ +export default interface SearchResult { + descrLink: string + fileName: string + fileSize: number + fileUrl: string + nbLeechers: number + nbSeeders: number + siteUrl: string +} \ No newline at end of file diff --git a/src/types/vuetorrent/search/SearchStatus.ts b/src/types/vuetorrent/search/SearchStatus.ts new file mode 100644 index 00000000..095297f0 --- /dev/null +++ b/src/types/vuetorrent/search/SearchStatus.ts @@ -0,0 +1,9 @@ +import type {SearchResult} from ".."; +import type {Optional} from "@/global"; + +export default interface SearchStatus { + id: number + status: 'Running' | 'Stopped' + interval: Optional<NodeJS.Timer> + results: SearchResult[] +} \ No newline at end of file diff --git a/src/views/Login.vue b/src/views/Login.vue index 3e97c81e..698dc2b6 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -46,7 +46,7 @@ <script> import { mdiLock, mdiAccount } from '@mdi/js' -import { isAuthenticated } from '@/services/auth.js' +import { isAuthenticated } from '@/services/auth.ts' export default { name: 'Login', diff --git a/tests/unit/filters.spec.js b/tests/unit/filters.spec.js index 8c605f51..697cb359 100644 --- a/tests/unit/filters.spec.js +++ b/tests/unit/filters.spec.js @@ -3,6 +3,9 @@ import { titleCase } from '@/filters' describe('Filters', () => { it('titleCase', () => { + expect(titleCase('')).toEqual('') + expect(titleCase('-')).toEqual('-') + expect(titleCase(' ')).toEqual(' ') expect(titleCase('test')).toEqual('Test') expect(titleCase('hello there')).toEqual('Hello There') }) diff --git a/tests/unit/helpers.spec.js b/tests/unit/helpers.spec.js index 69f036cb..89c6159a 100644 --- a/tests/unit/helpers.spec.js +++ b/tests/unit/helpers.spec.js @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { splitByUrl } from '../../src/helpers' +import { splitByUrl } from '@/helpers' describe('Helpers', () => { describe('splitByUrl()', () => { diff --git a/tsconfig.json b/tsconfig.json index 8a8ef868..2eaa25cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowJs": true, "baseUrl": ".", "esModuleInterop": true, "importHelpers": true, diff --git a/vite.config.js b/vite.config.js index a2eae32e..d573ce35 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,3 @@ -import { resolve, dirname } from 'path' import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue2' import { VitePWA } from 'vite-plugin-pwa' @@ -15,11 +14,22 @@ export default defineConfig(({ command, mode }) => { const proxyTarget = theEnv.VITE_QBITTORRENT_TARGET ?? 'http://127.0.0.1' return { - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - '~': fileURLToPath(new URL('./node_modules', import.meta.url)) - } + base: './', + build: { + target: 'esnext', + rollupOptions: { + output: { + manualChunks: { + vue: ['vue', 'vue-router', 'vue-router/composables', 'vuex', 'vuex-persist'], + vuetify: ['vuetify'] + } + } + }, + outDir: './vuetorrent/public' + }, + define: { + 'import.meta.env.VITE_PACKAGE_VERSION': version, + 'process.env': {} }, plugins: [ vue(), @@ -103,22 +113,14 @@ export default defineConfig(({ command, mode }) => { } }) ], - build: { - target: 'esnext', - rollupOptions: { - output: { - manualChunks: { - vue: ['vue', 'vue-router', 'vue-router/composables', 'vuex', 'vuex-persist'] - } - } - }, - outDir: './vuetorrent/public' - }, - define: { - 'import.meta.env.VITE_PACKAGE_VERSION': version - }, - base: './', publicDir: './public', + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '~': fileURLToPath(new URL('./node_modules', import.meta.url)) + }, + extensions: ['.js', '.json', '.jsx', '.ts', '.tsx', '.vue'] + }, server: { proxy: { '/api': `${proxyTarget}:${qBittorrentPort}`