diff --git a/src/constants.js b/src/constants.js index c3bfbf5a5ccd7..66a2f73751195 100644 --- a/src/constants.js +++ b/src/constants.js @@ -48,6 +48,8 @@ const IpcChannels = { SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization', GENERATE_PO_TOKEN: 'generate-po-token', + + WRITE_SCREENSHOT: 'write-screenshot', } const DBActions = { diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index edf728f842656..e0953ff4e7e96 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -77,6 +77,10 @@ class Settings { } } + static _findScreenshotFolderPath() { + return db.settings.findOneAsync({ _id: 'screenshotFolderPath' }) + } + static _updateBounds(value) { return db.settings.updateAsync({ _id: 'bounds' }, { _id: 'bounds', value }, { upsert: true }) } diff --git a/src/main/index.js b/src/main/index.js index d1cc1d660c8d8..6215903cff6bd 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1010,6 +1010,40 @@ function runApp() { }) } + ipcMain.handle(IpcChannels.WRITE_SCREENSHOT, async (event, filename, arrayBuffer) => { + if (!isFreeTubeUrl(event.senderFrame.url) || typeof filename !== 'string' || !(arrayBuffer instanceof ArrayBuffer)) { + return + } + + const screenshotFolderPath = await baseHandlers.settings._findScreenshotFolderPath() + + let directory + if (screenshotFolderPath && screenshotFolderPath.value.length > 0) { + directory = screenshotFolderPath.value + } else { + directory = path.join(app.getPath('pictures'), 'FreeTube') + } + + directory = path.normalize(directory) + + const filePath = path.resolve(directory, filename) + + // Ensure that we are only writing inside of the expected directory + if (path.dirname(filePath) !== directory) { + throw new Error('Invalid save location') + } + + try { + await asyncFs.mkdir(directory, { recursive: true }) + + await asyncFs.writeFile(filePath, new DataView(arrayBuffer)) + } catch (error) { + console.error('WRITE_SCREENSHOT failed', error) + // throw a new error so that we don't expose the real error to the renderer + throw new Error('Failed to save') + } + }) + ipcMain.on(IpcChannels.STOP_POWER_SAVE_BLOCKER, (_, id) => { powerSaveBlocker.stop(id) }) 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 85a883fa22132..2fa7df3495c8f 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 @@ -1,6 +1,3 @@ -import fs from 'fs/promises' -import path from 'path' - import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue' import shaka from 'shaka-player' import { useI18n } from '../../composables/use-i18n-polyfill' @@ -24,11 +21,9 @@ import { } from '../../helpers/player/utils' import { addKeyboardShortcutToActionTitle, - getPicturesPath, showToast, writeFileWithPicker } from '../../helpers/utils' -import { pathExists } from '../../helpers/filesystem' /** @typedef {import('../../helpers/sponsorblock').SponsorBlockCategory} SponsorBlockCategory */ @@ -344,11 +339,6 @@ export default defineComponent({ return store.getters.getScreenshotAskPath }) - /** @type {import('vue').ComputedRef} */ - const screenshotFolder = computed(() => { - return store.getters.getScreenshotFolderPath - }) - /** @type {import('vue').ComputedRef} */ const videoVolumeMouseScroll = computed(() => { return store.getters.getVideoVolumeMouseScroll @@ -1602,16 +1592,16 @@ export default defineComponent({ const filenameWithExtension = `${filename}.${format}` - if (!process.env.IS_ELECTRON || screenshotAskPath.value) { - const wasPlaying = !video_.paused - if (wasPlaying) { - video_.pause() - } + const wasPlaying = !video_.paused + if (wasPlaying) { + video_.pause() + } - try { - /** @type {Blob} */ - const blob = await new Promise((resolve) => canvas.toBlob(resolve, mimeType, imageQuality)) + try { + /** @type {Blob} */ + const blob = await new Promise((resolve) => canvas.toBlob(resolve, mimeType, imageQuality)) + if (!process.env.IS_ELECTRON || screenshotAskPath.value) { const saved = await writeFileWithPicker( filenameWithExtension, blob, @@ -1625,53 +1615,24 @@ export default defineComponent({ if (saved) { showToast(t('Screenshot Success')) } - } catch (error) { - console.error(error) - showToast(t('Screenshot Error', { error })) - } + } else { + const arrayBuffer = await blob.arrayBuffer() - canvas.remove() + const { ipcRenderer } = require('electron') - if (wasPlaying) { - video_.play() - } - } else { - let dirPath + await ipcRenderer.invoke(IpcChannels.WRITE_SCREENSHOT, filenameWithExtension, arrayBuffer) - if (screenshotFolder.value === '') { - dirPath = path.join(await getPicturesPath(), 'Freetube') - } else { - dirPath = screenshotFolder.value + showToast(t('Screenshot Success')) } + } catch (error) { + console.error(error) + showToast(t('Screenshot Error', { error })) + } finally { + canvas.remove() - if (!(await pathExists(dirPath))) { - try { - await fs.mkdir(dirPath, { recursive: true }) - } catch (err) { - console.error(err) - showToast(t('Screenshot Error', { error: err })) - canvas.remove() - return - } + if (wasPlaying) { + video_.play() } - - const filePath = path.join(dirPath, filenameWithExtension) - - canvas.toBlob((result) => { - result.arrayBuffer().then(ab => { - const arr = new Uint8Array(ab) - - fs.writeFile(filePath, arr) - .then(() => { - showToast(t('Screenshot Success')) - }) - .catch((err) => { - console.error(err) - showToast(t('Screenshot Error', { error: err })) - }) - }) - }, mimeType, imageQuality) - canvas.remove() } }