From 70baa873fb58f73e5a73827a0c74c1314fd1d9f1 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 25 Oct 2022 10:44:18 -0400 Subject: [PATCH] Move `Dialog` functions to `utils/helpers` (#2752) * Moving `FileFromDialog` helpers into helpers/utils * Moving `showDialog` functions to `utils/helpers` * Linting * Update src/renderer/helpers/utils.js Co-authored-by: absidue <48293849+absidue@users.noreply.github.com> * Update refs `showSaveDialog` in `ft-video-player` * Formatting long import to be multiline Co-authored-by: PikachuEXE Co-authored-by: absidue <48293849+absidue@users.noreply.github.com> Co-authored-by: PikachuEXE --- .../components/data-settings/data-settings.js | 55 ++++---- .../ft-video-player/ft-video-player.js | 7 +- src/renderer/helpers/utils.js | 128 ++++++++++++++++++ src/renderer/store/modules/utils.js | 127 +---------------- 4 files changed, 163 insertions(+), 154 deletions(-) diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index df5294ca..20d75ee6 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -10,7 +10,16 @@ import { MAIN_PROFILE_ID } from '../../../constants' import { opmlToJSON } from 'opml-to-json' import ytch from 'yt-channel-info' -import { calculateColorLuminance, copyToClipboard, getRandomColor, showToast } from '../../helpers/utils' +import { + calculateColorLuminance, + copyToClipboard, + getRandomColor, + readFileFromDialog, + showOpenDialog, + showSaveDialog, + showToast, + writeFileFromDialog +} from '../../helpers/utils' export default Vue.extend({ name: 'DataSettings', @@ -97,13 +106,13 @@ export default Vue.extend({ ] } - const response = await this.showOpenDialog(options) + const response = await showOpenDialog(options) if (response.canceled || response.filePaths?.length === 0) { return } let textDecode try { - textDecode = await this.readFileFromDialog({ response }) + textDecode = await readFileFromDialog(response) } catch (err) { const message = this.$t('Settings.Data Settings.Unable to read file') showToast(`${message}: ${err}`) @@ -503,13 +512,13 @@ export default Vue.extend({ ] } - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog return } try { - await this.writeFileFromDialog({ response, content: subscriptionsDb }) + await writeFileFromDialog(response, subscriptionsDb) } catch (writeErr) { const message = this.$t('Settings.Data Settings.Unable to read file') showToast(`${message}: ${writeErr}`) @@ -568,14 +577,14 @@ export default Vue.extend({ return object }) - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog return } try { - await this.writeFileFromDialog({ response, content: JSON.stringify(subscriptionsObject) }) + await writeFileFromDialog(response, JSON.stringify(subscriptionsObject)) } catch (writeErr) { const message = this.$t('Settings.Data Settings.Unable to write file') showToast(`${message}: ${writeErr}`) @@ -613,14 +622,14 @@ export default Vue.extend({ } }) - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog return } try { - await this.writeFileFromDialog({ response, content: opmlData }) + await writeFileFromDialog(response, opmlData) } catch (writeErr) { const message = this.$t('Settings.Data Settings.Unable to write file') showToast(`${message}: ${writeErr}`) @@ -652,14 +661,14 @@ export default Vue.extend({ exportText += `${channel.id},${channelUrl},${channelName}\n` }) exportText += '\n' - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog return } try { - await this.writeFileFromDialog({ response, content: exportText }) + await writeFileFromDialog(response, exportText) } catch (writeErr) { const message = this.$t('Settings.Data Settings.Unable to write file') showToast(`${message}: ${writeErr}`) @@ -699,13 +708,13 @@ export default Vue.extend({ newPipeObject.subscriptions.push(subscription) }) - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog return } try { - await this.writeFileFromDialog({ response, content: JSON.stringify(newPipeObject) }) + await writeFileFromDialog(response, JSON.stringify(newPipeObject)) } catch (writeErr) { const message = this.$t('Settings.Data Settings.Unable to write file') showToast(`${message}: ${writeErr}`) @@ -725,13 +734,13 @@ export default Vue.extend({ ] } - const response = await this.showOpenDialog(options) + const response = await showOpenDialog(options) if (response.canceled || response.filePaths?.length === 0) { return } let textDecode try { - textDecode = await this.readFileFromDialog({ response }) + textDecode = await readFileFromDialog(response) } catch (err) { const message = this.$t('Settings.Data Settings.Unable to read file') showToast(`${message}: ${err}`) @@ -799,14 +808,14 @@ export default Vue.extend({ ] } - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog return } try { - await this.writeFileFromDialog({ response, content: historyDb }) + await writeFileFromDialog(response, historyDb) } catch (writeErr) { const message = this.$t('Settings.Data Settings.Unable to write file') showToast(`${message}: ${writeErr}`) @@ -825,13 +834,13 @@ export default Vue.extend({ ] } - const response = await this.showOpenDialog(options) + const response = await showOpenDialog(options) if (response.canceled || response.filePaths?.length === 0) { return } let data try { - data = await this.readFileFromDialog({ response }) + data = await readFileFromDialog(response) } catch (err) { const message = this.$t('Settings.Data Settings.Unable to read file') showToast(`${message}: ${err}`) @@ -942,13 +951,13 @@ export default Vue.extend({ ] } - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog return } try { - await this.writeFileFromDialog({ response, content: JSON.stringify(this.allPlaylists) }) + await writeFileFromDialog(response, JSON.stringify(this.allPlaylists)) } catch (writeErr) { const message = this.$t('Settings.Data Settings.Unable to write file') showToast(`${message}: ${writeErr}`) @@ -1100,10 +1109,6 @@ export default Vue.extend({ 'updateShowProgressBar', 'updateHistory', 'compactHistory', - 'showOpenDialog', - 'readFileFromDialog', - 'showSaveDialog', - 'writeFileFromDialog', 'getUserDataPath', 'addPlaylist', 'addVideo' 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 ad10b3b9..6532f99e 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.js +++ b/src/renderer/components/ft-video-player/ft-video-player.js @@ -15,7 +15,7 @@ import 'videojs-mobile-ui' import 'videojs-mobile-ui/dist/videojs-mobile-ui.css' import { IpcChannels } from '../../../constants' import { sponsorBlockSkipSegments } from '../../helpers/sponsorblock' -import { calculateColorLuminance, colors, showToast } from '../../helpers/utils' +import { calculateColorLuminance, colors, showSaveDialog, showToast } from '../../helpers/utils' export default Vue.extend({ name: 'FtVideoPlayer', @@ -1396,7 +1396,7 @@ export default Vue.extend({ ] } - const response = await this.showSaveDialog(options) + const response = await showSaveDialog(options) if (wasPlaying) { this.player.play() } @@ -1921,8 +1921,7 @@ export default Vue.extend({ 'updateDefaultCaptionSettings', 'parseScreenshotCustomFileName', 'updateScreenshotFolderPath', - 'getPicturesPath', - 'showSaveDialog' + 'getPicturesPath' ]) } }) diff --git a/src/renderer/helpers/utils.js b/src/renderer/helpers/utils.js index 713f50e5..6ce47506 100644 --- a/src/renderer/helpers/utils.js +++ b/src/renderer/helpers/utils.js @@ -1,6 +1,7 @@ import { IpcChannels } from '../../constants' import FtToastEvents from '../components/ft-toast/ft-toast-events' import i18n from '../i18n/index' +import fs from 'fs' export const colors = [ { name: 'Red', value: '#d50000' }, @@ -246,6 +247,133 @@ export function openExternalLink(url) { } } +export async function showOpenDialog (options) { + if (process.env.IS_ELECTRON) { + const { ipcRenderer } = require('electron') + return await ipcRenderer.invoke(IpcChannels.SHOW_OPEN_DIALOG, options) + } else { + return await new Promise((resolve) => { + const fileInput = document.createElement('input') + fileInput.setAttribute('type', 'file') + if (options?.filters[0]?.extensions !== undefined) { + // this will map the given extensions from the options to the accept attribute of the input + fileInput.setAttribute('accept', options.filters[0].extensions.map((extension) => { return `.${extension}` }).join(', ')) + } + fileInput.onchange = () => { + const files = Array.from(fileInput.files) + resolve({ canceled: false, files, filePaths: files.map(({ name }) => { return name }) }) + delete fileInput.onchange + } + const listenForEnd = () => { + window.removeEventListener('focus', listenForEnd) + // 1 second timeout on the response from the file picker to prevent awaiting forever + setTimeout(() => { + if (fileInput.files.length === 0 && typeof fileInput.onchange === 'function') { + // if there are no files and the onchange has not been triggered, the file-picker was canceled + resolve({ canceled: true }) + delete fileInput.onchange + } + }, 1000) + } + window.addEventListener('focus', listenForEnd) + fileInput.click() + }) + } +} + +/** + * @param {object} response the response from `showOpenDialog` + * @param {number} index which file to read (defaults to the first in the response) + * @returns the text contents of the selected file + */ +export function readFileFromDialog(response, index = 0) { + return new Promise((resolve, reject) => { + if (process.env.IS_ELECTRON) { + // if this is Electron, use fs + fs.readFile(response.filePaths[index], (err, data) => { + if (err) { + reject(err) + return + } + resolve(new TextDecoder('utf-8').decode(data)) + }) + } else { + // if this is web, use FileReader + try { + const reader = new FileReader() + reader.onload = function (file) { + resolve(file.currentTarget.result) + } + reader.readAsText(response.files[index]) + } catch (exception) { + reject(exception) + } + } + }) +} + +export async function showSaveDialog (options) { + if (process.env.IS_ELECTRON) { + const { ipcRenderer } = require('electron') + return await ipcRenderer.invoke(IpcChannels.SHOW_SAVE_DIALOG, options) + } else { + // If the native filesystem api is available + if ('showSaveFilePicker' in window) { + return { + canceled: false, + handle: await window.showSaveFilePicker({ + suggestedName: options.defaultPath.split('/').at(-1), + types: options.filters[0]?.extensions?.map((extension) => { + return { + accept: { + 'application/octet-stream': '.' + extension + } + } + }) + }) + } + } else { + return { canceled: false, filePath: options.defaultPath } + } + } +} + +/** +* Write to a file picked out from the `showSaveDialog` picker +* @param {object} response the response from `showSaveDialog` +* @param {string} content the content to be written to the file selected by the dialog +*/ +export async function writeFileFromDialog (response, content) { + if (process.env.IS_ELECTRON) { + return await new Promise((resolve, reject) => { + const { filePath } = response + fs.writeFile(filePath, content, (error) => { + if (error) { + reject(error) + } else { + resolve() + } + }) + }) + } else { + if ('showOpenFilePicker' in window) { + const { handle } = response + const writableStream = await handle.createWritable() + await writableStream.write(content) + await writableStream.close() + } else { + // If the native filesystem api is not available, + const { filePath } = response + const filename = filePath.split('/').at(-1) + const a = document.createElement('a') + const url = URL.createObjectURL(new Blob([content], { type: 'application/octet-stream' })) + a.setAttribute('href', url) + a.setAttribute('download', encodeURI(filename)) + a.click() + } + } +} + /** * This creates an absolute web url from a given path. * It will assume all given paths are relative to the current window location. diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js index 25b86561..02a5269c 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -4,7 +4,7 @@ import path from 'path' import i18n from '../../i18n/index' import { IpcChannels } from '../../../constants' -import { createWebURL, openExternalLink, showToast } from '../../helpers/utils' +import { createWebURL, openExternalLink, showSaveDialog, showToast } from '../../helpers/utils' const state = { isSideNavOpen: false, @@ -170,7 +170,7 @@ const actions = { } ] } - const response = await dispatch('showSaveDialog', { options }) + const response = await showSaveDialog(options) if (response.canceled || response.filePath === '') { // User canceled the save dialog @@ -244,129 +244,6 @@ const actions = { return (await invokeIRC(context, IpcChannels.GET_SYSTEM_LOCALE, webCbk)) || 'en-US' }, - /** - * @param {Object} response the response from `showOpenDialog` - * @param {Number} index which file to read (defaults to the first in the response) - * @returns the text contents of the selected file - */ - async readFileFromDialog(context, { response, index = 0 }) { - return await new Promise((resolve, reject) => { - if (process.env.IS_ELECTRON) { - // if this is Electron, use fs - fs.readFile(response.filePaths[index], (err, data) => { - if (err) { - reject(err) - return - } - resolve(new TextDecoder('utf-8').decode(data)) - }) - } else { - // if this is web, use FileReader - try { - const reader = new FileReader() - reader.onload = function (file) { - resolve(file.currentTarget.result) - } - reader.readAsText(response.files[index]) - } catch (exception) { - reject(exception) - } - } - }) - }, - - async showOpenDialog (context, options) { - const webCbk = () => { - return new Promise((resolve) => { - const fileInput = document.createElement('input') - fileInput.setAttribute('type', 'file') - if (options?.filters[0]?.extensions !== undefined) { - // this will map the given extensions from the options to the accept attribute of the input - fileInput.setAttribute('accept', options.filters[0].extensions.map((extension) => { return `.${extension}` }).join(', ')) - } - fileInput.onchange = () => { - const files = Array.from(fileInput.files) - resolve({ canceled: false, files, filePaths: files.map(({ name }) => { return name }) }) - delete fileInput.onchange - } - const listenForEnd = () => { - window.removeEventListener('focus', listenForEnd) - // 1 second timeout on the response from the file picker to prevent awaiting forever - setTimeout(() => { - if (fileInput.files.length === 0 && typeof fileInput.onchange === 'function') { - // if there are no files and the onchange has not been triggered, the file-picker was canceled - resolve({ canceled: true }) - delete fileInput.onchange - } - }, 1000) - } - window.addEventListener('focus', listenForEnd) - fileInput.click() - }) - } - return await invokeIRC(context, IpcChannels.SHOW_OPEN_DIALOG, webCbk, options) - }, - - /** - * Write to a file picked out from the `showSaveDialog` picker - * @param {Object} response the response from `showSaveDialog` - * @param {String} content the content to be written to the file selected by the dialog - */ - async writeFileFromDialog (context, { response, content }) { - if (process.env.IS_ELECTRON) { - return await new Promise((resolve, reject) => { - const { filePath } = response - fs.writeFile(filePath, content, (error) => { - if (error) { - reject(error) - } else { - resolve() - } - }) - }) - } else { - if ('showOpenFilePicker' in window) { - const { handle } = response - const writableStream = await handle.createWritable() - await writableStream.write(content) - await writableStream.close() - } else { - // If the native filesystem api is not available, - const { filePath } = response - const filename = filePath.split('/').at(-1) - const a = document.createElement('a') - const url = URL.createObjectURL(new Blob([content], { type: 'application/octet-stream' })) - a.setAttribute('href', url) - a.setAttribute('download', encodeURI(filename)) - a.click() - } - } - }, - - async showSaveDialog (context, options) { - const webCbk = async () => { - // If the native filesystem api is available - if ('showSaveFilePicker' in window) { - return { - canceled: false, - handle: await window.showSaveFilePicker({ - suggestedName: options.defaultPath.split('/').at(-1), - types: options?.filters[0]?.extensions?.map((extension) => { - return { - accept: { - 'application/octet-stream': '.' + extension - } - } - }) - }) - } - } else { - return { canceled: false, filePath: options.defaultPath } - } - } - return await invokeIRC(context, IpcChannels.SHOW_SAVE_DIALOG, webCbk, options) - }, - async getUserDataPath (context) { // TODO: implement getUserDataPath web compatible callback const webCbk = () => null