Add functionality to import and export subscriptions / history

This commit is contained in:
Preston 2020-09-06 18:12:25 -04:00
parent 8308734716
commit d77c9aed49
12 changed files with 1036 additions and 65 deletions

159
package-lock.json generated
View File

@ -31,13 +31,13 @@
} }
}, },
"@babel/core": { "@babel/core": {
"version": "7.11.5", "version": "7.11.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.5.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz",
"integrity": "sha512-fsEANVOcZHzrsV6dMVWqpSeXClq3lNbYrfFGme6DE25FQWe7pyeYpXyx9guqUnpy466JLzZ8z4uwSr2iv60V5Q==", "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.11.5", "@babel/generator": "^7.11.6",
"@babel/helper-module-transforms": "^7.11.0", "@babel/helper-module-transforms": "^7.11.0",
"@babel/helpers": "^7.10.4", "@babel/helpers": "^7.10.4",
"@babel/parser": "^7.11.5", "@babel/parser": "^7.11.5",
@ -51,7 +51,7 @@
"lodash": "^4.17.19", "lodash": "^4.17.19",
"resolve": "^1.3.2", "resolve": "^1.3.2",
"semver": "^5.4.1", "semver": "^5.4.1",
"source-map": "^0.6.1" "source-map": "^0.5.0"
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": { "@babel/code-frame": {
@ -64,14 +64,14 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.11.5", "version": "7.11.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.5.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz",
"integrity": "sha512-9UqHWJ4IwRTy4l0o8gq2ef8ws8UPzvtMkVKjTLAiRmza9p9V6Z+OfuNd9fB1j5Q67F+dVJtPC2sZXI8NM9br4g==", "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.11.5", "@babel/types": "^7.11.5",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"source-map": "^0.6.1" "source-map": "^0.5.0"
} }
}, },
"@babel/highlight": { "@babel/highlight": {
@ -133,12 +133,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "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": { "@fortawesome/fontawesome-common-types": {
"version": "0.2.30", "version": "0.2.30",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz",
@ -1624,9 +1692,9 @@
} }
}, },
"@fortawesome/vue-fontawesome": { "@fortawesome/vue-fontawesome": {
"version": "0.1.10", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.10.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.0.tgz",
"integrity": "sha512-b2+SLF31h32LSepVcXe+BQ63yvbq5qmTCy4KfFogCYm2bn68H5sDWUnX+U7MBqnM2aeEk9M7xSoqGnu+wSdY6w==" "integrity": "sha512-N3VKw7KzRfOm8hShUVldpinlm13HpvLBQgT63QS+aCrIRLwjoEUXY5Rcmttbfb6HkzZaeqjLqd/aZCQ53UjQpg=="
}, },
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
@ -6956,9 +7024,9 @@
} }
}, },
"electron": { "electron": {
"version": "10.1.0", "version": "10.1.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-10.1.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-10.1.1.tgz",
"integrity": "sha512-DyS6WhQ59+ZXQsI1EkpsYkOXFt0Xbp+mbxPTJS9A7O21r3JDzaTC+1Jxz7g6J+Sbi9Y7UFdRs0tn/vqhHJx2gA==", "integrity": "sha512-ZJtZHMr17AvvBosuA6XUmpehwAlGM4/n46Mw9BcyD8tpgdI6IQd0X5OU9meE3X3M8Y6Ja2Kr2udTMgtjvot2hA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^1.0.1", "@electron/get": "^1.0.1",
@ -7853,12 +7921,13 @@
} }
}, },
"eslint": { "eslint": {
"version": "7.7.0", "version": "7.8.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz",
"integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "^7.0.0",
"@eslint/eslintrc": "^0.1.3",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@ -7868,7 +7937,7 @@
"eslint-scope": "^5.1.0", "eslint-scope": "^5.1.0",
"eslint-utils": "^2.1.0", "eslint-utils": "^2.1.0",
"eslint-visitor-keys": "^1.3.0", "eslint-visitor-keys": "^1.3.0",
"espree": "^7.2.0", "espree": "^7.3.0",
"esquery": "^1.2.0", "esquery": "^1.2.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"file-entry-cache": "^5.0.1", "file-entry-cache": "^5.0.1",
@ -8081,9 +8150,9 @@
"dev": true "dev": true
}, },
"supports-color": { "supports-color": {
"version": "7.1.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true, "dev": true,
"requires": { "requires": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
@ -8319,12 +8388,12 @@
"dev": true "dev": true
}, },
"espree": { "espree": {
"version": "7.2.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz",
"integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==", "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==",
"dev": true, "dev": true,
"requires": { "requires": {
"acorn": "^7.3.1", "acorn": "^7.4.0",
"acorn-jsx": "^5.2.0", "acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.3.0" "eslint-visitor-keys": "^1.3.0"
}, },
@ -16630,18 +16699,24 @@
} }
}, },
"sass-loader": { "sass-loader": {
"version": "10.0.1", "version": "10.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.1.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.2.tgz",
"integrity": "sha512-b2PSldKVTS3JcFPHSrEXh3BeAfR7XknGiGCAO5aHruR3Pf3kqLP3Gb2ypXLglRrAzgZkloNxLZ7GXEGDX0hBUQ==", "integrity": "sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg==",
"dev": true, "dev": true,
"requires": { "requires": {
"klona": "^2.0.3", "klona": "^2.0.3",
"loader-utils": "^2.0.0", "loader-utils": "^2.0.0",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^2.7.0", "schema-utils": "^2.7.1",
"semver": "^7.3.2" "semver": "^7.3.2"
}, },
"dependencies": { "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": { "ajv": {
"version": "6.12.4", "version": "6.12.4",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
@ -16696,14 +16771,14 @@
"dev": true "dev": true
}, },
"schema-utils": { "schema-utils": {
"version": "2.7.0", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/json-schema": "^7.0.4", "@types/json-schema": "^7.0.5",
"ajv": "^6.12.2", "ajv": "^6.12.4",
"ajv-keywords": "^3.4.1" "ajv-keywords": "^3.5.2"
} }
}, },
"semver": { "semver": {

View File

@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-solid-svg-icons": "^5.14.0", "@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", "@silvermine/videojs-quality-selector": "^1.2.4",
"autolinker": "^3.14.1", "autolinker": "^3.14.1",
"bulma-pro": "^0.2.0", "bulma-pro": "^0.2.0",
@ -53,7 +53,7 @@
}, },
"description": "A private YouTube client", "description": "A private YouTube client",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.11.5", "@babel/core": "^7.11.6",
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0", "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/preset-env": "^7.11.5", "@babel/preset-env": "^7.11.5",
@ -66,12 +66,12 @@
"copy-webpack-plugin": "^6.1.0", "copy-webpack-plugin": "^6.1.0",
"css-loader": "^4.2.2", "css-loader": "^4.2.2",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"electron": "^10.1.0", "electron": "^10.1.1",
"electron-builder": "^22.8.0", "electron-builder": "^22.8.0",
"electron-builder-squirrel-windows": "^22.8.1", "electron-builder-squirrel-windows": "^22.8.1",
"electron-debug": "^3.1.0", "electron-debug": "^3.1.0",
"electron-rebuild": "^2.0.1", "electron-rebuild": "^2.0.1",
"eslint": "^7.7.0", "eslint": "^7.8.1",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1", "eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0", "eslint-plugin-import": "^2.22.0",
@ -90,7 +90,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.1.1", "prettier": "^2.1.1",
"sass": "^1.26.10", "sass": "^1.26.10",
"sass-loader": "^10.0.1", "sass-loader": "^10.0.2",
"style-loader": "^1.2.1", "style-loader": "^1.2.1",
"tree-kill": "1.2.2", "tree-kill": "1.2.2",
"typescript": "^4.0.2", "typescript": "^4.0.2",

View File

@ -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 = '<opml version="1.1"><body><outline text="YouTube Subscriptions" title="YouTube Subscriptions">'
const endingOpmlString = '</outline></body></opml>'
let count = 0
this.profileList[0].subscriptions.forEach((channel) => {
const channelOpmlString = `<outline text="${channel.name}" title="${channel.name}" type="rss" xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id=${channel.id}"/>`
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'
])
}
})

