Implenting `showSaveDialog` in web (#2741)
* Implementing the web callback for `showSaveDialog` Using the native filesystem API when it is available * Adding the `writeFileFromDialog` function It normalizes the behavior of writing to a file handle given by a file picker between web and electron * Refactoring `exportFreeTubeSubscriptions` Now, it should use the profile data stored in memory rather than reading it from the file system (which may not be present in web browsers), and it should also work in web builds * Refactoring exportYouTubeSubscriptions Using the `writeFileFromDialog` function instead of `fs` * Refactoring the rest of the subscription exports Using the `writeFileFromDialog` function instead of `fs` * Refactoring exportHistory - Using the historyCache stored in memory instead of reading from `fs` to get the history data - Using `writeFileFromDialog` instead of `fs.writeFile` * Refactoring exportPlaylists Using `writeFileFromDialog` instead of `fs` * Removing no longer needed `fs` import * Removing something I was using to debug * Fixing issue with mime types non populating in save picker * Adding back an unintentionally removed line
This commit is contained in:
parent
d39512db0a
commit
41b3af033b
|
@ -8,13 +8,10 @@ import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
|||
import FtPrompt from '../ft-prompt/ft-prompt.vue'
|
||||
import { MAIN_PROFILE_ID } from '../../../constants'
|
||||
|
||||
import fs from 'fs'
|
||||
import { opmlToJSON } from 'opml-to-json'
|
||||
import ytch from 'yt-channel-info'
|
||||
import { calculateColorLuminance, copyToClipboard, getRandomColor, showToast } from '../../helpers/utils'
|
||||
|
||||
// FIXME: Missing web logic branching
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'DataSettings',
|
||||
components: {
|
||||
|
@ -60,6 +57,9 @@ export default Vue.extend({
|
|||
allPlaylists: function () {
|
||||
return this.$store.getters.getAllPlaylists
|
||||
},
|
||||
historyCache: function () {
|
||||
return this.$store.getters.getHistoryCache
|
||||
},
|
||||
exportSubscriptionsPromptNames: function () {
|
||||
const exportFreeTube = this.$t('Settings.Data Settings.Export FreeTube')
|
||||
const exportYouTube = this.$t('Settings.Data Settings.Export YouTube')
|
||||
|
@ -487,9 +487,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
exportFreeTubeSubscriptions: async function () {
|
||||
await this.compactProfiles()
|
||||
const userData = await this.getUserDataPath()
|
||||
const subscriptionsDb = `${userData}/profiles.db`
|
||||
const subscriptionsDb = this.profileList.map((profile) => {
|
||||
return JSON.stringify(profile)
|
||||
}).join('\n') + '\n'// a trailing line is expected
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'freetube-subscriptions-' + date + '.db'
|
||||
|
||||
|
@ -508,26 +508,14 @@ export default Vue.extend({
|
|||
// 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')
|
||||
showToast(`${message}: ${readErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
fs.writeFile(filePath, data, (writeErr) => {
|
||||
if (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
})
|
||||
})
|
||||
try {
|
||||
await this.writeFileFromDialog({ response, content: subscriptionsDb })
|
||||
} catch (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to read file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportYouTubeSubscriptions: async function () {
|
||||
|
@ -586,17 +574,14 @@ export default Vue.extend({
|
|||
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')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
})
|
||||
try {
|
||||
await this.writeFileFromDialog({ response, content: JSON.stringify(subscriptionsObject) })
|
||||
} catch (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportOpmlYouTubeSubscriptions: async function () {
|
||||
|
@ -634,17 +619,14 @@ export default Vue.extend({
|
|||
return
|
||||
}
|
||||
|
||||
const filePath = response.filePath
|
||||
|
||||
fs.writeFile(filePath, opmlData, (writeErr) => {
|
||||
if (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
})
|
||||
try {
|
||||
await this.writeFileFromDialog({ response, content: opmlData })
|
||||
} catch (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportCsvYouTubeSubscriptions: async function () {
|
||||
|
@ -676,16 +658,14 @@ export default Vue.extend({
|
|||
return
|
||||
}
|
||||
|
||||
const filePath = response.filePath
|
||||
fs.writeFile(filePath, exportText, (writeErr) => {
|
||||
if (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
})
|
||||
try {
|
||||
await this.writeFileFromDialog({ response, content: exportText })
|
||||
} catch (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportNewPipeSubscriptions: async function () {
|
||||
|
@ -724,18 +704,14 @@ export default Vue.extend({
|
|||
// 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')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
})
|
||||
try {
|
||||
await this.writeFileFromDialog({ response, content: JSON.stringify(newPipeObject) })
|
||||
} catch (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
importHistory: async function () {
|
||||
|
@ -807,9 +783,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
exportHistory: async function () {
|
||||
await this.compactHistory()
|
||||
const userData = await this.getUserDataPath()
|
||||
const historyDb = `${userData}/history.db`
|
||||
const historyDb = this.historyCache.map((historyEntry) => {
|
||||
return JSON.stringify(historyEntry)
|
||||
}).join('\n') + '\n'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'freetube-history-' + date + '.db'
|
||||
|
||||
|
@ -829,25 +805,13 @@ export default Vue.extend({
|
|||
return
|
||||
}
|
||||
|
||||
const filePath = response.filePath
|
||||
|
||||
fs.readFile(historyDb, (readErr, data) => {
|
||||
if (readErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to read file')
|
||||
showToast(`${message}: ${readErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
fs.writeFile(filePath, data, (writeErr) => {
|
||||
if (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
showToast(this.$t('Settings.Data Settings.All watched history has been successfully exported'))
|
||||
})
|
||||
})
|
||||
try {
|
||||
await this.writeFileFromDialog({ response, content: historyDb })
|
||||
} catch (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
}
|
||||
showToast(this.$t('Settings.Data Settings.All watched history has been successfully exported'))
|
||||
},
|
||||
|
||||
importPlaylists: async function () {
|
||||
|
@ -983,18 +947,14 @@ export default Vue.extend({
|
|||
// User canceled the save dialog
|
||||
return
|
||||
}
|
||||
|
||||
const filePath = response.filePath
|
||||
|
||||
fs.writeFile(filePath, JSON.stringify(this.allPlaylists), (writeErr) => {
|
||||
if (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
|
||||
showToast(this.$t('Settings.Data Settings.All playlists has been successfully exported'))
|
||||
})
|
||||
try {
|
||||
await this.writeFileFromDialog({ response, content: JSON.stringify(this.allPlaylists) })
|
||||
} catch (writeErr) {
|
||||
const message = this.$t('Settings.Data Settings.Unable to write file')
|
||||
showToast(`${message}: ${writeErr}`)
|
||||
return
|
||||
}
|
||||
showToast(`${this.$t('Settings.Data Settings.All playlists has been successfully exported')}`)
|
||||
},
|
||||
|
||||
convertOldFreeTubeFormatToNew(oldData) {
|
||||
|
@ -1143,6 +1103,7 @@ export default Vue.extend({
|
|||
'showOpenDialog',
|
||||
'readFileFromDialog',
|
||||
'showSaveDialog',
|
||||
'writeFileFromDialog',
|
||||
'getUserDataPath',
|
||||
'addPlaylist',
|
||||
'addVideo'
|
||||
|
|
|
@ -307,9 +307,63 @@ const actions = {
|
|||
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) {
|
||||
// TODO: implement showSaveDialog web compatible callback
|
||||
const webCbk = () => null
|
||||
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)
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue