Implementing showOpenDialog's web callback (#2608)

* Implementing the open file dialog in web

- Adding a new function to make loading files
from a dialog box easier in both web and electron

* Canceled should always be false

onchange is only triggered when the file picker
has a file path. If the user cancels, this function
is never called.

* Changing from `function ()` to `() => {`

* Adding a try around processing the history import

* Moving the try-catch to a smaller section

* Adding a listener to when the file picker is closed

* Fixing the grammar on my comment

* Refactoring playlist imports to use readFileFromDialog

* Refactoring handleFreetubeImportFile to use readFileFromDialog

* Refactoring handleYoutubeCsvImportFile to use readFileFromDialog

* Refactoring handleYoutubeImportFile to use readFileFromDialog

* Refactoring importOpmlYoutubeSubscriptions to use readFileFromDialog

* Refactoring importNewPipeSubscriptions to use readFileFromDialog

* Added a check

to prevent resolve from being called multiple times

* Moving the call to removeEventListener

to prevent this event from being triggered twice

* Adding extensions to the web file picker

* Hiding `Check for legacy subscriptions` in web

* Adding comments for better readability

* Correcting my vue syntax
This commit is contained in:
Emma 2022-09-23 12:15:49 -04:00 committed by GitHub
parent 7822f7423e
commit 1512178489
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 569 additions and 516 deletions

View File

