Typescript - Completed download manager

This commit is contained in:
Kiran Niranjan 2019-01-04 17:06:11 +05:30
parent eeb1ff295e
commit 883c0ba3af
6 changed files with 292 additions and 3 deletions

View File

@ -10,6 +10,7 @@ import { screenSnippet } from './screen-snippet-handler';
import { activate, handleKeyPress } from './window-actions';
import { windowHandler } from './window-handler';
import {
downloadManagerAction,
isValidWindow,
sanitize,
setDataUrl,
@ -131,7 +132,12 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
if (typeof displayId === 'string' && typeof id === 'number') {
windowHandler.createScreenSharingIndicatorWindow(event.sender, displayId, id);
}
break;
break;
case apiCmds.downloadManagerAction:
if (typeof arg.path === 'string') {
downloadManagerAction(arg.type, arg.path);
}
break;
default:
}

View File

@ -14,7 +14,7 @@ import { AppMenu } from './app-menu';
import { config, IConfig } from './config-handler';
import { handleChildWindow } from './pop-out-window-handler';
import { enterFullScreen, leaveFullScreen, throttledWindowChanges } from './window-actions';
import { createComponentWindow, getBounds } from './window-utils';
import { createComponentWindow, getBounds, handleDownloadManager } from './window-utils';
interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions {
winKey: string;
@ -206,6 +206,9 @@ export class WindowHandler {
this.mainWindow.show();
});
// Download manager
this.mainWindow.webContents.session.on('will-download', handleDownloadManager);
// store window ref
this.addWindow(this.windowOpts.winKey, this.mainWindow);

View File

@ -1,11 +1,13 @@
import * as electron from 'electron';
import { app, BrowserWindow, nativeImage } from 'electron';
import * as filesize from 'filesize';
import * as path from 'path';
import * as url from 'url';
import { isMac } from '../common/env';
import { i18n, LocaleType } from '../common/i18n';
import { logger } from '../common/logger';
import { getGuid } from '../common/utils';
import { screenSnippet } from './screen-snippet-handler';
import { windowHandler } from './window-handler';
@ -224,4 +226,56 @@ export const getBounds = (winPos: Electron.Rectangle, defaultWidth: number, defa
}
}
return { width: defaultWidth, height: defaultHeight };
};
};
/**
* Open downloaded file
* @param type
* @param filePath
*/
export const downloadManagerAction = (type, filePath) => {
if (type === 'open') {
const openResponse = electron.shell.openExternal(`file:///${filePath}`);
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
if (!openResponse && focusedWindow && !focusedWindow.isDestroyed()) {
electron.dialog.showMessageBox(focusedWindow, {
message: i18n.t('The file you are trying to open cannot be found in the specified path.')(),
title: i18n.t('File not Found')(),
type: 'error',
});
}
} else {
const showResponse = electron.shell.showItemInFolder(filePath);
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
if (!showResponse && focusedWindow && !focusedWindow.isDestroyed()) {
electron.dialog.showMessageBox(focusedWindow, {
message: i18n.t('The file you are trying to open cannot be found in the specified path.')(),
title: i18n.t('File not Found')(),
type: 'error',
});
}
}
};
/**
* Displays a UI with downloaded file similar to chrome
* Invoked by webContents session's will-download event
*
* @param _event
* @param item {Electron.DownloadItem}
* @param webContents {Electron.WebContents}
*/
export const handleDownloadManager = (_event, item: Electron.DownloadItem, webContents: Electron.WebContents) => {
// Send file path when download is complete
item.once('done', (_e, state) => {
if (state === 'completed') {
const data = {
_id: getGuid(),
savedPath: item.getSavePath() || '',
total: filesize(item.getTotalBytes() || 0),
fileName: item.getFilename() || 'No name',
};
webContents.send('downloadCompleted', data);
}
});
};

View File