View File

@ -0,0 +1 @@
@use "../../sass-partials/settings"

View File

@ -0,0 +1,44 @@
<template>
<ft-card
class="relative card"
>
<h3>
{{ $t("Settings.Data Settings.Data Settings") }}
</h3>
<ft-flex-box>
<ft-button
:label="$t('Settings.Data Settings.Import Subscriptions')"
@click="showImportSubscriptionsPrompt = true"
/>
<ft-button
:label="$t('Settings.Data Settings.Export Subscriptions')"
@click="showExportSubscriptionsPrompt = true"
/>
<ft-button
:label="$t('Settings.Data Settings.Import History')"
@click="importHistory"
/>
<ft-button
:label="$t('Settings.Data Settings.Export Subscriptions')"
@click="exportHistory"
/>
</ft-flex-box>
<ft-prompt
v-if="showImportSubscriptionsPrompt"
:label="$t('Settings.Data Settings.Select Import Type')"
:option-names="importSubscriptionsPromptNames"
:option-values="subscriptionsPromptValues"
@click="importSubscriptions"
/>
<ft-prompt
v-if="showExportSubscriptionsPrompt"
:label="$t('Settings.Data Settings.Select Export Type')"
:option-names="exportSubscriptionsPromptNames"
:option-values="subscriptionsPromptValues"
@click="exportSubscriptions"
/>
</ft-card>
</template>
<script src="./data-settings.js" />
<style scoped lang="sass" src="./data-settings.sass" />

View File

@ -20,7 +20,7 @@
top: 60px; top: 60px;
right: 10px; right: 10px;
min-width: 250px; min-width: 250px;
height: 300px; height: 400px;
padding: 5px; padding: 5px;
background-color: var(--card-bg-color); background-color: var(--card-bg-color);
box-shadow: 0 1px 2px rgba(0,0,0,.1); box-shadow: 0 1px 2px rgba(0,0,0,.1);
@ -33,7 +33,7 @@
.profileWrapper { .profileWrapper {
margin-top: 60px; margin-top: 60px;
height: 240px; height: 340px;
overflow-y: auto; overflow-y: auto;
} }

