Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed _scripts/_empty.js
Empty file.
4 changes: 0 additions & 4 deletions _scripts/webpack.web.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 1 addition & 2 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 = {
Expand Down
34 changes: 13 additions & 21 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'))
}
Expand Down
23 changes: 6 additions & 17 deletions src/renderer/components/watch-video-info/watch-video-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -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]
})
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
theme="secondary"
:icon="['fas', 'download']"
:return-index="true"
:dropdown-options="downloadLinkOptions"
:dropdown-options="downloadLinks"
@click="handleDownload"
/>
<ft-icon-button
Expand Down
14 changes: 0 additions & 14 deletions src/renderer/helpers/filesystem.js

This file was deleted.

30 changes: 0 additions & 30 deletions src/renderer/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,36 +408,6 @@ export async function writeFileWithPicker(
}
}

/**
* @param {{defaultPath: string, filters: {name: string, extensions: string[]}[]}} options
* @returns { Promise<import('electron').SaveDialogReturnValue> | {canceled: boolean?, filePath: string } | { canceled: boolean?, handle?: Promise<FileSystemFileHandle> }}
*/
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.
Expand Down
138 changes: 67 additions & 71 deletions src/renderer/store/modules/utils.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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 }))
}
}
},

Expand Down
Loading