From 29b92557b16e36b415c77b7a1360f5570f139d11 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Sat, 27 Jan 2024 22:14:11 +0700 Subject: [PATCH 01/47] Open in external for mobile safari --- .../BaseAnchorForAttachmentsOnly.tsx | 3 ++- src/libs/fileDownload/index.ts | 5 +++-- src/libs/fileDownload/types.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index df8a0a30b129..2323ccf901df 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -5,6 +5,7 @@ import AttachmentView from '@components/Attachments/AttachmentView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import * as Browser from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import * as ReportUtils from '@libs/ReportUtils'; import * as Download from '@userActions/Download'; @@ -43,7 +44,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow return; } Download.setDownload(sourceID, true); - fileDownload(sourceURLWithAuth, displayName).then(() => Download.setDownload(sourceID, false)); + fileDownload(sourceURLWithAuth, displayName, '', Browser.isMobileSafari()).then(() => Download.setDownload(sourceID, false)); }} onPressIn={onPressIn} onPressOut={onPressOut} diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts index 4f43b215925f..0c353fec7799 100644 --- a/src/libs/fileDownload/index.ts +++ b/src/libs/fileDownload/index.ts @@ -8,9 +8,10 @@ import type {FileDownload} from './types'; /** * The function downloads an attachment on web/desktop platforms. */ -const fileDownload: FileDownload = (url, fileName) => { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { const resolvedUrl = tryResolveUrlFromApiRoot(url); - if (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && !CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix))) { + if (shouldOpenExternalLink || (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && !CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix)))) { // Different origin URLs might pose a CORS issue during direct downloads. // Opening in a new tab avoids this limitation, letting the browser handle the download. Link.openExternalLink(url); diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index 6d92bddd5816..b62e7ddfa060 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -1,6 +1,6 @@ import type {Asset} from 'react-native-image-picker'; -type FileDownload = (url: string, fileName: string, successMessage?: string) => Promise; +type FileDownload = (url: string, fileName: string, successMessage?: string, shouldOpenExternalLink?: boolean) => Promise; type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; From 537ea84c76e2dc4f168c8ee2327d1baedd04541c Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 10:36:43 +0700 Subject: [PATCH 02/47] Replicate electron-dl --- .../downloadQueue/electronDownloadManager.ts | 253 ++++++++++++++++++ src/libs/downloadQueue/index.desktop.ts | 46 ++++ src/libs/downloadQueue/index.ts | 5 + src/libs/fileDownload/index.desktop.ts | 17 ++ 4 files changed, 321 insertions(+) create mode 100644 src/libs/downloadQueue/electronDownloadManager.ts create mode 100644 src/libs/downloadQueue/index.desktop.ts create mode 100644 src/libs/downloadQueue/index.ts create mode 100644 src/libs/fileDownload/index.desktop.ts diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts new file mode 100644 index 000000000000..289d198e21a1 --- /dev/null +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -0,0 +1,253 @@ +/** + * This file is a ported version of the `electron-dl` package. + * It provides a download manager for Electron applications. + * The `electron-dl` package simplifies the process of downloading files in Electron apps + * by providing a high-level API and handling various download-related tasks. + * This file contains the implementation of the Electron Download Manager. + */ +'use strict'; +import * as path from 'path'; +import { app, BrowserWindow, shell, dialog, Session, DownloadItem } from 'electron'; +import * as _ from 'underscore'; + +class CancelError extends Error {} + +const getFilenameFromMime = (name: string, mime: string): string => { + const extensions = mime.split('/').pop(); + return `${name}.${extensions}`; +}; + +const majorElectronVersion = (): number => { + const version = process.versions.electron.split('.'); + return Number.parseInt(version[0], 10); +}; + +const getWindowFromBrowserView = (webContents: Electron.WebContents): Electron.BrowserWindow | undefined => { + for (const currentWindow of BrowserWindow.getAllWindows()) { + for (const currentBrowserView of currentWindow.getBrowserViews()) { + if (currentBrowserView.webContents.id === webContents.id) { + return currentWindow; + } + } + } +}; + +const getWindowFromWebContents = (webContents: Electron.WebContents): Electron.BrowserWindow | undefined | null=> { + let window_: Electron.BrowserWindow | undefined | null; + const webContentsType = webContents.getType(); + switch (webContentsType) { + case 'webview': + window_ = BrowserWindow.fromWebContents(webContents.hostWebContents); + break; + case 'browserView': + window_ = getWindowFromBrowserView(webContents); + break; + default: + window_ = BrowserWindow.fromWebContents(webContents); + break; + } + + return window_; +}; + +interface Options { + showBadge?: boolean; + showProgressBar?: boolean; + directory?: string; + filename?: string; + overwrite?: boolean; + errorMessage?: string; + saveAs?: boolean; + dialogOptions?: Electron.SaveDialogOptions; + onProgress?: (progress: { percent: number; transferredBytes: number; totalBytes: number }) => void; + onTotalProgress?: (progress: { percent: number; transferredBytes: number; totalBytes: number }) => void; + onCancel?: (item: Electron.DownloadItem) => void; + onCompleted?: (info: { + fileName: string; + filename: string; + path: string; + fileSize: number; + mimeType: string; + url: string; + }) => void; + unregisterWhenDone?: boolean; + openFolderWhenDone?: boolean; + onStarted?: (item: Electron.DownloadItem) => void; +} + +const registerListener = ( + session: Electron.Session, + options: Options, + callback: (error: Error | null, item?: Electron.DownloadItem) => void = () => {} +): void => { + const downloadItems = new Set(); + let receivedBytes = 0; + let completedBytes = 0; + let totalBytes = 0; + const activeDownloadItems = (): number => downloadItems.size; + const progressDownloadItems = (): number => receivedBytes / totalBytes; + + options = { + showBadge: true, + showProgressBar: true, + ...options + }; + + const listener = (event: Electron.Event, item: Electron.DownloadItem, webContents: Electron.WebContents): void => { + downloadItems.add(item); + totalBytes += item.getTotalBytes(); + + const window_ = majorElectronVersion() >= 12 ? BrowserWindow.fromWebContents(webContents) : getWindowFromWebContents(webContents); + + if (options.directory && !path.isAbsolute(options.directory)) { + throw new Error('The `directory` option must be an absolute path'); + } + + const directory = options.directory || app.getPath('downloads'); + + let filePath: string; + if (options.filename) { + filePath = path.join(directory, options.filename); + } else { + const filename = item.getFilename(); + const name = path.extname(filename) ? filename : getFilenameFromMime(filename, item.getMimeType()); + + filePath = options.overwrite ? path.join(directory, name) : path.join(directory, name); + } + + + if (options.saveAs) { + item.setSaveDialogOptions({ defaultPath: filePath, ...options.dialogOptions }); + } else { + item.setSavePath(filePath); + } + + item.on('updated', () => { + receivedBytes = completedBytes; + for (const item of downloadItems) { + receivedBytes += item.getReceivedBytes(); + } + + if (options.showBadge && ['darwin', 'linux'].includes(process.platform)) { + app.badgeCount = activeDownloadItems(); + } + + if (!window_?.isDestroyed() && options.showProgressBar) { + window_?.setProgressBar(progressDownloadItems()); + } + + if (typeof options.onProgress === 'function') { + const itemTransferredBytes = item.getReceivedBytes(); + const itemTotalBytes = item.getTotalBytes(); + + options.onProgress({ + percent: itemTotalBytes ? itemTransferredBytes / itemTotalBytes : 0, + transferredBytes: itemTransferredBytes, + totalBytes: itemTotalBytes + }); + } + + if (typeof options.onTotalProgress === 'function') { + options.onTotalProgress({ + percent: progressDownloadItems(), + transferredBytes: receivedBytes, + totalBytes + }); + } + }); + + item.on('done', (event: Electron.Event, state: string) => { + completedBytes += item.getTotalBytes(); + downloadItems.delete(item); + + if (options.showBadge && ['darwin', 'linux'].includes(process.platform)) { + app.badgeCount = activeDownloadItems(); + } + + if (!window_?.isDestroyed() && !activeDownloadItems()) { + window_?.setProgressBar(-1); + receivedBytes = 0; + completedBytes = 0; + totalBytes = 0; + } + + if (options.unregisterWhenDone) { + session.removeListener('will-download', listener); + } + + if (state === 'cancelled') { + if (typeof options.onCancel === 'function') { + options.onCancel(item); + } + callback(new CancelError()); + } else if (state === 'interrupted') { + const errorMessage = `The download of ${path.basename(filePath)} was interrupted`; + + callback(new Error(errorMessage)); + } else if (state === 'completed') { + const savePath = item.getSavePath(); + + if (process.platform === 'darwin') { + app.dock.downloadFinished(savePath); + } + + if (options.openFolderWhenDone) { + shell.showItemInFolder(savePath); + } + + if (typeof options.onCompleted === 'function') { + options.onCompleted({ + fileName: item.getFilename(), // Just for backwards compatibility. TODO: Remove in the next major version. + filename: item.getFilename(), + path: savePath, + fileSize: item.getReceivedBytes(), + mimeType: item.getMimeType(), + url: item.getURL() + }); + } + + callback(null, item); + } + }); + + if (typeof options.onStarted === 'function') { + options.onStarted(item); + } + }; + + session.on('will-download', listener); +}; + +export default (options: any = {}): void => { + app.on('session-created', (session: Electron.Session) => { + registerListener(session, options, (error: Error | null, _) => { + if (error && !(error instanceof CancelError)) { + const errorTitle = options.errorTitle || 'Download Error'; + dialog.showErrorBox(errorTitle, error.message); + } + }); + }); +}; + +export const download = (window_: Electron.BrowserWindow, url: string, options: Options): Promise => { + options = { + ...options, + unregisterWhenDone: true + }; + + return new Promise((resolve, reject) => { + registerListener(window_.webContents.session, options, (error: Error | null, item?: Electron.DownloadItem) => { + if (error) { + reject(error); + } else if (item) { + resolve(item); + } else { + reject(new Error("Download item is undefined.")); + } + }); + + window_.webContents.downloadURL(url); + }); +}; + +export { CancelError }; \ No newline at end of file diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts new file mode 100644 index 000000000000..850efc2d8c54 --- /dev/null +++ b/src/libs/downloadQueue/index.desktop.ts @@ -0,0 +1,46 @@ +import { download as electronDownload } from '@libs/downloadQueue/electronDownloadManager'; + +interface DownloadItem { + win: any; + url: string; + options: { + onCompleted: () => void; + onCancel: () => void; + }; +} + +type DownloadQueue = DownloadItem[]; + +const createDownloadQueue = () => { + const queue: DownloadQueue = []; + + const pushDownloadItem = (item: DownloadItem): number => { + const len = queue.push(item); + if (queue.length === 1) { + downloadItem(queue[0]); + } + return len; + }; + + const shiftDownloadItem = (): DownloadItem | undefined => { + const item = queue.shift(); + if (queue.length > 0) { + downloadItem(queue[0]); + } + return item; + }; + + const downloadItem = (item: DownloadItem): void => { + item.options.onCompleted = () => { + shiftDownloadItem(); + }; + + item.options.onCancel = item.options.onCompleted; + + electronDownload(item.win, item.url, item.options); + }; + + return { pushDownloadItem, shiftDownloadItem }; +}; + +export default createDownloadQueue; \ No newline at end of file diff --git a/src/libs/downloadQueue/index.ts b/src/libs/downloadQueue/index.ts new file mode 100644 index 000000000000..7d3cbaccbb9b --- /dev/null +++ b/src/libs/downloadQueue/index.ts @@ -0,0 +1,5 @@ + +// no implementation except for desktop +const createDownloadQueue = () => {} + +export default createDownloadQueue; \ No newline at end of file diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts new file mode 100644 index 000000000000..abcd2e85557e --- /dev/null +++ b/src/libs/fileDownload/index.desktop.ts @@ -0,0 +1,17 @@ +import * as ApiUtils from '@libs/ApiUtils'; +import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import * as FileUtils from './FileUtils'; +import type {FileDownload} from './types'; + +/** + * The function downloads an attachment on desktop platforms. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { + + return Promise.resolve(); +}; + +export default fileDownload; From 2a019dffd97fafd77a87e3fedd3fefd4d2f8f31a Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 13:31:36 +0700 Subject: [PATCH 03/47] Implement download queue to fileDownload --- desktop/ELECTRON_EVENTS.js | 1 + desktop/main.js | 8 ++++++++ src/libs/downloadQueue/index.desktop.ts | 5 +---- src/libs/fileDownload/index.desktop.ts | 3 ++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.js index ee8c0521892e..fd28acaf1e64 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.js @@ -9,6 +9,7 @@ const ELECTRON_EVENTS = { KEYBOARD_SHORTCUTS_PAGE: 'keyboard-shortcuts-page', START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', + DOWNLOAD: 'download', }; module.exports = ELECTRON_EVENTS; diff --git a/desktop/main.js b/desktop/main.js index 4b38c5d36ab3..a6acaad793b2 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -10,6 +10,7 @@ const checkForUpdates = require('../src/libs/checkForUpdates'); const CONFIG = require('../src/CONFIG').default; const CONST = require('../src/CONST').default; const Localize = require('../src/libs/Localize'); +const createDownloadQueue = require('../src/libs/downloadQueue'); const port = process.env.PORT || 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; @@ -597,6 +598,13 @@ const mainWindow = () => { } }); + const downloadQueue = createDownloadQueue(); + + ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, async (event, info) => { + info.win = browserWindow + downloadQueue.push(info); + }); + return browserWindow; }) diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 850efc2d8c54..0b2c0e36151a 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -3,10 +3,7 @@ import { download as electronDownload } from '@libs/downloadQueue/electronDownlo interface DownloadItem { win: any; url: string; - options: { - onCompleted: () => void; - onCancel: () => void; - }; + options: Electron.Options } type DownloadQueue = DownloadItem[]; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index abcd2e85557e..10eb8675e593 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -4,13 +4,14 @@ import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; import * as FileUtils from './FileUtils'; import type {FileDownload} from './types'; +import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS'; /** * The function downloads an attachment on desktop platforms. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { - + window.electron.send(ELECTRON_EVENTS.DOWNLOAD, { url, filename: fileName }) return Promise.resolve(); }; From 90e745242a4cc3e571497040488c93a773c9882c Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 14:32:03 +0700 Subject: [PATCH 04/47] Electrone queue mechanism working --- desktop/contextBridge.js | 1 + desktop/main.js | 6 +++--- src/libs/downloadQueue/electronDownloadManager.ts | 2 +- src/libs/downloadQueue/index.desktop.ts | 13 ++++++++----- src/libs/downloadQueue/index.ts | 5 ++++- src/libs/fileDownload/index.desktop.ts | 15 +++++++++------ 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js index a8b89cdc0b64..2e0db79bb861 100644 --- a/desktop/contextBridge.js +++ b/desktop/contextBridge.js @@ -9,6 +9,7 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.REQUEST_VISIBILITY, ELECTRON_EVENTS.START_UPDATE, ELECTRON_EVENTS.LOCALE_UPDATED, + ELECTRON_EVENTS.DOWNLOAD, ]; const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR]; diff --git a/desktop/main.js b/desktop/main.js index a6acaad793b2..e3201f90abc8 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -4,13 +4,13 @@ const serve = require('electron-serve'); const contextMenu = require('electron-context-menu'); const {autoUpdater} = require('electron-updater'); const log = require('electron-log'); -const {machineId} = require('node-machine-id'); +const {machineId} = require('node-machine-id'); const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); const checkForUpdates = require('../src/libs/checkForUpdates'); const CONFIG = require('../src/CONFIG').default; const CONST = require('../src/CONST').default; const Localize = require('../src/libs/Localize'); -const createDownloadQueue = require('../src/libs/downloadQueue'); +const createDownloadQueue = require('../src/libs/downloadQueue').default; const port = process.env.PORT || 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; @@ -602,7 +602,7 @@ const mainWindow = () => { ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, async (event, info) => { info.win = browserWindow - downloadQueue.push(info); + downloadQueue.pushDownloadItem(info); }); return browserWindow; diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 289d198e21a1..2bbaf32270eb 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -250,4 +250,4 @@ export const download = (window_: Electron.BrowserWindow, url: string, options: }); }; -export { CancelError }; \ No newline at end of file +export type { CancelError, Options }; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 0b2c0e36151a..3c2445e2aa3a 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,9 +1,9 @@ -import { download as electronDownload } from '@libs/downloadQueue/electronDownloadManager'; +import { download as electronDownload, Options } from '@libs/downloadQueue/electronDownloadManager'; interface DownloadItem { win: any; url: string; - options: Electron.Options + options: Options; } type DownloadQueue = DownloadItem[]; @@ -18,7 +18,7 @@ const createDownloadQueue = () => { } return len; }; - + const shiftDownloadItem = (): DownloadItem | undefined => { const item = queue.shift(); if (queue.length > 0) { @@ -28,12 +28,15 @@ const createDownloadQueue = () => { }; const downloadItem = (item: DownloadItem): void => { +console.log('[wildebug] downloadItem item aosdifasdf', item); + item.options.onCompleted = () => { shiftDownloadItem(); }; - item.options.onCancel = item.options.onCompleted; - + item.options.onCancel = () => { + shiftDownloadItem(); + }; electronDownload(item.win, item.url, item.options); }; diff --git a/src/libs/downloadQueue/index.ts b/src/libs/downloadQueue/index.ts index 7d3cbaccbb9b..fd3e8f5f8c4a 100644 --- a/src/libs/downloadQueue/index.ts +++ b/src/libs/downloadQueue/index.ts @@ -1,5 +1,8 @@ // no implementation except for desktop -const createDownloadQueue = () => {} +const createDownloadQueue = () => { +console.log('asdiofjaisdjfiojsd') + +} export default createDownloadQueue; \ No newline at end of file diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 10eb8675e593..d57f099f63c8 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,17 +1,20 @@ -import * as ApiUtils from '@libs/ApiUtils'; -import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; -import * as FileUtils from './FileUtils'; import type {FileDownload} from './types'; import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS'; +import type { Options } from '@libs/downloadQueue/electronDownloadManager'; /** * The function downloads an attachment on desktop platforms. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { - window.electron.send(ELECTRON_EVENTS.DOWNLOAD, { url, filename: fileName }) + const options: Options ={ + filename: fileName, + saveAs: true, + //showing badge and progress bar only supported on macos and linux, better to disable it + showBadge: false, + showProgressBar: false + } + window.electron.send(ELECTRON_EVENTS.DOWNLOAD, { url, options} ) return Promise.resolve(); }; From 8af303aeb4edcbf13dd30a4efa28570088eba226 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 14:37:16 +0700 Subject: [PATCH 05/47] run prettier --- desktop/main.js | 4 +- .../downloadQueue/electronDownloadManager.ts | 49 +++++++------- src/libs/downloadQueue/index.desktop.ts | 66 +++++++++---------- src/libs/downloadQueue/index.ts | 8 +-- src/libs/fileDownload/index.desktop.ts | 12 ++-- 5 files changed, 67 insertions(+), 72 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index e3201f90abc8..e603917f7b3d 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -4,7 +4,7 @@ const serve = require('electron-serve'); const contextMenu = require('electron-context-menu'); const {autoUpdater} = require('electron-updater'); const log = require('electron-log'); -const {machineId} = require('node-machine-id'); +const {machineId} = require('node-machine-id'); const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); const checkForUpdates = require('../src/libs/checkForUpdates'); const CONFIG = require('../src/CONFIG').default; @@ -601,7 +601,7 @@ const mainWindow = () => { const downloadQueue = createDownloadQueue(); ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, async (event, info) => { - info.win = browserWindow + info.win = browserWindow; downloadQueue.pushDownloadItem(info); }); diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 2bbaf32270eb..d6ad64da14e4 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -6,10 +6,19 @@ * This file contains the implementation of the Electron Download Manager. */ 'use strict'; + +import {app, BrowserWindow, dialog, DownloadItem, Session, shell} from 'electron'; import * as path from 'path'; -import { app, BrowserWindow, shell, dialog, Session, DownloadItem } from 'electron'; import * as _ from 'underscore'; +/** + * This file is a ported version of the `electron-dl` package. + * It provides a download manager for Electron applications. + * The `electron-dl` package simplifies the process of downloading files in Electron apps + * by providing a high-level API and handling various download-related tasks. + * This file contains the implementation of the Electron Download Manager. + */ + class CancelError extends Error {} const getFilenameFromMime = (name: string, mime: string): string => { @@ -32,7 +41,7 @@ const getWindowFromBrowserView = (webContents: Electron.WebContents): Electron.B } }; -const getWindowFromWebContents = (webContents: Electron.WebContents): Electron.BrowserWindow | undefined | null=> { +const getWindowFromWebContents = (webContents: Electron.WebContents): Electron.BrowserWindow | undefined | null => { let window_: Electron.BrowserWindow | undefined | null; const webContentsType = webContents.getType(); switch (webContentsType) { @@ -59,27 +68,16 @@ interface Options { errorMessage?: string; saveAs?: boolean; dialogOptions?: Electron.SaveDialogOptions; - onProgress?: (progress: { percent: number; transferredBytes: number; totalBytes: number }) => void; - onTotalProgress?: (progress: { percent: number; transferredBytes: number; totalBytes: number }) => void; + onProgress?: (progress: {percent: number; transferredBytes: number; totalBytes: number}) => void; + onTotalProgress?: (progress: {percent: number; transferredBytes: number; totalBytes: number}) => void; onCancel?: (item: Electron.DownloadItem) => void; - onCompleted?: (info: { - fileName: string; - filename: string; - path: string; - fileSize: number; - mimeType: string; - url: string; - }) => void; + onCompleted?: (info: {fileName: string; filename: string; path: string; fileSize: number; mimeType: string; url: string}) => void; unregisterWhenDone?: boolean; openFolderWhenDone?: boolean; onStarted?: (item: Electron.DownloadItem) => void; } -const registerListener = ( - session: Electron.Session, - options: Options, - callback: (error: Error | null, item?: Electron.DownloadItem) => void = () => {} -): void => { +const registerListener = (session: Electron.Session, options: Options, callback: (error: Error | null, item?: Electron.DownloadItem) => void = () => {}): void => { const downloadItems = new Set(); let receivedBytes = 0; let completedBytes = 0; @@ -90,7 +88,7 @@ const registerListener = ( options = { showBadge: true, showProgressBar: true, - ...options + ...options, }; const listener = (event: Electron.Event, item: Electron.DownloadItem, webContents: Electron.WebContents): void => { @@ -115,9 +113,8 @@ const registerListener = ( filePath = options.overwrite ? path.join(directory, name) : path.join(directory, name); } - if (options.saveAs) { - item.setSaveDialogOptions({ defaultPath: filePath, ...options.dialogOptions }); + item.setSaveDialogOptions({defaultPath: filePath, ...options.dialogOptions}); } else { item.setSavePath(filePath); } @@ -143,7 +140,7 @@ const registerListener = ( options.onProgress({ percent: itemTotalBytes ? itemTransferredBytes / itemTotalBytes : 0, transferredBytes: itemTransferredBytes, - totalBytes: itemTotalBytes + totalBytes: itemTotalBytes, }); } @@ -151,7 +148,7 @@ const registerListener = ( options.onTotalProgress({ percent: progressDownloadItems(), transferredBytes: receivedBytes, - totalBytes + totalBytes, }); } }); @@ -202,7 +199,7 @@ const registerListener = ( path: savePath, fileSize: item.getReceivedBytes(), mimeType: item.getMimeType(), - url: item.getURL() + url: item.getURL(), }); } @@ -232,7 +229,7 @@ export default (options: any = {}): void => { export const download = (window_: Electron.BrowserWindow, url: string, options: Options): Promise => { options = { ...options, - unregisterWhenDone: true + unregisterWhenDone: true, }; return new Promise((resolve, reject) => { @@ -242,7 +239,7 @@ export const download = (window_: Electron.BrowserWindow, url: string, options: } else if (item) { resolve(item); } else { - reject(new Error("Download item is undefined.")); + reject(new Error('Download item is undefined.')); } }); @@ -250,4 +247,4 @@ export const download = (window_: Electron.BrowserWindow, url: string, options: }); }; -export type { CancelError, Options }; +export type {CancelError, Options}; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 3c2445e2aa3a..5778abec2f05 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,46 +1,46 @@ -import { download as electronDownload, Options } from '@libs/downloadQueue/electronDownloadManager'; +import {download as electronDownload, Options} from '@libs/downloadQueue/electronDownloadManager'; interface DownloadItem { - win: any; - url: string; - options: Options; + win: any; + url: string; + options: Options; } type DownloadQueue = DownloadItem[]; const createDownloadQueue = () => { - const queue: DownloadQueue = []; - - const pushDownloadItem = (item: DownloadItem): number => { - const len = queue.push(item); - if (queue.length === 1) { - downloadItem(queue[0]); - } - return len; - }; - - const shiftDownloadItem = (): DownloadItem | undefined => { - const item = queue.shift(); - if (queue.length > 0) { - downloadItem(queue[0]); - } - return item; - }; - - const downloadItem = (item: DownloadItem): void => { -console.log('[wildebug] downloadItem item aosdifasdf', item); - - item.options.onCompleted = () => { - shiftDownloadItem(); + const queue: DownloadQueue = []; + + const pushDownloadItem = (item: DownloadItem): number => { + const len = queue.push(item); + if (queue.length === 1) { + downloadItem(queue[0]); + } + return len; }; - item.options.onCancel = () => { - shiftDownloadItem(); + const shiftDownloadItem = (): DownloadItem | undefined => { + const item = queue.shift(); + if (queue.length > 0) { + downloadItem(queue[0]); + } + return item; }; - electronDownload(item.win, item.url, item.options); - }; - return { pushDownloadItem, shiftDownloadItem }; + const downloadItem = (item: DownloadItem): void => { + console.log('[wildebug] downloadItem item aosdifasdf', item); + + item.options.onCompleted = () => { + shiftDownloadItem(); + }; + + item.options.onCancel = () => { + shiftDownloadItem(); + }; + electronDownload(item.win, item.url, item.options); + }; + + return {pushDownloadItem, shiftDownloadItem}; }; -export default createDownloadQueue; \ No newline at end of file +export default createDownloadQueue; diff --git a/src/libs/downloadQueue/index.ts b/src/libs/downloadQueue/index.ts index fd3e8f5f8c4a..51ddc7dc7ba0 100644 --- a/src/libs/downloadQueue/index.ts +++ b/src/libs/downloadQueue/index.ts @@ -1,8 +1,6 @@ - // no implementation except for desktop const createDownloadQueue = () => { -console.log('asdiofjaisdjfiojsd') - -} + console.log('asdiofjaisdjfiojsd'); +}; -export default createDownloadQueue; \ No newline at end of file +export default createDownloadQueue; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index d57f099f63c8..cc66739b169b 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,20 +1,20 @@ -import type {FileDownload} from './types'; +import type {Options} from '@libs/downloadQueue/electronDownloadManager'; import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS'; -import type { Options } from '@libs/downloadQueue/electronDownloadManager'; +import type {FileDownload} from './types'; /** * The function downloads an attachment on desktop platforms. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { - const options: Options ={ + const options: Options = { filename: fileName, saveAs: true, //showing badge and progress bar only supported on macos and linux, better to disable it showBadge: false, - showProgressBar: false - } - window.electron.send(ELECTRON_EVENTS.DOWNLOAD, { url, options} ) + showProgressBar: false, + }; + window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); return Promise.resolve(); }; From 539ac277922e8846827bdfdddad0e45d2d011744 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 14:51:10 +0700 Subject: [PATCH 06/47] fix lint --- desktop/main.js | 2 +- .../downloadQueue/electronDownloadManager.ts | 37 +++++++------------ src/libs/downloadQueue/index.ts | 1 - src/libs/fileDownload/index.desktop.ts | 2 +- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index e603917f7b3d..b632617a0b08 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -600,7 +600,7 @@ const mainWindow = () => { const downloadQueue = createDownloadQueue(); - ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, async (event, info) => { + ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, async (_, info) => { info.win = browserWindow; downloadQueue.pushDownloadItem(info); }); diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index d6ad64da14e4..22bf7fd9e584 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -1,13 +1,4 @@ -/** - * This file is a ported version of the `electron-dl` package. - * It provides a download manager for Electron applications. - * The `electron-dl` package simplifies the process of downloading files in Electron apps - * by providing a high-level API and handling various download-related tasks. - * This file contains the implementation of the Electron Download Manager. - */ -'use strict'; - -import {app, BrowserWindow, dialog, DownloadItem, Session, shell} from 'electron'; +import { app, BrowserWindow, dialog, DownloadItem, Session, shell, SaveDialogOptions, WebContents, Event } from 'electron'; import * as path from 'path'; import * as _ from 'underscore'; @@ -31,7 +22,7 @@ const majorElectronVersion = (): number => { return Number.parseInt(version[0], 10); }; -const getWindowFromBrowserView = (webContents: Electron.WebContents): Electron.BrowserWindow | undefined => { +const getWindowFromBrowserView = (webContents: WebContents): BrowserWindow | undefined => { for (const currentWindow of BrowserWindow.getAllWindows()) { for (const currentBrowserView of currentWindow.getBrowserViews()) { if (currentBrowserView.webContents.id === webContents.id) { @@ -41,8 +32,8 @@ const getWindowFromBrowserView = (webContents: Electron.WebContents): Electron.B } }; -const getWindowFromWebContents = (webContents: Electron.WebContents): Electron.BrowserWindow | undefined | null => { - let window_: Electron.BrowserWindow | undefined | null; +const getWindowFromWebContents = (webContents: WebContents): BrowserWindow | undefined | null => { + let window_: BrowserWindow | undefined | null; const webContentsType = webContents.getType(); switch (webContentsType) { case 'webview': @@ -67,18 +58,18 @@ interface Options { overwrite?: boolean; errorMessage?: string; saveAs?: boolean; - dialogOptions?: Electron.SaveDialogOptions; + dialogOptions?: SaveDialogOptions; onProgress?: (progress: {percent: number; transferredBytes: number; totalBytes: number}) => void; onTotalProgress?: (progress: {percent: number; transferredBytes: number; totalBytes: number}) => void; - onCancel?: (item: Electron.DownloadItem) => void; + onCancel?: (item: DownloadItem) => void; onCompleted?: (info: {fileName: string; filename: string; path: string; fileSize: number; mimeType: string; url: string}) => void; unregisterWhenDone?: boolean; openFolderWhenDone?: boolean; - onStarted?: (item: Electron.DownloadItem) => void; + onStarted?: (item: DownloadItem) => void; } -const registerListener = (session: Electron.Session, options: Options, callback: (error: Error | null, item?: Electron.DownloadItem) => void = () => {}): void => { - const downloadItems = new Set(); +const registerListener = (session: Session, options: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { + const downloadItems = new Set(); let receivedBytes = 0; let completedBytes = 0; let totalBytes = 0; @@ -91,7 +82,7 @@ const registerListener = (session: Electron.Session, options: Options, callback: ...options, }; - const listener = (event: Electron.Event, item: Electron.DownloadItem, webContents: Electron.WebContents): void => { + const listener = (event: Event, item: DownloadItem, webContents: WebContents): void => { downloadItems.add(item); totalBytes += item.getTotalBytes(); @@ -153,7 +144,7 @@ const registerListener = (session: Electron.Session, options: Options, callback: } }); - item.on('done', (event: Electron.Event, state: string) => { + item.on('done', (event: Event, state: string) => { completedBytes += item.getTotalBytes(); downloadItems.delete(item); @@ -216,7 +207,7 @@ const registerListener = (session: Electron.Session, options: Options, callback: }; export default (options: any = {}): void => { - app.on('session-created', (session: Electron.Session) => { + app.on('session-created', (session: Session) => { registerListener(session, options, (error: Error | null, _) => { if (error && !(error instanceof CancelError)) { const errorTitle = options.errorTitle || 'Download Error'; @@ -226,14 +217,14 @@ export default (options: any = {}): void => { }); }; -export const download = (window_: Electron.BrowserWindow, url: string, options: Options): Promise => { +export const download = (window_: BrowserWindow, url: string, options: Options): Promise => { options = { ...options, unregisterWhenDone: true, }; return new Promise((resolve, reject) => { - registerListener(window_.webContents.session, options, (error: Error | null, item?: Electron.DownloadItem) => { + registerListener(window_.webContents.session, options, (error: Error | null, item?: DownloadItem) => { if (error) { reject(error); } else if (item) { diff --git a/src/libs/downloadQueue/index.ts b/src/libs/downloadQueue/index.ts index 51ddc7dc7ba0..18a1b6f612c8 100644 --- a/src/libs/downloadQueue/index.ts +++ b/src/libs/downloadQueue/index.ts @@ -1,6 +1,5 @@ // no implementation except for desktop const createDownloadQueue = () => { - console.log('asdiofjaisdjfiojsd'); }; export default createDownloadQueue; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index cc66739b169b..e9e24e7d7e0f 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -10,7 +10,7 @@ const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOp const options: Options = { filename: fileName, saveAs: true, - //showing badge and progress bar only supported on macos and linux, better to disable it + // showing badge and progress bar only supported on macos and linux, better to disable it showBadge: false, showProgressBar: false, }; From 1f2b9c102eb4335a6fd24f5d399cbcc0286d39ae Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 14:55:14 +0700 Subject: [PATCH 07/47] lint fix --- package.json | 2 +- src/libs/downloadQueue/electronDownloadManager.ts | 5 +++-- src/libs/downloadQueue/index.desktop.ts | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a6c16d3dedd1..04984e8b7583 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "android-build-e2edelta": "bundle exec fastlane android build_e2edelta", "test": "TZ=utc jest", "typecheck": "tsc", - "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", + "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint --fix", "lint-changed": "eslint --fix $(git diff --diff-filter=AM --name-only main -- \"*.js\" \"*.ts\" \"*.tsx\")", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 22bf7fd9e584..2b8edae02faa 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -1,4 +1,5 @@ -import { app, BrowserWindow, dialog, DownloadItem, Session, shell, SaveDialogOptions, WebContents, Event } from 'electron'; +import type { DownloadItem, Session, SaveDialogOptions, WebContents, Event } from 'electron'; +import { app, BrowserWindow, dialog, shell } from 'electron'; import * as path from 'path'; import * as _ from 'underscore'; @@ -50,7 +51,7 @@ const getWindowFromWebContents = (webContents: WebContents): BrowserWindow | und return window_; }; -interface Options { +type Options = { showBadge?: boolean; showProgressBar?: boolean; directory?: string; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 5778abec2f05..aff76274a716 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,6 +1,7 @@ -import {download as electronDownload, Options} from '@libs/downloadQueue/electronDownloadManager'; +import type { Options} from './electronDownloadManager'; +import {download as electronDownload} from './electronDownloadManager'; -interface DownloadItem { +type DownloadItem = { win: any; url: string; options: Options; From f3c9b04b62cf7044198d9ead5b9c5ff1db5b8cd4 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 15:15:04 +0700 Subject: [PATCH 08/47] fix lint --- desktop/main.js | 9 +++-- .../downloadQueue/electronDownloadManager.ts | 35 +++++++++---------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index b632617a0b08..84f3e7be9b69 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -600,9 +600,12 @@ const mainWindow = () => { const downloadQueue = createDownloadQueue(); - ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, async (_, info) => { - info.win = browserWindow; - downloadQueue.pushDownloadItem(info); + ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { + const downloadItem = { + ...downloadData, + win: browserWindow, + }; + downloadQueue.pushDownloadItem(downloadItem); }); return browserWindow; diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 2b8edae02faa..0cea853eebf9 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -1,7 +1,6 @@ import type { DownloadItem, Session, SaveDialogOptions, WebContents, Event } from 'electron'; import { app, BrowserWindow, dialog, shell } from 'electron'; import * as path from 'path'; -import * as _ from 'underscore'; /** * This file is a ported version of the `electron-dl` package. @@ -34,21 +33,21 @@ const getWindowFromBrowserView = (webContents: WebContents): BrowserWindow | und }; const getWindowFromWebContents = (webContents: WebContents): BrowserWindow | undefined | null => { - let window_: BrowserWindow | undefined | null; + let electronWindow: BrowserWindow | undefined | null; const webContentsType = webContents.getType(); switch (webContentsType) { case 'webview': - window_ = BrowserWindow.fromWebContents(webContents.hostWebContents); + electronWindow = BrowserWindow.fromWebContents(webContents.hostWebContents); break; case 'browserView': - window_ = getWindowFromBrowserView(webContents); + electronWindow = getWindowFromBrowserView(webContents); break; default: - window_ = BrowserWindow.fromWebContents(webContents); + electronWindow = BrowserWindow.fromWebContents(webContents); break; } - return window_; + return electronWindow; }; type Options = { @@ -69,7 +68,7 @@ type Options = { onStarted?: (item: DownloadItem) => void; } -const registerListener = (session: Session, options: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { +const registerListener = (session: Session, prevOptions: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { const downloadItems = new Set(); let receivedBytes = 0; let completedBytes = 0; @@ -77,23 +76,23 @@ const registerListener = (session: Session, options: Options, callback: (error: const activeDownloadItems = (): number => downloadItems.size; const progressDownloadItems = (): number => receivedBytes / totalBytes; - options = { + const options = { showBadge: true, showProgressBar: true, - ...options, + ...prevOptions, }; const listener = (event: Event, item: DownloadItem, webContents: WebContents): void => { downloadItems.add(item); totalBytes += item.getTotalBytes(); - const window_ = majorElectronVersion() >= 12 ? BrowserWindow.fromWebContents(webContents) : getWindowFromWebContents(webContents); + const electronWindow = majorElectronVersion() >= 12 ? BrowserWindow.fromWebContents(webContents) : getWindowFromWebContents(webContents); if (options.directory && !path.isAbsolute(options.directory)) { throw new Error('The `directory` option must be an absolute path'); } - const directory = options.directory || app.getPath('downloads'); + const directory = options.directory ?? app.getPath('downloads'); let filePath: string; if (options.filename) { @@ -121,8 +120,8 @@ const registerListener = (session: Session, options: Options, callback: (error: app.badgeCount = activeDownloadItems(); } - if (!window_?.isDestroyed() && options.showProgressBar) { - window_?.setProgressBar(progressDownloadItems()); + if (!electronWindow?.isDestroyed() && options.showProgressBar) { + electronWindow?.setProgressBar(progressDownloadItems()); } if (typeof options.onProgress === 'function') { @@ -153,8 +152,8 @@ const registerListener = (session: Session, options: Options, callback: (error: app.badgeCount = activeDownloadItems(); } - if (!window_?.isDestroyed() && !activeDownloadItems()) { - window_?.setProgressBar(-1); + if (!electronWindow?.isDestroyed() && !activeDownloadItems()) { + electronWindow?.setProgressBar(-1); receivedBytes = 0; completedBytes = 0; totalBytes = 0; @@ -218,14 +217,14 @@ export default (options: any = {}): void => { }); }; -export const download = (window_: BrowserWindow, url: string, options: Options): Promise => { +export const download = (electronWindow: BrowserWindow, url: string, options: Options): Promise => { options = { ...options, unregisterWhenDone: true, }; return new Promise((resolve, reject) => { - registerListener(window_.webContents.session, options, (error: Error | null, item?: DownloadItem) => { + registerListener(electronWindow.webContents.session, options, (error: Error | null, item?: DownloadItem) => { if (error) { reject(error); } else if (item) { @@ -235,7 +234,7 @@ export const download = (window_: BrowserWindow, url: string, options: Options): } }); - window_.webContents.downloadURL(url); + electronWindow.webContents.downloadURL(url); }); }; From d994805e0c1f26b235bd036e5f569948cdcab15a Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 16:19:58 +0700 Subject: [PATCH 09/47] Resolve cyclical dependency, npm lint --- .../downloadQueue/electronDownloadManager.ts | 27 ++++++----- src/libs/downloadQueue/index.desktop.ts | 46 +++++++++---------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 0cea853eebf9..442d8ee9f2b4 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -112,8 +112,8 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err item.on('updated', () => { receivedBytes = completedBytes; - for (const item of downloadItems) { - receivedBytes += item.getReceivedBytes(); + for (const downloadItem of downloadItems) { + receivedBytes += downloadItem.getReceivedBytes(); } if (options.showBadge && ['darwin', 'linux'].includes(process.platform)) { @@ -144,7 +144,7 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err } }); - item.on('done', (event: Event, state: string) => { + item.on('done', (doneEvent: Event, state: string) => { completedBytes += item.getTotalBytes(); downloadItems.delete(item); @@ -206,20 +206,22 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err session.on('will-download', listener); }; -export default (options: any = {}): void => { + +export default (options: Options = {}): void => { app.on('session-created', (session: Session) => { - registerListener(session, options, (error: Error | null, _) => { - if (error && !(error instanceof CancelError)) { - const errorTitle = options.errorTitle || 'Download Error'; - dialog.showErrorBox(errorTitle, error.message); + registerListener(session, options, (error: Error | null) => { + if (!error || error instanceof CancelError) { + return; } + + const errorTitle = options.errorMessage ?? 'Download Error'; + dialog.showErrorBox(errorTitle, error.message); }); }); }; - -export const download = (electronWindow: BrowserWindow, url: string, options: Options): Promise => { - options = { - ...options, +const download = (electronWindow: BrowserWindow, url: string, prevOptions: Options): Promise => { + const options = { + ...prevOptions, unregisterWhenDone: true, }; @@ -238,4 +240,5 @@ export const download = (electronWindow: BrowserWindow, url: string, options: Op }); }; +export {download}; export type {CancelError, Options}; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index aff76274a716..b78aa80f162e 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,8 +1,9 @@ +import type { BrowserWindow } from 'electron'; import type { Options} from './electronDownloadManager'; import {download as electronDownload} from './electronDownloadManager'; type DownloadItem = { - win: any; + win: BrowserWindow; url: string; options: Options; } @@ -12,36 +13,33 @@ type DownloadQueue = DownloadItem[]; const createDownloadQueue = () => { const queue: DownloadQueue = []; - const pushDownloadItem = (item: DownloadItem): number => { - const len = queue.push(item); - if (queue.length === 1) { - downloadItem(queue[0]); - } - return len; - }; - - const shiftDownloadItem = (): DownloadItem | undefined => { + const processQueue = (): void => { const item = queue.shift(); - if (queue.length > 0) { - downloadItem(queue[0]); + if (!item) { + return; } - return item; - }; - - const downloadItem = (item: DownloadItem): void => { - console.log('[wildebug] downloadItem item aosdifasdf', item); - item.options.onCompleted = () => { - shiftDownloadItem(); + const newItem = { + ...item, + options: { + ...item.options, + onCompleted: processQueue, + onCancel: processQueue, + }, }; - item.options.onCancel = () => { - shiftDownloadItem(); - }; - electronDownload(item.win, item.url, item.options); + electronDownload(newItem.win, newItem.url, newItem.options); + }; + + const pushDownloadItem = (item: DownloadItem): number => { + const len = queue.push(item); + if (queue.length === 1) { + processQueue(); + } + return len; }; - return {pushDownloadItem, shiftDownloadItem}; + return {pushDownloadItem}; }; export default createDownloadQueue; From ef60c65eb18afb8c5a0cda968de6a3731d940117 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 16:21:22 +0700 Subject: [PATCH 10/47] Refactor import statements in electronDownloadManager.ts and index.desktop.ts --- src/libs/downloadQueue/electronDownloadManager.ts | 7 +++---- src/libs/downloadQueue/index.desktop.ts | 6 +++--- src/libs/downloadQueue/index.ts | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 442d8ee9f2b4..f24b64d121df 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -1,5 +1,5 @@ -import type { DownloadItem, Session, SaveDialogOptions, WebContents, Event } from 'electron'; -import { app, BrowserWindow, dialog, shell } from 'electron'; +import type {DownloadItem, Event, SaveDialogOptions, Session, WebContents} from 'electron'; +import {app, BrowserWindow, dialog, shell} from 'electron'; import * as path from 'path'; /** @@ -66,7 +66,7 @@ type Options = { unregisterWhenDone?: boolean; openFolderWhenDone?: boolean; onStarted?: (item: DownloadItem) => void; -} +}; const registerListener = (session: Session, prevOptions: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { const downloadItems = new Set(); @@ -206,7 +206,6 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err session.on('will-download', listener); }; - export default (options: Options = {}): void => { app.on('session-created', (session: Session) => { registerListener(session, options, (error: Error | null) => { diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index b78aa80f162e..b8a55a00d645 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,12 +1,12 @@ -import type { BrowserWindow } from 'electron'; -import type { Options} from './electronDownloadManager'; +import type {BrowserWindow} from 'electron'; +import type {Options} from './electronDownloadManager'; import {download as electronDownload} from './electronDownloadManager'; type DownloadItem = { win: BrowserWindow; url: string; options: Options; -} +}; type DownloadQueue = DownloadItem[]; diff --git a/src/libs/downloadQueue/index.ts b/src/libs/downloadQueue/index.ts index 18a1b6f612c8..b7506290d9ae 100644 --- a/src/libs/downloadQueue/index.ts +++ b/src/libs/downloadQueue/index.ts @@ -1,5 +1,4 @@ // no implementation except for desktop -const createDownloadQueue = () => { -}; +const createDownloadQueue = () => {}; export default createDownloadQueue; From 183e04ed3c8ed7a553a6b0f3a0b6b2d67e7a61b9 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 16:22:18 +0700 Subject: [PATCH 11/47] revert package.json change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04984e8b7583..a6c16d3dedd1 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "android-build-e2edelta": "bundle exec fastlane android build_e2edelta", "test": "TZ=utc jest", "typecheck": "tsc", - "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint --fix", + "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "eslint --fix $(git diff --diff-filter=AM --name-only main -- \"*.js\" \"*.ts\" \"*.tsx\")", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", From d06d3fc318be65d58b74ddb1a3b2c431f4ff76da Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 16:44:50 +0700 Subject: [PATCH 12/47] add delay to keep loading spinner showing fix ts errors --- src/libs/downloadQueue/index.desktop.ts | 31 ++++++++++++++++--------- src/libs/fileDownload/index.desktop.ts | 14 ++++++++--- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index b8a55a00d645..371212fccf17 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,6 +1,6 @@ -import type {BrowserWindow} from 'electron'; -import type {Options} from './electronDownloadManager'; -import {download as electronDownload} from './electronDownloadManager'; +import type { BrowserWindow } from 'electron'; +import type { Options } from './electronDownloadManager'; +import { download as electronDownload } from './electronDownloadManager'; type DownloadItem = { win: BrowserWindow; @@ -9,22 +9,31 @@ type DownloadItem = { }; type DownloadQueue = DownloadItem[]; - const createDownloadQueue = () => { const queue: DownloadQueue = []; - const processQueue = (): void => { + const shiftDownloadItem = (): DownloadItem | undefined => { const item = queue.shift(); - if (!item) { - return; + if (queue.length > 0) { + // This code block contains a cyclic dependency between functions, + // so one of them should have the eslint-disable-next-line comment + // eslint-disable-next-line @typescript-eslint/no-use-before-define + downloadItem(queue[0]); } + return item; + }; + const downloadItem = (item: DownloadItem): void => { const newItem = { ...item, options: { ...item.options, - onCompleted: processQueue, - onCancel: processQueue, + onCompleted: () => { + shiftDownloadItem(); + }, + onCancel: () => { + shiftDownloadItem(); + }, }, }; @@ -34,12 +43,12 @@ const createDownloadQueue = () => { const pushDownloadItem = (item: DownloadItem): number => { const len = queue.push(item); if (queue.length === 1) { - processQueue(); + downloadItem(queue[0]); } return len; }; - return {pushDownloadItem}; + return { pushDownloadItem, shiftDownloadItem }; }; export default createDownloadQueue; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index e9e24e7d7e0f..c54f066795ef 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -5,8 +5,7 @@ import type {FileDownload} from './types'; /** * The function downloads an attachment on desktop platforms. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { +const fileDownload: FileDownload = (url, fileName) => { const options: Options = { filename: fileName, saveAs: true, @@ -15,7 +14,16 @@ const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOp showProgressBar: false, }; window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); - return Promise.resolve(); + + /** + * Adds a 1000ms delay to keep showing the loading spinner + * and prevent rapid clicks on the same download link. + */ + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1000); + }); }; export default fileDownload; From 7349cb60fb941509f7b2aeb7d0d498450cd0c2b8 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 23 Feb 2024 21:12:34 +0700 Subject: [PATCH 13/47] run prettier --- src/libs/downloadQueue/index.desktop.ts | 10 +++++----- src/libs/fileDownload/index.desktop.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 371212fccf17..6cb31a61ef24 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,6 +1,6 @@ -import type { BrowserWindow } from 'electron'; -import type { Options } from './electronDownloadManager'; -import { download as electronDownload } from './electronDownloadManager'; +import type {BrowserWindow} from 'electron'; +import type {Options} from './electronDownloadManager'; +import {download as electronDownload} from './electronDownloadManager'; type DownloadItem = { win: BrowserWindow; @@ -15,7 +15,7 @@ const createDownloadQueue = () => { const shiftDownloadItem = (): DownloadItem | undefined => { const item = queue.shift(); if (queue.length > 0) { - // This code block contains a cyclic dependency between functions, + // This code block contains a cyclic dependency between functions, // so one of them should have the eslint-disable-next-line comment // eslint-disable-next-line @typescript-eslint/no-use-before-define downloadItem(queue[0]); @@ -48,7 +48,7 @@ const createDownloadQueue = () => { return len; }; - return { pushDownloadItem, shiftDownloadItem }; + return {pushDownloadItem, shiftDownloadItem}; }; export default createDownloadQueue; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index c54f066795ef..5acf86cda3fc 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -16,7 +16,7 @@ const fileDownload: FileDownload = (url, fileName) => { window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); /** - * Adds a 1000ms delay to keep showing the loading spinner + * Adds a 1000ms delay to keep showing the loading spinner * and prevent rapid clicks on the same download link. */ return new Promise((resolve) => { From 10cdaefe55e4dd76f3861d58633a781a30431f9f Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 26 Feb 2024 08:42:47 +0700 Subject: [PATCH 14/47] Add comment, move electronDownloadManager type to a seperate file --- desktop/main.js | 18 +-- .../downloadQueue/electronDownloadManager.ts | 113 +++++++++++---- .../electronDownloadManagerType.ts | 133 ++++++++++++++++++ src/libs/downloadQueue/index.desktop.ts | 2 +- src/libs/fileDownload/index.desktop.ts | 2 +- 5 files changed, 229 insertions(+), 39 deletions(-) create mode 100644 src/libs/downloadQueue/electronDownloadManagerType.ts diff --git a/desktop/main.js b/desktop/main.js index 84f3e7be9b69..b40231df81f3 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -11,7 +11,9 @@ const CONFIG = require('../src/CONFIG').default; const CONST = require('../src/CONST').default; const Localize = require('../src/libs/Localize'); const createDownloadQueue = require('../src/libs/downloadQueue').default; +const electronDownloadManager = require('../src/libs/downloadQueue/electronDownloadManager').default; +electronDownloadManager(); const port = process.env.PORT || 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; @@ -598,15 +600,15 @@ const mainWindow = () => { } }); - const downloadQueue = createDownloadQueue(); + // const downloadQueue = createDownloadQueue(); - ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { - const downloadItem = { - ...downloadData, - win: browserWindow, - }; - downloadQueue.pushDownloadItem(downloadItem); - }); + // ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { + // const downloadItem = { + // ...downloadData, + // win: browserWindow, + // }; + // downloadQueue.pushDownloadItem(downloadItem); + // }); return browserWindow; }) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index f24b64d121df..dc84884e3319 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -1,7 +1,3 @@ -import type {DownloadItem, Event, SaveDialogOptions, Session, WebContents} from 'electron'; -import {app, BrowserWindow, dialog, shell} from 'electron'; -import * as path from 'path'; - /** * This file is a ported version of the `electron-dl` package. * It provides a download manager for Electron applications. @@ -10,18 +6,41 @@ import * as path from 'path'; * This file contains the implementation of the Electron Download Manager. */ -class CancelError extends Error {} +import type { DownloadItem, Event, Session, WebContents, BrowserView } from 'electron'; +import { app, BrowserWindow, dialog, shell } from 'electron'; +import * as path from 'path'; +import { Options } from './electronDownloadManagerType'; + +/** +Error thrown if `item.cancel()` was called. +*/ +declare class CancelError extends Error { } +/** + * Returns the filename with extension based on the given name and MIME type. + * @param name - The name of the file. + * @param mime - The MIME type of the file. + * @returns The filename with extension. + */ const getFilenameFromMime = (name: string, mime: string): string => { const extensions = mime.split('/').pop(); return `${name}.${extensions}`; }; +/** + * Returns the major version number of Electron. + * @returns The major version number of Electron. + */ const majorElectronVersion = (): number => { const version = process.versions.electron.split('.'); return Number.parseInt(version[0], 10); }; +/** + * Retrieves the parent BrowserWindow associated with the given WebContents. + * @param webContents The WebContents object to find the parent BrowserWindow for. + * @returns The parent BrowserWindow if found, otherwise undefined. + */ const getWindowFromBrowserView = (webContents: WebContents): BrowserWindow | undefined => { for (const currentWindow of BrowserWindow.getAllWindows()) { for (const currentBrowserView of currentWindow.getBrowserViews()) { @@ -32,6 +51,11 @@ const getWindowFromBrowserView = (webContents: WebContents): BrowserWindow | und } }; +/** + * Retrieves the Electron BrowserWindow associated with the given WebContents. + * @param webContents The WebContents object to retrieve the BrowserWindow from. + * @returns The associated BrowserWindow, or undefined if not found. + */ const getWindowFromWebContents = (webContents: WebContents): BrowserWindow | undefined | null => { let electronWindow: BrowserWindow | undefined | null; const webContentsType = webContents.getType(); @@ -50,25 +74,14 @@ const getWindowFromWebContents = (webContents: WebContents): BrowserWindow | und return electronWindow; }; -type Options = { - showBadge?: boolean; - showProgressBar?: boolean; - directory?: string; - filename?: string; - overwrite?: boolean; - errorMessage?: string; - saveAs?: boolean; - dialogOptions?: SaveDialogOptions; - onProgress?: (progress: {percent: number; transferredBytes: number; totalBytes: number}) => void; - onTotalProgress?: (progress: {percent: number; transferredBytes: number; totalBytes: number}) => void; - onCancel?: (item: DownloadItem) => void; - onCompleted?: (info: {fileName: string; filename: string; path: string; fileSize: number; mimeType: string; url: string}) => void; - unregisterWhenDone?: boolean; - openFolderWhenDone?: boolean; - onStarted?: (item: DownloadItem) => void; -}; - -const registerListener = (session: Session, prevOptions: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { +/** + * Registers a listener for download events in Electron session. + * + * @param session - The Electron session to register the listener on. + * @param prevOptions - The previous options for the download manager. + * @param callback - The callback function to be called when a download event occurs. + */ +const registerListener = (session: Session, prevOptions: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => { }): void => { const downloadItems = new Set(); let receivedBytes = 0; let completedBytes = 0; @@ -105,7 +118,7 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err } if (options.saveAs) { - item.setSaveDialogOptions({defaultPath: filePath, ...options.dialogOptions}); + item.setSaveDialogOptions({ defaultPath: filePath, ...options.dialogOptions }); } else { item.setSavePath(filePath); } @@ -185,7 +198,6 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err if (typeof options.onCompleted === 'function') { options.onCompleted({ - fileName: item.getFilename(), // Just for backwards compatibility. TODO: Remove in the next major version. filename: item.getFilename(), path: savePath, fileSize: item.getReceivedBytes(), @@ -206,6 +218,23 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err session.on('will-download', listener); }; +/** +Register the helper for all windows. + +@example +``` +import {app, BrowserWindow} from 'electron'; +import electronDownloadManager = require('electron-dl'); + +electronDownloadManager(); + +let win; +(async () => { + await app.whenReady(); + win = new BrowserWindow(); +})(); +``` +*/ export default (options: Options = {}): void => { app.on('session-created', (session: Session) => { registerListener(session, options, (error: Error | null) => { @@ -218,7 +247,33 @@ export default (options: Options = {}): void => { }); }); }; -const download = (electronWindow: BrowserWindow, url: string, prevOptions: Options): Promise => { + +/** +This can be useful if you need download functionality in a reusable module. + +@param window - Window to register the behavior on. +@param url - URL to download. +@returns A promise for the downloaded file. +@throws {CancelError} An error if the user calls `item.cancel()`. +@throws {Error} An error if the download fails. + +@example +``` +import {BrowserWindow, ipcMain} from 'electron'; +import electronDownloadManager = require('electron-dl'); + +ipcMain.on('download-button', async (event, {url}) => { + const win = BrowserWindow.getFocusedWindow(); + console.log(await electronDownloadManager.download(win, url)); +}); +``` +*/ + +const download = ( + electronWindow: BrowserWindow | BrowserView, + url: string, + prevOptions?: Options +): Promise => { const options = { ...prevOptions, unregisterWhenDone: true, @@ -239,5 +294,5 @@ const download = (electronWindow: BrowserWindow, url: string, prevOptions: Optio }); }; -export {download}; -export type {CancelError, Options}; +export { download }; +export type { CancelError, Options }; diff --git a/src/libs/downloadQueue/electronDownloadManagerType.ts b/src/libs/downloadQueue/electronDownloadManagerType.ts new file mode 100644 index 000000000000..7627d3f8c423 --- /dev/null +++ b/src/libs/downloadQueue/electronDownloadManagerType.ts @@ -0,0 +1,133 @@ +import type { DownloadItem, SaveDialogOptions } from 'electron'; + +type Progress = { + percent: number; + transferredBytes: number; + totalBytes: number; +}; +type File = { + filename: string; + path: string; + fileSize: number; + mimeType: string; + url: string; +}; +type Options = { + /** + Show a `Save As…` dialog instead of downloading immediately. + + Note: Only use this option when strictly necessary. Downloading directly without a prompt is a much better user experience. + + @default false + */ + readonly saveAs?: boolean; + + /** + The directory to save the file in. + + Must be an absolute path. + + Default: [User's downloads directory](https://electronjs.org/docs/api/app/#appgetpathname) + */ + readonly directory?: string; + + /** + Name of the saved file. + This option only makes sense for `electronDownloadManager.download()`. + + Default: [`downloadItem.getFilename()`](https://electronjs.org/docs/api/download-item/#downloaditemgetfilename) + */ + readonly filename?: string; + + /** + Title of the error dialog. Can be customized for localization. + + Note: Error dialog will not be shown in `electronDownloadManager.download()`. Please handle error manually. + + @default 'Download Error' + */ + readonly errorTitle?: string; + + /** + Message of the error dialog. `{filename}` is replaced with the name of the actual file. Can be customized for localization. + + Note: Error dialog will not be shown in `electronDownloadManager.download()`. Please handle error manually. + + @default 'The download of {filename} was interrupted' + */ + readonly errorMessage?: string; + + /** + Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item). + You can use this for advanced handling such as canceling the item like `item.cancel()`. + */ + readonly onStarted?: (item: DownloadItem) => void; + + /** + Optional callback that receives an object containing information about the progress of the current download item. + */ + readonly onProgress?: (progress: Progress) => void; + + /** + Optional callback that receives an object containing information about the combined progress of all download items done within any registered window. + + Each time a new download is started, the next callback will include it. The progress percentage could therefore become smaller again. + This callback provides the same data that is used for the progress bar on the app icon. + */ + readonly onTotalProgress?: (progress: Progress) => void; + + /** + Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item) for which the download has been cancelled. + */ + readonly onCancel?: (item: DownloadItem) => void; + + /** + Optional callback that receives an object with information about an item that has been completed. It is called for each completed item. + */ + readonly onCompleted?: (file: File) => void; + + /** + Reveal the downloaded file in the system file manager, and if possible, select the file. + + @default false + */ + readonly openFolderWhenDone?: boolean; + + /** + Show a file count badge on the macOS/Linux dock/taskbar icon when a download is in progress. + + @default true + */ + readonly showBadge?: boolean; + + /** + Show a progress bar on the dock/taskbar icon when a download is in progress. + + @default true + */ + readonly showProgressBar?: boolean; + + /** + Allow downloaded files to overwrite files with the same name in the directory they are saved to. + + The default behavior is to append a number to the filename. + + @default false + */ + readonly overwrite?: boolean; + + /** + Customize the save dialog. + + If `defaultPath` is not explicity defined, a default value is assigned based on the file path. + + @default {} + */ + readonly dialogOptions?: SaveDialogOptions; + + /** Unregister the listener when the download is done. */ + readonly unregisterWhenDone?: boolean; +}; + +export type { Options, File, Progress }; + diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 6cb31a61ef24..70a14c37719c 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,5 +1,5 @@ import type {BrowserWindow} from 'electron'; -import type {Options} from './electronDownloadManager'; +import type { Options } from './electronDownloadManagerType'; import {download as electronDownload} from './electronDownloadManager'; type DownloadItem = { diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 5acf86cda3fc..84f6055ea630 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,4 +1,4 @@ -import type {Options} from '@libs/downloadQueue/electronDownloadManager'; +import type { Options } from '@libs/downloadQueue/electronDownloadManagerType'; import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS'; import type {FileDownload} from './types'; From e1a4b627ea71338ca5e558c86364716c3991168c Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 26 Feb 2024 08:55:33 +0700 Subject: [PATCH 15/47] Run prettier, remove unnecessary code --- desktop/main.js | 18 +++++------ .../downloadQueue/electronDownloadManager.ts | 31 ++++++++----------- .../electronDownloadManagerType.ts | 5 ++- src/libs/downloadQueue/index.desktop.ts | 2 +- src/libs/fileDownload/index.desktop.ts | 2 +- 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index b40231df81f3..84f3e7be9b69 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -11,9 +11,7 @@ const CONFIG = require('../src/CONFIG').default; const CONST = require('../src/CONST').default; const Localize = require('../src/libs/Localize'); const createDownloadQueue = require('../src/libs/downloadQueue').default; -const electronDownloadManager = require('../src/libs/downloadQueue/electronDownloadManager').default; -electronDownloadManager(); const port = process.env.PORT || 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; @@ -600,15 +598,15 @@ const mainWindow = () => { } }); - // const downloadQueue = createDownloadQueue(); + const downloadQueue = createDownloadQueue(); - // ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { - // const downloadItem = { - // ...downloadData, - // win: browserWindow, - // }; - // downloadQueue.pushDownloadItem(downloadItem); - // }); + ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { + const downloadItem = { + ...downloadData, + win: browserWindow, + }; + downloadQueue.pushDownloadItem(downloadItem); + }); return browserWindow; }) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index dc84884e3319..7a55ae90d2ba 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -1,20 +1,20 @@ /** * This file is a ported version of the `electron-dl` package. * It provides a download manager for Electron applications. - * The `electron-dl` package simplifies the process of downloading files in Electron apps + * The package simplifies the process of downloading files in Electron apps * by providing a high-level API and handling various download-related tasks. - * This file contains the implementation of the Electron Download Manager. + * We decided to replicate the functionality of the `electron-dl` package for easier maintenance. + * More context: https://github.com/Expensify/App/issues/35189#issuecomment-1959681109 */ - -import type { DownloadItem, Event, Session, WebContents, BrowserView } from 'electron'; -import { app, BrowserWindow, dialog, shell } from 'electron'; +import type {BrowserView, DownloadItem, Event, Session, WebContents} from 'electron'; +import {app, BrowserWindow, dialog, shell} from 'electron'; import * as path from 'path'; -import { Options } from './electronDownloadManagerType'; +import type {Options} from './electronDownloadManagerType'; /** Error thrown if `item.cancel()` was called. */ -declare class CancelError extends Error { } +declare class CancelError extends Error {} /** * Returns the filename with extension based on the given name and MIME type. @@ -76,12 +76,12 @@ const getWindowFromWebContents = (webContents: WebContents): BrowserWindow | und /** * Registers a listener for download events in Electron session. - * + * * @param session - The Electron session to register the listener on. * @param prevOptions - The previous options for the download manager. * @param callback - The callback function to be called when a download event occurs. */ -const registerListener = (session: Session, prevOptions: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => { }): void => { +const registerListener = (session: Session, prevOptions: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { const downloadItems = new Set(); let receivedBytes = 0; let completedBytes = 0; @@ -118,7 +118,7 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err } if (options.saveAs) { - item.setSaveDialogOptions({ defaultPath: filePath, ...options.dialogOptions }); + item.setSaveDialogOptions({defaultPath: filePath, ...options.dialogOptions}); } else { item.setSavePath(filePath); } @@ -268,12 +268,7 @@ ipcMain.on('download-button', async (event, {url}) => { }); ``` */ - -const download = ( - electronWindow: BrowserWindow | BrowserView, - url: string, - prevOptions?: Options -): Promise => { +const download = (electronWindow: BrowserWindow | BrowserView, url: string, prevOptions?: Options): Promise => { const options = { ...prevOptions, unregisterWhenDone: true, @@ -294,5 +289,5 @@ const download = ( }); }; -export { download }; -export type { CancelError, Options }; +export {download}; +export type {CancelError, Options}; diff --git a/src/libs/downloadQueue/electronDownloadManagerType.ts b/src/libs/downloadQueue/electronDownloadManagerType.ts index 7627d3f8c423..3fc0ebb33dfd 100644 --- a/src/libs/downloadQueue/electronDownloadManagerType.ts +++ b/src/libs/downloadQueue/electronDownloadManagerType.ts @@ -1,4 +1,4 @@ -import type { DownloadItem, SaveDialogOptions } from 'electron'; +import type {DownloadItem, SaveDialogOptions} from 'electron'; type Progress = { percent: number; @@ -129,5 +129,4 @@ type Options = { readonly unregisterWhenDone?: boolean; }; -export type { Options, File, Progress }; - +export type {Options, File, Progress}; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 70a14c37719c..bf6ec429480c 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,6 +1,6 @@ import type {BrowserWindow} from 'electron'; -import type { Options } from './electronDownloadManagerType'; import {download as electronDownload} from './electronDownloadManager'; +import type {Options} from './electronDownloadManagerType'; type DownloadItem = { win: BrowserWindow; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 84f6055ea630..043d12c5eeef 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,4 +1,4 @@ -import type { Options } from '@libs/downloadQueue/electronDownloadManagerType'; +import type {Options} from '@libs/downloadQueue/electronDownloadManagerType'; import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS'; import type {FileDownload} from './types'; From 5af3db7a0b8668241e0209a1ef8f1b8da938895c Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 26 Feb 2024 09:05:27 +0700 Subject: [PATCH 16/47] Fix comment reference --- src/libs/downloadQueue/electronDownloadManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 7a55ae90d2ba..4f83b6121770 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -224,7 +224,7 @@ Register the helper for all windows. @example ``` import {app, BrowserWindow} from 'electron'; -import electronDownloadManager = require('electron-dl'); +import electronDownloadManager = require('./electronDownloadManager'); electronDownloadManager(); @@ -260,7 +260,7 @@ This can be useful if you need download functionality in a reusable module. @example ``` import {BrowserWindow, ipcMain} from 'electron'; -import electronDownloadManager = require('electron-dl'); +import electronDownloadManager = require('./electronDownloadManager'); ipcMain.on('download-button', async (event, {url}) => { const win = BrowserWindow.getFocusedWindow(); From 7dabcdbf5e90a9e51e73b6b04cc2dc7a26a3668d Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 26 Feb 2024 09:15:44 +0700 Subject: [PATCH 17/47] add more comments for downloadQueue --- src/libs/downloadQueue/index.desktop.ts | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index bf6ec429480c..6ea03c4f30ac 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,17 +1,36 @@ + import type {BrowserWindow} from 'electron'; import {download as electronDownload} from './electronDownloadManager'; import type {Options} from './electronDownloadManagerType'; type DownloadItem = { + // The window where the download will be initiated win: BrowserWindow; + + // The URL of the file to be downloaded url: string; + + // The options for the download, such as save path, file name, etc. options: Options; }; +/** + * Represents a queue of download items. + */ type DownloadQueue = DownloadItem[]; + +/** + * Creates a download queue. + * @returns An object with methods to push and shift download items from the queue. + */ const createDownloadQueue = () => { const queue: DownloadQueue = []; + /** + * Shifts and returns the first item from the download queue. + * If the queue is not empty, it triggers the download of the next item. + * @returns The shifted DownloadItem or undefined if the queue is empty. + */ const shiftDownloadItem = (): DownloadItem | undefined => { const item = queue.shift(); if (queue.length > 0) { @@ -23,6 +42,10 @@ const createDownloadQueue = () => { return item; }; + /** + * Downloads the specified item. + * @param item - The item to be downloaded. + */ const downloadItem = (item: DownloadItem): void => { const newItem = { ...item, @@ -40,6 +63,12 @@ const createDownloadQueue = () => { electronDownload(newItem.win, newItem.url, newItem.options); }; + /** + * Pushes a download item to the queue and returns the new length of the queue. + * If the queue was empty before pushing the item, it will immediately start downloading the item. + * @param item The download item to be pushed to the queue. + * @returns The new length of the queue after pushing the item. + */ const pushDownloadItem = (item: DownloadItem): number => { const len = queue.push(item); if (queue.length === 1) { From 32faeebf45999f20051d27b81d81a6530e1fde74 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 26 Feb 2024 09:16:21 +0700 Subject: [PATCH 18/47] run prettier --- src/libs/downloadQueue/index.desktop.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 6ea03c4f30ac..b41ee1e27372 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,4 +1,3 @@ - import type {BrowserWindow} from 'electron'; import {download as electronDownload} from './electronDownloadManager'; import type {Options} from './electronDownloadManagerType'; @@ -6,10 +5,10 @@ import type {Options} from './electronDownloadManagerType'; type DownloadItem = { // The window where the download will be initiated win: BrowserWindow; - + // The URL of the file to be downloaded url: string; - + // The options for the download, such as save path, file name, etc. options: Options; }; From 1d0e837e544147f39f9b14d3fc7971033817cedb Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 26 Feb 2024 09:20:15 +0700 Subject: [PATCH 19/47] add more comments for types Progress and File --- .../downloadQueue/electronDownloadManagerType.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/libs/downloadQueue/electronDownloadManagerType.ts b/src/libs/downloadQueue/electronDownloadManagerType.ts index 3fc0ebb33dfd..8e9f92623adf 100644 --- a/src/libs/downloadQueue/electronDownloadManagerType.ts +++ b/src/libs/downloadQueue/electronDownloadManagerType.ts @@ -1,15 +1,30 @@ import type {DownloadItem, SaveDialogOptions} from 'electron'; type Progress = { + // The percentage of the download that has been completed percent: number; + + // The number of bytes that have been downloaded so far transferredBytes: number; + + // The total number of bytes in the file being downloaded totalBytes: number; }; + type File = { + // The name of the file being downloaded filename: string; + + // The path where the file is being downloaded to path: string; + + // The size of the file being downloaded, in bytes fileSize: number; + + // The MIME type of the file being downloaded mimeType: string; + + // The URL of the file being downloaded url: string; }; type Options = { From 662c1a5a2c7f250f7a9e3c1040286d72a4056e04 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 26 Feb 2024 09:40:54 +0700 Subject: [PATCH 20/47] change terms "ported" to "replicated" --- src/libs/downloadQueue/electronDownloadManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 4f83b6121770..a8c0efecfb78 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -1,5 +1,5 @@ /** - * This file is a ported version of the `electron-dl` package. + * This file is a replicated version of the `electron-dl` package. * It provides a download manager for Electron applications. * The package simplifies the process of downloading files in Electron apps * by providing a high-level API and handling various download-related tasks. From 12007011e2af9d3d32a213bda7e7e0b5d1960c3b Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 27 Feb 2024 08:31:02 +0700 Subject: [PATCH 21/47] Remove unused functionalities / types --- .../downloadQueue/electronDownloadManager.ts | 155 +----------------- .../electronDownloadManagerType.ts | 46 +----- src/libs/downloadQueue/index.desktop.ts | 2 +- src/libs/fileDownload/index.desktop.ts | 3 - 4 files changed, 8 insertions(+), 198 deletions(-) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index a8c0efecfb78..3bdef885ef34 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -6,8 +6,8 @@ * We decided to replicate the functionality of the `electron-dl` package for easier maintenance. * More context: https://github.com/Expensify/App/issues/35189#issuecomment-1959681109 */ -import type {BrowserView, DownloadItem, Event, Session, WebContents} from 'electron'; -import {app, BrowserWindow, dialog, shell} from 'electron'; +import type {BrowserView, BrowserWindow, DownloadItem, Event, Session} from 'electron'; +import {app, shell} from 'electron'; import * as path from 'path'; import type {Options} from './electronDownloadManagerType'; @@ -27,79 +27,17 @@ const getFilenameFromMime = (name: string, mime: string): string => { return `${name}.${extensions}`; }; -/** - * Returns the major version number of Electron. - * @returns The major version number of Electron. - */ -const majorElectronVersion = (): number => { - const version = process.versions.electron.split('.'); - return Number.parseInt(version[0], 10); -}; - -/** - * Retrieves the parent BrowserWindow associated with the given WebContents. - * @param webContents The WebContents object to find the parent BrowserWindow for. - * @returns The parent BrowserWindow if found, otherwise undefined. - */ -const getWindowFromBrowserView = (webContents: WebContents): BrowserWindow | undefined => { - for (const currentWindow of BrowserWindow.getAllWindows()) { - for (const currentBrowserView of currentWindow.getBrowserViews()) { - if (currentBrowserView.webContents.id === webContents.id) { - return currentWindow; - } - } - } -}; - -/** - * Retrieves the Electron BrowserWindow associated with the given WebContents. - * @param webContents The WebContents object to retrieve the BrowserWindow from. - * @returns The associated BrowserWindow, or undefined if not found. - */ -const getWindowFromWebContents = (webContents: WebContents): BrowserWindow | undefined | null => { - let electronWindow: BrowserWindow | undefined | null; - const webContentsType = webContents.getType(); - switch (webContentsType) { - case 'webview': - electronWindow = BrowserWindow.fromWebContents(webContents.hostWebContents); - break; - case 'browserView': - electronWindow = getWindowFromBrowserView(webContents); - break; - default: - electronWindow = BrowserWindow.fromWebContents(webContents); - break; - } - - return electronWindow; -}; - /** * Registers a listener for download events in Electron session. * * @param session - The Electron session to register the listener on. - * @param prevOptions - The previous options for the download manager. + * @param options - The previous options for the download manager. * @param callback - The callback function to be called when a download event occurs. */ -const registerListener = (session: Session, prevOptions: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { +const registerListener = (session: Session, options: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { const downloadItems = new Set(); - let receivedBytes = 0; - let completedBytes = 0; - let totalBytes = 0; - const activeDownloadItems = (): number => downloadItems.size; - const progressDownloadItems = (): number => receivedBytes / totalBytes; - - const options = { - showBadge: true, - showProgressBar: true, - ...prevOptions, - }; - - const listener = (event: Event, item: DownloadItem, webContents: WebContents): void => { + const listener = (event: Event, item: DownloadItem): void => { downloadItems.add(item); - totalBytes += item.getTotalBytes(); - - const electronWindow = majorElectronVersion() >= 12 ? BrowserWindow.fromWebContents(webContents) : getWindowFromWebContents(webContents); if (options.directory && !path.isAbsolute(options.directory)) { throw new Error('The `directory` option must be an absolute path'); @@ -123,55 +61,9 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err item.setSavePath(filePath); } - item.on('updated', () => { - receivedBytes = completedBytes; - for (const downloadItem of downloadItems) { - receivedBytes += downloadItem.getReceivedBytes(); - } - - if (options.showBadge && ['darwin', 'linux'].includes(process.platform)) { - app.badgeCount = activeDownloadItems(); - } - - if (!electronWindow?.isDestroyed() && options.showProgressBar) { - electronWindow?.setProgressBar(progressDownloadItems()); - } - - if (typeof options.onProgress === 'function') { - const itemTransferredBytes = item.getReceivedBytes(); - const itemTotalBytes = item.getTotalBytes(); - - options.onProgress({ - percent: itemTotalBytes ? itemTransferredBytes / itemTotalBytes : 0, - transferredBytes: itemTransferredBytes, - totalBytes: itemTotalBytes, - }); - } - - if (typeof options.onTotalProgress === 'function') { - options.onTotalProgress({ - percent: progressDownloadItems(), - transferredBytes: receivedBytes, - totalBytes, - }); - } - }); - item.on('done', (doneEvent: Event, state: string) => { - completedBytes += item.getTotalBytes(); downloadItems.delete(item); - if (options.showBadge && ['darwin', 'linux'].includes(process.platform)) { - app.badgeCount = activeDownloadItems(); - } - - if (!electronWindow?.isDestroyed() && !activeDownloadItems()) { - electronWindow?.setProgressBar(-1); - receivedBytes = 0; - completedBytes = 0; - totalBytes = 0; - } - if (options.unregisterWhenDone) { session.removeListener('will-download', listener); } @@ -209,45 +101,11 @@ const registerListener = (session: Session, prevOptions: Options, callback: (err callback(null, item); } }); - - if (typeof options.onStarted === 'function') { - options.onStarted(item); - } }; session.on('will-download', listener); }; -/** -Register the helper for all windows. - -@example -``` -import {app, BrowserWindow} from 'electron'; -import electronDownloadManager = require('./electronDownloadManager'); - -electronDownloadManager(); - -let win; -(async () => { - await app.whenReady(); - win = new BrowserWindow(); -})(); -``` -*/ -export default (options: Options = {}): void => { - app.on('session-created', (session: Session) => { - registerListener(session, options, (error: Error | null) => { - if (!error || error instanceof CancelError) { - return; - } - - const errorTitle = options.errorMessage ?? 'Download Error'; - dialog.showErrorBox(errorTitle, error.message); - }); - }); -}; - /** This can be useful if you need download functionality in a reusable module. @@ -289,5 +147,4 @@ const download = (electronWindow: BrowserWindow | BrowserView, url: string, prev }); }; -export {download}; -export type {CancelError, Options}; +export default download; diff --git a/src/libs/downloadQueue/electronDownloadManagerType.ts b/src/libs/downloadQueue/electronDownloadManagerType.ts index 8e9f92623adf..2d552b686c0d 100644 --- a/src/libs/downloadQueue/electronDownloadManagerType.ts +++ b/src/libs/downloadQueue/electronDownloadManagerType.ts @@ -1,16 +1,5 @@ import type {DownloadItem, SaveDialogOptions} from 'electron'; -type Progress = { - // The percentage of the download that has been completed - percent: number; - - // The number of bytes that have been downloaded so far - transferredBytes: number; - - // The total number of bytes in the file being downloaded - totalBytes: number; -}; - type File = { // The name of the file being downloaded filename: string; @@ -72,25 +61,6 @@ type Options = { */ readonly errorMessage?: string; - /** - Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item). - You can use this for advanced handling such as canceling the item like `item.cancel()`. - */ - readonly onStarted?: (item: DownloadItem) => void; - - /** - Optional callback that receives an object containing information about the progress of the current download item. - */ - readonly onProgress?: (progress: Progress) => void; - - /** - Optional callback that receives an object containing information about the combined progress of all download items done within any registered window. - - Each time a new download is started, the next callback will include it. The progress percentage could therefore become smaller again. - This callback provides the same data that is used for the progress bar on the app icon. - */ - readonly onTotalProgress?: (progress: Progress) => void; - /** Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item) for which the download has been cancelled. */ @@ -108,20 +78,6 @@ type Options = { */ readonly openFolderWhenDone?: boolean; - /** - Show a file count badge on the macOS/Linux dock/taskbar icon when a download is in progress. - - @default true - */ - readonly showBadge?: boolean; - - /** - Show a progress bar on the dock/taskbar icon when a download is in progress. - - @default true - */ - readonly showProgressBar?: boolean; - /** Allow downloaded files to overwrite files with the same name in the directory they are saved to. @@ -144,4 +100,4 @@ type Options = { readonly unregisterWhenDone?: boolean; }; -export type {Options, File, Progress}; +export type {Options, File}; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index b41ee1e27372..bc3fabbb2cf6 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,5 +1,5 @@ import type {BrowserWindow} from 'electron'; -import {download as electronDownload} from './electronDownloadManager'; +import electronDownload from './electronDownloadManager'; import type {Options} from './electronDownloadManagerType'; type DownloadItem = { diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 043d12c5eeef..a562984b28fb 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -9,9 +9,6 @@ const fileDownload: FileDownload = (url, fileName) => { const options: Options = { filename: fileName, saveAs: true, - // showing badge and progress bar only supported on macos and linux, better to disable it - showBadge: false, - showProgressBar: false, }; window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); From 71cfde7b840eb0f2e8c14855a696f44800b52668 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 13 Mar 2024 04:13:08 +0700 Subject: [PATCH 22/47] Use relative import --- src/libs/fileDownload/index.desktop.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index a562984b28fb..da330f302e09 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,5 +1,5 @@ import type {Options} from '@libs/downloadQueue/electronDownloadManagerType'; -import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS'; +import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; import type {FileDownload} from './types'; /** From 4c11059be66ff0d93653f6e1aa14d5ae32e94333 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 13 Mar 2024 04:24:30 +0700 Subject: [PATCH 23/47] run prettier --- src/libs/fileDownload/index.desktop.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index da330f302e09..b02006d70188 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,5 +1,5 @@ -import type {Options} from '@libs/downloadQueue/electronDownloadManagerType'; import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; +import type {Options} from '@libs/downloadQueue/electronDownloadManagerType'; import type {FileDownload} from './types'; /** From 45e5c1d49aa2e24da7432d9d10d81f0c07deb93d Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 13 Mar 2024 06:36:59 +0700 Subject: [PATCH 24/47] Fix cancel error --- src/libs/downloadQueue/electronDownloadManager.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 3bdef885ef34..2f274c55f59c 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -14,7 +14,7 @@ import type {Options} from './electronDownloadManagerType'; /** Error thrown if `item.cancel()` was called. */ -declare class CancelError extends Error {} +class CancelError extends Error {} /** * Returns the filename with extension based on the given name and MIME type. @@ -69,9 +69,12 @@ const registerListener = (session: Session, options: Options, callback: (error: } if (state === 'cancelled') { + console.log('[wildebug] if (state === cancelled) {') if (typeof options.onCancel === 'function') { options.onCancel(item); } + + // console.log('[wildebug] new CancelError()', new CancelError()) callback(new CancelError()); } else if (state === 'interrupted') { const errorMessage = `The download of ${path.basename(filePath)} was interrupted`; From a6ecfdae917c0926bcb20346bfbe7e9e00081470 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 13 Mar 2024 06:47:30 +0700 Subject: [PATCH 25/47] remove debug code --- src/libs/downloadQueue/electronDownloadManager.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index 2f274c55f59c..afb55872cc9d 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -69,12 +69,10 @@ const registerListener = (session: Session, options: Options, callback: (error: } if (state === 'cancelled') { - console.log('[wildebug] if (state === cancelled) {') if (typeof options.onCancel === 'function') { options.onCancel(item); } - // console.log('[wildebug] new CancelError()', new CancelError()) callback(new CancelError()); } else if (state === 'interrupted') { const errorMessage = `The download of ${path.basename(filePath)} was interrupted`; From b8e7a60d36397e2b2bc9fe819ddf175d293470f3 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 13 Mar 2024 11:32:43 +0700 Subject: [PATCH 26/47] Listen for download completion and cancellation events instead of relying on setTimeout. --- desktop/ELECTRON_EVENTS.js | 2 ++ desktop/contextBridge.js | 9 ++++++++- src/libs/downloadQueue/index.desktop.ts | 3 +++ src/libs/fileDownload/index.desktop.ts | 18 ++++++++++++------ src/types/modules/electron.d.ts | 2 +- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.js index fd28acaf1e64..026c64b0d607 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.js @@ -10,6 +10,8 @@ const ELECTRON_EVENTS = { START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', DOWNLOAD: 'download', + DOWNLOAD_COMPLETED: 'download-completed', + DOWNLOAD_CANCELLED: 'download-cancelled', }; module.exports = ELECTRON_EVENTS; diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js index 2e0db79bb861..d6ab4e02c7bf 100644 --- a/desktop/contextBridge.js +++ b/desktop/contextBridge.js @@ -12,7 +12,14 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.DOWNLOAD, ]; -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR]; +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ + ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, + ELECTRON_EVENTS.UPDATE_DOWNLOADED, + ELECTRON_EVENTS.FOCUS, + ELECTRON_EVENTS.BLUR, + ELECTRON_EVENTS.DOWNLOAD_COMPLETED, + ELECTRON_EVENTS.DOWNLOAD_CANCELLED, +]; const getErrorMessage = (channel) => `Electron context bridge cannot be used with channel '${channel}'`; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index bc3fabbb2cf6..b1fd6892eadd 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,4 +1,5 @@ import type {BrowserWindow} from 'electron'; +import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; import electronDownload from './electronDownloadManager'; import type {Options} from './electronDownloadManagerType'; @@ -52,9 +53,11 @@ const createDownloadQueue = () => { ...item.options, onCompleted: () => { shiftDownloadItem(); + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, {url: item.url}); }, onCancel: () => { shiftDownloadItem(); + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELLED, {url: item.url}); }, }, }; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index b02006d70188..5660d500479a 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -12,14 +12,20 @@ const fileDownload: FileDownload = (url, fileName) => { }; window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); - /** - * Adds a 1000ms delay to keep showing the loading spinner - * and prevent rapid clicks on the same download link. - */ return new Promise((resolve) => { - setTimeout(() => { + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, (args) => { + if (args.url !== url) { + return; + } resolve(); - }, 1000); + }); + + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_CANCELLED, (args) => { + if (args.url !== url) { + return; + } + resolve(); + }); }); }; diff --git a/src/types/modules/electron.d.ts b/src/types/modules/electron.d.ts index 09e33f29ba38..ec2a8b1fcd7b 100644 --- a/src/types/modules/electron.d.ts +++ b/src/types/modules/electron.d.ts @@ -3,7 +3,7 @@ type ContextBridgeApi = { send: (channel: string, data?: unknown) => void; sendSync: (channel: string, data?: unknown) => unknown; invoke: (channel: string, ...args: unknown) => Promise; - on: (channel: string, func: () => void) => void; + on: (channel: string, func: (args) => void) => void; removeAllListeners: (channel: string) => void; }; From 0c6f4a3c92b44d08db07362c95dd193806b8f792 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 13 Mar 2024 14:39:14 +0700 Subject: [PATCH 27/47] cancel download when interrupted. e.g. sudden network error / offline --- src/libs/downloadQueue/electronDownloadManager.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/src/libs/downloadQueue/electronDownloadManager.ts index afb55872cc9d..21d0a78466b0 100644 --- a/src/libs/downloadQueue/electronDownloadManager.ts +++ b/src/libs/downloadQueue/electronDownloadManager.ts @@ -61,6 +61,16 @@ const registerListener = (session: Session, options: Options, callback: (error: item.setSavePath(filePath); } + item.on('updated', (doneEvent: Event, state: string) => { + if (state !== 'interrupted') { + return; + } + + const errorMessage = `The download of ${path.basename(filePath)} was interrupted`; + item.cancel(); + callback(new Error(errorMessage)); + }); + item.on('done', (doneEvent: Event, state: string) => { downloadItems.delete(item); From ee4902e9681f91be2924065bb6a7f9513dacdd66 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 15 Mar 2024 10:11:56 +0700 Subject: [PATCH 28/47] move electronDownloadManager to desktop folder --- .../libs/downloadQueue => desktop}/electronDownloadManager.ts | 0 .../downloadQueue => desktop}/electronDownloadManagerType.ts | 0 src/libs/downloadQueue/index.desktop.ts | 4 ++-- src/libs/fileDownload/index.desktop.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename {src/libs/downloadQueue => desktop}/electronDownloadManager.ts (100%) rename {src/libs/downloadQueue => desktop}/electronDownloadManagerType.ts (100%) diff --git a/src/libs/downloadQueue/electronDownloadManager.ts b/desktop/electronDownloadManager.ts similarity index 100% rename from src/libs/downloadQueue/electronDownloadManager.ts rename to desktop/electronDownloadManager.ts diff --git a/src/libs/downloadQueue/electronDownloadManagerType.ts b/desktop/electronDownloadManagerType.ts similarity index 100% rename from src/libs/downloadQueue/electronDownloadManagerType.ts rename to desktop/electronDownloadManagerType.ts diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index b1fd6892eadd..fe376ff01b5d 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -1,7 +1,7 @@ import type {BrowserWindow} from 'electron'; import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; -import electronDownload from './electronDownloadManager'; -import type {Options} from './electronDownloadManagerType'; +import electronDownload from '@desktop/electronDownloadManager'; +import type {Options} from '@desktop/electronDownloadManagerType'; type DownloadItem = { // The window where the download will be initiated diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 5660d500479a..90a69dfae721 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,5 +1,5 @@ import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; -import type {Options} from '@libs/downloadQueue/electronDownloadManagerType'; +import type {Options} from '@desktop/electronDownloadManagerType'; import type {FileDownload} from './types'; /** From 95a70be1a87c5ec6fc7f74695e81f921e5fa7cda Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 15 Mar 2024 10:35:44 +0700 Subject: [PATCH 29/47] Use download started event instead of download completed to stop download spinner --- desktop/ELECTRON_EVENTS.js | 2 +- desktop/contextBridge.js | 2 +- desktop/electronDownloadManager.ts | 4 ++++ desktop/electronDownloadManagerType.ts | 6 ++++++ src/libs/downloadQueue/index.desktop.ts | 3 +++ src/libs/fileDownload/index.desktop.ts | 2 +- 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.js index 026c64b0d607..1e5561875338 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.js @@ -10,7 +10,7 @@ const ELECTRON_EVENTS = { START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', DOWNLOAD: 'download', - DOWNLOAD_COMPLETED: 'download-completed', + DOWNLOAD_STARTED: 'download-started', DOWNLOAD_CANCELLED: 'download-cancelled', }; diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js index d6ab4e02c7bf..f7783edbd5f7 100644 --- a/desktop/contextBridge.js +++ b/desktop/contextBridge.js @@ -17,7 +17,7 @@ const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR, - ELECTRON_EVENTS.DOWNLOAD_COMPLETED, + ELECTRON_EVENTS.DOWNLOAD_STARTED, ELECTRON_EVENTS.DOWNLOAD_CANCELLED, ]; diff --git a/desktop/electronDownloadManager.ts b/desktop/electronDownloadManager.ts index 21d0a78466b0..acf048f3053e 100644 --- a/desktop/electronDownloadManager.ts +++ b/desktop/electronDownloadManager.ts @@ -112,6 +112,10 @@ const registerListener = (session: Session, options: Options, callback: (error: callback(null, item); } }); + + if (typeof options.onStarted === 'function') { + options.onStarted(item); + } }; session.on('will-download', listener); diff --git a/desktop/electronDownloadManagerType.ts b/desktop/electronDownloadManagerType.ts index 2d552b686c0d..0a0bea9f36b6 100644 --- a/desktop/electronDownloadManagerType.ts +++ b/desktop/electronDownloadManagerType.ts @@ -61,6 +61,12 @@ type Options = { */ readonly errorMessage?: string; + /** + Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item). + You can use this for advanced handling such as canceling the item like `item.cancel()`. + */ + readonly onStarted?: (item: DownloadItem) => void; + /** Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item) for which the download has been cancelled. */ diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index fe376ff01b5d..38b810bd4fc6 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -51,6 +51,9 @@ const createDownloadQueue = () => { ...item, options: { ...item.options, + onStarted: () => { + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); + }, onCompleted: () => { shiftDownloadItem(); item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, {url: item.url}); diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 90a69dfae721..1568d7ab4180 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -13,7 +13,7 @@ const fileDownload: FileDownload = (url, fileName) => { window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); return new Promise((resolve) => { - window.electron.on(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, (args) => { + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (args) => { if (args.url !== url) { return; } From 2dd08b0636d7ebd765fbf9d924dc9a808fe84f40 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 15 Mar 2024 10:39:19 +0700 Subject: [PATCH 30/47] remove download-cancelled event --- desktop/ELECTRON_EVENTS.js | 1 - desktop/contextBridge.js | 1 - src/libs/downloadQueue/index.desktop.ts | 2 -- src/libs/fileDownload/index.desktop.ts | 7 ------- 4 files changed, 11 deletions(-) diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.js index 1e5561875338..43f9f72037a0 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.js @@ -11,7 +11,6 @@ const ELECTRON_EVENTS = { UPDATE_DOWNLOADED: 'update-downloaded', DOWNLOAD: 'download', DOWNLOAD_STARTED: 'download-started', - DOWNLOAD_CANCELLED: 'download-cancelled', }; module.exports = ELECTRON_EVENTS; diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js index f7783edbd5f7..553d84c7d644 100644 --- a/desktop/contextBridge.js +++ b/desktop/contextBridge.js @@ -18,7 +18,6 @@ const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR, ELECTRON_EVENTS.DOWNLOAD_STARTED, - ELECTRON_EVENTS.DOWNLOAD_CANCELLED, ]; const getErrorMessage = (channel) => `Electron context bridge cannot be used with channel '${channel}'`; diff --git a/src/libs/downloadQueue/index.desktop.ts b/src/libs/downloadQueue/index.desktop.ts index 38b810bd4fc6..536a1f211dd1 100644 --- a/src/libs/downloadQueue/index.desktop.ts +++ b/src/libs/downloadQueue/index.desktop.ts @@ -56,11 +56,9 @@ const createDownloadQueue = () => { }, onCompleted: () => { shiftDownloadItem(); - item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, {url: item.url}); }, onCancel: () => { shiftDownloadItem(); - item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELLED, {url: item.url}); }, }, }; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 1568d7ab4180..7f5d528eb629 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -19,13 +19,6 @@ const fileDownload: FileDownload = (url, fileName) => { } resolve(); }); - - window.electron.on(ELECTRON_EVENTS.DOWNLOAD_CANCELLED, (args) => { - if (args.url !== url) { - return; - } - resolve(); - }); }); }; From 7e97db52f449d8a1d80db646aede356a8b9a93f3 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 15 Mar 2024 10:55:18 +0700 Subject: [PATCH 31/47] move downloadQueue to desktop folder --- .../index.desktop.ts => desktop/createDownloadQueue.ts | 0 desktop/main.js | 3 +-- src/libs/downloadQueue/index.ts | 4 ---- 3 files changed, 1 insertion(+), 6 deletions(-) rename src/libs/downloadQueue/index.desktop.ts => desktop/createDownloadQueue.ts (100%) delete mode 100644 src/libs/downloadQueue/index.ts diff --git a/src/libs/downloadQueue/index.desktop.ts b/desktop/createDownloadQueue.ts similarity index 100% rename from src/libs/downloadQueue/index.desktop.ts rename to desktop/createDownloadQueue.ts diff --git a/desktop/main.js b/desktop/main.js index 84f3e7be9b69..ac85b80843eb 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -10,8 +10,7 @@ const checkForUpdates = require('../src/libs/checkForUpdates'); const CONFIG = require('../src/CONFIG').default; const CONST = require('../src/CONST').default; const Localize = require('../src/libs/Localize'); -const createDownloadQueue = require('../src/libs/downloadQueue').default; - +const createDownloadQueue = require('./createDownloadQueue').default; const port = process.env.PORT || 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; diff --git a/src/libs/downloadQueue/index.ts b/src/libs/downloadQueue/index.ts deleted file mode 100644 index b7506290d9ae..000000000000 --- a/src/libs/downloadQueue/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// no implementation except for desktop -const createDownloadQueue = () => {}; - -export default createDownloadQueue; From 6a4d4a7bb8b9058a01743966825e49d994ad1bbd Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 15 Mar 2024 11:36:37 +0700 Subject: [PATCH 32/47] separate queue and electron specific code --- desktop/createDownloadQueue.ts | 75 +++++++++++++++++----------------- desktop/main.js | 3 +- src/libs/Queue/Queue.ts | 43 +++++++++++++++++++ src/libs/Queue/QueueType.ts | 9 ++++ 4 files changed, 91 insertions(+), 39 deletions(-) create mode 100644 src/libs/Queue/Queue.ts create mode 100644 src/libs/Queue/QueueType.ts diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 536a1f211dd1..b7962133fa31 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -1,7 +1,8 @@ import type {BrowserWindow} from 'electron'; -import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; -import electronDownload from '@desktop/electronDownloadManager'; -import type {Options} from '@desktop/electronDownloadManagerType'; +import createQueue from '@libs/Queue/Queue'; +import ELECTRON_EVENTS from './ELECTRON_EVENTS'; +import electronDownload from './electronDownloadManager'; +import type {Options} from './electronDownloadManagerType'; type DownloadItem = { // The window where the download will be initiated @@ -14,33 +15,12 @@ type DownloadItem = { options: Options; }; -/** - * Represents a queue of download items. - */ -type DownloadQueue = DownloadItem[]; - /** * Creates a download queue. - * @returns An object with methods to push and shift download items from the queue. + * @returns An object with methods to enqueue and dequeue download items from the queue. */ const createDownloadQueue = () => { - const queue: DownloadQueue = []; - - /** - * Shifts and returns the first item from the download queue. - * If the queue is not empty, it triggers the download of the next item. - * @returns The shifted DownloadItem or undefined if the queue is empty. - */ - const shiftDownloadItem = (): DownloadItem | undefined => { - const item = queue.shift(); - if (queue.length > 0) { - // This code block contains a cyclic dependency between functions, - // so one of them should have the eslint-disable-next-line comment - // eslint-disable-next-line @typescript-eslint/no-use-before-define - downloadItem(queue[0]); - } - return item; - }; + const queue = createQueue(); /** * Downloads the specified item. @@ -55,10 +35,26 @@ const createDownloadQueue = () => { item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); }, onCompleted: () => { - shiftDownloadItem(); + queue.dequeue(); + if (queue.isEmpty()) { + return; + } + + const nextItem = queue.peek(); + if (nextItem !== undefined) { + downloadItem(nextItem); + } }, onCancel: () => { - shiftDownloadItem(); + queue.dequeue(); + if (queue.isEmpty()) { + return; + } + + const nextItem = queue.peek(); + if (nextItem !== undefined) { + downloadItem(nextItem); + } }, }, }; @@ -67,20 +63,23 @@ const createDownloadQueue = () => { }; /** - * Pushes a download item to the queue and returns the new length of the queue. - * If the queue was empty before pushing the item, it will immediately start downloading the item. - * @param item The download item to be pushed to the queue. - * @returns The new length of the queue after pushing the item. + * Enqueues a download item to the queue and returns the new length of the queue. + * If the queue was empty before enqueuing the item, it will immediately start downloading the item. + * @param item The download item to be enqueued to the queue. + * @returns The new length of the queue after enqueuing the item. */ - const pushDownloadItem = (item: DownloadItem): number => { - const len = queue.push(item); - if (queue.length === 1) { - downloadItem(queue[0]); + const enqueueDownloadItem = (item: DownloadItem): number => { + queue.enqueue(item); + if (queue.size() === 1) { + const peekItem = queue.peek(); + if (peekItem !== undefined) { + downloadItem(peekItem); + } } - return len; + return queue.size(); }; - return {pushDownloadItem, shiftDownloadItem}; + return {enqueueDownloadItem, dequeueDownloadItem: queue.dequeue}; }; export default createDownloadQueue; diff --git a/desktop/main.js b/desktop/main.js index ac85b80843eb..cd422d0e8bfa 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -11,6 +11,7 @@ const CONFIG = require('../src/CONFIG').default; const CONST = require('../src/CONST').default; const Localize = require('../src/libs/Localize'); const createDownloadQueue = require('./createDownloadQueue').default; + const port = process.env.PORT || 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; @@ -604,7 +605,7 @@ const mainWindow = () => { ...downloadData, win: browserWindow, }; - downloadQueue.pushDownloadItem(downloadItem); + downloadQueue.enqueueDownloadItem(downloadItem); }); return browserWindow; diff --git a/src/libs/Queue/Queue.ts b/src/libs/Queue/Queue.ts new file mode 100644 index 000000000000..fed45e4bdf78 --- /dev/null +++ b/src/libs/Queue/Queue.ts @@ -0,0 +1,43 @@ +import type Queue from './QueueType'; + +// Function to create a new queue +function createQueue(): Queue { + // Array to hold the elements of the queue + const elements: T[] = []; + + // Function to add an item to the end of the queue + function enqueue(item: T): void { + elements.push(item); + } + + // Function to remove an item from the front of the queue + function dequeue(): T | undefined { + return elements.shift(); + } + + // Function to check if the queue is empty + function isEmpty(): boolean { + return elements.length === 0; + } + + // Function to get the item at the front of the queue without removing it + function peek(): T | undefined { + return elements.length > 0 ? elements[0] : undefined; + } + + // Function to get the number of items in the queue + function size(): number { + return elements.length; + } + + // Return an object with the queue operations + return { + enqueue, + dequeue, + isEmpty, + peek, + size, + }; +} + +export default createQueue; diff --git a/src/libs/Queue/QueueType.ts b/src/libs/Queue/QueueType.ts new file mode 100644 index 000000000000..1b834e6b5010 --- /dev/null +++ b/src/libs/Queue/QueueType.ts @@ -0,0 +1,9 @@ +type Queue = { + enqueue: (item: T) => void; + dequeue: () => T | undefined; + isEmpty: () => boolean; + peek: () => T | undefined; + size: () => number; +}; + +export default Queue; From 9a4429365c3f2ef59f9e778d267d215d097afc0f Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 16 Mar 2024 09:39:16 +0700 Subject: [PATCH 33/47] fix on event args usage --- desktop/contextBridge.ts | 9 ++++++++- desktop/main.ts | 1 + src/libs/fileDownload/index.desktop.ts | 11 +++++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts index 689c69de0cc8..87cf278a43ea 100644 --- a/desktop/contextBridge.ts +++ b/desktop/contextBridge.ts @@ -16,9 +16,16 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.REQUEST_VISIBILITY, ELECTRON_EVENTS.START_UPDATE, ELECTRON_EVENTS.LOCALE_UPDATED, + ELECTRON_EVENTS.DOWNLOAD, ] as const; -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR] as const; +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ + ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, + ELECTRON_EVENTS.UPDATE_DOWNLOADED, + ELECTRON_EVENTS.FOCUS, + ELECTRON_EVENTS.BLUR, + ELECTRON_EVENTS.DOWNLOAD_STARTED, +] as const; const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; diff --git a/desktop/main.ts b/desktop/main.ts index 0c0b8a9801fb..b9a61c2e3265 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -14,6 +14,7 @@ import type {TranslationPaths} from '@src/languages/types'; import type PlatformSpecificUpdater from '@src/setup/platformSetup/types'; import type {Locale} from '@src/types/onyx'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; + const createDownloadQueue = require('./createDownloadQueue').default; const port = process.env.PORT ?? 8082; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 7f5d528eb629..f02659f6a14c 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -13,10 +13,17 @@ const fileDownload: FileDownload = (url, fileName) => { window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); return new Promise((resolve) => { - window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (args) => { - if (args.url !== url) { + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (...args: unknown[]) => { + if (!Array.isArray(args) || args[0] === null || typeof args[0] !== 'object' || !('url' in args[0])) { return; } + + const {url: eventUrl} = args[0] as {url: string}; + + if (eventUrl !== url) { + return; + } + resolve(); }); }); From b9cd5487013009fea15ba8917626308625dcb338 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 2 Apr 2024 08:26:21 +0700 Subject: [PATCH 34/47] re-register context bridge --- desktop/contextBridge.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts index 689c69de0cc8..dc4f73382080 100644 --- a/desktop/contextBridge.ts +++ b/desktop/contextBridge.ts @@ -16,9 +16,10 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.REQUEST_VISIBILITY, ELECTRON_EVENTS.START_UPDATE, ELECTRON_EVENTS.LOCALE_UPDATED, + ELECTRON_EVENTS.DOWNLOAD, ] as const; -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR] as const; +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR, ELECTRON_EVENTS.DOWNLOAD_STARTED] as const; const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; From 5d1665208f33cf4876699f8ea8591c8cd3ec16af Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 2 Apr 2024 08:28:25 +0700 Subject: [PATCH 35/47] fix lint --- desktop/contextBridge.ts | 8 +++++++- desktop/main.ts | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts index dc4f73382080..87cf278a43ea 100644 --- a/desktop/contextBridge.ts +++ b/desktop/contextBridge.ts @@ -19,7 +19,13 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.DOWNLOAD, ] as const; -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR, ELECTRON_EVENTS.DOWNLOAD_STARTED] as const; +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ + ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, + ELECTRON_EVENTS.UPDATE_DOWNLOADED, + ELECTRON_EVENTS.FOCUS, + ELECTRON_EVENTS.BLUR, + ELECTRON_EVENTS.DOWNLOAD_STARTED, +] as const; const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; diff --git a/desktop/main.ts b/desktop/main.ts index 4411aa92fc2b..fa670866bb77 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -14,6 +14,7 @@ import type {TranslationPaths} from '@src/languages/types'; import type PlatformSpecificUpdater from '@src/setup/platformSetup/types'; import type {Locale} from '@src/types/onyx'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; + const createDownloadQueue = require('./createDownloadQueue').default; const port = process.env.PORT ?? 8082; From e45193c81de6fb3b01b854a76b51e6f045ff2e20 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 2 Apr 2024 08:47:54 +0700 Subject: [PATCH 36/47] optimize desktop fileDownload --- src/libs/fileDownload/index.desktop.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 7f5d528eb629..8c9b33d7203a 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -11,13 +11,16 @@ const fileDownload: FileDownload = (url, fileName) => { saveAs: true, }; window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); - return new Promise((resolve) => { - window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (args) => { - if (args.url !== url) { - return; + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (...args: unknown[]) => { + const arg = Array.isArray(args) ? args[0] : null; + const eventUrl = arg && typeof arg === 'object' && 'url' in arg ? arg.url : null; + + // This event is triggered for all active download instances. We intentionally keep other promises waiting. + // Early resolution or rejection of other promises could prematurely stop the loading spinner or prevent the promise from being resolved. + if (eventUrl === url) { + resolve(); } - resolve(); }); }); }; From 3fb286a2950f2abbb79ba0221d8f8328d08d5ee2 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 2 Apr 2024 09:47:55 +0700 Subject: [PATCH 37/47] Add processNextItem to queue, refactor createDownloadQueue --- desktop/createDownloadQueue.ts | 38 ++++++---------------------------- src/libs/Queue/Queue.ts | 22 ++++++++++++++++++-- src/libs/Queue/QueueType.ts | 1 + 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index b7962133fa31..21c2d11f9b8c 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -20,12 +20,8 @@ type DownloadItem = { * @returns An object with methods to enqueue and dequeue download items from the queue. */ const createDownloadQueue = () => { - const queue = createQueue(); + let queue: ReturnType>; - /** - * Downloads the specified item. - * @param item - The item to be downloaded. - */ const downloadItem = (item: DownloadItem): void => { const newItem = { ...item, @@ -34,34 +30,16 @@ const createDownloadQueue = () => { onStarted: () => { item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); }, - onCompleted: () => { - queue.dequeue(); - if (queue.isEmpty()) { - return; - } - - const nextItem = queue.peek(); - if (nextItem !== undefined) { - downloadItem(nextItem); - } - }, - onCancel: () => { - queue.dequeue(); - if (queue.isEmpty()) { - return; - } - - const nextItem = queue.peek(); - if (nextItem !== undefined) { - downloadItem(nextItem); - } - }, + onCompleted: queue.processNextItem, + onCancel: queue.processNextItem, }, }; electronDownload(newItem.win, newItem.url, newItem.options); }; + queue = createQueue(downloadItem); + /** * Enqueues a download item to the queue and returns the new length of the queue. * If the queue was empty before enqueuing the item, it will immediately start downloading the item. @@ -71,14 +49,10 @@ const createDownloadQueue = () => { const enqueueDownloadItem = (item: DownloadItem): number => { queue.enqueue(item); if (queue.size() === 1) { - const peekItem = queue.peek(); - if (peekItem !== undefined) { - downloadItem(peekItem); - } + downloadItem(item); } return queue.size(); }; - return {enqueueDownloadItem, dequeueDownloadItem: queue.dequeue}; }; diff --git a/src/libs/Queue/Queue.ts b/src/libs/Queue/Queue.ts index fed45e4bdf78..496111f18c08 100644 --- a/src/libs/Queue/Queue.ts +++ b/src/libs/Queue/Queue.ts @@ -1,7 +1,7 @@ import type Queue from './QueueType'; // Function to create a new queue -function createQueue(): Queue { +function createQueue(processItem: (item: T) => void): Queue { // Array to hold the elements of the queue const elements: T[] = []; @@ -30,6 +30,23 @@ function createQueue(): Queue { return elements.length; } + /** + * Processes the next item in the queue. + * If the queue is not empty, it dequeues the next item and processes it. + * If the queue is empty, it does nothing. + */ + function processNextItem(): void { + dequeue(); + if (isEmpty()) { + return; + } + + const nextItem = peek(); + if (nextItem !== undefined) { + processItem(nextItem); + } + } + // Return an object with the queue operations return { enqueue, @@ -37,7 +54,8 @@ function createQueue(): Queue { isEmpty, peek, size, + processNextItem, }; } -export default createQueue; +export default createQueue; \ No newline at end of file diff --git a/src/libs/Queue/QueueType.ts b/src/libs/Queue/QueueType.ts index 1b834e6b5010..5990cf29579d 100644 --- a/src/libs/Queue/QueueType.ts +++ b/src/libs/Queue/QueueType.ts @@ -4,6 +4,7 @@ type Queue = { isEmpty: () => boolean; peek: () => T | undefined; size: () => number; + processNextItem: () => void; }; export default Queue; From 049d98ab2221f4f6fd12fe9b87e4fc079f9f601e Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 2 Apr 2024 10:00:42 +0700 Subject: [PATCH 38/47] prettier --- src/libs/Queue/Queue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Queue/Queue.ts b/src/libs/Queue/Queue.ts index 496111f18c08..d2266d48950a 100644 --- a/src/libs/Queue/Queue.ts +++ b/src/libs/Queue/Queue.ts @@ -58,4 +58,4 @@ function createQueue(processItem: (item: T) => void): Queue { }; } -export default createQueue; \ No newline at end of file +export default createQueue; From 51c1126ace7f85f2693ea3f34323a180a119f01a Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 23 Apr 2024 10:51:37 +0700 Subject: [PATCH 39/47] Add run function to Queue --- desktop/createDownloadQueue.ts | 38 +++++++---------------- src/libs/Queue/Queue.ts | 55 +++++++++++++++++++--------------- src/libs/Queue/QueueType.ts | 2 +- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 21c2d11f9b8c..66afe0ff9e1e 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -15,43 +15,25 @@ type DownloadItem = { options: Options; }; -/** - * Creates a download queue. - * @returns An object with methods to enqueue and dequeue download items from the queue. - */ const createDownloadQueue = () => { - let queue: ReturnType>; - - const downloadItem = (item: DownloadItem): void => { - const newItem = { - ...item, - options: { + const downloadItem = (item: DownloadItem): Promise => + new Promise((resolve) => { + const options = { ...item.options, onStarted: () => { item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); }, - onCompleted: queue.processNextItem, - onCancel: queue.processNextItem, - }, - }; + onCompleted: () => resolve(), + onCancel: () => resolve(), + }; - electronDownload(newItem.win, newItem.url, newItem.options); - }; + electronDownload(item.win, item.url, options); + }); - queue = createQueue(downloadItem); + const queue = createQueue(downloadItem); - /** - * Enqueues a download item to the queue and returns the new length of the queue. - * If the queue was empty before enqueuing the item, it will immediately start downloading the item. - * @param item The download item to be enqueued to the queue. - * @returns The new length of the queue after enqueuing the item. - */ - const enqueueDownloadItem = (item: DownloadItem): number => { + const enqueueDownloadItem = (item: DownloadItem): void => { queue.enqueue(item); - if (queue.size() === 1) { - downloadItem(item); - } - return queue.size(); }; return {enqueueDownloadItem, dequeueDownloadItem: queue.dequeue}; }; diff --git a/src/libs/Queue/Queue.ts b/src/libs/Queue/Queue.ts index d2266d48950a..358817e62d9b 100644 --- a/src/libs/Queue/Queue.ts +++ b/src/libs/Queue/Queue.ts @@ -1,14 +1,10 @@ import type Queue from './QueueType'; // Function to create a new queue -function createQueue(processItem: (item: T) => void): Queue { +function createQueue(processItem: (item: T) => Promise): Queue { // Array to hold the elements of the queue const elements: T[] = []; - - // Function to add an item to the end of the queue - function enqueue(item: T): void { - elements.push(item); - } + let isProcessing = false; // Function to remove an item from the front of the queue function dequeue(): T | undefined { @@ -20,6 +16,34 @@ function createQueue(processItem: (item: T) => void): Queue { return elements.length === 0; } + // Initiates the processing of items in the queue. + // Continues to dequeue and process items as long as the queue is not empty. + // Sets the `isProcessing` flag to true at the start and resets it to false once all items have been processed. + function run(): Promise { + return new Promise((resolve) => { + isProcessing = true; + function processNext() { + if (!isEmpty()) { + const nextItem = dequeue(); + if (nextItem) { + processItem(nextItem).then(processNext); + } + } else { + isProcessing = false; + resolve(); + } + } + processNext(); + }); + } + // Adds an item to the queue and initiates processing if not already in progress + function enqueue(item: T): void { + elements.push(item); + if (!isProcessing) { + run(); + } + } + // Function to get the item at the front of the queue without removing it function peek(): T | undefined { return elements.length > 0 ? elements[0] : undefined; @@ -30,31 +54,14 @@ function createQueue(processItem: (item: T) => void): Queue { return elements.length; } - /** - * Processes the next item in the queue. - * If the queue is not empty, it dequeues the next item and processes it. - * If the queue is empty, it does nothing. - */ - function processNextItem(): void { - dequeue(); - if (isEmpty()) { - return; - } - - const nextItem = peek(); - if (nextItem !== undefined) { - processItem(nextItem); - } - } - // Return an object with the queue operations return { + run, enqueue, dequeue, isEmpty, peek, size, - processNextItem, }; } diff --git a/src/libs/Queue/QueueType.ts b/src/libs/Queue/QueueType.ts index 5990cf29579d..a38eb0864e0a 100644 --- a/src/libs/Queue/QueueType.ts +++ b/src/libs/Queue/QueueType.ts @@ -1,10 +1,10 @@ type Queue = { + run(): Promise; enqueue: (item: T) => void; dequeue: () => T | undefined; isEmpty: () => boolean; peek: () => T | undefined; size: () => number; - processNextItem: () => void; }; export default Queue; From 36a80b5edede3ac0e36d50578a73bf3625438613 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 3 May 2024 20:15:24 +0700 Subject: [PATCH 40/47] extract processNext --- src/libs/Queue/Queue.ts | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/libs/Queue/Queue.ts b/src/libs/Queue/Queue.ts index 358817e62d9b..0a70c342c57c 100644 --- a/src/libs/Queue/Queue.ts +++ b/src/libs/Queue/Queue.ts @@ -16,26 +16,29 @@ function createQueue(processItem: (item: T) => Promise): Queue { return elements.length === 0; } - // Initiates the processing of items in the queue. - // Continues to dequeue and process items as long as the queue is not empty. - // Sets the `isProcessing` flag to true at the start and resets it to false once all items have been processed. - function run(): Promise { + // Function to process the next item in the queue + function processNext(): Promise { return new Promise((resolve) => { - isProcessing = true; - function processNext() { - if (!isEmpty()) { - const nextItem = dequeue(); - if (nextItem) { - processItem(nextItem).then(processNext); - } - } else { - isProcessing = false; - resolve(); + if (!isEmpty()) { + const nextItem = dequeue(); + if (nextItem) { + processItem(nextItem).then(() => processNext().then(resolve)); } + } else { + isProcessing = false; + resolve(); } - processNext(); }); } + + // Initiates the processing of items in the queue. + // Continues to dequeue and process items as long as the queue is not empty. + // Sets the `isProcessing` flag to true at the start and resets it to false once all items have been processed. + function run(): Promise { + isProcessing = true; + return processNext(); + } + // Adds an item to the queue and initiates processing if not already in progress function enqueue(item: T): void { elements.push(item); From be98d19d559d388d22a739fa62f4c6b6a4928d00 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 3 May 2024 20:30:32 +0700 Subject: [PATCH 41/47] run prettier, add resolve with timeout to fileDownload to prevent indefinite hanging --- src/libs/Queue/Queue.ts | 2 +- src/libs/fileDownload/index.desktop.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/Queue/Queue.ts b/src/libs/Queue/Queue.ts index 0a70c342c57c..99f7a57b7e40 100644 --- a/src/libs/Queue/Queue.ts +++ b/src/libs/Queue/Queue.ts @@ -38,7 +38,7 @@ function createQueue(processItem: (item: T) => Promise): Queue { isProcessing = true; return processNext(); } - + // Adds an item to the queue and initiates processing if not already in progress function enqueue(item: T): void { elements.push(item); diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 8c9b33d7203a..84e3200ecb37 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -12,6 +12,11 @@ const fileDownload: FileDownload = (url, fileName) => { }; window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); return new Promise((resolve) => { + // This sets a timeout that will resolve the promise after 5 seconds to prevent indefinite hanging + const timeout = setTimeout(() => { + resolve(); + }, 5000); + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (...args: unknown[]) => { const arg = Array.isArray(args) ? args[0] : null; const eventUrl = arg && typeof arg === 'object' && 'url' in arg ? arg.url : null; @@ -19,6 +24,7 @@ const fileDownload: FileDownload = (url, fileName) => { // This event is triggered for all active download instances. We intentionally keep other promises waiting. // Early resolution or rejection of other promises could prematurely stop the loading spinner or prevent the promise from being resolved. if (eventUrl === url) { + clearTimeout(timeout); resolve(); } }); From 2e5b16933eba98a52ca197ca37eae5edad5787c9 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 6 May 2024 22:20:33 +0700 Subject: [PATCH 42/47] remove electrondownloadmanager abstraction, handle error message, refactor processor name --- desktop/createDownloadQueue.ts | 49 ++++++--- desktop/electronDownloadManager.ts | 165 ----------------------------- src/libs/Queue/Queue.ts | 12 ++- 3 files changed, 48 insertions(+), 178 deletions(-) delete mode 100644 desktop/electronDownloadManager.ts diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 66afe0ff9e1e..265f4c19b511 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -1,7 +1,7 @@ import type {BrowserWindow} from 'electron'; +import {app} from 'electron'; import createQueue from '@libs/Queue/Queue'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; -import electronDownload from './electronDownloadManager'; import type {Options} from './electronDownloadManagerType'; type DownloadItem = { @@ -16,21 +16,46 @@ type DownloadItem = { }; const createDownloadQueue = () => { - const downloadItem = (item: DownloadItem): Promise => - new Promise((resolve) => { - const options = { - ...item.options, - onStarted: () => { - item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); - }, - onCompleted: () => resolve(), - onCancel: () => resolve(), + const downloadItemProcessor = (item: DownloadItem): Promise => + new Promise((resolve, reject) => { + item.win.webContents.downloadURL(item.url); + + const listener = (event: Electron.Event, downloadItem: Electron.DownloadItem) => { + const cleanup = () => item.win.webContents.session.removeListener('will-download', listener); + const errorMessage = `The download of ${downloadItem.getFilename()} was interrupted`; + + downloadItem.on('updated', (_, state) => { + if (state !== 'interrupted') { + return; + } + + cleanup(); + reject(new Error(errorMessage)); + downloadItem.cancel(); + }); + + downloadItem.on('done', (_, state) => { + cleanup(); + if (state === 'cancelled') { + resolve(); + } else if (state === 'interrupted') { + reject(new Error(errorMessage)); + } else if (state === 'completed') { + if (process.platform === 'darwin') { + const savePath = downloadItem.getSavePath(); + app.dock.downloadFinished(savePath); + } + resolve(); + } + }); + + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); }; - electronDownload(item.win, item.url, options); + item.win.webContents.session.on('will-download', listener); }); - const queue = createQueue(downloadItem); + const queue = createQueue(downloadItemProcessor); const enqueueDownloadItem = (item: DownloadItem): void => { queue.enqueue(item); diff --git a/desktop/electronDownloadManager.ts b/desktop/electronDownloadManager.ts deleted file mode 100644 index acf048f3053e..000000000000 --- a/desktop/electronDownloadManager.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * This file is a replicated version of the `electron-dl` package. - * It provides a download manager for Electron applications. - * The package simplifies the process of downloading files in Electron apps - * by providing a high-level API and handling various download-related tasks. - * We decided to replicate the functionality of the `electron-dl` package for easier maintenance. - * More context: https://github.com/Expensify/App/issues/35189#issuecomment-1959681109 - */ -import type {BrowserView, BrowserWindow, DownloadItem, Event, Session} from 'electron'; -import {app, shell} from 'electron'; -import * as path from 'path'; -import type {Options} from './electronDownloadManagerType'; - -/** -Error thrown if `item.cancel()` was called. -*/ -class CancelError extends Error {} - -/** - * Returns the filename with extension based on the given name and MIME type. - * @param name - The name of the file. - * @param mime - The MIME type of the file. - * @returns The filename with extension. - */ -const getFilenameFromMime = (name: string, mime: string): string => { - const extensions = mime.split('/').pop(); - return `${name}.${extensions}`; -}; - -/** - * Registers a listener for download events in Electron session. - * - * @param session - The Electron session to register the listener on. - * @param options - The previous options for the download manager. - * @param callback - The callback function to be called when a download event occurs. - */ -const registerListener = (session: Session, options: Options, callback: (error: Error | null, item?: DownloadItem) => void = () => {}): void => { - const downloadItems = new Set(); - const listener = (event: Event, item: DownloadItem): void => { - downloadItems.add(item); - - if (options.directory && !path.isAbsolute(options.directory)) { - throw new Error('The `directory` option must be an absolute path'); - } - - const directory = options.directory ?? app.getPath('downloads'); - - let filePath: string; - if (options.filename) { - filePath = path.join(directory, options.filename); - } else { - const filename = item.getFilename(); - const name = path.extname(filename) ? filename : getFilenameFromMime(filename, item.getMimeType()); - - filePath = options.overwrite ? path.join(directory, name) : path.join(directory, name); - } - - if (options.saveAs) { - item.setSaveDialogOptions({defaultPath: filePath, ...options.dialogOptions}); - } else { - item.setSavePath(filePath); - } - - item.on('updated', (doneEvent: Event, state: string) => { - if (state !== 'interrupted') { - return; - } - - const errorMessage = `The download of ${path.basename(filePath)} was interrupted`; - item.cancel(); - callback(new Error(errorMessage)); - }); - - item.on('done', (doneEvent: Event, state: string) => { - downloadItems.delete(item); - - if (options.unregisterWhenDone) { - session.removeListener('will-download', listener); - } - - if (state === 'cancelled') { - if (typeof options.onCancel === 'function') { - options.onCancel(item); - } - - callback(new CancelError()); - } else if (state === 'interrupted') { - const errorMessage = `The download of ${path.basename(filePath)} was interrupted`; - - callback(new Error(errorMessage)); - } else if (state === 'completed') { - const savePath = item.getSavePath(); - - if (process.platform === 'darwin') { - app.dock.downloadFinished(savePath); - } - - if (options.openFolderWhenDone) { - shell.showItemInFolder(savePath); - } - - if (typeof options.onCompleted === 'function') { - options.onCompleted({ - filename: item.getFilename(), - path: savePath, - fileSize: item.getReceivedBytes(), - mimeType: item.getMimeType(), - url: item.getURL(), - }); - } - - callback(null, item); - } - }); - - if (typeof options.onStarted === 'function') { - options.onStarted(item); - } - }; - - session.on('will-download', listener); -}; - -/** -This can be useful if you need download functionality in a reusable module. - -@param window - Window to register the behavior on. -@param url - URL to download. -@returns A promise for the downloaded file. -@throws {CancelError} An error if the user calls `item.cancel()`. -@throws {Error} An error if the download fails. - -@example -``` -import {BrowserWindow, ipcMain} from 'electron'; -import electronDownloadManager = require('./electronDownloadManager'); - -ipcMain.on('download-button', async (event, {url}) => { - const win = BrowserWindow.getFocusedWindow(); - console.log(await electronDownloadManager.download(win, url)); -}); -``` -*/ -const download = (electronWindow: BrowserWindow | BrowserView, url: string, prevOptions?: Options): Promise => { - const options = { - ...prevOptions, - unregisterWhenDone: true, - }; - - return new Promise((resolve, reject) => { - registerListener(electronWindow.webContents.session, options, (error: Error | null, item?: DownloadItem) => { - if (error) { - reject(error); - } else if (item) { - resolve(item); - } else { - reject(new Error('Download item is undefined.')); - } - }); - - electronWindow.webContents.downloadURL(url); - }); -}; - -export default download; diff --git a/src/libs/Queue/Queue.ts b/src/libs/Queue/Queue.ts index 99f7a57b7e40..8b2011e64371 100644 --- a/src/libs/Queue/Queue.ts +++ b/src/libs/Queue/Queue.ts @@ -1,3 +1,5 @@ +import Log from '@libs/Log'; +import CONST from '@src/CONST'; import type Queue from './QueueType'; // Function to create a new queue @@ -22,7 +24,15 @@ function createQueue(processItem: (item: T) => Promise): Queue { if (!isEmpty()) { const nextItem = dequeue(); if (nextItem) { - processItem(nextItem).then(() => processNext().then(resolve)); + processItem(nextItem) + .catch((error: Error) => { + const errorMessage = error.message ?? CONST.ERROR.UNKNOWN_ERROR; + + Log.hmmm('Queue error:', {errorMessage}); + }) + .finally(() => { + processNext().then(resolve); + }); } } else { isProcessing = false; From 8625cf3dc0f9657586924f34bc96992325f2e9a0 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 6 May 2024 23:40:06 +0700 Subject: [PATCH 43/47] fix bug in file name --- desktop/createDownloadQueue.ts | 47 +++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 265f4c19b511..086b086e079c 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -1,5 +1,6 @@ import type {BrowserWindow} from 'electron'; import {app} from 'electron'; +import * as path from 'path'; import createQueue from '@libs/Queue/Queue'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; import type {Options} from './electronDownloadManagerType'; @@ -15,26 +16,60 @@ type DownloadItem = { options: Options; }; +/** + * Returns the filename with extension based on the given name and MIME type. + * @param name - The name of the file. + * @param mime - The MIME type of the file. + * @returns The filename with extension. + */ +const getFilenameFromMime = (name: string, mime: string): string => { + const extensions = mime.split('/').pop(); + return `${name}.${extensions}`; +}; + const createDownloadQueue = () => { const downloadItemProcessor = (item: DownloadItem): Promise => new Promise((resolve, reject) => { item.win.webContents.downloadURL(item.url); - const listener = (event: Electron.Event, downloadItem: Electron.DownloadItem) => { + const listener = (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => { + const options = item.options; const cleanup = () => item.win.webContents.session.removeListener('will-download', listener); - const errorMessage = `The download of ${downloadItem.getFilename()} was interrupted`; + const errorMessage = `The download of ${electronDownloadItem.getFilename()} was interrupted`; + + if (options.directory && !path.isAbsolute(options.directory)) { + throw new Error('The `directory` option must be an absolute path'); + } + + const directory = options.directory ?? app.getPath('downloads'); + + let filePath: string; + if (options.filename) { + filePath = path.join(directory, options.filename); + } else { + const filename = electronDownloadItem.getFilename(); + const name = path.extname(filename) ? filename : getFilenameFromMime(filename, electronDownloadItem.getMimeType()); + + filePath = options.overwrite ? path.join(directory, name) : path.join(directory, name); + } + + if (options.saveAs) { + electronDownloadItem.setSaveDialogOptions({defaultPath: filePath, ...options.dialogOptions}); + } else { + electronDownloadItem.setSavePath(filePath); + } - downloadItem.on('updated', (_, state) => { + electronDownloadItem.on('updated', (_, state) => { if (state !== 'interrupted') { return; } cleanup(); reject(new Error(errorMessage)); - downloadItem.cancel(); + electronDownloadItem.cancel(); }); - downloadItem.on('done', (_, state) => { + electronDownloadItem.on('done', (_, state) => { cleanup(); if (state === 'cancelled') { resolve(); @@ -42,7 +77,7 @@ const createDownloadQueue = () => { reject(new Error(errorMessage)); } else if (state === 'completed') { if (process.platform === 'darwin') { - const savePath = downloadItem.getSavePath(); + const savePath = electronDownloadItem.getSavePath(); app.dock.downloadFinished(savePath); } resolve(); From 9403c7d7190a062313890a1f90e10ee3f30d8f5a Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 10 May 2024 05:41:50 +0700 Subject: [PATCH 44/47] add timeout to downloadItemProcessor --- desktop/createDownloadQueue.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 086b086e079c..aa562cbcc9f2 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -30,11 +30,20 @@ const getFilenameFromMime = (name: string, mime: string): string => { const createDownloadQueue = () => { const downloadItemProcessor = (item: DownloadItem): Promise => new Promise((resolve, reject) => { - item.win.webContents.downloadURL(item.url); + let downloadTimeout: NodeJS.Timeout; + let downloadListener: (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => void; + + // Define the timeout function + const timeoutFunction = () => { + item.win.webContents.session.removeListener('will-download', downloadListener); + resolve(); + }; + + const listenerFunction = (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => { + clearTimeout(downloadTimeout); - const listener = (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => { const options = item.options; - const cleanup = () => item.win.webContents.session.removeListener('will-download', listener); + const cleanup = () => item.win.webContents.session.removeListener('will-download', listenerFunction); const errorMessage = `The download of ${electronDownloadItem.getFilename()} was interrupted`; if (options.directory && !path.isAbsolute(options.directory)) { @@ -87,7 +96,11 @@ const createDownloadQueue = () => { item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); }; - item.win.webContents.session.on('will-download', listener); + downloadTimeout = setTimeout(timeoutFunction, 5000); + downloadListener = listenerFunction; + + item.win.webContents.downloadURL(item.url); + item.win.webContents.session.on('will-download', downloadListener); }); const queue = createQueue(downloadItemProcessor); From 6c5d186c3179251d83f1abb437a3e4616a1191c0 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 10 May 2024 05:53:19 +0700 Subject: [PATCH 45/47] move delay time to const --- desktop/createDownloadQueue.ts | 3 ++- src/CONST.ts | 1 + src/libs/fileDownload/index.desktop.ts | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index aa562cbcc9f2..579e66d2691b 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -2,6 +2,7 @@ import type {BrowserWindow} from 'electron'; import {app} from 'electron'; import * as path from 'path'; import createQueue from '@libs/Queue/Queue'; +import CONST from '@src/CONST'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; import type {Options} from './electronDownloadManagerType'; @@ -96,7 +97,7 @@ const createDownloadQueue = () => { item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); }; - downloadTimeout = setTimeout(timeoutFunction, 5000); + downloadTimeout = setTimeout(timeoutFunction, CONST.DOWNLOADS_TIMEOUT); downloadListener = listenerFunction; item.win.webContents.downloadURL(item.url); diff --git a/src/CONST.ts b/src/CONST.ts index efb0463457b1..66313ae35994 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4734,6 +4734,7 @@ const CONST = { MAX_TAX_RATE_DECIMAL_PLACES: 4, DOWNLOADS_PATH: '/Downloads', + DOWNLOADS_TIMEOUT: 5000, NEW_EXPENSIFY_PATH: '/New Expensify', ENVIRONMENT_SUFFIX: { diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 84e3200ecb37..49fca11c729d 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,5 +1,6 @@ import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; import type {Options} from '@desktop/electronDownloadManagerType'; +import CONST from '@src/CONST'; import type {FileDownload} from './types'; /** @@ -13,9 +14,9 @@ const fileDownload: FileDownload = (url, fileName) => { window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); return new Promise((resolve) => { // This sets a timeout that will resolve the promise after 5 seconds to prevent indefinite hanging - const timeout = setTimeout(() => { + const downloadTimeout = setTimeout(() => { resolve(); - }, 5000); + }, CONST.DOWNLOADS_TIMEOUT); window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (...args: unknown[]) => { const arg = Array.isArray(args) ? args[0] : null; @@ -24,7 +25,7 @@ const fileDownload: FileDownload = (url, fileName) => { // This event is triggered for all active download instances. We intentionally keep other promises waiting. // Early resolution or rejection of other promises could prematurely stop the loading spinner or prevent the promise from being resolved. if (eventUrl === url) { - clearTimeout(timeout); + clearTimeout(downloadTimeout); resolve(); } }); From 99d3e044debe60dce01278b8e8b57ee5934e1e37 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 10 May 2024 08:43:35 +0700 Subject: [PATCH 46/47] remove unnecessary type --- desktop/createDownloadQueue.ts | 2 +- desktop/electronDownloadManagerType.ts | 64 +------------------------- src/libs/fileDownload/index.desktop.ts | 2 +- 3 files changed, 4 insertions(+), 64 deletions(-) diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 579e66d2691b..29ce595fc5cc 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import createQueue from '@libs/Queue/Queue'; import CONST from '@src/CONST'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; -import type {Options} from './electronDownloadManagerType'; +import type Options from './electronDownloadManagerType'; type DownloadItem = { // The window where the download will be initiated diff --git a/desktop/electronDownloadManagerType.ts b/desktop/electronDownloadManagerType.ts index 0a0bea9f36b6..755efe173887 100644 --- a/desktop/electronDownloadManagerType.ts +++ b/desktop/electronDownloadManagerType.ts @@ -1,21 +1,5 @@ -import type {DownloadItem, SaveDialogOptions} from 'electron'; +import type {SaveDialogOptions} from 'electron'; -type File = { - // The name of the file being downloaded - filename: string; - - // The path where the file is being downloaded to - path: string; - - // The size of the file being downloaded, in bytes - fileSize: number; - - // The MIME type of the file being downloaded - mimeType: string; - - // The URL of the file being downloaded - url: string; -}; type Options = { /** Show a `Save As…` dialog instead of downloading immediately. @@ -43,47 +27,6 @@ type Options = { */ readonly filename?: string; - /** - Title of the error dialog. Can be customized for localization. - - Note: Error dialog will not be shown in `electronDownloadManager.download()`. Please handle error manually. - - @default 'Download Error' - */ - readonly errorTitle?: string; - - /** - Message of the error dialog. `{filename}` is replaced with the name of the actual file. Can be customized for localization. - - Note: Error dialog will not be shown in `electronDownloadManager.download()`. Please handle error manually. - - @default 'The download of {filename} was interrupted' - */ - readonly errorMessage?: string; - - /** - Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item). - You can use this for advanced handling such as canceling the item like `item.cancel()`. - */ - readonly onStarted?: (item: DownloadItem) => void; - - /** - Optional callback that receives the [download item](https://electronjs.org/docs/api/download-item) for which the download has been cancelled. - */ - readonly onCancel?: (item: DownloadItem) => void; - - /** - Optional callback that receives an object with information about an item that has been completed. It is called for each completed item. - */ - readonly onCompleted?: (file: File) => void; - - /** - Reveal the downloaded file in the system file manager, and if possible, select the file. - - @default false - */ - readonly openFolderWhenDone?: boolean; - /** Allow downloaded files to overwrite files with the same name in the directory they are saved to. @@ -101,9 +44,6 @@ type Options = { @default {} */ readonly dialogOptions?: SaveDialogOptions; - - /** Unregister the listener when the download is done. */ - readonly unregisterWhenDone?: boolean; }; -export type {Options, File}; +export default Options; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 49fca11c729d..90faa1285bee 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,5 +1,5 @@ import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; -import type {Options} from '@desktop/electronDownloadManagerType'; +import type Options from '@desktop/electronDownloadManagerType'; import CONST from '@src/CONST'; import type {FileDownload} from './types'; From c792724c1dbd6a78dcd818984b2fa10ffa5ac5f7 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Fri, 17 May 2024 15:53:30 +0700 Subject: [PATCH 47/47] change download_started to completed, failed and canceled --- desktop/ELECTRON_EVENTS.ts | 4 +++- desktop/contextBridge.ts | 4 +++- desktop/createDownloadQueue.ts | 7 ++++--- src/libs/fileDownload/index.desktop.ts | 10 ++++++---- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/desktop/ELECTRON_EVENTS.ts b/desktop/ELECTRON_EVENTS.ts index cb0b05f0974f..b06794567c7d 100644 --- a/desktop/ELECTRON_EVENTS.ts +++ b/desktop/ELECTRON_EVENTS.ts @@ -10,7 +10,9 @@ const ELECTRON_EVENTS = { START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', DOWNLOAD: 'download', - DOWNLOAD_STARTED: 'download-started', + DOWNLOAD_COMPLETED: 'download-completed', + DOWNLOAD_FAILED: 'download-started', + DOWNLOAD_CANCELED: 'download-canceled', SILENT_UPDATE: 'silent-update', } as const; diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts index e9c61e69539e..a18fb408b1ec 100644 --- a/desktop/contextBridge.ts +++ b/desktop/contextBridge.ts @@ -25,7 +25,9 @@ const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR, - ELECTRON_EVENTS.DOWNLOAD_STARTED, + ELECTRON_EVENTS.DOWNLOAD_COMPLETED, + ELECTRON_EVENTS.DOWNLOAD_FAILED, + ELECTRON_EVENTS.DOWNLOAD_CANCELED, ] as const; const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 29ce595fc5cc..132848c5da9e 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -34,7 +34,6 @@ const createDownloadQueue = () => { let downloadTimeout: NodeJS.Timeout; let downloadListener: (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => void; - // Define the timeout function const timeoutFunction = () => { item.win.webContents.session.removeListener('will-download', downloadListener); resolve(); @@ -74,6 +73,7 @@ const createDownloadQueue = () => { return; } + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELED, {url: item.url}); cleanup(); reject(new Error(errorMessage)); electronDownloadItem.cancel(); @@ -82,19 +82,20 @@ const createDownloadQueue = () => { electronDownloadItem.on('done', (_, state) => { cleanup(); if (state === 'cancelled') { + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELED, {url: item.url}); resolve(); } else if (state === 'interrupted') { + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_FAILED, {url: item.url}); reject(new Error(errorMessage)); } else if (state === 'completed') { if (process.platform === 'darwin') { const savePath = electronDownloadItem.getSavePath(); app.dock.downloadFinished(savePath); } + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, {url: item.url}); resolve(); } }); - - item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_STARTED, {url: item.url}); }; downloadTimeout = setTimeout(timeoutFunction, CONST.DOWNLOADS_TIMEOUT); diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 90faa1285bee..8e682225b79a 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -18,17 +18,19 @@ const fileDownload: FileDownload = (url, fileName) => { resolve(); }, CONST.DOWNLOADS_TIMEOUT); - window.electron.on(ELECTRON_EVENTS.DOWNLOAD_STARTED, (...args: unknown[]) => { + const handleDownloadStatus = (...args: unknown[]) => { const arg = Array.isArray(args) ? args[0] : null; const eventUrl = arg && typeof arg === 'object' && 'url' in arg ? arg.url : null; - // This event is triggered for all active download instances. We intentionally keep other promises waiting. - // Early resolution or rejection of other promises could prematurely stop the loading spinner or prevent the promise from being resolved. if (eventUrl === url) { clearTimeout(downloadTimeout); resolve(); } - }); + }; + + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, handleDownloadStatus); + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_FAILED, handleDownloadStatus); + window.electron.on(ELECTRON_EVENTS.DOWNLOAD_CANCELED, handleDownloadStatus); }); };