View File

@ -19,6 +19,7 @@ export default Vue.extend({
return { return {
showSearchCachePrompt: false, showSearchCachePrompt: false,
showRemoveHistoryPrompt: false, showRemoveHistoryPrompt: false,
showRemoveSubscriptionsPrompt: false,
promptValues: [ promptValues: [
'yes', 'yes',
'no' 'no'
@ -32,6 +33,12 @@ export default Vue.extend({
saveWatchedProgress: function () { saveWatchedProgress: function () {
return this.$store.getters.getSaveWatchedProgress 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 () { promptNames: function () {
return [ return [
this.$t('Yes'), 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([ ...mapActions([
'updateRememberHistory', 'updateRememberHistory',
'removeAllHistory', 'removeAllHistory',
'updateSaveWatchedProgress', 'updateSaveWatchedProgress',
'clearSessionSearchHistory', 'clearSessionSearchHistory',
'updateProfile',
'removeProfile',
'updateActiveProfile',
'showToast' 'showToast'
]) ])
} }

View File

@ -37,6 +37,12 @@
background-color="var(--primary-color)" background-color="var(--primary-color)"
@click="showRemoveHistoryPrompt = true" @click="showRemoveHistoryPrompt = true"
/> />
<ft-button
:label="$t('Settings.Privacy Settings.Remove All Subscriptions / Profiles')"
text-color="var(--text-with-main-color)"
background-color="var(--primary-color)"
@click="showRemoveSubscriptionsPrompt = true"
/>
</ft-flex-box> </ft-flex-box>
<ft-prompt <ft-prompt
v-if="showSearchCachePrompt" v-if="showSearchCachePrompt"
@ -52,6 +58,13 @@
:option-values="promptValues" :option-values="promptValues"
@click="handleRemoveHistory" @click="handleRemoveHistory"
/> />
<ft-prompt
v-if="showRemoveSubscriptionsPrompt"
:label="removeSubscriptionsPromptMessage"
:option-names="promptNames"
:option-values="promptValues"
@click="handleRemoveSubscriptions"
/>
</ft-card> </ft-card>
</template> </template>

View File

@ -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 GeneralSettings from '../../components/general-settings/general-settings.vue'
import ThemeSettings from '../../components/theme-settings/theme-settings.vue' import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
import PlayerSettings from '../../components/player-settings/player-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 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({ export default Vue.extend({
name: 'Settings', name: 'Settings',
@ -15,14 +16,8 @@ export default Vue.extend({
'general-settings': GeneralSettings, 'general-settings': GeneralSettings,
'theme-settings': ThemeSettings, 'theme-settings': ThemeSettings,
'player-settings': PlayerSettings, 'player-settings': PlayerSettings,
'subscription-settings': SubscriptionSettings,
'privacy-settings': PrivacySettings, 'privacy-settings': PrivacySettings,
'subscription-settings': SubscriptionSettings 'data-settings': DataSettings
},
mounted: function () {
},
methods: {
handleToggleSwitch: function (event) {
console.log(event)
}
} }
}) })

View File

@ -5,6 +5,7 @@
<player-settings /> <player-settings />
<subscription-settings /> <subscription-settings />
<privacy-settings /> <privacy-settings />
<data-settings />
</div> </div>
</template> </template>

View File

@ -1,4 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
import { mapActions } from 'vuex'
import FtCard from '../../components/ft-card/ft-card.vue' import FtCard from '../../components/ft-card/ft-card.vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue' import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
@ -121,7 +122,7 @@ export default Vue.extend({
console.log(err) console.log(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)') const errorMessage = this.$t('Invidious API Error (Click to copy)')
this.showToast({ this.showToast({
message: `${errorMessage}: ${err}`, message: `${errorMessage}: ${err.responseText}`,
time: 10000, time: 10000,
action: () => { action: () => {
navigator.clipboard.writeText(err) navigator.clipboard.writeText(err)
@ -137,6 +138,10 @@ export default Vue.extend({
this.isLoading = false this.isLoading = false
} }
}) })
} },
...mapActions([
'showToast'
])
} }
}) })

View File

@ -178,18 +178,40 @@ Settings:
Are you sure you want to remove your entire watch history?: Are you sure you want Are you sure you want to remove your entire watch history?: Are you sure you want
to remove your entire watch history? to remove your entire watch history?
Watch history has been cleared: Watch history has been cleared 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: Subscription Settings Subscription Settings: Subscription Settings
Hide Videos on Watch: Hide Videos on Watch Hide Videos on Watch: Hide Videos on Watch
Fetch Feeds from RSS: Fetch Feeds from RSS 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 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 Subscriptions: Import Subscriptions
Import FreeTube: Import FreeTube
Import YouTube: Import YouTube
Import NewPipe: Import NewPipe
Export Subscriptions: Export Subscriptions 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? How do I import my subscriptions?: How do I import my subscriptions?
Advanced Settings: Advanced Settings:
Advanced Settings: Advanced Settings Advanced Settings: Advanced Settings