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 <pikachuexe@gmail.com>

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
This commit is contained in:
Emma 2022-10-25 10:44:18 -04:00 committed by GitHub
parent a236b787e3
commit 70baa873fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 154 deletions

View File

@ -10,7 +10,16 @@ import { MAIN_PROFILE_ID } from '../../../constants'
import { opmlToJSON } from 'opml-to-json' import { opmlToJSON } from 'opml-to-json'
import ytch from 'yt-channel-info' 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({ export default Vue.extend({
name: 'DataSettings', 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) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
let textDecode let textDecode
try { try {
textDecode = await this.readFileFromDialog({ response }) textDecode = await readFileFromDialog(response)
} catch (err) { } catch (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
showToast(`${message}: ${err}`) 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 === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
return return
} }
try { try {
await this.writeFileFromDialog({ response, content: subscriptionsDb }) await writeFileFromDialog(response, subscriptionsDb)
} catch (writeErr) { } catch (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
showToast(`${message}: ${writeErr}`) showToast(`${message}: ${writeErr}`)
@ -568,14 +577,14 @@ export default Vue.extend({
return object return object
}) })
const response = await this.showSaveDialog(options) const response = await showSaveDialog(options)
if (response.canceled || response.filePath === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
return return
} }
try { try {
await this.writeFileFromDialog({ response, content: JSON.stringify(subscriptionsObject) }) await writeFileFromDialog(response, JSON.stringify(subscriptionsObject))
} catch (writeErr) { } catch (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to write file') const message = this.$t('Settings.Data Settings.Unable to write file')
showToast(`${message}: ${writeErr}`) 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 === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
return return
} }
try { try {
await this.writeFileFromDialog({ response, content: opmlData }) await writeFileFromDialog(response, opmlData)
} catch (writeErr) { } catch (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to write file') const message = this.$t('Settings.Data Settings.Unable to write file')
showToast(`${message}: ${writeErr}`) showToast(`${message}: ${writeErr}`)
@ -652,14 +661,14 @@ export default Vue.extend({
exportText += `${channel.id},${channelUrl},${channelName}\n` exportText += `${channel.id},${channelUrl},${channelName}\n`
}) })
exportText += '\n' exportText += '\n'
const response = await this.showSaveDialog(options) const response = await showSaveDialog(options)
if (response.canceled || response.filePath === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
return return
} }
try { try {
await this.writeFileFromDialog({ response, content: exportText }) await writeFileFromDialog(response, exportText)
} catch (writeErr) { } catch (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to write file') const message = this.$t('Settings.Data Settings.Unable to write file')
showToast(`${message}: ${writeErr}`) showToast(`${message}: ${writeErr}`)
@ -699,13 +708,13 @@ export default Vue.extend({
newPipeObject.subscriptions.push(subscription) newPipeObject.subscriptions.push(subscription)
}) })
const response = await this.showSaveDialog(options) const response = await showSaveDialog(options)
if (response.canceled || response.filePath === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
return return
} }
try { try {
await this.writeFileFromDialog({ response, content: JSON.stringify(newPipeObject) }) await writeFileFromDialog(response, JSON.stringify(newPipeObject))
} catch (writeErr) { } catch (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to write file') const message = this.$t('Settings.Data Settings.Unable to write file')
showToast(`${message}: ${writeErr}`) 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) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
let textDecode let textDecode
try { try {
textDecode = await this.readFileFromDialog({ response }) textDecode = await readFileFromDialog(response)
} catch (err) { } catch (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
showToast(`${message}: ${err}`) 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 === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
return return
} }
try { try {
await this.writeFileFromDialog({ response, content: historyDb }) await writeFileFromDialog(response, historyDb)
} catch (writeErr) { } catch (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to write file') const message = this.$t('Settings.Data Settings.Unable to write file')
showToast(`${message}: ${writeErr}`) 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) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
let data let data
try { try {
data = await this.readFileFromDialog({ response }) data = await readFileFromDialog(response)
} catch (err) { } catch (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
showToast(`${message}: ${err}`) 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 === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
return return
} }
try { try {
await this.writeFileFromDialog({ response, content: JSON.stringify(this.allPlaylists) }) await writeFileFromDialog(response, JSON.stringify(this.allPlaylists))
} catch (writeErr) { } catch (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to write file') const message = this.$t('Settings.Data Settings.Unable to write file')
showToast(`${message}: ${writeErr}`) showToast(`${message}: ${writeErr}`)
@ -1100,10 +1109,6 @@ export default Vue.extend({
'updateShowProgressBar', 'updateShowProgressBar',
'updateHistory', 'updateHistory',
'compactHistory', 'compactHistory',
'showOpenDialog',
'readFileFromDialog',
'showSaveDialog',
'writeFileFromDialog',
'getUserDataPath', 'getUserDataPath',
'addPlaylist', 'addPlaylist',
'addVideo' 'addVideo'

View File

@ -15,7 +15,7 @@ import 'videojs-mobile-ui'
import 'videojs-mobile-ui/dist/videojs-mobile-ui.css' import 'videojs-mobile-ui/dist/videojs-mobile-ui.css'
import { IpcChannels } from '../../../constants' import { IpcChannels } from '../../../constants'
import { sponsorBlockSkipSegments } from '../../helpers/sponsorblock' import { sponsorBlockSkipSegments } from '../../helpers/sponsorblock'
import { calculateColorLuminance, colors, showToast } from '../../helpers/utils' import { calculateColorLuminance, colors, showSaveDialog, showToast } from '../../helpers/utils'
export default Vue.extend({ export default Vue.extend({
name: 'FtVideoPlayer', name: 'FtVideoPlayer',
@ -1396,7 +1396,7 @@ export default Vue.extend({
] ]
} }
const response = await this.showSaveDialog(options) const response = await showSaveDialog(options)
if (wasPlaying) { if (wasPlaying) {
this.player.play() this.player.play()
} }
@ -1921,8 +1921,7 @@ export default Vue.extend({
'updateDefaultCaptionSettings', 'updateDefaultCaptionSettings',
'parseScreenshotCustomFileName', 'parseScreenshotCustomFileName',
'updateScreenshotFolderPath', 'updateScreenshotFolderPath',
'getPicturesPath', 'getPicturesPath'
'showSaveDialog'
]) ])
} }
}) })

