From af0353ea3256396b178d15facc03b420710ed355 Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Sat, 22 May 2021 00:49:48 +0100 Subject: [PATCH] Refactor: Erase `@electron/remote` references and other dangerous calls The `remote` module is deprecated and `@electron/remote` is unnecessary, since the `ipcMain` and `ipcRenderer` can replace their functionality, providing better performance and better security. All other dangerous calls (mainly pulling main process constructs into the renderer process) have also been removed. --- src/main/index.js | 39 +- src/renderer/App.js | 42 +- .../components/data-settings/data-settings.js | 617 +++++++++--------- .../components/ft-list-video/ft-list-video.js | 33 +- .../ft-share-button/ft-share-button.js | 22 +- .../ft-video-player/ft-video-player.js | 20 +- .../general-settings/general-settings.js | 18 +- .../components/playlist-info/playlist-info.js | 12 +- .../proxy-settings/proxy-settings.js | 8 +- .../theme-settings/theme-settings.js | 8 +- src/renderer/components/top-nav/top-nav.js | 12 +- .../watch-video-info/watch-video-info.js | 13 +- .../watch-video-info/watch-video-info.vue | 2 +- src/renderer/store/modules/history.js | 4 +- src/renderer/store/modules/playlist.js | 5 +- src/renderer/store/modules/profile.js | 5 +- src/renderer/store/modules/settings.js | 14 +- src/renderer/store/modules/utils.js | 60 ++ src/renderer/views/About/About.js | 13 - src/renderer/views/Watch/Watch.js | 17 +- 20 files changed, 505 insertions(+), 459 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 558f05bc..363de176 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,4 +1,7 @@ -import { app, BrowserWindow, Menu, ipcMain, screen } from 'electron' +import { + app, BrowserWindow, dialog, Menu, + ipcMain, powerSaveBlocker, screen, shell +} from 'electron' import Datastore from 'nedb' if (process.argv.includes('--version')) { @@ -9,8 +12,6 @@ if (process.argv.includes('--version')) { } function runApp() { - require('@electron/remote/main').initialize() - require('electron-context-menu')({ showSearchWithGoogle: false, showSaveImageAs: true, @@ -416,6 +417,38 @@ function runApp() { mainWindow.webContents.session.setProxy({}) }) + ipcMain.on('openExternalLink', (_, url) => { + if (typeof url === 'string') shell.openExternal(url) + }) + + ipcMain.handle('getLocale', () => { + return app.getLocale() + }) + + ipcMain.handle('getUserDataPath', () => { + return app.getPath('userData') + }) + + ipcMain.on('getUserDataPathSync', (event) => { + event.returnValue = app.getPath('userData') + }) + + ipcMain.handle('showOpenDialog', async (_, options) => { + return await dialog.showOpenDialog(options) + }) + + ipcMain.handle('showSaveDialog', async (_, options) => { + return await dialog.showSaveDialog(options) + }) + + ipcMain.on('stopPowerSaveBlocker', (_, id) => { + powerSaveBlocker.stop(id) + }) + + ipcMain.handle('startPowerSaveBlocker', (_, type) => { + return powerSaveBlocker.start(type) + }) + ipcMain.on('createNewWindow', () => { createWindow(false, '', false) }) diff --git a/src/renderer/App.js b/src/renderer/App.js index 81d9013f..6be3fca7 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -10,23 +10,17 @@ import FtButton from './components/ft-button/ft-button.vue' import FtToast from './components/ft-toast/ft-toast.vue' import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue' import $ from 'jquery' -import { app } from '@electron/remote' import { markdown } from 'markdown' import Parser from 'rss-parser' -let useElectron -let shell -let electron +let useElectron = false +let ipcRenderer = null Vue.directive('observe-visibility', ObserveVisibility) if (window && window.process && window.process.type === 'renderer') { - /* eslint-disable-next-line */ - electron = require('electron') - shell = electron.shell useElectron = true -} else { - useElectron = false + ipcRenderer = require('electron').ipcRenderer } export default Vue.extend({ @@ -90,12 +84,12 @@ export default Vue.extend({ mounted: function () { const v = this this.$store.dispatch('grabUserSettings').then(() => { - this.$store.dispatch('grabAllProfiles', this.$t('Profile.All Channels')).then(() => { + this.$store.dispatch('grabAllProfiles', this.$t('Profile.All Channels')).then(async () => { this.$store.dispatch('grabHistory') this.$store.dispatch('grabAllPlaylists') this.$store.commit('setUsingElectron', useElectron) this.checkThemeSettings() - this.checkLocale() + await this.checkLocale() v.dataReady = true @@ -115,14 +109,15 @@ export default Vue.extend({ }) }, methods: { - checkLocale: function () { + checkLocale: async function () { const locale = localStorage.getItem('locale') if (locale === null || locale === 'system') { - const systemLocale = app.getLocale().replace(/-|_/, '_') + const systemLocale = await this.getLocale() + const findLocale = Object.keys(this.$i18n.messages).find((locale) => { - const localeName = locale.replace(/-|_/, '_') - return localeName.includes(systemLocale) + const localeName = locale.replace('-', '_') + return localeName.includes(systemLocale.replace('-', '_')) }) if (typeof findLocale !== 'undefined') { @@ -248,7 +243,7 @@ export default Vue.extend({ handleNewBlogBannerClick: function (response) { if (response) { - shell.openExternal(this.latestBlogUrl) + this.openExternalLink(this.latestBlogUrl) } this.showBlogBanner = false @@ -256,7 +251,7 @@ export default Vue.extend({ openDownloadsPage: function () { const url = 'https://freetubeapp.io#download' - shell.openExternal(url) + this.openExternalLink(url) this.showReleaseNotes = false this.showUpdatesBanner = false }, @@ -301,9 +296,7 @@ export default Vue.extend({ this.handleYoutubeLink(el.href) } else { // Open links externally by default - if (typeof (shell) !== 'undefined') { - shell.openExternal(el.href) - } + this.openExternalLink(el.href) } }) }, @@ -385,23 +378,24 @@ export default Vue.extend({ enableOpenUrl: function () { const v = this - electron.ipcRenderer.on('openUrl', function (event, url) { + ipcRenderer.on('openUrl', function (event, url) { if (url) { v.handleYoutubeLink(url) } }) - electron.ipcRenderer.send('appReady') + ipcRenderer.send('appReady') }, setBoundsOnClose: function () { window.onbeforeunload = (e) => { - electron.ipcRenderer.send('setBounds') + ipcRenderer.send('setBounds') } }, ...mapActions([ - 'showToast' + 'showToast', + 'openExternalLink' ]) } }) diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index eb99a75b..102d1cec 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -10,9 +10,7 @@ import fs from 'fs' import { opmlToJSON } from 'opml-to-json' import ytch from 'yt-channel-info' -const remote = require('@electron/remote') -const app = remote.app -const dialog = remote.dialog +// FIXME: Missing web logic branching export default Vue.extend({ name: 'DataSettings', @@ -210,7 +208,7 @@ export default Vue.extend({ }) }, - importFreeTubeSubscriptions: function () { + importFreeTubeSubscriptions: async function () { const options = { properties: ['openFile'], filters: [ @@ -221,14 +219,13 @@ export default Vue.extend({ ] } - dialog.showOpenDialog(options).then((response) => { - if (response.canceled || response.filePaths.length === 0) { - return - } + const response = await this.showOpenDialog(options) + if (response.canceled || response.filePaths.length === 0) { + return + } - const filePath = response.filePaths[0] - this.handleFreetubeImportFile(filePath) - }) + const filePath = response.filePaths[0] + this.handleFreetubeImportFile(filePath) }, handleYoutubeImportFile: function (filePath) { @@ -313,7 +310,7 @@ export default Vue.extend({ }) }, - importYouTubeSubscriptions: function () { + importYouTubeSubscriptions: async function () { const options = { properties: ['openFile'], filters: [ @@ -324,17 +321,16 @@ export default Vue.extend({ ] } - dialog.showOpenDialog(options).then(async (response) => { - if (response.canceled || response.filePaths.length === 0) { - return - } + const response = await this.showOpenDialog(options) + if (response.canceled || response.filePaths.length === 0) { + return + } - const filePath = response.filePaths[0] - this.handleYoutubeImportFile(filePath) - }) + const filePath = response.filePaths[0] + this.handleYoutubeImportFile(filePath) }, - importOpmlYouTubeSubscriptions: function () { + importOpmlYouTubeSubscriptions: async function () { const options = { properties: ['openFile'], filters: [ @@ -345,148 +341,38 @@ export default Vue.extend({ ] } - 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).then((json) => { - let feedData = json.children[0].children - - if (typeof feedData === 'undefined') { - if (json.title.includes('gPodder')) { - feedData = json.children - } else { - 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) - - let count = 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) - } - - if (typeof channelInfo.author !== 'undefined') { - const subscription = { - id: channelId, - name: channelInfo.author, - thumbnail: channelInfo.authorThumbnails[1].url - } - - const subExists = primaryProfile.subscriptions.findIndex((sub) => { - return sub.id === subscription.id || sub.name === subscription.name - }) - - if (subExists === -1) { - subscriptions.push(subscription) - } - } - - count++ - - const progressPercentage = (count / feedData.length) * 100 - this.setProgressBarPercentage(progressPercentage) - - if (count === feedData.length) { - primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions) - this.updateProfile(primaryProfile) - - if (subscriptions.length < count) { - this.showToast({ - message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') - }) - } else { - this.showToast({ - message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') - }) - } - - this.updateShowProgressBar(false) - } - }) - }).catch((err) => { - console.log(err) - console.log('error reading') - const message = this.$t('Settings.Data Settings.Invalid subscriptions file') - this.showToast({ - message: `${message}: ${err}` - }) - }) - }) - }) - }, - - importNewPipeSubscriptions: function () { - const options = { - properties: ['openFile'], - filters: [ - { - name: 'Database File', - extensions: ['json'] - } - ] + const response = await this.showOpenDialog(options) + if (response.canceled || response.filePaths.length === 0) { + return } - dialog.showOpenDialog(options).then(async (response) => { - if (response.canceled || response.filePaths.length === 0) { + 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 filePath = response.filePaths[0] + opmlToJSON(data).then((json) => { + let feedData = json.children[0].children - 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 + if (typeof feedData === 'undefined') { + if (json.title.includes('gPodder')) { + feedData = json.children + } else { + const message = this.$t('Settings.Data Settings.Invalid subscriptions file') + this.showToast({ + message: message + }) + + 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 = [] @@ -499,8 +385,8 @@ export default Vue.extend({ let count = 0 - newPipeSubscriptions.forEach(async (channel, index) => { - const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '') + 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) @@ -526,10 +412,10 @@ export default Vue.extend({ count++ - const progressPercentage = (count / newPipeSubscriptions.length) * 100 + const progressPercentage = (count / feedData.length) * 100 this.setProgressBarPercentage(progressPercentage) - if (count === newPipeSubscriptions.length) { + if (count === feedData.length) { primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions) this.updateProfile(primaryProfile) @@ -546,6 +432,114 @@ export default Vue.extend({ this.updateShowProgressBar(false) } }) + }).catch((err) => { + console.log(err) + console.log('error reading') + const message = this.$t('Settings.Data Settings.Invalid subscriptions file') + this.showToast({ + message: `${message}: ${err}` + }) + }) + }) + }, + + importNewPipeSubscriptions: async function () { + const options = { + properties: ['openFile'], + filters: [ + { + name: 'Database File', + extensions: ['json'] + } + ] + } + + const response = await this.showOpenDialog(options) + 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) + + let count = 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) + } + + if (typeof channelInfo.author !== 'undefined') { + const subscription = { + id: channelId, + name: channelInfo.author, + thumbnail: channelInfo.authorThumbnails[1].url + } + + const subExists = primaryProfile.subscriptions.findIndex((sub) => { + return sub.id === subscription.id || sub.name === subscription.name + }) + + if (subExists === -1) { + subscriptions.push(subscription) + } + } + + count++ + + const progressPercentage = (count / newPipeSubscriptions.length) * 100 + this.setProgressBarPercentage(progressPercentage) + + if (count === newPipeSubscriptions.length) { + primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions) + this.updateProfile(primaryProfile) + + if (subscriptions.length < count) { + this.showToast({ + message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') + }) + } else { + this.showToast({ + message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') + }) + } + + this.updateShowProgressBar(false) + } }) }) }, @@ -575,7 +569,7 @@ export default Vue.extend({ exportFreeTubeSubscriptions: async function () { await this.compactProfiles() - const userData = app.getPath('userData') + const userData = await this.getUserDataPath() const subscriptionsDb = `${userData}/profiles.db` const date = new Date() let dateMonth = date.getMonth() + 1 @@ -603,41 +597,40 @@ export default Vue.extend({ ] } - dialog.showSaveDialog(options).then((response) => { - if (response.canceled || response.filePath === '') { - // User canceled the save dialog + const response = await this.showSaveDialog(options) + 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 } - const filePath = response.filePath - - fs.readFile(subscriptionsDb, (readErr, data) => { - if (readErr) { - const message = this.$t('Settings.Data Settings.Unable to read file') + fs.writeFile(filePath, data, (writeErr) => { + if (writeErr) { + const message = this.$t('Settings.Data Settings.Unable to write file') this.showToast({ - message: `${message}: ${readErr}` + message: `${message}: ${writeErr}` }) 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.Subscriptions have been successfully exported') - }) + this.showToast({ + message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported') }) }) }) }, - exportYouTubeSubscriptions: function () { + exportYouTubeSubscriptions: async function () { const date = new Date() let dateMonth = date.getMonth() + 1 @@ -700,26 +693,25 @@ export default Vue.extend({ return object }) - dialog.showSaveDialog(options).then((response) => { - if (response.canceled || response.filePath === '') { - // User canceled the save dialog + const response = await this.showSaveDialog(options) + if (response.canceled || response.filePath === '') { + // User canceled the save dialog + return + } + + const filePath = response.filePath + + fs.writeFile(filePath, JSON.stringify(subscriptionsObject), (writeErr) => { + if (writeErr) { + const message = this.$t('Settings.Data Settings.Unable to write file') + this.showToast({ + message: `${message}: ${writeErr}` + }) return } - const filePath = response.filePath - - fs.writeFile(filePath, JSON.stringify(subscriptionsObject), (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.Subscriptions have been successfully exported') - }) + this.showToast({ + message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported') }) }) }, @@ -766,31 +758,30 @@ export default Vue.extend({ } }) - dialog.showSaveDialog(options).then((response) => { - if (response.canceled || response.filePath === '') { - // User canceled the save dialog + const response = await this.showSaveDialog(options) + 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 } - 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.Subscriptions have been successfully exported') - }) + this.showToast({ + message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported') }) }) }, - exportNewPipeSubscriptions: function () { + exportNewPipeSubscriptions: async function () { const date = new Date() let dateMonth = date.getMonth() + 1 @@ -834,32 +825,31 @@ export default Vue.extend({ newPipeObject.subscriptions.push(subscription) }) - dialog.showSaveDialog(options).then((response) => { - if (response.canceled || response.filePath === '') { - // User canceled the save dialog + const response = await this.showSaveDialog(options) + 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 } - 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.Subscriptions have been successfully exported') - }) + this.showToast({ + message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported') }) }) }, - checkForLegacySubscriptions: function () { - let dbLocation = app.getPath('userData') + checkForLegacySubscriptions: async function () { + let dbLocation = await this.getUserDataPath() dbLocation = dbLocation + '/subscriptions.db' this.handleFreetubeImportFile(dbLocation) fs.unlink(dbLocation, (err) => { @@ -869,7 +859,7 @@ export default Vue.extend({ }) }, - importHistory: function () { + importHistory: async function () { const options = { properties: ['openFile'], filters: [ @@ -880,79 +870,78 @@ export default Vue.extend({ ] } - dialog.showOpenDialog(options).then((response) => { - if (response.canceled || response.filePaths.length === 0) { + const response = await this.showOpenDialog(options) + 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 filePath = response.filePaths[0] + let textDecode = new TextDecoder('utf-8').decode(data) + textDecode = textDecode.split('\n') + textDecode.pop() - 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 - } + 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' + ] - let textDecode = new TextDecoder('utf-8').decode(data) - textDecode = textDecode.split('\n') - textDecode.pop() + const historyObject = {} - 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 - 2)) { + Object.keys(historyData).forEach((key) => { + if (!requiredKeys.includes(key)) { this.showToast({ - message: this.$t('Settings.Data Settings.History object has insufficient data, skipping item') + message: `Unknown data key: ${key}` }) } else { - this.updateHistory(historyObject) + historyObject[key] = historyData[key] } }) - this.showToast({ - message: this.$t('Settings.Data Settings.All watched history has been successfully imported') - }) + if (Object.keys(historyObject).length < (requiredKeys.length - 2)) { + 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: async function () { await this.compactHistory() - const userData = app.getPath('userData') + const userData = await this.getUserDataPath() const historyDb = `${userData}/history.db` const date = new Date() let dateMonth = date.getMonth() + 1 @@ -980,35 +969,34 @@ export default Vue.extend({ ] } - dialog.showSaveDialog(options).then((response) => { - if (response.canceled || response.filePath === '') { - // User canceled the save dialog + const response = await this.showSaveDialog(options) + 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 } - const filePath = response.filePath - - fs.readFile(historyDb, (readErr, data) => { - if (readErr) { - const message = this.$t('Settings.Data Settings.Unable to read file') + fs.writeFile(filePath, data, (writeErr) => { + if (writeErr) { + const message = this.$t('Settings.Data Settings.Unable to write file') this.showToast({ - message: `${message}: ${readErr}` + message: `${message}: ${writeErr}` }) 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') - }) + this.showToast({ + message: this.$t('Settings.Data Settings.All watched history has been successfully exported') }) }) }) @@ -1114,7 +1102,10 @@ export default Vue.extend({ 'compactHistory', 'showToast', 'getRandomColor', - 'calculateColorLuminance' + 'calculateColorLuminance', + 'showOpenDialog', + 'showSaveDialog', + 'getUserDataPath' ]), ...mapMutations([ diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js index c18fc419..498cd8ed 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -60,10 +60,6 @@ export default Vue.extend({ } }, computed: { - usingElectron: function () { - return this.$store.getters.getUsingElectron - }, - historyCache: function () { return this.$store.getters.getHistoryCache }, @@ -216,10 +212,7 @@ export default Vue.extend({ }) break case 'openYoutube': - if (this.usingElectron) { - const shell = require('electron').shell - shell.openExternal(this.youtubeUrl) - } + this.openExternalLink(this.youtubeUrl) break case 'copyYoutubeEmbed': navigator.clipboard.writeText(this.youtubeEmbedUrl) @@ -228,10 +221,7 @@ export default Vue.extend({ }) break case 'openYoutubeEmbed': - if (this.usingElectron) { - const shell = require('electron').shell - shell.openExternal(this.youtubeEmbedUrl) - } + this.openExternalLink(this.youtubeEmbedUrl) break case 'copyInvidious': navigator.clipboard.writeText(this.invidiousUrl) @@ -240,11 +230,7 @@ export default Vue.extend({ }) break case 'openInvidious': - if (this.usingElectron) { - console.log('using electron') - const shell = require('electron').shell - shell.openExternal(this.invidiousUrl) - } + this.openExternalLink(this.invidiousUrl) break case 'copyYoutubeChannel': navigator.clipboard.writeText(this.youtubeChannelUrl) @@ -253,10 +239,7 @@ export default Vue.extend({ }) break case 'openYoutubeChannel': - if (this.usingElectron) { - const shell = require('electron').shell - shell.openExternal(this.youtubeChannelUrl) - } + this.openExternalLink(this.youtubeChannelUrl) break case 'copyInvidiousChannel': navigator.clipboard.writeText(this.invidiousChannelUrl) @@ -265,10 +248,7 @@ export default Vue.extend({ }) break case 'openInvidiousChannel': - if (this.usingElectron) { - const shell = require('electron').shell - shell.openExternal(this.invidiousChannelUrl) - } + this.openExternalLink(this.invidiousChannelUrl) break } }, @@ -460,7 +440,8 @@ export default Vue.extend({ 'updateHistory', 'removeFromHistory', 'addVideo', - 'removeVideo' + 'removeVideo', + 'openExternalLink' ]) } }) diff --git a/src/renderer/components/ft-share-button/ft-share-button.js b/src/renderer/components/ft-share-button/ft-share-button.js index febecc36..f2745597 100644 --- a/src/renderer/components/ft-share-button/ft-share-button.js +++ b/src/renderer/components/ft-share-button/ft-share-button.js @@ -34,10 +34,6 @@ export default Vue.extend({ return this.$store.getters.getInvidiousInstance }, - usingElectron: function () { - return this.$store.getters.getUsingElectron - }, - invidiousURL() { return `${this.invidiousInstance}/watch?v=${this.id}` }, @@ -63,15 +59,8 @@ export default Vue.extend({ navigator.clipboard.writeText(text) }, - open(url) { - if (this.usingElectron) { - const shell = require('electron').shell - shell.openExternal(url) - } - }, - openInvidious() { - this.open(this.getFinalUrl(this.invidiousURL)) + this.openExternalLink(this.getFinalUrl(this.invidiousURL)) this.$refs.iconButton.focusOut() }, @@ -84,7 +73,7 @@ export default Vue.extend({ }, openYoutube() { - this.open(this.getFinalUrl(this.youtubeURL)) + this.openExternalLink(this.getFinalUrl(this.youtubeURL)) this.$refs.iconButton.focusOut() }, @@ -97,7 +86,7 @@ export default Vue.extend({ }, openYoutubeEmbed() { - this.open(this.getFinalUrl(this.youtubeEmbedURL)) + this.openExternalLink(this.getFinalUrl(this.youtubeEmbedURL)) this.$refs.iconButton.focusOut() }, @@ -110,7 +99,7 @@ export default Vue.extend({ }, openInvidiousEmbed() { - this.open(this.getFinalUrl(this.invidiousEmbedURL)) + this.openExternalLink(this.getFinalUrl(this.invidiousEmbedURL)) this.$refs.iconButton.focusOut() }, @@ -134,7 +123,8 @@ export default Vue.extend({ }, ...mapActions([ - 'showToast' + 'showToast', + 'openExternalLink' ]) } }) diff --git a/src/renderer/components/ft-video-player/ft-video-player.js b/src/renderer/components/ft-video-player/ft-video-player.js index fd9e07e8..54b099c9 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.js +++ b/src/renderer/components/ft-video-player/ft-video-player.js @@ -30,8 +30,8 @@ export default Vue.extend({ } if (this.usingElectron && this.powerSaveBlocker !== null) { - const { powerSaveBlocker } = require('electron') - powerSaveBlocker.stop(this.powerSaveBlocker) + const { ipcRenderer } = require('electron') + ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker) } }, props: { @@ -189,8 +189,8 @@ export default Vue.extend({ } if (this.usingElectron && this.powerSaveBlocker !== null) { - const { powerSaveBlocker } = require('electron') - powerSaveBlocker.stop(this.powerSaveBlocker) + const { ipcRenderer } = require('electron') + ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker) } }, methods: { @@ -274,18 +274,18 @@ export default Vue.extend({ v.$emit('error', error.target.player.error_) }) - this.player.on('play', function () { + this.player.on('play', async function () { if (this.usingElectron) { - const { powerSaveBlocker } = require('electron') - - this.powerSaveBlocker = powerSaveBlocker.start('prevent-display-sleep') + const { ipcRenderer } = require('electron') + this.powerSaveBlocker = + await ipcRenderer.invoke('startPowerSaveBlocker', 'prevent-display-sleep') } }) this.player.on('pause', function () { if (this.usingElectron && this.powerSaveBlocker !== null) { - const { powerSaveBlocker } = require('electron') - powerSaveBlocker.stop(this.powerSaveBlocker) + const { ipcRenderer } = require('electron') + ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker) this.powerSaveBlocker = null } }) diff --git a/src/renderer/components/general-settings/general-settings.js b/src/renderer/components/general-settings/general-settings.js index 3f73c049..4fb15f13 100644 --- a/src/renderer/components/general-settings/general-settings.js +++ b/src/renderer/components/general-settings/general-settings.js @@ -1,7 +1,6 @@ import Vue from 'vue' import $ from 'jquery' import { mapActions } from 'vuex' -import { app } from '@electron/remote' import FtCard from '../ft-card/ft-card.vue' import FtSelect from '../ft-select/ft-select.vue' import FtInput from '../ft-input/ft-input.vue' @@ -59,6 +58,11 @@ export default Vue.extend({ isDev: function () { return process.env.NODE_ENV === 'development' }, + + usingElectron: function () { + return this.$store.getters.getUsingElectron + }, + invidiousInstance: function () { return this.$store.getters.getInvidiousInstance }, @@ -192,12 +196,13 @@ export default Vue.extend({ } }, - updateLocale: function (locale) { + updateLocale: async function (locale) { if (locale === 'system') { - const systemLocale = app.getLocale().replace(/-|_/, '_') + const systemLocale = await this.getLocale() + const findLocale = Object.keys(this.$i18n.messages).find((locale) => { - const localeName = locale.replace(/-|_/, '_') - return localeName.includes(systemLocale) + const localeName = locale.replace('-', '_') + return localeName.includes(systemLocale.replace('-', '_')) }) if (typeof findLocale !== 'undefined') { @@ -240,7 +245,8 @@ export default Vue.extend({ 'updateThumbnailPreference', 'updateInvidiousInstance', 'updateForceLocalBackendForLegacy', - 'getRegionData' + 'getRegionData', + 'getLocale' ]) } }) diff --git a/src/renderer/components/playlist-info/playlist-info.js b/src/renderer/components/playlist-info/playlist-info.js index 48aee47d..7df785e4 100644 --- a/src/renderer/components/playlist-info/playlist-info.js +++ b/src/renderer/components/playlist-info/playlist-info.js @@ -1,6 +1,6 @@ import Vue from 'vue' +import { mapActions } from 'vuex' import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue' -import { shell } from 'electron' export default Vue.extend({ name: 'FtElementList', @@ -101,15 +101,19 @@ export default Vue.extend({ navigator.clipboard.writeText(youtubeUrl) break case 'openYoutube': - shell.openExternal(youtubeUrl) + this.openExternalLink(youtubeUrl) break case 'copyInvidious': navigator.clipboard.writeText(invidiousUrl) break case 'openInvidious': - shell.openExternal(invidiousUrl) + this.openExternalLink(invidiousUrl) break } - } + }, + + ...mapActions([ + 'openExternalLink' + ]) } }) diff --git a/src/renderer/components/proxy-settings/proxy-settings.js b/src/renderer/components/proxy-settings/proxy-settings.js index 599c342b..7635b486 100644 --- a/src/renderer/components/proxy-settings/proxy-settings.js +++ b/src/renderer/components/proxy-settings/proxy-settings.js @@ -9,7 +9,9 @@ import FtInput from '../ft-input/ft-input.vue' import FtLoader from '../ft-loader/ft-loader.vue' import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' -import electron from 'electron' +// FIXME: Missing web logic branching + +import { ipcRenderer } from 'electron' import debounce from 'lodash.debounce' export default Vue.extend({ @@ -109,11 +111,11 @@ export default Vue.extend({ }, enableProxy: function () { - electron.ipcRenderer.send('enableProxy', this.proxyUrl) + ipcRenderer.send('enableProxy', this.proxyUrl) }, disableProxy: function () { - electron.ipcRenderer.send('disableProxy') + ipcRenderer.send('disableProxy') }, testProxy: function () { diff --git a/src/renderer/components/theme-settings/theme-settings.js b/src/renderer/components/theme-settings/theme-settings.js index bc9dbdf9..6b12ec76 100644 --- a/src/renderer/components/theme-settings/theme-settings.js +++ b/src/renderer/components/theme-settings/theme-settings.js @@ -146,6 +146,7 @@ export default Vue.extend({ }, handleUiScale: function (value) { + // FIXME: No electron safeguard const { webFrame } = require('electron') const zoomFactor = value / 100 webFrame.setZoomFactor(zoomFactor) @@ -167,12 +168,13 @@ export default Vue.extend({ this.updateDisableSmoothScrolling(this.disableSmoothScrollingToggleValue) - const electron = require('electron') + // FIXME: No electron safeguard + const { ipcRenderer } = require('electron') if (this.disableSmoothScrollingToggleValue) { - electron.ipcRenderer.send('disableSmoothScrolling') + ipcRenderer.send('disableSmoothScrolling') } else { - electron.ipcRenderer.send('enableSmoothScrolling') + ipcRenderer.send('enableSmoothScrolling') } }, diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 92f28af4..8d0d4e02 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -6,7 +6,6 @@ import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue' import $ from 'jquery' import debounce from 'lodash.debounce' import ytSuggest from 'youtube-suggest' -const { ipcRenderer } = require('electron') export default Vue.extend({ name: 'TopNav', @@ -24,6 +23,10 @@ export default Vue.extend({ } }, computed: { + usingElectron: function () { + return this.$store.getters.getUsingElectron + }, + enableSearchSuggestions: function () { return this.$store.getters.getEnableSearchSuggestions }, @@ -255,7 +258,12 @@ export default Vue.extend({ }, createNewWindow: function () { - ipcRenderer.send('createNewWindow') + if (this.usingElectron) { + const { ipcRenderer } = require('electron') + ipcRenderer.send('createNewWindow') + } else { + // Web placeholder + } }, ...mapActions([ diff --git a/src/renderer/components/watch-video-info/watch-video-info.js b/src/renderer/components/watch-video-info/watch-video-info.js index 0c35afb8..a7c9b751 100644 --- a/src/renderer/components/watch-video-info/watch-video-info.js +++ b/src/renderer/components/watch-video-info/watch-video-info.js @@ -6,7 +6,6 @@ import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue' import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' import FtIconButton from '../ft-icon-button/ft-icon-button.vue' import FtShareButton from '../ft-share-button/ft-share-button.vue' -// import { shell } from 'electron' export default Vue.extend({ name: 'WatchVideoInfo', @@ -111,10 +110,6 @@ export default Vue.extend({ return this.$store.getters.getInvidiousInstance }, - usingElectron: function () { - return this.$store.getters.getUsingElectron - }, - profileList: function () { return this.$store.getters.getProfileList }, @@ -346,11 +341,6 @@ export default Vue.extend({ } }, - handleDownloadLink: function (url) { - const shell = require('electron').shell - shell.openExternal(url) - }, - addToPlaylist: function () { const videoData = { videoId: this.id, @@ -396,7 +386,8 @@ export default Vue.extend({ 'showToast', 'updateProfile', 'addVideo', - 'removeVideo' + 'removeVideo', + 'openExternalLink' ]) } }) diff --git a/src/renderer/components/watch-video-info/watch-video-info.vue b/src/renderer/components/watch-video-info/watch-video-info.vue index 0d4d85f6..2d446b32 100644 --- a/src/renderer/components/watch-video-info/watch-video-info.vue +++ b/src/renderer/components/watch-video-info/watch-video-info.vue @@ -86,7 +86,7 @@ icon="download" :dropdown-names="downloadLinkNames" :dropdown-values="downloadLinkValues" - @click="handleDownloadLink" + @click="openExternalLink" /> { + if (navigator && navigator.language) { + return navigator.language + } + } + + return await invokeIRC(context, 'getLocale', webCbk) || 'en-US' + }, + + async showOpenDialog (context, options) { + // TODO: implement showOpenDialog web compatible callback + const webCbk = () => null + return await invokeIRC(context, 'showOpenDialog', webCbk, options) + }, + + async showSaveDialog (context, options) { + // TODO: implement showSaveDialog web compatible callback + const webCbk = () => null + return await invokeIRC(context, 'showSaveDialog', webCbk, options) + }, + + async getUserDataPath (context) { + // TODO: implement getUserDataPath web compatible callback + const webCbk = () => null + return await invokeIRC(context, 'getUserDataPath', webCbk) + }, + updateShowProgressBar ({ commit }, value) { commit('setShowProgressBar', value) }, diff --git a/src/renderer/views/About/About.js b/src/renderer/views/About/About.js index 6d749f80..c1366731 100644 --- a/src/renderer/views/About/About.js +++ b/src/renderer/views/About/About.js @@ -88,18 +88,5 @@ export default Vue.extend({ } ] } - }, - computed: { - usingElectron: function () { - return this.$store.getters.getUsingElectron - } - }, - methods: { - openUrl: function (url) { - if (this.usingElectron) { - const shell = require('electron').shell - shell.openExternal(url) - } - } } }) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 0c28c649..c9697005 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -14,8 +14,6 @@ import WatchVideoLiveChat from '../../components/watch-video-live-chat/watch-vid import WatchVideoPlaylist from '../../components/watch-video-playlist/watch-video-playlist.vue' import WatchVideoRecommendations from '../../components/watch-video-recommendations/watch-video-recommendations.vue' -const remote = require('@electron/remote') - export default Vue.extend({ name: 'Watch', components: { @@ -931,7 +929,7 @@ export default Vue.extend({ const countDownIntervalId = setInterval(showCountDownMessage, 1000) }, - handleRouteChange: function () { + handleRouteChange: async function () { clearTimeout(this.playNextTimeout) this.handleWatchProgress() @@ -961,7 +959,7 @@ export default Vue.extend({ } if (this.removeVideoMetaFiles) { - const userData = remote.app.getPath('userData') + const userData = await this.getUserDataPath() if (this.isDev) { const dashFileLocation = `dashFiles/${this.videoId}.xml` const vttFileLocation = `storyboards/${this.videoId}.vtt` @@ -1004,9 +1002,9 @@ export default Vue.extend({ } }, - createLocalDashManifest: function (formats) { + createLocalDashManifest: async function (formats) { const xmlData = ytDashGen.generate_dash_file_from_formats(formats, this.videoLengthSeconds) - const userData = remote.app.getPath('userData') + const userData = await this.getUserDataPath() let fileLocation let uriSchema if (this.isDev) { @@ -1076,8 +1074,8 @@ export default Vue.extend({ }) }) // TODO: MAKE A VARIABLE WHICH CAN CHOOSE BETWEEN STROYBOARD ARRAY ELEMENTS - this.buildVTTFileLocally(storyboardArray[1]).then((results) => { - const userData = remote.app.getPath('userData') + this.buildVTTFileLocally(storyboardArray[1]).then(async (results) => { + const userData = await this.getUserDataPath() let fileLocation let uriSchema @@ -1195,7 +1193,8 @@ export default Vue.extend({ 'showToast', 'buildVTTFileLocally', 'updateHistory', - 'updateWatchProgress' + 'updateWatchProgress', + 'getUserDataPath' ]) } })