@ -20,6 +20,7 @@ export enum apiCmds {
keyPress = 'key-press',
closeWindow = 'close-window',
openScreenSharingIndicator = 'open-screen-sharing-indicator',
downloadManagerAction = 'download-manager-action',
}
export enum apiName {
@ -43,6 +44,8 @@ export interface IApiArgs {
keyCode: number;
windowType: WindowTypes;
displayId: string;
path: string;
type: string;
}
export type WindowTypes = 'screen-picker';

View File

@ -0,0 +1,215 @@
import { ipcRenderer } from 'electron';
import * as React from 'react';
import { apiCmds, apiName } from '../../common/api-interface';
import { i18n } from '../../common/i18n-preload';
const DOWNLOAD_MANAGER_NAMESPACE = 'DownloadManager';
interface IDownloadManager {
_id: string;
fileName: string;
savedPath: string;
total: number;
flashing: boolean;
}
interface IManagerState {
items: IDownloadManager[];
showMainComponent: boolean;
}
type mouseEventLi = React.MouseEvent<HTMLLIElement>;
type mouseEventDiv = React.MouseEvent<HTMLDivElement>;
export default class DownloadManager extends React.Component<{}, IManagerState> {
private readonly eventHandlers = {
onClose: () => this.close(),
onInjectItem: (_event, item: IDownloadManager) => this.injectItem(item),
onOpenFile: (id: string) => (_event: mouseEventLi | mouseEventDiv) => this.openFile(id),
onShowInFinder: (id: string) => (_event: mouseEventLi) => this.showInFinder(id),
};
constructor(props) {
super(props);
this.state = {
items: [],
showMainComponent: false,
};
this.getFileDisplayName = this.getFileDisplayName.bind(this);
}
public componentDidMount(): void {
ipcRenderer.on('downloadCompleted', this.eventHandlers.onInjectItem);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('downloadCompleted', this.eventHandlers.onInjectItem);
}
/**
* Main react render component
*/
public render(): React.ReactNode {
const mainFooter = document.getElementById('footer');
const { items } = this.state;
if (mainFooter) {
items && items.length ? mainFooter.classList.remove('hidden') : mainFooter.classList.add('hidden');
}
return (
<div>
<div id='download-manager-footer' className='download-bar'>
<ul id='download-main'>
{this.renderItems()}
</ul>
<span
id='close-download-bar'
className='close-download-bar tempo-icon tempo-icon--close'
onClick={this.eventHandlers.onClose}/>
</div>
</div>
);
}
/**
* Loop through the items downloaded
* @param item
*/
private mapItems(item: IDownloadManager): JSX.Element | undefined {
if (!item) return;
const { _id, total, fileName }: IDownloadManager = item;
const fileDisplayName = this.getFileDisplayName(fileName);
// TODO: fix flashing issue
return (
<li id={_id} className='download-element'>
<div className='download-item' id='dl-item' onClick={this.eventHandlers.onOpenFile(_id)}>
<div className='file'>
<div id='download-progress' className='download-complete flash'>
<span className='tempo-icon tempo-icon--download download-complete-color'/>
</div>
</div>
<div className='downloaded-filename'>
<h1 className='text-cutoff'>
{fileDisplayName}
</h1>
<span id='per'>
{total}{i18n.t('Downloaded', DOWNLOAD_MANAGER_NAMESPACE)()}
</span>
</div>
</div>
<div id='menu' className='caret tempo-icon tempo-icon--dropdown'>
<div id='download-action-menu' className='download-action-menu'>
<ul id={_id}>
<li id='download-open' onClick={this.eventHandlers.onOpenFile(_id)}>
{i18n.t('Open', DOWNLOAD_MANAGER_NAMESPACE)()}
</li>
<li id='download-show-in-folder' onClick={this.eventHandlers.onShowInFinder(_id)}>
{i18n.t('Show in Folder', DOWNLOAD_MANAGER_NAMESPACE)()}
</li>
</ul>
</div>
</div>
</li>
);
}
/**
* Inject items to global var
* @param args
*/
private injectItem(args: IDownloadManager): void {
const { items } = this.state;
const allItems = [ ...items, ...[args] ];
this.setState({ items: allItems, showMainComponent: true });
}
/**
* Show or hide main footer which comes from the client
*/
private close(): void {
this.setState({
showMainComponent: !this.state.showMainComponent,
items: [],
});
}
/**
* Opens the downloaded file
*
* @param id {string}
*/
private openFile(id: string): void {
const { items } = this.state;
const fileIndex = items.findIndex((item) => {
return item._id === id;
});
if (fileIndex !== -1) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.downloadManagerAction,
path: items[ fileIndex ].savedPath,
type: 'open',
});
}
}
/**
* Opens the downloaded file in finder/explorer
*
* @param id {string}
*/
private showInFinder(id: string): void {
const { items } = this.state;
const fileIndex = items.findIndex((item) => {
return item._id === id;
});
if (fileIndex !== -1) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.downloadManagerAction,
path: items[ fileIndex ].savedPath,
type: 'show',
});
}
}
/**
* Checks and constructs file name
*
* @param fileName
*/
private getFileDisplayName(fileName: string): string {
const { items } = this.state;
let fileNameCount = 0;
let fileDisplayName = fileName;
/* Check if a file with the same name exists
* (akin to the user downloading a file with the same name again)
* in the download bar
*/
for (const value of items) {
if (fileName === value.fileName) {
fileNameCount++;
}
}
/* If it exists, add a count to the name like how Chrome does */
if (fileNameCount) {
const extLastIndex = fileDisplayName.lastIndexOf('.');
const fileCount = ' (' + fileNameCount + ')';
fileDisplayName = fileDisplayName.slice(0, extLastIndex) + fileCount + fileDisplayName.slice(extLastIndex);
}
return fileDisplayName;
}
/**
* Map of items
*/
private renderItems(): Array<JSX.Element | undefined> {
return this.state.items.map((items) => this.mapItems(items)).reverse();
}
}

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { i18n } from '../common/i18n-preload';
import DownloadManager from './components/download-manager';
import SnackBar from './components/snack-bar';
import WindowsTitleBar from './components/windows-title-bar';
import { SSFApi } from './ssf-api';
@ -51,6 +52,13 @@ ipcRenderer.on('page-load', (_event, { isWindowsOS, locale, resources }) => {
// injects snack bar
const snackBar = React.createElement(SnackBar);
ReactDOM.render(snackBar, document.body.appendChild(document.createElement( 'div' )));
// injects download manager contents
const downloadManager = React.createElement(DownloadManager);
const footerSFE = document.getElementById('footer');
if (footerSFE) {
ReactDOM.render(downloadManager, footerSFE);
}
});
// Creates a custom tile bar for Windows