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:
parent
7822f7423e
commit
1512178489
|
@ -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',
|
||||||
|
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue