Skip to content

feat: handle download links #840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions src/app/applyDownloadNotification.js

This file was deleted.

90 changes: 90 additions & 0 deletions src/app/downloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import path from 'node:path'
import { Notification, shell } from 'electron'
import type { BrowserWindow } from 'electron'

/**
* Suggested filenames for download URLs.
* In a Web-Browser, "download" attributes is used to suggest a filename for a download URL.
* Unfortunately, Electron does not support the `download` attribute on anchor tags.
* And there is no way to set the filename in the webContents.downloadURL API to get it on `will-download` event.
* It is necessary to keep track of suggested filenames for download URLs manually.
*/
const suggestedNames: Map<string, string> = new Map()

/**
* Push a suggested filename for a download URL
* @param url - The URL to suggest a filename for
* @param filename - The suggested filename
*/
export function pushDownloadUrlFilenameSuggestion(url: string, filename: string) {
suggestedNames.set(url, filename)
}

/**
* Get a suggested filename for a download URL and remove it from the list
* @param url - The URL to pop a suggested filename for
* @return - The suggested filename if any
*/
function popDownloadUrlFilenameSuggestion(url: string): string | undefined {
const name = suggestedNames.get(url)
suggestedNames.delete(url)
return name
}

/**
* Trigger a download of a URL
* @param browserWindow - Browser window to use as a download context
* @param url - URL to download
* @param filename - Suggested filename for the download if any, otherwise determined from the URL
*/
export function triggerDownloadUrl(browserWindow: BrowserWindow, url: string, filename?: string) {
if (filename) {
pushDownloadUrlFilenameSuggestion(url, filename)
}
browserWindow.webContents.downloadURL(url)
}

/**
* Handle downloads from a browser window to:
* - show notifications
* - use suggested filenames
* @param browserWindow - Browser window
*/
export function applyDownloadHandler(browserWindow: BrowserWindow) {
browserWindow.webContents.session.on('will-download', (event, item) => {
const suggestedFilename = popDownloadUrlFilenameSuggestion(item.getURL())
if (suggestedFilename) {
item.setSaveDialogOptions({
defaultPath: suggestedFilename,
})
}

item.once('done', (event, state) => {
const pathToFile = item.getSavePath()
const { base, dir } = path.parse(pathToFile)
let notification

if (state === 'completed') {
notification = new Notification({
title: 'Download complete',
body: `File '${base}' can be found at '${dir}'.`,
})
notification.on('click', () => {
shell.showItemInFolder(pathToFile)
})
} else if (state === 'interrupted') {
notification = new Notification({
title: 'Download Failed',
body: `Something went wrong with the download of '${base}'.`,
})
}

notification?.show()
})
})
}
1 change: 0 additions & 1 deletion src/app/externalLinkHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ function isExternalLink(url) {
* @return {{action: 'deny'} | {action: 'allow', outlivesOpener?: boolean, overrideBrowserWindowOptions?: import('electron').BrowserWindowConstructorOptions}}
*/
function windowOpenExternalLinkHandler(details, browserWindowOptions = {}) {
// TODO: Should handle different types of details.disposition? I.e. save-to-disk?
if (isExternalLink(details.url)) {
shell.openExternal(details.url)
return { action: 'deny' }
Expand Down
3 changes: 3 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { createTalkWindow } = require('./talk/talk.window.js')
const { createWelcomeWindow } = require('./welcome/welcome.window.js')
const { installVueDevtools } = require('./install-vue-devtools.js')
const { loadAppConfig, getAppConfig, setAppConfig } = require('./app/AppConfig.ts')
const { triggerDownloadUrl } = require('./app/downloads.ts')

/**
* Parse command line arguments
Expand Down Expand Up @@ -275,6 +276,8 @@ app.whenReady().then(async () => {
isInWindowRelaunch = false
})

ipcMain.on('app:downloadURL', (event, url, filename) => triggerDownloadUrl(mainWindow, url, filename))

// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on('activate', () => {
Expand Down
6 changes: 6 additions & 0 deletions src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ const TALK_DESKTOP = {
* @return {Promise<void>}
*/
setAppConfig: (key, value) => ipcRenderer.invoke('app:config:set', key, value),
/**
* Trigger download of a URL
* @param {string} url - URL to download
* @param {string} [filename] - Filename suggestion for the download
*/
downloadURL: (url, filename) => ipcRenderer.send('app:downloadURL', url, filename),
/**
* Send appData to main process on restore
*
Expand Down
14 changes: 14 additions & 0 deletions src/shared/setupWebPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,19 @@ function applyHeaderHeight() {
document.documentElement.style.setProperty('--header-height', `${TITLE_BAR_HEIGHT}px`, 'important')
}

/**
* Handle download links
*/
function applyDownloadLinkHandler() {
document.addEventListener('click', (event) => {
const link = event.target.closest('a')
if (link && link.hasAttribute('download')) {
event.preventDefault()
window.TALK_DESKTOP.downloadURL(link.href, link.download)
}
})
}

/**
* Make all required initial setup for the web page for authorized user: server-rendered data, globals and ect.
*/
Expand All @@ -214,4 +227,5 @@ export async function setupWebPage() {
applyHeaderHeight()
applyAxiosInterceptors()
await applyL10n()
applyDownloadLinkHandler()
}
4 changes: 2 additions & 2 deletions src/talk/talk.window.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const { BrowserWindow, screen, nativeTheme } = require('electron')
const { applyExternalLinkHandler } = require('../app/externalLinkHandlers.js')
const { applyContextMenu } = require('../app/applyContextMenu.js')
const { applyDownloadNotification } = require('../app/applyDownloadNotification.js')
const { applyDownloadHandler } = require('../app/downloads.ts')
const { applyWheelZoom } = require('../app/applyWheelZoom.js')
const { setupTray } = require('../app/app.tray.js')
const { getBrowserWindowIcon, getTrayIcon } = require('../shared/icons.utils.js')
Expand Down Expand Up @@ -63,7 +63,7 @@ function createTalkWindow() {
})

applyContextMenu(window)
applyDownloadNotification(window)
applyDownloadHandler(window)
applyWheelZoom(window)

const tray = setupTray(window)
Expand Down