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: {
|
||||
type: Array,
|
||||
default: () => { return [] }
|
||||
},
|
||||
relatedVideoTitle: {
|
||||
type: String,
|
||||
default: () => { return '' },
|
||||
require: false
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
|
@ -55,6 +60,18 @@ export default Vue.extend({
|
|||
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 () {
|
||||
this.id = `iconButton${this._uid}`
|
||||
},
|
||||
|
@ -111,7 +128,12 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,13 @@ export default Vue.extend({
|
|||
|
||||
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 }
|
||||
toast.timeout = setTimeout(this.close, time || 3000, toast)
|
||||
setImmediate(() => { toast.isOpen = true })
|
||||
|
|
|
@ -113,7 +113,6 @@ export default Vue.extend({
|
|||
'chaptersButton',
|
||||
'descriptionsButton',
|
||||
'subsCapsButton',
|
||||
'audioTrackButton',
|
||||
'pictureInPictureToggle',
|
||||
'toggleTheatreModeButton',
|
||||
'fullWindowButton',
|
||||
|
|
|
@ -455,7 +455,8 @@ export default Vue.extend({
|
|||
'updateProfile',
|
||||
'addVideo',
|
||||
'removeVideo',
|
||||
'openExternalLink'
|
||||
'openExternalLink',
|
||||
'downloadMedia'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -86,7 +86,8 @@
|
|||
icon="download"
|
||||
:dropdown-names="downloadLinkNames"
|
||||
:dropdown-values="downloadLinkValues"
|
||||
@click="openExternalLink"
|
||||
:related-video-title="title"
|
||||
@click="downloadMedia"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="!isUpcoming"
|
||||
|
|
|
@ -216,7 +216,8 @@ const state = {
|
|||
useRssFeeds: false,
|
||||
useSponsorBlock: false,
|
||||
videoVolumeMouseScroll: false,
|
||||
videoPlaybackRateMouseScroll: false
|
||||
videoPlaybackRateMouseScroll: false,
|
||||
downloadFolderPath: ''
|
||||
}
|
||||
|
||||
const stateWithSideEffects = {
|
||||
|
|
|
@ -3,6 +3,7 @@ import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
|||
import fs from 'fs'
|
||||
|
||||
import { IpcChannels } from '../../../constants'
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
const state = {
|
||||
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) {
|
||||
const webCbk = () => {
|
||||
if (navigator && navigator.language) {
|
||||
|
@ -680,7 +790,9 @@ const actions = {
|
|||
},
|
||||
|
||||
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) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
|
|||
import PlayerSettings from '../../components/player-settings/player-settings.vue'
|
||||
import ExternalPlayerSettings from '../../components/external-player-settings/external-player-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 DataSettings from '../../components/data-settings/data-settings.vue'
|
||||
import DistractionSettings from '../../components/distraction-settings/distraction-settings.vue'
|
||||
|
@ -26,7 +27,8 @@ export default Vue.extend({
|
|||
'data-settings': DataSettings,
|
||||
'distraction-settings': DistractionSettings,
|
||||
'proxy-settings': ProxySettings,
|
||||
'sponsor-block-settings': SponsorBlockSettings
|
||||
'sponsor-block-settings': SponsorBlockSettings,
|
||||
'download-settings': DownloadSettings
|
||||
},
|
||||
computed: {
|
||||
usingElectron: function () {
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
<proxy-settings />
|
||||
<hr>
|
||||
<sponsor-block-settings />
|
||||
<hr>
|
||||
<download-settings v-if="usingElectron" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -325,6 +325,10 @@ Settings:
|
|||
Enable SponsorBlock: Enable SponsorBlock
|
||||
'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
|
||||
Download Settings:
|
||||
Download Settings: Download Settings
|
||||
Ask Download Path: Ask for download path
|
||||
Choose Path: Choose Path
|
||||
About:
|
||||
#On About page
|
||||
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
|
||||
loop to continue playing'
|
||||
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
|
||||
No: No
|
||||
|
|
|
@ -377,6 +377,10 @@ Settings:
|
|||
External Player Settings: Paramètres du lecteur externe
|
||||
Custom External Player Arguments: Arguments personnalisés du lecteur externe
|
||||
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:
|
||||
#On About page
|
||||
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 ?
|
||||
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
|
||||
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