View File

@ -1,6 +1,7 @@
import { IpcChannels } from '../../constants' import { IpcChannels } from '../../constants'
import FtToastEvents from '../components/ft-toast/ft-toast-events' import FtToastEvents from '../components/ft-toast/ft-toast-events'
import i18n from '../i18n/index' import i18n from '../i18n/index'
import fs from 'fs'
export const colors = [ export const colors = [
{ name: 'Red', value: '#d50000' }, { 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. * This creates an absolute web url from a given path.
* It will assume all given paths are relative to the current window location. * It will assume all given paths are relative to the current window location.

View File

@ -4,7 +4,7 @@ import path from 'path'
import i18n from '../../i18n/index' import i18n from '../../i18n/index'
import { IpcChannels } from '../../../constants' import { IpcChannels } from '../../../constants'
import { createWebURL, openExternalLink, showToast } from '../../helpers/utils' import { createWebURL, openExternalLink, showSaveDialog, showToast } from '../../helpers/utils'
const state = { const state = {
isSideNavOpen: false, isSideNavOpen: false,
@ -170,7 +170,7 @@ const actions = {
} }
] ]
} }
const response = await dispatch('showSaveDialog', { options }) const response = await showSaveDialog(options)
if (response.canceled || response.filePath === '') { if (response.canceled || response.filePath === '') {
// User canceled the save dialog // User canceled the save dialog
@ -244,129 +244,6 @@ const actions = {
return (await invokeIRC(context, IpcChannels.GET_SYSTEM_LOCALE, webCbk)) || 'en-US' 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) { async getUserDataPath (context) {
// TODO: implement getUserDataPath web compatible callback // TODO: implement getUserDataPath web compatible callback
const webCbk = () => null const webCbk = () => null