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:
constraintAutomaton 2022-01-30 12:49:16 -05:00 committed by GitHub
parent c8264c0d4d
commit 609996d175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 267 additions and 8 deletions

View File

@ -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'
])
}
})

View File

@ -0,0 +1,8 @@
@use "../../sass-partials/settings"
@media only screen and (max-width: 500px)
.downloadSettingsFlexBox
justify-content: flex-start
.folderDisplay
width: 50vh

View File

@ -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" />

View File

@ -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()
}
}

View File

@ -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 })

View File

@ -113,7 +113,6 @@ export default Vue.extend({
'chaptersButton',
'descriptionsButton',
'subsCapsButton',
'audioTrackButton',
'pictureInPictureToggle',
'toggleTheatreModeButton',
'fullWindowButton',

View File

@ -455,7 +455,8 @@ export default Vue.extend({
'updateProfile',
'addVideo',
'removeVideo',
'openExternalLink'
'openExternalLink',
'downloadMedia'
])
}
})

View File

@ -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"

View File

@ -216,7 +216,8 @@ const state = {
useRssFeeds: false,
useSponsorBlock: false,
videoVolumeMouseScroll: false,
videoPlaybackRateMouseScroll: false
videoPlaybackRateMouseScroll: false,
downloadFolderPath: ''
}
const stateWithSideEffects = {

View File

@ -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) {

View File

@ -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 () {

View File

@ -19,6 +19,8 @@
<proxy-settings />
<hr>
<sponsor-block-settings />
<hr>
<download-settings v-if="usingElectron" />
</div>
</template>

View File

@ -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

View File

@ -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é'