diff --git a/package-lock.json b/package-lock.json
index b193785e..76b25d09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,13 +31,13 @@
}
},
"@babel/core": {
- "version": "7.11.5",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.5.tgz",
- "integrity": "sha512-fsEANVOcZHzrsV6dMVWqpSeXClq3lNbYrfFGme6DE25FQWe7pyeYpXyx9guqUnpy466JLzZ8z4uwSr2iv60V5Q==",
+ "version": "7.11.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz",
+ "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
- "@babel/generator": "^7.11.5",
+ "@babel/generator": "^7.11.6",
"@babel/helper-module-transforms": "^7.11.0",
"@babel/helpers": "^7.10.4",
"@babel/parser": "^7.11.5",
@@ -51,7 +51,7 @@
"lodash": "^4.17.19",
"resolve": "^1.3.2",
"semver": "^5.4.1",
- "source-map": "^0.6.1"
+ "source-map": "^0.5.0"
},
"dependencies": {
"@babel/code-frame": {
@@ -64,14 +64,14 @@
}
},
"@babel/generator": {
- "version": "7.11.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.5.tgz",
- "integrity": "sha512-9UqHWJ4IwRTy4l0o8gq2ef8ws8UPzvtMkVKjTLAiRmza9p9V6Z+OfuNd9fB1j5Q67F+dVJtPC2sZXI8NM9br4g==",
+ "version": "7.11.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz",
+ "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==",
"dev": true,
"requires": {
"@babel/types": "^7.11.5",
"jsesc": "^2.5.1",
- "source-map": "^0.6.1"
+ "source-map": "^0.5.0"
}
},
"@babel/highlight": {
@@ -133,12 +133,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
}
}
},
@@ -1602,6 +1596,80 @@
}
}
},
+ "@eslint/eslintrc": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz",
+ "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^12.1.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "lodash": "^4.17.19",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "6.12.4",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
+ "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "globals": {
+ "version": "12.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+ "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.8.1"
+ }
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ }
+ }
+ },
"@fortawesome/fontawesome-common-types": {
"version": "0.2.30",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz",
@@ -1624,9 +1692,9 @@
}
},
"@fortawesome/vue-fontawesome": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.10.tgz",
- "integrity": "sha512-b2+SLF31h32LSepVcXe+BQ63yvbq5qmTCy4KfFogCYm2bn68H5sDWUnX+U7MBqnM2aeEk9M7xSoqGnu+wSdY6w=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.0.tgz",
+ "integrity": "sha512-N3VKw7KzRfOm8hShUVldpinlm13HpvLBQgT63QS+aCrIRLwjoEUXY5Rcmttbfb6HkzZaeqjLqd/aZCQ53UjQpg=="
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@@ -6956,9 +7024,9 @@
}
},
"electron": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/electron/-/electron-10.1.0.tgz",
- "integrity": "sha512-DyS6WhQ59+ZXQsI1EkpsYkOXFt0Xbp+mbxPTJS9A7O21r3JDzaTC+1Jxz7g6J+Sbi9Y7UFdRs0tn/vqhHJx2gA==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-10.1.1.tgz",
+ "integrity": "sha512-ZJtZHMr17AvvBosuA6XUmpehwAlGM4/n46Mw9BcyD8tpgdI6IQd0X5OU9meE3X3M8Y6Ja2Kr2udTMgtjvot2hA==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",
@@ -7853,12 +7921,13 @@
}
},
"eslint": {
- "version": "7.7.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz",
- "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==",
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz",
+ "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
+ "@eslint/eslintrc": "^0.1.3",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@@ -7868,7 +7937,7 @@
"eslint-scope": "^5.1.0",
"eslint-utils": "^2.1.0",
"eslint-visitor-keys": "^1.3.0",
- "espree": "^7.2.0",
+ "espree": "^7.3.0",
"esquery": "^1.2.0",
"esutils": "^2.0.2",
"file-entry-cache": "^5.0.1",
@@ -8081,9 +8150,9 @@
"dev": true
},
"supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
- "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
@@ -8319,12 +8388,12 @@
"dev": true
},
"espree": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz",
- "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz",
+ "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==",
"dev": true,
"requires": {
- "acorn": "^7.3.1",
+ "acorn": "^7.4.0",
"acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.3.0"
},
@@ -16630,18 +16699,24 @@
}
},
"sass-loader": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.1.tgz",
- "integrity": "sha512-b2PSldKVTS3JcFPHSrEXh3BeAfR7XknGiGCAO5aHruR3Pf3kqLP3Gb2ypXLglRrAzgZkloNxLZ7GXEGDX0hBUQ==",
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.2.tgz",
+ "integrity": "sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg==",
"dev": true,
"requires": {
"klona": "^2.0.3",
"loader-utils": "^2.0.0",
"neo-async": "^2.6.2",
- "schema-utils": "^2.7.0",
+ "schema-utils": "^2.7.1",
"semver": "^7.3.2"
},
"dependencies": {
+ "@types/json-schema": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
+ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
+ "dev": true
+ },
"ajv": {
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
@@ -16696,14 +16771,14 @@
"dev": true
},
"schema-utils": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
- "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
+ "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
"dev": true,
"requires": {
- "@types/json-schema": "^7.0.4",
- "ajv": "^6.12.2",
- "ajv-keywords": "^3.4.1"
+ "@types/json-schema": "^7.0.5",
+ "ajv": "^6.12.4",
+ "ajv-keywords": "^3.5.2"
}
},
"semver": {
diff --git a/package.json b/package.json
index 24fc7771..a458c3a7 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
- "@fortawesome/vue-fontawesome": "^0.1.10",
+ "@fortawesome/vue-fontawesome": "^2.0.0",
"@silvermine/videojs-quality-selector": "^1.2.4",
"autolinker": "^3.14.1",
"bulma-pro": "^0.2.0",
@@ -53,7 +53,7 @@
},
"description": "A private YouTube client",
"devDependencies": {
- "@babel/core": "^7.11.5",
+ "@babel/core": "^7.11.6",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/preset-env": "^7.11.5",
@@ -66,12 +66,12 @@
"copy-webpack-plugin": "^6.1.0",
"css-loader": "^4.2.2",
"devtron": "^1.4.0",
- "electron": "^10.1.0",
+ "electron": "^10.1.1",
"electron-builder": "^22.8.0",
"electron-builder-squirrel-windows": "^22.8.1",
"electron-debug": "^3.1.0",
"electron-rebuild": "^2.0.1",
- "eslint": "^7.7.0",
+ "eslint": "^7.8.1",
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0",
@@ -90,7 +90,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"sass": "^1.26.10",
- "sass-loader": "^10.0.1",
+ "sass-loader": "^10.0.2",
"style-loader": "^1.2.1",
"tree-kill": "1.2.2",
"typescript": "^4.0.2",
diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js
new file mode 100644
index 00000000..a31f054c
--- /dev/null
+++ b/src/renderer/components/data-settings/data-settings.js
@@ -0,0 +1,782 @@
+import Vue from 'vue'
+import { mapActions, mapMutations } from 'vuex'
+import FtCard from '../ft-card/ft-card.vue'
+import FtButton from '../ft-button/ft-button.vue'
+import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
+import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
+import FtPrompt from '../ft-prompt/ft-prompt.vue'
+
+import { remote } from 'electron'
+import fs from 'fs'
+import opmlToJson from 'opml-to-json'
+import ytch from 'yt-channel-info'
+
+const app = remote.app
+const dialog = remote.dialog
+
+export default Vue.extend({
+ name: 'DataSettings',
+ components: {
+ 'ft-card': FtCard,
+ 'ft-button': FtButton,
+ 'ft-toggle-switch': FtToggleSwitch,
+ 'ft-flex-box': FtFlexBox,
+ 'ft-prompt': FtPrompt
+ },
+ data: function () {
+ return {
+ showImportSubscriptionsPrompt: false,
+ showExportSubscriptionsPrompt: false,
+ subscriptionsPromptValues: [
+ 'freetube',
+ 'youtube',
+ 'newpipe'
+ ]
+ }
+ },
+ computed: {
+ rememberHistory: function () {
+ return this.$store.getters.getRememberHistory
+ },
+ saveWatchedProgress: function () {
+ return this.$store.getters.getSaveWatchedProgress
+ },
+ backendPreference: function () {
+ return this.$store.getters.getBackendPreference
+ },
+ backendFallback: function () {
+ return this.$store.getters.getBackendFallback
+ },
+ invidiousInstance: function () {
+ return this.$store.getters.getInvidiousInstance
+ },
+ profileList: function () {
+ return this.$store.getters.getProfileList
+ },
+ importSubscriptionsPromptNames: function () {
+ const importFreeTube = this.$t('Settings.Data Settings.Import FreeTube')
+ const importYouTube = this.$t('Settings.Data Settings.Import YouTube')
+ const importNewPipe = this.$t('Settings.Data Settings.Import NewPipe')
+ return [
+ `${importFreeTube} (.db)`,
+ `${importYouTube} (.opml)`,
+ `${importNewPipe} (.json)`
+ ]
+ },
+ exportSubscriptionsPromptNames: function () {
+ const exportFreeTube = this.$t('Settings.Data Settings.Export FreeTube')
+ const exportYouTube = this.$t('Settings.Data Settings.Export YouTube')
+ const exportNewPipe = this.$t('Settings.Data Settings.Export NewPipe')
+ return [
+ `${exportFreeTube} (.db)`,
+ `${exportYouTube} (.opml)`,
+ `${exportNewPipe} (.json)`
+ ]
+ }
+ },
+ methods: {
+ importSubscriptions: function (option) {
+ this.showImportSubscriptionsPrompt = false
+
+ if (option === null) {
+ return
+ }
+
+ switch (option) {
+ case 'freetube':
+ this.importFreeTubeSubscriptions()
+ break
+ case 'youtube':
+ this.importYouTubeSubscriptions()
+ break
+ case 'newpipe':
+ this.importNewPipeSubscriptions()
+ break
+ }
+ },
+
+ importFreeTubeSubscriptions: function () {
+ const options = {
+ properties: ['openFile'],
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['db']
+ }
+ ]
+ }
+
+ dialog.showOpenDialog(options).then((response) => {
+ if (response.canceled || response.filePaths.length === 0) {
+ return
+ }
+
+ const filePath = response.filePaths[0]
+
+ fs.readFile(filePath, async (err, data) => {
+ if (err) {
+ const message = this.$t('Settings.Data Settings.Unable to read file')
+ this.showToast({
+ message: `${message}: ${err}`
+ })
+ return
+ }
+
+ let textDecode = new TextDecoder('utf-8').decode(data)
+ textDecode = textDecode.split('\n')
+ textDecode.pop()
+
+ textDecode.forEach((data) => {
+ const profileData = JSON.parse(data)
+ // We would technically already be done by the time the data is parsed,
+ // however we want to limit the possibility of malicious data being sent
+ // to the app, so we'll only grab the data we need here.
+
+ const requiredKeys = [
+ '_id',
+ 'name',
+ 'bgColor',
+ 'textColor',
+ 'subscriptions'
+ ]
+
+ const profileObject = {}
+
+ Object.keys(profileData).forEach((key) => {
+ if (!requiredKeys.includes(key)) {
+ const message = this.$t('Settings.Data Settings.Unknown data key')
+ this.showToast({
+ message: `${message}: ${key}`
+ })
+ } else {
+ profileObject[key] = profileData[key]
+ }
+ })
+
+ if (Object.keys(profileObject).length < requiredKeys.length) {
+ const message = this.$t('Settings.Data Settings.Profile object has insufficient data, skipping item')
+ this.showToast({
+ message: message
+ })
+ } else {
+ this.updateProfile(profileObject)
+ }
+ })
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All subscriptions and profiles have been successfully imported')
+ })
+ })
+ })
+ },
+
+ importYouTubeSubscriptions: function () {
+ const options = {
+ properties: ['openFile'],
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['*']
+ }
+ ]
+ }
+
+ dialog.showOpenDialog(options).then(async (response) => {
+ if (response.canceled || response.filePaths.length === 0) {
+ return
+ }
+
+ const filePath = response.filePaths[0]
+
+ fs.readFile(filePath, async (err, data) => {
+ if (err) {
+ const message = this.$t('Settings.Data Settings.Unable to read file')
+ this.showToast({
+ message: `${message}: ${err}`
+ })
+ return
+ }
+
+ opmlToJson(data, async (err, json) => {
+ if (err) {
+ console.log(err)
+ const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
+ this.showToast({
+ message: `${message}: ${err}`
+ })
+ return
+ }
+
+ const feedData = json.children[0].children
+
+ if (typeof feedData === 'undefined') {
+ const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
+ this.showToast({
+ message: message
+ })
+
+ return
+ }
+
+ const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
+ const subscriptions = []
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.This might take a while, please wait')
+ })
+
+ this.updateShowProgressBar(true)
+ this.setProgressBarPercentage(0)
+
+ feedData.forEach(async (channel, index) => {
+ const channelId = channel.xmlurl.replace('https://www.youtube.com/feeds/videos.xml?channel_id=', '')
+ let channelInfo
+ if (this.backendPreference === 'invidious') {
+ channelInfo = await this.getChannelInfoInvidious(channelId)
+ } else {
+ channelInfo = await this.getChannelInfoLocal(channelId)
+ }
+
+ const subscription = {
+ id: channelId,
+ name: channelInfo.author,
+ thumbnail: channelInfo.authorThumbnails[1].url
+ }
+
+ subscriptions.push(subscription)
+
+ const progressPercentage = ((subscriptions.length + 1) / feedData.length) * 100
+ this.setProgressBarPercentage(progressPercentage)
+
+ if (subscriptions.length === feedData.length) {
+ primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
+ this.updateProfile(primaryProfile)
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported')
+ })
+ this.updateShowProgressBar(false)
+ }
+ })
+ })
+ })
+ })
+ },
+
+ importNewPipeSubscriptions: function () {
+ const options = {
+ properties: ['openFile'],
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['json']
+ }
+ ]
+ }
+
+ dialog.showOpenDialog(options).then(async (response) => {
+ if (response.canceled || response.filePaths.length === 0) {
+ return
+ }
+
+ const filePath = response.filePaths[0]
+
+ fs.readFile(filePath, async (err, data) => {
+ if (err) {
+ const message = this.$t('Settings.Data Settings.Unable to read file')
+ this.showToast({
+ message: `${message}: ${err}`
+ })
+ return
+ }
+
+ const newPipeData = JSON.parse(data)
+
+ if (typeof newPipeData.subscriptions === 'undefined') {
+ this.showToast({
+ message: this.$t('Settings.Data Settings.Invalid subscriptions file')
+ })
+
+ return
+ }
+
+ const newPipeSubscriptions = newPipeData.subscriptions
+
+ const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
+ const subscriptions = []
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.This might take a while, please wait')
+ })
+
+ this.updateShowProgressBar(true)
+ this.setProgressBarPercentage(0)
+
+ newPipeSubscriptions.forEach(async (channel, index) => {
+ const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
+ let channelInfo
+ if (this.backendPreference === 'invidious') {
+ channelInfo = await this.getChannelInfoInvidious(channelId)
+ } else {
+ channelInfo = await this.getChannelInfoLocal(channelId)
+ }
+
+ const subscription = {
+ id: channelId,
+ name: channelInfo.author,
+ thumbnail: channelInfo.authorThumbnails[1].url
+ }
+
+ subscriptions.push(subscription)
+
+ const progressPercentage = ((subscriptions.length + 1) / newPipeSubscriptions.length) * 100
+ this.setProgressBarPercentage(progressPercentage)
+
+ if (subscriptions.length === newPipeSubscriptions.length) {
+ primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
+ this.updateProfile(primaryProfile)
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported')
+ })
+ this.updateShowProgressBar(false)
+ }
+ })
+ })
+ })
+ },
+
+ exportSubscriptions: function (option) {
+ this.showExportSubscriptionsPrompt = false
+
+ if (option === null) {
+ return
+ }
+
+ switch (option) {
+ case 'freetube':
+ this.exportFreeTubeSubscriptions()
+ break
+ case 'youtube':
+ this.exportYouTubeSubscriptions()
+ break
+ case 'newpipe':
+ this.exportNewPipeSubscriptions()
+ break
+ }
+ },
+
+ exportFreeTubeSubscriptions: function () {
+ const userData = app.getPath('userData')
+ const subscriptionsDb = `${userData}/profiles.db`
+ const date = new Date()
+ let dateMonth = date.getMonth() + 1
+
+ if (dateMonth < 10) {
+ dateMonth = '0' + dateMonth
+ }
+
+ let dateDay = date.getDate()
+
+ if (dateDay < 10) {
+ dateDay = '0' + dateDay
+ }
+
+ const dateYear = date.getFullYear()
+ const exportFileName = 'freetube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.db'
+
+ const options = {
+ defaultPath: exportFileName,
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['db']
+ }
+ ]
+ }
+
+ dialog.showSaveDialog(options).then((response) => {
+ if (response.canceled || response.filePath === '') {
+ // User canceled the save dialog
+ return
+ }
+
+ const filePath = response.filePath
+
+ fs.readFile(subscriptionsDb, (readErr, data) => {
+ if (readErr) {
+ const message = this.$t('Settings.Data Settings.Unable to read file')
+ this.showToast({
+ message: `${message}: ${readErr}`
+ })
+ return
+ }
+
+ fs.writeFile(filePath, data, (writeErr) => {
+ if (writeErr) {
+ const message = this.$t('Settings.Data Settings.Unable to write file')
+ this.showToast({
+ message: `${message}: ${writeErr}`
+ })
+ return
+ }
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All subscriptions have been successfully exported')
+ })
+ })
+ })
+ })
+ },
+
+ exportYouTubeSubscriptions: async function () {
+ const date = new Date()
+ let dateMonth = date.getMonth() + 1
+
+ if (dateMonth < 10) {
+ dateMonth = '0' + dateMonth
+ }
+
+ let dateDay = date.getDate()
+
+ if (dateDay < 10) {
+ dateDay = '0' + dateDay
+ }
+
+ const dateYear = date.getFullYear()
+ const exportFileName = 'youtube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.opml'
+
+ const options = {
+ defaultPath: exportFileName,
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['opml']
+ }
+ ]
+ }
+
+ let opmlData = ''
+ const endingOpmlString = ''
+
+ let count = 0
+
+ this.profileList[0].subscriptions.forEach((channel) => {
+ const channelOpmlString = ``
+ count++
+ opmlData += channelOpmlString
+
+ if (count === this.profileList[0].subscriptions.length) {
+ opmlData += endingOpmlString
+ }
+ })
+
+ dialog.showSaveDialog(options).then((response) => {
+ if (response.canceled || response.filePath === '') {
+ // User canceled the save dialog
+ return
+ }
+
+ const filePath = response.filePath
+
+ fs.writeFile(filePath, opmlData, (writeErr) => {
+ if (writeErr) {
+ const message = this.$t('Settings.Data Settings.Unable to write file')
+ this.showToast({
+ message: `${message}: ${writeErr}`
+ })
+ return
+ }
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All subscriptions have been successfully exported')
+ })
+ })
+ })
+ },
+
+ exportNewPipeSubscriptions: function () {
+ const date = new Date()
+ let dateMonth = date.getMonth() + 1
+
+ if (dateMonth < 10) {
+ dateMonth = '0' + dateMonth
+ }
+
+ let dateDay = date.getDate()
+
+ if (dateDay < 10) {
+ dateDay = '0' + dateDay
+ }
+
+ const dateYear = date.getFullYear()
+ const exportFileName = 'newpipe-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.json'
+
+ const options = {
+ defaultPath: exportFileName,
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['json']
+ }
+ ]
+ }
+
+ const newPipeObject = {
+ app_version: '0.19.8',
+ app_version_int: 953,
+ subscriptions: []
+ }
+
+ this.profileList[0].subscriptions.forEach((channel) => {
+ const channelUrl = `https://www.youtube.com/channel/${channel.id}`
+ const subscription = {
+ service_id: 0,
+ url: channelUrl,
+ name: channel.name
+ }
+
+ newPipeObject.subscriptions.push(subscription)
+ })
+
+ dialog.showSaveDialog(options).then((response) => {
+ if (response.canceled || response.filePath === '') {
+ // User canceled the save dialog
+ return
+ }
+
+ const filePath = response.filePath
+
+ fs.writeFile(filePath, JSON.stringify(newPipeObject), (writeErr) => {
+ if (writeErr) {
+ const message = this.$t('Settings.Data Settings.Unable to write file')
+ this.showToast({
+ message: `${message}: ${writeErr}`
+ })
+ return
+ }
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All subscriptions have been successfully exported')
+ })
+ })
+ })
+ },
+
+ importHistory: function () {
+ const options = {
+ properties: ['openFile'],
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['db']
+ }
+ ]
+ }
+
+ dialog.showOpenDialog(options).then((response) => {
+ if (response.canceled || response.filePaths.length === 0) {
+ return
+ }
+
+ const filePath = response.filePaths[0]
+
+ fs.readFile(filePath, async (err, data) => {
+ if (err) {
+ const message = this.$t('Settings.Data Settings.Unable to read file')
+ this.showToast({
+ message: `${message}: ${err}`
+ })
+ return
+ }
+
+ let textDecode = new TextDecoder('utf-8').decode(data)
+ textDecode = textDecode.split('\n')
+ textDecode.pop()
+
+ textDecode.forEach((history) => {
+ const historyData = JSON.parse(history)
+ // We would technically already be done by the time the data is parsed,
+ // however we want to limit the possibility of malicious data being sent
+ // to the app, so we'll only grab the data we need here.
+ const requiredKeys = [
+ '_id',
+ 'author',
+ 'authorId',
+ 'description',
+ 'isLive',
+ 'lengthSeconds',
+ 'paid',
+ 'published',
+ 'timeWatched',
+ 'title',
+ 'type',
+ 'videoId',
+ 'viewCount',
+ 'watchProgress'
+ ]
+
+ const historyObject = {}
+
+ Object.keys(historyData).forEach((key) => {
+ if (!requiredKeys.includes(key)) {
+ this.showToast({
+ message: `Unknown data key: ${key}`
+ })
+ } else {
+ historyObject[key] = historyData[key]
+ }
+ })
+
+ if (Object.keys(historyObject).length < requiredKeys.length) {
+ this.showToast({
+ message: this.$t('Settings.Data Settings.History object has insufficient data, skipping item')
+ })
+ } else {
+ this.updateHistory(historyObject)
+ }
+ })
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All watched history has been successfully imported')
+ })
+ })
+ })
+ },
+
+ exportHistory: function () {
+ const userData = app.getPath('userData')
+ const historyDb = `${userData}/history.db`
+ const date = new Date()
+ let dateMonth = date.getMonth() + 1
+
+ if (dateMonth < 10) {
+ dateMonth = '0' + dateMonth
+ }
+
+ let dateDay = date.getDate()
+
+ if (dateDay < 10) {
+ dateDay = '0' + dateDay
+ }
+
+ const dateYear = date.getFullYear()
+ const exportFileName = 'freetube-history-' + dateYear + '-' + dateMonth + '-' + dateDay + '.db'
+
+ const options = {
+ defaultPath: exportFileName,
+ filters: [
+ {
+ name: 'Database File',
+ extensions: ['db']
+ }
+ ]
+ }
+
+ dialog.showSaveDialog(options).then((response) => {
+ if (response.canceled || response.filePath === '') {
+ // User canceled the save dialog
+ return
+ }
+
+ const filePath = response.filePath
+
+ fs.readFile(historyDb, (readErr, data) => {
+ if (readErr) {
+ const message = this.$t('Settings.Data Settings.Unable to read file')
+ this.showToast({
+ message: `${message}: ${readErr}`
+ })
+ return
+ }
+
+ fs.writeFile(filePath, data, (writeErr) => {
+ if (writeErr) {
+ const message = this.$t('Settings.Data Settings.Unable to write file')
+ this.showToast({
+ message: `${message}: ${writeErr}`
+ })
+ return
+ }
+
+ this.showToast({
+ message: this.$t('Settings.Data Settings.All watched history has been successfully exported')
+ })
+ })
+ })
+ })
+ },
+
+ getChannelInfoInvidious: function (channelId) {
+ return new Promise((resolve, reject) => {
+ const subscriptionsPayload = {
+ resource: 'channels',
+ id: channelId,
+ params: {}
+ }
+
+ this.invidiousAPICall(subscriptionsPayload).then((response) => {
+ resolve(response)
+ }).catch((err) => {
+ console.log(err)
+ const errorMessage = this.$t('Invidious API Error (Click to copy)')
+ this.showToast({
+ message: `${errorMessage}: ${err.responseText}`,
+ time: 10000,
+ action: () => {
+ navigator.clipboard.writeText(err)
+ }
+ })
+
+ if (this.backendFallback) {
+ this.showToast({
+ message: this.$t('Falling back to the local API')
+ })
+ resolve(this.getChannelInfoLocal(channelId))
+ } else {
+ resolve([])
+ }
+ })
+ })
+ },
+
+ getChannelInfoLocal: function (channelId) {
+ return new Promise((resolve, reject) => {
+ ytch.getChannelInfo(channelId, 'latest').then(async (response) => {
+ resolve(response)
+ }).catch((err) => {
+ console.log(err)
+ const errorMessage = this.$t('Local API Error (Click to copy)')
+ this.showToast({
+ message: `${errorMessage}: ${err}`,
+ time: 10000,
+ action: () => {
+ navigator.clipboard.writeText(err)
+ }
+ })
+
+ if (this.backendFallback) {
+ this.showToast({
+ message: this.$t('Falling back to the Invidious API')
+ })
+ resolve(this.getChannelInfoInvidious(channelId))
+ } else {
+ resolve([])
+ }
+ })
+ })
+ },
+
+ ...mapActions([
+ 'invidiousAPICall',
+ 'updateProfile',
+ 'updateShowProgressBar',
+ 'updateHistory',
+ 'showToast'
+ ]),
+
+ ...mapMutations([
+ 'setProgressBarPercentage'
+ ])
+ }
+})
diff --git a/src/renderer/components/data-settings/data-settings.sass b/src/renderer/components/data-settings/data-settings.sass
new file mode 100644
index 00000000..05cb0dfb
--- /dev/null
+++ b/src/renderer/components/data-settings/data-settings.sass
@@ -0,0 +1 @@
+@use "../../sass-partials/settings"
diff --git a/src/renderer/components/data-settings/data-settings.vue b/src/renderer/components/data-settings/data-settings.vue
new file mode 100644
index 00000000..faae68ff
--- /dev/null
+++ b/src/renderer/components/data-settings/data-settings.vue
@@ -0,0 +1,44 @@
+
+
+
+ {{ $t("Settings.Data Settings.Data Settings") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/ft-profile-selector/ft-profile-selector.css b/src/renderer/components/ft-profile-selector/ft-profile-selector.css
index 1d4559f5..8ed7427e 100644
--- a/src/renderer/components/ft-profile-selector/ft-profile-selector.css
+++ b/src/renderer/components/ft-profile-selector/ft-profile-selector.css
@@ -20,7 +20,7 @@
top: 60px;
right: 10px;
min-width: 250px;
- height: 300px;
+ height: 400px;
padding: 5px;
background-color: var(--card-bg-color);
box-shadow: 0 1px 2px rgba(0,0,0,.1);
@@ -33,7 +33,7 @@
.profileWrapper {
margin-top: 60px;
- height: 240px;
+ height: 340px;
overflow-y: auto;
}
diff --git a/src/renderer/components/privacy-settings/privacy-settings.js b/src/renderer/components/privacy-settings/privacy-settings.js
index 5e06553a..836fc746 100644
--- a/src/renderer/components/privacy-settings/privacy-settings.js
+++ b/src/renderer/components/privacy-settings/privacy-settings.js
@@ -19,6 +19,7 @@ export default Vue.extend({
return {
showSearchCachePrompt: false,
showRemoveHistoryPrompt: false,
+ showRemoveSubscriptionsPrompt: false,
promptValues: [
'yes',
'no'
@@ -32,6 +33,12 @@ export default Vue.extend({
saveWatchedProgress: function () {
return this.$store.getters.getSaveWatchedProgress
},
+ profileList: function () {
+ return this.$store.getters.getProfileList
+ },
+ removeSubscriptionsPromptMessage: function () {
+ return this.$t('Settings.Privacy Settings["Are you sure you want to remove all subscriptions and profiles? This cannot be undone."]')
+ },
promptNames: function () {
return [
this.$t('Yes'),
@@ -62,11 +69,37 @@ export default Vue.extend({
}
},
+ handleRemoveSubscriptions: function (option) {
+ this.showRemoveSubscriptionsPrompt = false
+
+ this.updateActiveProfile(0)
+
+ if (option === 'yes') {
+ this.profileList.forEach((profile) => {
+ if (profile._id === 'allChannels') {
+ const newProfile = {
+ _id: 'allChannels',
+ name: profile.name,
+ bgColor: profile.bgColor,
+ textColor: profile.textColor,
+ subscriptions: []
+ }
+ this.updateProfile(newProfile)
+ } else {
+ this.removeProfile(profile._id)
+ }
+ })
+ }
+ },
+
...mapActions([
'updateRememberHistory',
'removeAllHistory',
'updateSaveWatchedProgress',
'clearSessionSearchHistory',
+ 'updateProfile',
+ 'removeProfile',
+ 'updateActiveProfile',
'showToast'
])
}
diff --git a/src/renderer/components/privacy-settings/privacy-settings.vue b/src/renderer/components/privacy-settings/privacy-settings.vue
index 6a450da3..1cb1233a 100644
--- a/src/renderer/components/privacy-settings/privacy-settings.vue
+++ b/src/renderer/components/privacy-settings/privacy-settings.vue
@@ -37,6 +37,12 @@
background-color="var(--primary-color)"
@click="showRemoveHistoryPrompt = true"
/>
+
+
diff --git a/src/renderer/views/Settings/Settings.js b/src/renderer/views/Settings/Settings.js
index b43a3646..9f7b1350 100644
--- a/src/renderer/views/Settings/Settings.js
+++ b/src/renderer/views/Settings/Settings.js
@@ -4,8 +4,9 @@ import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import GeneralSettings from '../../components/general-settings/general-settings.vue'
import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
import PlayerSettings from '../../components/player-settings/player-settings.vue'
-import PrivacySettings from '../../components/privacy-settings/privacy-settings.vue'
import SubscriptionSettings from '../../components/subscription-settings/subscription-settings.vue'
+import PrivacySettings from '../../components/privacy-settings/privacy-settings.vue'
+import DataSettings from '../../components/data-settings/data-settings.vue'
export default Vue.extend({
name: 'Settings',
@@ -15,14 +16,8 @@ export default Vue.extend({
'general-settings': GeneralSettings,
'theme-settings': ThemeSettings,
'player-settings': PlayerSettings,
+ 'subscription-settings': SubscriptionSettings,
'privacy-settings': PrivacySettings,
- 'subscription-settings': SubscriptionSettings
- },
- mounted: function () {
- },
- methods: {
- handleToggleSwitch: function (event) {
- console.log(event)
- }
+ 'data-settings': DataSettings
}
})
diff --git a/src/renderer/views/Settings/Settings.vue b/src/renderer/views/Settings/Settings.vue
index 16f22727..85a99dab 100644
--- a/src/renderer/views/Settings/Settings.vue
+++ b/src/renderer/views/Settings/Settings.vue
@@ -5,6 +5,7 @@
+
diff --git a/src/renderer/views/Trending/Trending.js b/src/renderer/views/Trending/Trending.js
index 77b125d4..203072c6 100644
--- a/src/renderer/views/Trending/Trending.js
+++ b/src/renderer/views/Trending/Trending.js
@@ -1,4 +1,5 @@
import Vue from 'vue'
+import { mapActions } from 'vuex'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
@@ -121,7 +122,7 @@ export default Vue.extend({
console.log(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
this.showToast({
- message: `${errorMessage}: ${err}`,
+ message: `${errorMessage}: ${err.responseText}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
@@ -137,6 +138,10 @@ export default Vue.extend({
this.isLoading = false
}
})
- }
+ },
+
+ ...mapActions([
+ 'showToast'
+ ])
}
})
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index 99394fd8..caf69b02 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -178,18 +178,40 @@ Settings:
Are you sure you want to remove your entire watch history?: Are you sure you want
to remove your entire watch history?
Watch history has been cleared: Watch history has been cleared
+ Remove All Subscriptions / Profiles: Remove All Subscriptions / Profiles
+ Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: Are you sure you want to remove all subscriptions and profiles? This cannot be undone.
Subscription Settings:
Subscription Settings: Subscription Settings
Hide Videos on Watch: Hide Videos on Watch
Fetch Feeds from RSS: Fetch Feeds from RSS
- Subscriptions Export Format:
- Subscriptions Export Format: Subscriptions Export Format
- #& Freetube
- Newpipe: Newpipe
- OPML: OPML
Manage Subscriptions: Manage Subscriptions
+ Data Settings:
+ Data Settings: Data Settings
+ Select Import Type: Select Import Type
+ Select Export Type: Select Export Type
Import Subscriptions: Import Subscriptions
+ Import FreeTube: Import FreeTube
+ Import YouTube: Import YouTube
+ Import NewPipe: Import NewPipe
Export Subscriptions: Export Subscriptions
+ Export FreeTube: Export FreeTube
+ Export YouTube: Export YouTube
+ Export NewPipe: Export NewPipe
+ Import History: Import History
+ Export History: Export History
+ Profile object has insufficient data, skipping item: Profile object has insufficient data, skipping item
+ All subscriptions and profiles have been successfully imported: All subscriptions and profiles have been successfully imported
+ All subscriptions have been successfully imported: All subscriptions have been successfully imported
+ Invalid subscriptions file: Invalid subscriptions file
+ This might take a while, please wait: This might take a while, please wait
+ Invalid history file: Invalid history file
+ Subscriptions have been successfully exported: Subscriptions have been successfully exported
+ History object has insufficient data, skipping item: History object has insufficient data, skipping item
+ All watched history has been successfully imported: All watched history has been successfully imported
+ All watched history has been successfully exported: All watched history has been successfully exported
+ Unable to read file: Unable to read file
+ Unable to write file: Unable to write file
+ Unknown data key: Unknown data key
How do I import my subscriptions?: How do I import my subscriptions?
Advanced Settings:
Advanced Settings: Advanced Settings