diff --git a/_scripts/_empty.js b/_scripts/_empty.js deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index 53a471d046f5b..8b0f5461175d8 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -161,10 +161,6 @@ const config = { // change to "shaka-player.ui.debug.js" to get debug logs (update jsconfig to get updated types) 'shaka-player$': 'shaka-player/dist/shaka-player.ui.js', }, - fallback: { - 'fs/promises': path.resolve(__dirname, '_empty.js'), - path: require.resolve('path-browserify'), - }, extensions: ['.js', '.vue'] }, target: 'web', diff --git a/package.json b/package.json index ecb633a219159..cd13f88c6b375 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "bgutils-js": "^3.2.0", "electron-context-menu": "^4.0.5", "marked": "^15.0.11", - "path-browserify": "^1.0.1", "portal-vue": "^2.1.7", "process": "^0.11.10", "shaka-player": "^4.14.9", diff --git a/src/constants.js b/src/constants.js index 941559563b0f0..1c6d908a4392e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,7 +5,6 @@ const IpcChannels = { OPEN_EXTERNAL_LINK: 'open-external-link', GET_SYSTEM_LOCALE: 'get-system-locale', GET_NAVIGATION_HISTORY: 'get-navigation-history', - SHOW_SAVE_DIALOG: 'show-save-dialog', STOP_POWER_SAVE_BLOCKER: 'stop-power-save-blocker', START_POWER_SAVE_BLOCKER: 'start-power-save-blocker', CREATE_NEW_WINDOW: 'create-new-window', @@ -49,7 +48,7 @@ const IpcChannels = { GET_SCREENSHOT_FALLBACK_FOLDER: 'get-screenshot-fallback-folder', CHOOSE_DEFAULT_FOLDER: 'choose-default-folder', - WRITE_SCREENSHOT: 'write-screenshot', + WRITE_TO_DEFAULT_FOLDER: 'write-to-default-folder', } const DBActions = { diff --git a/src/main/index.js b/src/main/index.js index e5d49ecba95a0..c9497eaf8d04c 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1045,20 +1045,6 @@ function runApp() { sender.executeJavaScript('document.querySelector("video.player").ui.getControls().togglePiP()', true) }) - ipcMain.handle(IpcChannels.SHOW_SAVE_DIALOG, async ({ sender }, options) => { - const senderWindow = findSenderWindow(sender) - if (senderWindow) { - return await dialog.showSaveDialog(senderWindow, options) - } - return await dialog.showSaveDialog(options) - }) - - function findSenderWindow(sender) { - return BrowserWindow.getAllWindows().find((window) => { - return window.webContents.id === sender.id - }) - } - ipcMain.handle(IpcChannels.GET_SCREENSHOT_FALLBACK_FOLDER, (event) => { if (!isFreeTubeUrl(event.senderFrame.url)) { return @@ -1113,18 +1099,24 @@ function runApp() { }) }) - ipcMain.handle(IpcChannels.WRITE_SCREENSHOT, async (event, filename, arrayBuffer) => { - if (!isFreeTubeUrl(event.senderFrame.url) || typeof filename !== 'string' || !(arrayBuffer instanceof ArrayBuffer)) { + ipcMain.handle(IpcChannels.WRITE_TO_DEFAULT_FOLDER, async (event, kind, filename, arrayBuffer) => { + if ( + !isFreeTubeUrl(event.senderFrame.url) || + (kind !== DefaultFolderKind.DOWNLOADS && kind !== DefaultFolderKind.SCREENSHOTS) || + typeof filename !== 'string' || + !(arrayBuffer instanceof ArrayBuffer)) { return } - const screenshotFolderPath = await baseHandlers.settings._findOne('screenshotFolderPath') + const settingId = kind === DefaultFolderKind.DOWNLOADS ? 'downloadFolderPath' : 'screenshotFolderPath' + + const folderPath = await baseHandlers.settings._findOne(settingId) let directory - if (screenshotFolderPath && screenshotFolderPath.value.length > 0) { - directory = screenshotFolderPath.value + if (typeof currentPath === 'string' && folderPath.value.length > 0) { + directory = folderPath.value } else { - directory = path.join(app.getPath('pictures'), 'FreeTube') + directory = path.join(app.getPath(kind === DefaultFolderKind.DOWNLOADS ? 'downloads' : 'pictures'), 'FreeTube') } directory = path.normalize(directory) @@ -1141,7 +1133,7 @@ function runApp() { await asyncFs.writeFile(filePath, new DataView(arrayBuffer)) } catch (error) { - console.error('WRITE_SCREENSHOT failed', error) + console.error('WRITE_TO_DEFAULT_FOLDER failed', error) // throw a new error so that we don't expose the real error to the renderer throw new Error('Failed to save') } diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 36ef8bbac67bf..1d4f3115f4bdf 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -3,7 +3,7 @@ import shaka from 'shaka-player' import { useI18n } from '../../composables/use-i18n-polyfill' import store from '../../store/index' -import { IpcChannels, KeyboardShortcuts } from '../../../constants' +import { DefaultFolderKind, IpcChannels, KeyboardShortcuts } from '../../../constants' import { AudioTrackSelection } from './player-components/AudioTrackSelection' import { FullWindowButton } from './player-components/FullWindowButton' import { LegacyQualitySelection } from './player-components/LegacyQualitySelection' @@ -1658,7 +1658,12 @@ export default defineComponent({ const { ipcRenderer } = require('electron') - await ipcRenderer.invoke(IpcChannels.WRITE_SCREENSHOT, filenameWithExtension, arrayBuffer) + await ipcRenderer.invoke( + IpcChannels.WRITE_TO_DEFAULT_FOLDER, + DefaultFolderKind.SCREENSHOTS, + filenameWithExtension, + arrayBuffer + ) showToast(t('Screenshot Success')) } diff --git a/src/renderer/components/watch-video-info/watch-video-info.js b/src/renderer/components/watch-video-info/watch-video-info.js index 98fe04c64e435..fe510f6be2f2e 100644 --- a/src/renderer/components/watch-video-info/watch-video-info.js +++ b/src/renderer/components/watch-video-info/watch-video-info.js @@ -156,15 +156,6 @@ export default defineComponent({ return this.$store.getters.getWatchedProgressSavingMode === 'semi-auto' }, - downloadLinkOptions: function () { - return this.downloadLinks.map((download) => { - return { - label: download.label, - value: download.url - } - }) - }, - downloadBehavior: function () { return this.$store.getters.getDownloadBehavior }, @@ -332,18 +323,16 @@ export default defineComponent({ }, handleDownload: function (index) { - const selectedDownloadLinkOption = this.downloadLinkOptions[index] - const url = selectedDownloadLinkOption.value - const linkName = selectedDownloadLinkOption.label - const extension = this.grabExtensionFromUrl(linkName) + const selectedDownloadLinkOption = this.downloadLinks[index] + const mimeTypeUrl = selectedDownloadLinkOption.value.split('||') - if (this.downloadBehavior === 'open') { - openExternalLink(url) + if (!process.env.IS_ELECTRON || this.downloadBehavior === 'open') { + openExternalLink(mimeTypeUrl[1]) } else { this.downloadMedia({ - url: url, + url: mimeTypeUrl[1], title: this.title, - extension: extension + mimeType: mimeTypeUrl[0] }) } }, diff --git a/src/renderer/components/watch-video-info/watch-video-info.vue b/src/renderer/components/watch-video-info/watch-video-info.vue index ff0379412c98f..a599e0d474bda 100644 --- a/src/renderer/components/watch-video-info/watch-video-info.vue +++ b/src/renderer/components/watch-video-info/watch-video-info.vue @@ -126,7 +126,7 @@ theme="secondary" :icon="['fas', 'download']" :return-index="true" - :dropdown-options="downloadLinkOptions" + :dropdown-options="downloadLinks" @click="handleDownload" /> | {canceled: boolean?, filePath: string } | { canceled: boolean?, handle?: Promise }} - */ -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 } - } - } -} - /** * 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 6ff79e983da18..c806cc55441de 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -1,19 +1,14 @@ -import fs from 'fs/promises' -import path from 'path' import i18n from '../../i18n/index' import { set as vueSet } from 'vue' -import { IpcChannels } from '../../../constants' -import { pathExists } from '../../helpers/filesystem' +import { DefaultFolderKind, IpcChannels } from '../../../constants' import { CHANNEL_HANDLE_REGEX, createWebURL, getVideoParamsFromUrl, - openExternalLink, replaceFilenameForbiddenChars, searchFiltersMatch, showExternalPlayerUnsupportedActionToast, - showSaveDialog, showToast } from '../../helpers/utils' @@ -200,88 +195,89 @@ const actions = { commit('setOutlinesHidden', true) }, - async downloadMedia({ rootState }, { url, title, extension }) { - if (!process.env.IS_ELECTRON) { - openExternalLink(url) - return - } + async downloadMedia({ rootState }, { url, title, mimeType }) { + const extension = mimeType === 'audio/mp4' ? 'm4a' : mimeType.split('/')[1] const fileName = `${replaceFilenameForbiddenChars(title)}.${extension}` - const errorMessage = i18n.t('Downloading failed', { videoTitle: title }) - const askFolderPath = rootState.settings.downloadAskPath - let folderPath = rootState.settings.downloadFolderPath - - if (askFolderPath) { - const options = { - defaultPath: fileName, - filters: [ - { - name: extension.toUpperCase(), - extensions: [extension] - } - ] - } - const response = await showSaveDialog(options) - - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - folderPath = response.filePath - } else { - if (!(await pathExists(folderPath))) { - try { - await fs.mkdir(folderPath, { recursive: true }) - } catch (err) { - console.error(err) - showToast(err) + if (rootState.settings.downloadAskPath) { + /** @type {FileSystemFileHandle} */ + let handle + + try { + handle = await window.showSaveFilePicker({ + excludeAcceptAllOption: true, + id: 'downloads', + startIn: 'downloads', + suggestedName: fileName, + types: [{ + accept: { + [mimeType]: [`.${extension}`] + } + }] + }) + } catch (error) { + // user pressed cancel in the file picker + if (error.name === 'AbortError') { return } + + console.error(error) + showToast(i18n.t('Downloading failed', { videoTitle: title })) + return } - folderPath = path.join(folderPath, fileName) - } - showToast(i18n.t('Starting download', { videoTitle: title })) + showToast(i18n.t('Starting download', { videoTitle: title })) - const response = await fetch(url).catch((error) => { - console.error(error) - showToast(errorMessage) - }) + let writeableFileStream - const reader = response.body.getReader() - const chunks = [] + try { + const response = await fetch(url) - const handleError = (err) => { - console.error(err) - showToast(errorMessage) - } + if (response.ok) { + writeableFileStream = await handle.createWritable() - const processText = async ({ done, value }) => { - if (done) { - return + await response.body.pipeTo(writeableFileStream, { preventClose: true }) + showToast(i18n.t('Downloading has completed', { videoTitle: title })) + } else { + throw new Error(`Bad status code: ${response.status}`) + } + } catch (error) { + console.error(error) + showToast(i18n.t('Downloading failed', { videoTitle: title })) + } finally { + if (writeableFileStream) { + await writeableFileStream.close() + } } + } else { + showToast(i18n.t('Starting download', { videoTitle: title })) - chunks.push(value) - // Can be used in the future to determine download percentage - // const contentLength = response.headers.get('Content-Length') - // const receivedLength = value.length - // const percentage = receivedLength / contentLength - await reader.read().then(processText).catch(handleError) - } + try { + const response = await fetch(url) - await reader.read().then(processText).catch(handleError) + if (response.ok) { + const arrayBuffer = await response.arrayBuffer() - const blobFile = new Blob(chunks) - const buffer = await blobFile.arrayBuffer() + if (process.env.IS_ELECTRON) { + const { ipcRenderer } = require('electron') - try { - await fs.writeFile(folderPath, new DataView(buffer)) + await ipcRenderer.invoke( + IpcChannels.WRITE_TO_DEFAULT_FOLDER, + DefaultFolderKind.DOWNLOADS, + fileName, + arrayBuffer + ) + } - showToast(i18n.t('Downloading has completed', { videoTitle: title })) - } catch (err) { - console.error(err) - showToast(errorMessage) + showToast(i18n.t('Downloading has completed', { videoTitle: title })) + } else { + throw new Error(`Bad status code: ${response.status}`) + } + } catch (error) { + console.error(error) + showToast(i18n.t('Downloading failed', { videoTitle: title })) + } } }, diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 1bcbf2234d812..18a90724c1bab 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -730,7 +730,7 @@ export default defineComponent({ } downloadLinks.push({ - url: format.freeTubeUrl, + value: `${type}||${format.freeTubeUrl}`, label: label }) } @@ -782,7 +782,7 @@ export default defineComponent({ const label = `${caption.label} (${caption.language}) - text/vtt` return { - url: caption.url, + value: `${caption.mimeType}||${caption.url}`, label: label } }) @@ -1006,7 +1006,7 @@ export default defineComponent({ } } const object = { - url: format.url, + value: `${type}||${format.url}`, label: label } @@ -1014,7 +1014,7 @@ export default defineComponent({ }).reverse().concat(result.captions.map((caption) => { const label = `${caption.label} (${caption.languageCode}) - text/vtt` const object = { - url: caption.url, + value: `text/vtt||${caption.url}`, label: label } diff --git a/yarn.lock b/yarn.lock index 1f54d00cb1194..eb8b109ae36b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6975,11 +6975,6 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" -path-browserify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"