@ -80,6 +80,9 @@ export default Vue.extend({
`${exportYouTube} (.opml)`, `${exportYouTube} (.opml)`,
`${exportNewPipe} (.json)` `${exportNewPipe} (.json)`
] ]
},
usingElectron: function () {
return process.env.IS_ELECTRON
} }
}, },
methods: { methods: {
@ -115,17 +118,17 @@ export default Vue.extend({
} }
}, },
handleFreetubeImportFile: function (filePath) { handleFreetubeImportFile: async function (response) {
fs.readFile(filePath, async (err, data) => { let textDecode
if (err) { try {
textDecode = await this.readFileFromDialog({ response })
} catch (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
this.showToast({ this.showToast({
message: `${message}: ${err}` message: `${message}: ${err}`
}) })
return return
} }
let textDecode = new TextDecoder('utf-8').decode(data)
textDecode = textDecode.split('\n') textDecode = textDecode.split('\n')
textDecode.pop() textDecode.pop()
textDecode = textDecode.map(data => JSON.parse(data)) textDecode = textDecode.map(data => JSON.parse(data))
@ -215,7 +218,6 @@ export default Vue.extend({
this.showToast({ this.showToast({
message: this.$t('Settings.Data Settings.All subscriptions and profiles have been successfully imported') message: this.$t('Settings.Data Settings.All subscriptions and profiles have been successfully imported')
}) })
})
}, },
importFreeTubeSubscriptions: async function () { importFreeTubeSubscriptions: async function () {
@ -230,24 +232,24 @@ export default Vue.extend({
} }
const response = await this.showOpenDialog(options) const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
const filePath = response.filePaths[0] this.handleFreetubeImportFile(response)
this.handleFreetubeImportFile(filePath)
}, },
handleYoutubeCsvImportFile: function(filePath) { // first row = header, last row = empty handleYoutubeCsvImportFile: async function(response) { // first row = header, last row = empty
fs.readFile(filePath, async (err, data) => { let textDecode
if (err) { try {
textDecode = await this.readFileFromDialog({ response })
} catch (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
this.showToast({ this.showToast({
message: `${message}: ${err}` message: `${message}: ${err}`
}) })
return return
} }
const textDecode = new TextDecoder('utf-8').decode(data)
const youtubeSubscriptions = textDecode.split('\n').filter(sub => { const youtubeSubscriptions = textDecode.split('\n').filter(sub => {
return sub !== '' return sub !== ''
}) })
@ -305,20 +307,19 @@ export default Vue.extend({
this.updateShowProgressBar(false) this.updateShowProgressBar(false)
} }
} }
})
}, },
handleYoutubeImportFile: function (filePath) { handleYoutubeImportFile: async function (response) {
fs.readFile(filePath, async (err, data) => { let textDecode
if (err) { try {
textDecode = await this.readFileFromDialog({ response })
} catch (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
this.showToast({ this.showToast({
message: `${message}: ${err}` message: `${message}: ${err}`
}) })
return return
} }
let textDecode = new TextDecoder('utf-8').decode(data)
textDecode = JSON.parse(textDecode) textDecode = JSON.parse(textDecode)
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0])) const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
@ -385,7 +386,6 @@ export default Vue.extend({
this.updateShowProgressBar(false) this.updateShowProgressBar(false)
} }
}) })
})
}, },
importCsvYouTubeSubscriptions: async function () { importCsvYouTubeSubscriptions: async function () {
@ -399,12 +399,11 @@ export default Vue.extend({
] ]
} }
const response = await this.showOpenDialog(options) const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
const filePath = response.filePaths[0] this.handleYoutubeCsvImportFile(response)
this.handleYoutubeCsvImportFile(filePath)
}, },
importYouTubeSubscriptions: async function () { importYouTubeSubscriptions: async function () {
@ -419,12 +418,11 @@ export default Vue.extend({
} }
const response = await this.showOpenDialog(options) const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
const filePath = response.filePaths[0] this.handleYoutubeImportFile(response)
this.handleYoutubeImportFile(filePath)
}, },
importOpmlYouTubeSubscriptions: async function () { importOpmlYouTubeSubscriptions: async function () {
@ -439,14 +437,14 @@ export default Vue.extend({
} }
const response = await this.showOpenDialog(options) const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
const filePath = response.filePaths[0] let data
try {
fs.readFile(filePath, async (err, data) => { data = await this.readFileFromDialog({ response })
if (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')
this.showToast({ this.showToast({
message: `${message}: ${err}` message: `${message}: ${err}`
@ -454,9 +452,20 @@ export default Vue.extend({
return return
} }
opmlToJSON(data).then((json) => { let json
let feedData = json.children[0].children try {
json = await opmlToJSON(data)
} catch (err) {
console.error(err)
console.error('error reading')
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
this.showToast({
message: `${message}: ${err}`
})
}
if (json !== undefined) {
let feedData = json.children[0].children
if (typeof feedData === 'undefined') { if (typeof feedData === 'undefined') {
if (json.title.includes('gPodder')) { if (json.title.includes('gPodder')) {
feedData = json.children feedData = json.children
@ -527,14 +536,7 @@ export default Vue.extend({
this.updateShowProgressBar(false) this.updateShowProgressBar(false)
} }
}) })
}).catch((err) => { }
console.error(err)
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
this.showToast({
message: `${message}: ${err}`
})
})
})
}, },
importNewPipeSubscriptions: async function () { importNewPipeSubscriptions: async function () {
@ -549,14 +551,14 @@ export default Vue.extend({
} }
const response = await this.showOpenDialog(options) const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
const filePath = response.filePaths[0] let data
try {
fs.readFile(filePath, async (err, data) => { data = await this.readFileFromDialog({ response })
if (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')
this.showToast({ this.showToast({
message: `${message}: ${err}` message: `${message}: ${err}`
@ -636,7 +638,6 @@ export default Vue.extend({
this.updateShowProgressBar(false) this.updateShowProgressBar(false)
} }
}) })
})
}, },
exportSubscriptions: function (option) { exportSubscriptions: function (option) {
@ -942,7 +943,7 @@ export default Vue.extend({
checkForLegacySubscriptions: async function () { checkForLegacySubscriptions: async function () {
let dbLocation = await this.getUserDataPath() let dbLocation = await this.getUserDataPath()
dbLocation = dbLocation + '/subscriptions.db' dbLocation = dbLocation + '/subscriptions.db'
this.handleFreetubeImportFile(dbLocation) this.handleFreetubeImportFile({ canceled: false, filePaths: [dbLocation] })
fs.unlink(dbLocation, (err) => { fs.unlink(dbLocation, (err) => {
if (err) { if (err) {
console.error(err) console.error(err)
@ -962,22 +963,19 @@ export default Vue.extend({
} }
const response = await this.showOpenDialog(options) const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
let textDecode
const filePath = response.filePaths[0] try {
textDecode = await this.readFileFromDialog({ response })
fs.readFile(filePath, async (err, data) => { } catch (err) {
if (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
this.showToast({ this.showToast({
message: `${message}: ${err}` message: `${message}: ${err}`
}) })
return return
} }
let textDecode = new TextDecoder('utf-8').decode(data)
textDecode = textDecode.split('\n') textDecode = textDecode.split('\n')
textDecode.pop() textDecode.pop()
@ -1027,7 +1025,6 @@ export default Vue.extend({
this.showToast({ this.showToast({
message: this.$t('Settings.Data Settings.All watched history has been successfully imported') message: this.$t('Settings.Data Settings.All watched history has been successfully imported')
}) })
})
}, },
exportHistory: async function () { exportHistory: async function () {
@ -1092,21 +1089,19 @@ export default Vue.extend({
} }
const response = await this.showOpenDialog(options) const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) { if (response.canceled || response.filePaths?.length === 0) {
return return
} }
let data
const filePath = response.filePaths[0] try {
data = await this.readFileFromDialog({ response })
fs.readFile(filePath, async (err, data) => { } catch (err) {
if (err) {
const message = this.$t('Settings.Data Settings.Unable to read file') const message = this.$t('Settings.Data Settings.Unable to read file')
this.showToast({ this.showToast({
message: `${message}: ${err}` message: `${message}: ${err}`
}) })
return return
} }
const playlists = JSON.parse(data) const playlists = JSON.parse(data)
playlists.forEach(async (playlistData) => { playlists.forEach(async (playlistData) => {
@ -1202,7 +1197,6 @@ export default Vue.extend({
this.showToast({ this.showToast({
message: this.$t('Settings.Data Settings.All playlists has been successfully imported') message: this.$t('Settings.Data Settings.All playlists has been successfully imported')
}) })
})
}, },
exportPlaylists: async function () { exportPlaylists: async function () {
@ -1344,6 +1338,7 @@ export default Vue.extend({
'getRandomColor', 'getRandomColor',
'calculateColorLuminance', 'calculateColorLuminance',
'showOpenDialog', 'showOpenDialog',
'readFileFromDialog',
'showSaveDialog', 'showSaveDialog',
'getUserDataPath', 'getUserDataPath',
'addPlaylist', 'addPlaylist',

View File

@ -12,6 +12,7 @@
@click="showImportSubscriptionsPrompt = true" @click="showImportSubscriptionsPrompt = true"
/> />
<ft-button <ft-button
v-if="usingElectron"
:label="$t('Settings.Data Settings.Check for Legacy Subscriptions')" :label="$t('Settings.Data Settings.Check for Legacy Subscriptions')"
@click="checkForLegacySubscriptions" @click="checkForLegacySubscriptions"
/> />

View File

@ -395,9 +395,66 @@ 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) { async showOpenDialog (context, options) {
// TODO: implement showOpenDialog web compatible callback const webCbk = () => {
const webCbk = () => null 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 })
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) return await invokeIRC(context, IpcChannels.SHOW_OPEN_DIALOG, webCbk, options)
}, },