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