In app download (#1971)
* src/renderer/store/modules/utils.js, src/renderer/components/watch-video-info/watch-video-info.vue, src/renderer/components/watch-video-info/watch-video-info.js, src/renderer/components/ft-icon-button/ft-icon-button.js, src/main/index.js in-app download in hardcoded path * download into variable folder supported download can be done into a specify folder defined in the settings or can be done by choosing a folder just before the downloading * src/renderer/store/modules/utils.js: folder is asked before downloading when appropriate * src/renderer/store/modules/utils.js: toast added for success and faillure * src/renderer/store/modules/utils.js: mecanism to show download progress * src/renderer/store/modules/utils.js: percentage symbol added to toast message when displaying progress * src/renderer/components/download-settings/download-settings.js: clarification comment about electron * src/renderer/store/modules/utils.js: typo in comment resolved * src/renderer/store/modules/utils.js: show a toast when there is a file error * static/locales/fr-FR.yaml: resolved typo in Choose Path * src/renderer/store/modules/utils.js: download progress notification toast deleted * corrections of typos, changes in toast messages, toast messages translatable by modifying the ft-toast component to allow translatable strings * cleaner code for translatable toast * downloadMedia argument changed from array to object * src/renderer/components/download-settings/download-settings.sass: trailling space added * Apply suggestions from code review folder changed for folderPath Co-authored-by: PikachuEXE <pikachuexe@gmail.com> * fix forgotten folderPath renaming * extra space deleted * starting toast displayed after download folder asks * audio button deleted * experimental electron web library deleted because can cause performance issues * placeholder for web support * made better condition for web and electon compatibility and small variable renaming * better error message when user cancel the download * falling back to asking the user if the download repository doesn't exist * falling back mode implemented * console.log for debugging deleted * useless import deleted * small renaming Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
This commit is contained in:
parent
c8264c0d4d
commit
609996d175
|
@ -0,0 +1,49 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||||
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||||
|
import FtButton from '../ft-button/ft-button.vue'
|
||||||
|
import FtInput from '../ft-input/ft-input.vue'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
import { IpcChannels } from '../../../constants'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'DownloadSettings',
|
||||||
|
components: {
|
||||||
|
'ft-toggle-switch': FtToggleSwitch,
|
||||||
|
'ft-flex-box': FtFlexBox,
|
||||||
|
'ft-button': FtButton,
|
||||||
|
'ft-input': FtInput
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
askForDownloadPath: this.$store.getters.getDownloadFolderPath === ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
downloadPath: function() {
|
||||||
|
return this.$store.getters.getDownloadFolderPath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleDownloadingSettingChange: function (value) {
|
||||||
|
this.askForDownloadPath = value
|
||||||
|
if (value === true) {
|
||||||
|
this.updateDownloadFolderPath('')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chooseDownloadingFolder: async function() {
|
||||||
|
// only use with electron
|
||||||
|
const folder = await ipcRenderer.invoke(
|
||||||
|
IpcChannels.SHOW_OPEN_DIALOG,
|
||||||
|
{ properties: ['openDirectory'] }
|
||||||
|
)
|
||||||
|
|
||||||
|
this.updateDownloadFolderPath(folder.filePaths[0])
|
||||||
|
},
|
||||||
|
...mapActions([
|
||||||
|
'updateDownloadFolderPath'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,8 @@
|
||||||
|
@use "../../sass-partials/settings"
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px)
|
||||||
|
.downloadSettingsFlexBox
|
||||||
|
justify-content: flex-start
|
||||||
|
|
||||||
|
.folderDisplay
|
||||||
|
width: 50vh
|
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<h3>
|
||||||
|
{{ $t("Settings.Download Settings.Download Settings") }}
|
||||||
|
</h3>
|
||||||
|
</summary>
|
||||||
|
<hr>
|
||||||
|
<ft-flex-box class="downloadSettingsFlexBox">
|
||||||
|
<ft-toggle-switch
|
||||||
|
:label="$t('Settings.Download Settings.Ask Download Path')"
|
||||||
|
:default-value="askForDownloadPath"
|
||||||
|
@change="handleDownloadingSettingChange"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<div
|
||||||
|
v-if="!askForDownloadPath"
|
||||||
|
>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-input
|
||||||
|
class="folderDisplay"
|
||||||
|
:placeholder="downloadPath"
|
||||||
|
:show-action-button="false"
|
||||||
|
:show-label="false"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<ft-button
|
||||||
|
:label="$t('Settings.Download Settings.Choose Path')"
|
||||||
|
@click="chooseDownloadingFolder"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./download-settings.js" />
|
||||||
|
<style scoped lang="sass" src="./download-settings.sass" />
|
|
@ -47,6 +47,11 @@ export default Vue.extend({
|
||||||
dropdownValues: {
|
dropdownValues: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => { return [] }
|
default: () => { return [] }
|
||||||
|
},
|
||||||
|
relatedVideoTitle: {
|
||||||
|
type: String,
|
||||||
|
default: () => { return '' },
|
||||||
|
require: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -55,6 +60,18 @@ export default Vue.extend({
|
||||||
id: ''
|
id: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
filesExtensions: function() {
|
||||||
|
const regex = /\/(\w*)/i
|
||||||
|
return this.dropdownNames.slice().map((el) => {
|
||||||
|
const group = el.match(regex)
|
||||||
|
if (group.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return group[1]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.id = `iconButton${this._uid}`
|
this.id = `iconButton${this._uid}`
|
||||||
},
|
},
|
||||||
|
@ -111,7 +128,12 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDropdownClick: function (index) {
|
handleDropdownClick: function (index) {
|
||||||
this.$emit('click', this.dropdownValues[index])
|
this.$emit('click', {
|
||||||
|
url: this.dropdownValues[index],
|
||||||
|
title: this.relatedVideoTitle,
|
||||||
|
extension: this.filesExtensions[index],
|
||||||
|
folderPath: this.$store.getters.getDownloadFolderPath
|
||||||
|
})
|
||||||
this.focusOut()
|
this.focusOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,13 @@ export default Vue.extend({
|
||||||
|
|
||||||
toast.isOpen = false
|
toast.isOpen = false
|
||||||
},
|
},
|
||||||
open: function (message, action, time) {
|
open: function (message, action, time, translate = false, formatArgs = []) {
|
||||||
|
if (translate) {
|
||||||
|
message = this.$t(message)
|
||||||
|
for (const arg of formatArgs) {
|
||||||
|
message = message.replace('$', arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
const toast = { message: message, action: action || (() => { }), isOpen: false, timeout: null }
|
const toast = { message: message, action: action || (() => { }), isOpen: false, timeout: null }
|
||||||
toast.timeout = setTimeout(this.close, time || 3000, toast)
|
toast.timeout = setTimeout(this.close, time || 3000, toast)
|
||||||
setImmediate(() => { toast.isOpen = true })
|
setImmediate(() => { toast.isOpen = true })
|
||||||
|
|
|
@ -113,7 +113,6 @@ export default Vue.extend({
|
||||||
'chaptersButton',
|
'chaptersButton',
|
||||||
'descriptionsButton',
|
'descriptionsButton',
|
||||||
'subsCapsButton',
|
'subsCapsButton',
|
||||||
'audioTrackButton',
|
|
||||||
'pictureInPictureToggle',
|
'pictureInPictureToggle',
|
||||||
'toggleTheatreModeButton',
|
'toggleTheatreModeButton',
|
||||||
'fullWindowButton',
|
'fullWindowButton',
|
||||||
|
|
|
@ -455,7 +455,8 @@ export default Vue.extend({
|
||||||
'updateProfile',
|
'updateProfile',
|
||||||
'addVideo',
|
'addVideo',
|
||||||
'removeVideo',
|
'removeVideo',
|
||||||
'openExternalLink'
|
'openExternalLink',
|
||||||
|
'downloadMedia'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -86,7 +86,8 @@
|
||||||
icon="download"
|
icon="download"
|
||||||
:dropdown-names="downloadLinkNames"
|
:dropdown-names="downloadLinkNames"
|
||||||
:dropdown-values="downloadLinkValues"
|
:dropdown-values="downloadLinkValues"
|
||||||
@click="openExternalLink"
|
:related-video-title="title"
|
||||||
|
@click="downloadMedia"
|
||||||
/>
|
/>
|
||||||
<ft-icon-button
|
<ft-icon-button
|
||||||
v-if="!isUpcoming"
|
v-if="!isUpcoming"
|
||||||
|
|
|
@ -216,7 +216,8 @@ const state = {
|
||||||
useRssFeeds: false,
|
useRssFeeds: false,
|
||||||
useSponsorBlock: false,
|
useSponsorBlock: false,
|
||||||
videoVolumeMouseScroll: false,
|
videoVolumeMouseScroll: false,
|
||||||
videoPlaybackRateMouseScroll: false
|
videoPlaybackRateMouseScroll: false,
|
||||||
|
downloadFolderPath: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateWithSideEffects = {
|
const stateWithSideEffects = {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
import { IpcChannels } from '../../../constants'
|
import { IpcChannels } from '../../../constants'
|
||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
isSideNavOpen: false,
|
isSideNavOpen: false,
|
||||||
|
@ -175,6 +176,115 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async downloadMedia({ rootState, dispatch }, { url, title, extension, folderPath, fallingBackPath }) {
|
||||||
|
const usingElectron = rootState.settings.usingElectron
|
||||||
|
const askFolderPath = folderPath === ''
|
||||||
|
let filePathSelected
|
||||||
|
const successMsg = 'Downloading has completed'
|
||||||
|
|
||||||
|
if (askFolderPath && usingElectron) {
|
||||||
|
const resp = await ipcRenderer.invoke(
|
||||||
|
IpcChannels.SHOW_SAVE_DIALOG,
|
||||||
|
{ defaultPath: `${title}.${extension}` }
|
||||||
|
)
|
||||||
|
filePathSelected = resp.filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallingBackPath !== undefined) {
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: 'Download folder does not exist',
|
||||||
|
translate: true,
|
||||||
|
formatArgs: [fallingBackPath]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: 'Starting download', translate: true, formatArgs: [title]
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(url)
|
||||||
|
// mechanism to show the download progress reference https://javascript.info/fetch-progress
|
||||||
|
const reader = response.body.getReader()
|
||||||
|
|
||||||
|
const contentLength = response.headers.get('Content-Length')
|
||||||
|
|
||||||
|
let receivedLength = 0
|
||||||
|
const chunks = []
|
||||||
|
// manage frequency notifications to the user
|
||||||
|
const intervalPercentageNotification = 0.2
|
||||||
|
let lastPercentageNotification = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.push(value)
|
||||||
|
receivedLength += value.length
|
||||||
|
const percentage = receivedLength / contentLength
|
||||||
|
if (percentage > (lastPercentageNotification + intervalPercentageNotification)) {
|
||||||
|
// mechanism kept for an upcoming download page
|
||||||
|
lastPercentageNotification = percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunksAll = new Uint8Array(receivedLength)
|
||||||
|
let position = 0
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
chunksAll.set(chunk, position)
|
||||||
|
position += chunk.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the file into the hardrive
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`"Unable to download ${title}, return status code ${response.status}`)
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: 'Downloading failed', translate: true, formatArgs: [title, response.status]
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const blobFile = new Blob(chunks)
|
||||||
|
const buffer = await blobFile.arrayBuffer()
|
||||||
|
|
||||||
|
if (usingElectron && !askFolderPath) {
|
||||||
|
fs.writeFile(`${folderPath}/${title}.${extension}`, new DataView(buffer), (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
dispatch('updateDownloadFolderPath', '')
|
||||||
|
dispatch('downloadMedia', { url: url, title: title, extension: extension, folderPath: '', fallingBackPath: folderPath })
|
||||||
|
} else {
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: successMsg, translate: true, formatArgs: [title]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (usingElectron) {
|
||||||
|
fs.writeFile(filePathSelected, new DataView(buffer), (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
if (filePathSelected === '') {
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: 'Downloading canceled',
|
||||||
|
translate: true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: successMsg, translate: true, formatArgs: [title]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Web placeholder
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async getSystemLocale (context) {
|
async getSystemLocale (context) {
|
||||||
const webCbk = () => {
|
const webCbk = () => {
|
||||||
if (navigator && navigator.language) {
|
if (navigator && navigator.language) {
|
||||||
|
@ -680,7 +790,9 @@ const actions = {
|
||||||
},
|
},
|
||||||
|
|
||||||
showToast (_, payload) {
|
showToast (_, payload) {
|
||||||
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time)
|
const formatArgs = 'formatArgs' in payload ? payload.formatArgs : []
|
||||||
|
const translate = 'translate' in payload ? payload.translate : false
|
||||||
|
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time, translate, formatArgs)
|
||||||
},
|
},
|
||||||
|
|
||||||
showExternalPlayerUnsupportedActionToast: function ({ dispatch }, payload) {
|
showExternalPlayerUnsupportedActionToast: function ({ dispatch }, payload) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
|
||||||
import PlayerSettings from '../../components/player-settings/player-settings.vue'
|
import PlayerSettings from '../../components/player-settings/player-settings.vue'
|
||||||
import ExternalPlayerSettings from '../../components/external-player-settings/external-player-settings.vue'
|
import ExternalPlayerSettings from '../../components/external-player-settings/external-player-settings.vue'
|
||||||
import SubscriptionSettings from '../../components/subscription-settings/subscription-settings.vue'
|
import SubscriptionSettings from '../../components/subscription-settings/subscription-settings.vue'
|
||||||
|
import DownloadSettings from '../../components/download-settings/download-settings.vue'
|
||||||
import PrivacySettings from '../../components/privacy-settings/privacy-settings.vue'
|
import PrivacySettings from '../../components/privacy-settings/privacy-settings.vue'
|
||||||
import DataSettings from '../../components/data-settings/data-settings.vue'
|
import DataSettings from '../../components/data-settings/data-settings.vue'
|
||||||
import DistractionSettings from '../../components/distraction-settings/distraction-settings.vue'
|
import DistractionSettings from '../../components/distraction-settings/distraction-settings.vue'
|
||||||
|
@ -26,7 +27,8 @@ export default Vue.extend({
|
||||||
'data-settings': DataSettings,
|
'data-settings': DataSettings,
|
||||||
'distraction-settings': DistractionSettings,
|
'distraction-settings': DistractionSettings,
|
||||||
'proxy-settings': ProxySettings,
|
'proxy-settings': ProxySettings,
|
||||||
'sponsor-block-settings': SponsorBlockSettings
|
'sponsor-block-settings': SponsorBlockSettings,
|
||||||
|
'download-settings': DownloadSettings
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
usingElectron: function () {
|
usingElectron: function () {
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
<proxy-settings />
|
<proxy-settings />
|
||||||
<hr>
|
<hr>
|
||||||
<sponsor-block-settings />
|
<sponsor-block-settings />
|
||||||
|
<hr>
|
||||||
|
<download-settings v-if="usingElectron" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -325,6 +325,10 @@ Settings:
|
||||||
Enable SponsorBlock: Enable SponsorBlock
|
Enable SponsorBlock: Enable SponsorBlock
|
||||||
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': SponsorBlock API Url (Default is https://sponsor.ajay.app)
|
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': SponsorBlock API Url (Default is https://sponsor.ajay.app)
|
||||||
Notify when sponsor segment is skipped: Notify when sponsor segment is skipped
|
Notify when sponsor segment is skipped: Notify when sponsor segment is skipped
|
||||||
|
Download Settings:
|
||||||
|
Download Settings: Download Settings
|
||||||
|
Ask Download Path: Ask for download path
|
||||||
|
Choose Path: Choose Path
|
||||||
About:
|
About:
|
||||||
#On About page
|
#On About page
|
||||||
About: About
|
About: About
|
||||||
|
@ -709,6 +713,11 @@ Default Invidious instance has been cleared: Default Invidious instance has been
|
||||||
'The playlist has ended. Enable loop to continue playing': 'The playlist has ended. Enable
|
'The playlist has ended. Enable loop to continue playing': 'The playlist has ended. Enable
|
||||||
loop to continue playing'
|
loop to continue playing'
|
||||||
External link opening has been disabled in the general settings: 'External link opening has been disabled in the general settings'
|
External link opening has been disabled in the general settings: 'External link opening has been disabled in the general settings'
|
||||||
|
Downloading has completed: 'Downloading "$" has completed'
|
||||||
|
Starting download: 'Downloading "$" has started'
|
||||||
|
Downloading failed: 'Unable to download "$", return http request status code $'
|
||||||
|
Downloading canceled: The dowload is canceled by the user
|
||||||
|
Download folder does not exist: The download directory "$" doesn't exist. Falling back to "ask folder" mode.
|
||||||
|
|
||||||
Yes: Yes
|
Yes: Yes
|
||||||
No: No
|
No: No
|
||||||
|
|
|
@ -377,6 +377,10 @@ Settings:
|
||||||
External Player Settings: Paramètres du lecteur externe
|
External Player Settings: Paramètres du lecteur externe
|
||||||
Custom External Player Arguments: Arguments personnalisés du lecteur externe
|
Custom External Player Arguments: Arguments personnalisés du lecteur externe
|
||||||
Custom External Player Executable: Exécutable de lecteur externe personnalisé
|
Custom External Player Executable: Exécutable de lecteur externe personnalisé
|
||||||
|
Download Settings:
|
||||||
|
Download Settings: 'Paramètres de téléchargement'
|
||||||
|
Ask Download Path: 'Demander l'emplacement de téléchargement'
|
||||||
|
Choose Path: 'Choisir l'emplacement de téléchargement'
|
||||||
About:
|
About:
|
||||||
#On About page
|
#On About page
|
||||||
About: 'À propos'
|
About: 'À propos'
|
||||||
|
@ -836,3 +840,9 @@ Search Bar:
|
||||||
Are you sure you want to open this link?: Êtes-vous sûr(e) de vouloir ouvrir ce lien ?
|
Are you sure you want to open this link?: Êtes-vous sûr(e) de vouloir ouvrir ce lien ?
|
||||||
External link opening has been disabled in the general settings: L'ouverture des liens
|
External link opening has been disabled in the general settings: L'ouverture des liens
|
||||||
externes a été désactivée dans les paramètres généraux
|
externes a été désactivée dans les paramètres généraux
|
||||||
|
Downloading has completed: 'Le téléchargement de "$" est fini'
|
||||||
|
Starting download: 'Le téléchargement de "$" a commencé'
|
||||||
|
Downloading failed: 'Incapable de téléchargement "$", retourne l'erreur http $'
|
||||||
|
Downloading canceled: 'Le téléchargement est annulé par l'utilisateur'
|
||||||
|
Download folder does not exist: 'Le répertoire "$" de téléchargement n'existe pas. Mode sans répertoire activé'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue