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}`