mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
ELECTRON-1368 - Fix SnackBar & DownloadManager in pop-out (#740)
This commit is contained in:
committed by
Vishwas Shashidhar
parent
9c12dd468e
commit
abfda29141
@@ -72,7 +72,9 @@
|
||||
<br/>
|
||||
<input type="button" id="download-file1" value="Download"/>
|
||||
<input type="button" id="download-file2" value="Download"/>
|
||||
<div id="footer"></div>
|
||||
<div id="footer" class="hidden">
|
||||
<div id="download-manager-footer" class="download-bar"></div>
|
||||
</div>
|
||||
<br>
|
||||
<hr>
|
||||
<p>
|
||||
|
@@ -2,6 +2,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test pop-out window</title>
|
||||
<link rel="stylesheet" href="download-manager.css">
|
||||
</head>
|
||||
Test Window has been opened
|
||||
<p>
|
||||
@@ -18,8 +19,14 @@ Test Window has been opened
|
||||
<br>
|
||||
|
||||
<a href="https://symphony.com" target="_blank">Open Symphony</a>
|
||||
|
||||
<hr>
|
||||
<textarea id="text-val" rows="4">Writes some thing to the file</textarea>
|
||||
<br/>
|
||||
<input type="button" id="download-file1" value="Download"/>
|
||||
<input type="button" id="download-file2" value="Download"/>
|
||||
<div id="footer" class="hidden">
|
||||
<div id="download-manager-footer" class="download-bar"></div>
|
||||
</div>
|
||||
<p>Badge Count:<p>
|
||||
<button id='inc-badge'>increment badge count</button>
|
||||
<br>
|
||||
@@ -47,5 +54,34 @@ Test Window has been opened
|
||||
window.open('https://symphony.com');
|
||||
});
|
||||
|
||||
/**
|
||||
* Download Manager api handler
|
||||
*/
|
||||
const download = (filename, text) => {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
// Start file download.
|
||||
document.getElementById('download-file1').addEventListener('click', () => {
|
||||
const filename = "hello.txt";
|
||||
const text = document.getElementById("text-val").value;
|
||||
download(filename, text);
|
||||
}, false);
|
||||
|
||||
document.getElementById('download-file2').addEventListener('click', () => {
|
||||
const filename = "bye.txt";
|
||||
const text = document.getElementById("text-val").value;
|
||||
download(filename, text);
|
||||
}, false);
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
@@ -1,19 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`download manager should render correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="download-bar"
|
||||
id="download-manager-footer"
|
||||
>
|
||||
<ul
|
||||
id="download-main"
|
||||
/>
|
||||
<span
|
||||
className="close-download-bar tempo-icon tempo-icon--close"
|
||||
id="close-download-bar"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`snack bar should render correctly 1`] = `<div />`;
|
@@ -1,124 +0,0 @@
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import DownloadManager from '../src/renderer/components/download-manager';
|
||||
|
||||
describe('download manager', () => {
|
||||
it('should render correctly', () => {
|
||||
const wrapper = shallow(React.createElement(DownloadManager));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call `close` correctly', () => {
|
||||
const spy: jest.SpyInstance = jest.spyOn(DownloadManager.prototype, 'setState');
|
||||
const wrapper: ShallowWrapper = shallow(React.createElement(DownloadManager));
|
||||
|
||||
wrapper.find('#close-download-bar').simulate('click');
|
||||
|
||||
expect(spy).toBeCalledWith({ items: [], showMainComponent: true });
|
||||
});
|
||||
|
||||
it('should call `injectItem` correctly', () => {
|
||||
const { ipcRenderer } = require('./__mocks__/electron');
|
||||
const spy: jest.SpyInstance = jest.spyOn(DownloadManager.prototype, 'setState');
|
||||
const objectDownloadCompleted: object = {
|
||||
_id: 1,
|
||||
fileName: 'test.png',
|
||||
savedPath: 'path://test',
|
||||
total: 1,
|
||||
flashing: true,
|
||||
};
|
||||
|
||||
shallow(React.createElement(DownloadManager));
|
||||
|
||||
ipcRenderer.send('downloadCompleted', objectDownloadCompleted);
|
||||
|
||||
expect(spy).toBeCalledWith({
|
||||
items: [{
|
||||
_id: 1,
|
||||
count: 0,
|
||||
fileName: 'test.png',
|
||||
savedPath: 'path://test',
|
||||
total: 1,
|
||||
flashing: true,
|
||||
}], showMainComponent: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('download element ready', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(React.createElement(DownloadManager));
|
||||
|
||||
wrapper.setState({
|
||||
items: [
|
||||
{
|
||||
_id: 1,
|
||||
fileName: 'test.png',
|
||||
savedPath: 'path://test',
|
||||
total: 1,
|
||||
flashing: true,
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
it('should exist `download-element` class when there are items', () => {
|
||||
expect(wrapper.find('.download-element')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call `onOpenFile` correctly', () => {
|
||||
const { ipcRenderer } = require('./__mocks__/electron');
|
||||
const sendEventLabel: string = 'send';
|
||||
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, sendEventLabel);
|
||||
|
||||
wrapper.find('#download-open').simulate('click');
|
||||
|
||||
expect(spy).toBeCalledWith('symphony-api', {
|
||||
cmd: 'download-manager-action',
|
||||
path: 'path://test',
|
||||
type: 'open',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call `showInFinder` correctly', () => {
|
||||
const { ipcRenderer } = require('./__mocks__/electron');
|
||||
const sendEventLabel: string = 'send';
|
||||
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, sendEventLabel);
|
||||
|
||||
wrapper.find('#download-show-in-folder').simulate('click');
|
||||
|
||||
expect(spy).toBeCalledWith('symphony-api', {
|
||||
cmd: 'download-manager-action',
|
||||
path: 'path://test',
|
||||
type: 'show',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('download completed event', () => {
|
||||
const { ipcRenderer } = require('./__mocks__/electron');
|
||||
const downloadCompletedLabelEvent: string = 'downloadCompleted';
|
||||
const onLabelEvent: string = 'on';
|
||||
const removeListenerLabelEvent: string = 'removeListener';
|
||||
|
||||
it('should call the `downloadCompleted event when component is mounted', () => {
|
||||
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, onLabelEvent);
|
||||
|
||||
shallow(React.createElement(DownloadManager));
|
||||
|
||||
expect(spy).toBeCalledWith(downloadCompletedLabelEvent, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should remove listen `downloadCompleted` when component is unmounted', () => {
|
||||
const spyMount: jest.SpyInstance = jest.spyOn(ipcRenderer, onLabelEvent);
|
||||
const spyUnmount: jest.SpyInstance = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
|
||||
const wrapper: ShallowWrapper = shallow(React.createElement(DownloadManager));
|
||||
|
||||
expect(spyMount).toBeCalledWith(downloadCompletedLabelEvent, expect.any(Function));
|
||||
|
||||
wrapper.unmount();
|
||||
|
||||
expect(spyUnmount).toBeCalledWith(downloadCompletedLabelEvent, expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,56 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import SnackBar from '../src/renderer/components/snack-bar';
|
||||
import { ipcRenderer } from './__mocks__/electron';
|
||||
|
||||
describe('snack bar', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
// events label
|
||||
const onEventLabel = 'on';
|
||||
const removeListenerEventLabel = 'removeListener';
|
||||
const windowEnterFullScreenEventLabel = 'window-enter-full-screen';
|
||||
const windowLeaveFullScreenEventLabel = 'window-leave-full-screen';
|
||||
|
||||
it('should render correctly', () => {
|
||||
const wrapper = shallow(React.createElement(SnackBar));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call mount correctly', () => {
|
||||
const spy = jest.spyOn(ipcRenderer, onEventLabel);
|
||||
shallow(React.createElement(SnackBar));
|
||||
expect(spy).nthCalledWith(1, windowEnterFullScreenEventLabel, expect.any(Function));
|
||||
expect(spy).nthCalledWith(2, windowLeaveFullScreenEventLabel, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should call unmount correctly', () => {
|
||||
const spy = jest.spyOn(ipcRenderer, removeListenerEventLabel);
|
||||
shallow(React.createElement(SnackBar)).unmount();
|
||||
expect(spy).nthCalledWith(1, windowEnterFullScreenEventLabel, expect.any(Function));
|
||||
expect(spy).nthCalledWith(2, windowLeaveFullScreenEventLabel, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should call `removeSnackBar` correctly', () => {
|
||||
const spy = jest.spyOn(SnackBar.prototype, 'setState');
|
||||
const expectedValue = { show: false };
|
||||
shallow(React.createElement(SnackBar));
|
||||
ipcRenderer.send(windowLeaveFullScreenEventLabel, null);
|
||||
expect(spy).lastCalledWith(expectedValue);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
it('should call `showSnackBar` correctly', () => {
|
||||
const spy = jest.spyOn(SnackBar.prototype, 'setState');
|
||||
const expectedValueFirst = { show: true };
|
||||
const expectedValueSecond = { show: false };
|
||||
shallow(React.createElement(SnackBar));
|
||||
ipcRenderer.send(windowEnterFullScreenEventLabel, null);
|
||||
expect(setTimeout).lastCalledWith(expect.any(Function), 3000);
|
||||
expect(spy).nthCalledWith(1, expectedValueFirst);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(spy).nthCalledWith(2, expectedValueSecond);
|
||||
});
|
||||
});
|
@@ -38,7 +38,6 @@ const saveWindowSettings = async (): Promise<void> => {
|
||||
if (browserWindow.winName === apiName.mainWindowName) {
|
||||
const isMaximized = browserWindow.isMaximized();
|
||||
const isFullScreen = browserWindow.isFullScreen();
|
||||
browserWindow.webContents.send(isFullScreen ? 'window-enter-full-screen' : 'window-leave-full-screen');
|
||||
const { mainWinPos } = config.getUserConfigFields([ 'mainWinPos' ]);
|
||||
await config.updateUserConfig({ mainWinPos: { ...mainWinPos, ...{ x, y, width, height, isMaximized, isFullScreen } } });
|
||||
}
|
||||
@@ -47,20 +46,24 @@ const saveWindowSettings = async (): Promise<void> => {
|
||||
|
||||
};
|
||||
|
||||
const windowMaximized = async (): Promise<void> => {
|
||||
const windowMaximized = async (args: string | undefined): Promise<void> => {
|
||||
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
|
||||
if (browserWindow && windowExists(browserWindow) && browserWindow.winName === apiName.mainWindowName) {
|
||||
if (browserWindow && windowExists(browserWindow)) {
|
||||
const isMaximized = browserWindow.isMaximized();
|
||||
const isFullScreen = browserWindow.isFullScreen();
|
||||
browserWindow.webContents.send(isFullScreen ? 'window-enter-full-screen' : 'window-leave-full-screen');
|
||||
const { mainWinPos } = config.getUserConfigFields([ 'mainWinPos' ]);
|
||||
await config.updateUserConfig({ mainWinPos: { ...mainWinPos, ...{ isMaximized, isFullScreen } } });
|
||||
if (args && typeof args === 'string') {
|
||||
browserWindow.webContents.send(isFullScreen ? 'window-enter-full-screen' : 'window-leave-full-screen');
|
||||
}
|
||||
if (browserWindow.winName === apiName.mainWindowName) {
|
||||
const { mainWinPos } = config.getUserConfigFields([ 'mainWinPos' ]);
|
||||
await config.updateUserConfig({ mainWinPos: { ...mainWinPos, ...{ isMaximized, isFullScreen } } });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const throttledWindowChanges = throttle(async () => {
|
||||
const throttledWindowChanges = throttle(async (args) => {
|
||||
await saveWindowSettings();
|
||||
await windowMaximized();
|
||||
await windowMaximized(args);
|
||||
notification.moveNotificationToTop();
|
||||
}, 1000);
|
||||
|
||||
@@ -202,10 +205,10 @@ export const monitorWindowActions = (window: BrowserWindow): void => {
|
||||
window.on(event, throttledWindowChanges);
|
||||
}
|
||||
});
|
||||
window.on('enter-full-screen', throttledWindowChanges);
|
||||
window.on('enter-full-screen', throttledWindowChanges.bind(null, 'enter-full-screen'));
|
||||
window.on('maximize', throttledWindowChanges);
|
||||
|
||||
window.on('leave-full-screen', throttledWindowChanges);
|
||||
window.on('leave-full-screen', throttledWindowChanges.bind(null,'leave-full-screen'));
|
||||
window.on('unmaximize', throttledWindowChanges);
|
||||
|
||||
if ((window as ICustomBrowserWindow).winName === apiName.mainWindowName) {
|
||||
|
@@ -16,7 +16,7 @@ import { config, IConfig } from './config-handler';
|
||||
import { SpellChecker } from './spell-check-handler';
|
||||
import { checkIfBuildExpired } from './ttl-handler';
|
||||
import DesktopCapturerSource = Electron.DesktopCapturerSource;
|
||||
import { IVersionInfo, versionHandler } from './version-handler.js';
|
||||
import { IVersionInfo, versionHandler } from './version-handler';
|
||||
import { handlePermissionRequests, monitorWindowActions } from './window-actions';
|
||||
import {
|
||||
createComponentWindow,
|
||||
|
266
src/renderer/components/download-manager.ts
Normal file
266
src/renderer/components/download-manager.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
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;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface IManagerState {
|
||||
items: IDownloadManager[];
|
||||
showMainComponent: boolean;
|
||||
}
|
||||
|
||||
export default class DownloadManager {
|
||||
|
||||
private readonly eventHandlers = {
|
||||
onInjectItem: (_event, item: IDownloadManager) => this.injectItem(item),
|
||||
};
|
||||
private readonly itemsContainer: HTMLElement | null;
|
||||
private readonly closeButton: HTMLElement | null;
|
||||
|
||||
private domParser: DOMParser;
|
||||
private state: IManagerState;
|
||||
|
||||
constructor() {
|
||||
this.state = {
|
||||
items: [],
|
||||
showMainComponent: false,
|
||||
};
|
||||
this.domParser = new DOMParser();
|
||||
const parsedDownloadBar = this.domParser.parseFromString(this.render(), 'text/html');
|
||||
this.itemsContainer = parsedDownloadBar.getElementById('download-main');
|
||||
this.closeButton = parsedDownloadBar.getElementById('close-download-bar');
|
||||
|
||||
if (this.closeButton) {
|
||||
this.closeButton.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
this.getFileDisplayName = this.getFileDisplayName.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* initializes the event listeners
|
||||
*/
|
||||
public initDownloadManager(): void {
|
||||
ipcRenderer.on('downloadCompleted', this.eventHandlers.onInjectItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main react render component
|
||||
*/
|
||||
public render(): string {
|
||||
return (`
|
||||
<div id='download-manager' class='download-bar'>
|
||||
<ul id='download-main' />
|
||||
<span
|
||||
id='close-download-bar'
|
||||
class='close-download-bar tempo-icon tempo-icon--close' />
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles footer visibility class based on download items
|
||||
*/
|
||||
private showOrHideDownloadBar(): void {
|
||||
const mainFooter = document.getElementById('footer');
|
||||
const { items } = this.state;
|
||||
if (mainFooter) {
|
||||
items && items.length ? mainFooter.classList.remove('hidden') : mainFooter.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop through the items downloaded
|
||||
*
|
||||
* @param item {IDownloadManager}
|
||||
*/
|
||||
private renderItem(item: IDownloadManager): void {
|
||||
const { _id, total, fileName }: IDownloadManager = item;
|
||||
const fileDisplayName = this.getFileDisplayName(fileName, item);
|
||||
const itemContainer = document.getElementById('download-main');
|
||||
const parsedItem = this.domParser.parseFromString(`
|
||||
<li id=${_id} class='download-element' title="${fileDisplayName}">
|
||||
<div class='download-item' id='dl-item'>
|
||||
<div class='file'>
|
||||
<div id='download-progress' class='download-complete flash'>
|
||||
<span class='tempo-icon tempo-icon--download download-complete-color'/>
|
||||
</div>
|
||||
</div>
|
||||
<div class='downloaded-filename'>
|
||||
<h1 class='text-cutoff'>
|
||||
${fileDisplayName}
|
||||
</h1>
|
||||
<span id='per' title="${total} ${i18n.t('downloaded', DOWNLOAD_MANAGER_NAMESPACE)()}">
|
||||
${total} ${i18n.t('downloaded', DOWNLOAD_MANAGER_NAMESPACE)()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id='menu' class='caret tempo-icon tempo-icon--dropdown'>
|
||||
<div id='download-action-menu' class='download-action-menu' style={width: '200px'}>
|
||||
<ul id={_id}>
|
||||
<li id='download-open' title="${i18n.t('Open', DOWNLOAD_MANAGER_NAMESPACE)()}">
|
||||
${i18n.t('Open', DOWNLOAD_MANAGER_NAMESPACE)()}
|
||||
</li>
|
||||
<li id='download-show-in-folder' title="${i18n.t('Show in Folder', DOWNLOAD_MANAGER_NAMESPACE)()}">
|
||||
${i18n.t('Show in Folder', DOWNLOAD_MANAGER_NAMESPACE)()}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>`,
|
||||
'text/html');
|
||||
const progress = parsedItem.getElementById('download-progress');
|
||||
const domItem = parsedItem.getElementById(_id);
|
||||
|
||||
// add event listeners
|
||||
this.attachEventListener('dl-item', parsedItem, _id);
|
||||
this.attachEventListener('download-open', parsedItem, _id);
|
||||
this.attachEventListener('download-show-in-folder', parsedItem, _id);
|
||||
|
||||
if (itemContainer && domItem) {
|
||||
itemContainer.prepend(domItem);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (progress) {
|
||||
progress.classList.remove('flash');
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject items to global var
|
||||
*
|
||||
* @param args {IDownloadManager}
|
||||
*/
|
||||
private injectItem(args: IDownloadManager): void {
|
||||
const { items } = this.state;
|
||||
let itemCount = 0;
|
||||
for (const item of items) {
|
||||
if (args.fileName === item.fileName) {
|
||||
itemCount++;
|
||||
}
|
||||
}
|
||||
args.count = itemCount;
|
||||
const newItem = { ...args, ...{ flashing: true } };
|
||||
const allItems = [ ...items, ...[ newItem ] ];
|
||||
this.state = { items: allItems, showMainComponent: true };
|
||||
|
||||
// inserts download bar once
|
||||
const downloadBar = document.getElementById('download-manager-footer');
|
||||
if (this.itemsContainer && this.closeButton) {
|
||||
this.showOrHideDownloadBar();
|
||||
if (downloadBar) {
|
||||
downloadBar.appendChild(this.itemsContainer);
|
||||
downloadBar.appendChild(this.closeButton);
|
||||
}
|
||||
}
|
||||
|
||||
// appends items to the download bar
|
||||
this.renderItem(newItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* adds event listener for the give id
|
||||
*
|
||||
* @param id {String}
|
||||
* @param item {Document}
|
||||
* @param itemId {String}
|
||||
*/
|
||||
private attachEventListener(id: string, item: Document, itemId: string): void {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = item.getElementById(id);
|
||||
if (element) {
|
||||
switch (id) {
|
||||
case 'dl-item':
|
||||
case 'download-open':
|
||||
element.addEventListener('click', () => this.openFile(itemId));
|
||||
break;
|
||||
case 'download-show-in-folder':
|
||||
element.addEventListener('click', () => this.showInFinder(itemId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide main footer which comes from the client
|
||||
*/
|
||||
private close(): void {
|
||||
this.state = {
|
||||
showMainComponent: !this.state.showMainComponent,
|
||||
items: [],
|
||||
};
|
||||
if (this.itemsContainer) {
|
||||
this.itemsContainer.innerHTML = '';
|
||||
}
|
||||
this.showOrHideDownloadBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {String}
|
||||
* @param item {IDownloadManager}
|
||||
*/
|
||||
private getFileDisplayName(fileName: string, item: IDownloadManager): string {
|
||||
/* If it exists, add a count to the name like how Chrome does */
|
||||
if (item.count > 0) {
|
||||
const extLastIndex = fileName.lastIndexOf('.');
|
||||
const fileCount = ' (' + item.count + ')';
|
||||
|
||||
fileName = fileName.slice(0, extLastIndex) + fileCount + fileName.slice(extLastIndex);
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
}
|
@@ -1,220 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
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;
|
||||
count: number;
|
||||
}
|
||||
|
||||
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, flashing }: IDownloadManager = item;
|
||||
setTimeout(() => {
|
||||
const { items } = this.state;
|
||||
const index = items.findIndex((i) => i._id === _id);
|
||||
if (index !== -1) {
|
||||
items[index].flashing = false;
|
||||
this.setState({
|
||||
items,
|
||||
});
|
||||
}
|
||||
}, 4000);
|
||||
const fileDisplayName = this.getFileDisplayName(fileName, item);
|
||||
return (
|
||||
<li key={_id} 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={classNames('download-complete', { flash: flashing })}>
|
||||
<span className='tempo-icon tempo-icon--download download-complete-color'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='downloaded-filename'>
|
||||
<h1 className='text-cutoff' title={fileDisplayName}>
|
||||
{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' style={{width: '200px'}}>
|
||||
<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;
|
||||
let itemCount = 0;
|
||||
for (const item of items) {
|
||||
if (args.fileName === item.fileName) {
|
||||
itemCount++;
|
||||
}
|
||||
}
|
||||
args.count = itemCount;
|
||||
const allItems = [ ...items, ...[ { ...args, ...{ flashing: true } } ] ];
|
||||
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
|
||||
* @param item
|
||||
*/
|
||||
private getFileDisplayName(fileName: string, item: IDownloadManager): string {
|
||||
/* If it exists, add a count to the name like how Chrome does */
|
||||
if (item.count > 0) {
|
||||
const extLastIndex = fileName.lastIndexOf('.');
|
||||
const fileCount = ' (' + item.count + ')';
|
||||
|
||||
fileName = fileName.slice(0, extLastIndex) + fileCount + fileName.slice(extLastIndex);
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of items
|
||||
*/
|
||||
private renderItems(): Array<JSX.Element | undefined> {
|
||||
return this.state.items.map((items) => this.mapItems(items)).reverse();
|
||||
}
|
||||
}
|
79
src/renderer/components/snack-bar.ts
Normal file
79
src/renderer/components/snack-bar.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import Timer = NodeJS.Timer;
|
||||
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
|
||||
const SNACKBAR_NAMESPACE = 'SnackBar';
|
||||
|
||||
export default class SnackBar {
|
||||
private snackBarTimer: Timer | undefined;
|
||||
private domParser: DOMParser;
|
||||
|
||||
private readonly body: HTMLCollectionOf<Element>;
|
||||
private readonly snackBar: HTMLElement | null;
|
||||
|
||||
constructor() {
|
||||
this.body = document.getElementsByTagName('body');
|
||||
|
||||
this.domParser = new DOMParser();
|
||||
const snackBar = this.domParser.parseFromString(this.render(), 'text/html');
|
||||
this.snackBar = snackBar.getElementById('snack-bar');
|
||||
}
|
||||
|
||||
/**
|
||||
* initializes the event listeners
|
||||
*/
|
||||
public initSnackBar(): void {
|
||||
this.showSnackBar = this.showSnackBar.bind(this);
|
||||
this.removeSnackBar = this.removeSnackBar.bind(this);
|
||||
|
||||
ipcRenderer.on('window-enter-full-screen', this.showSnackBar);
|
||||
ipcRenderer.on('window-leave-full-screen', this.removeSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays snackbar for 3sec
|
||||
*/
|
||||
public showSnackBar(): void {
|
||||
setTimeout(() => {
|
||||
if (this.body && this.body.length > 0 && this.snackBar) {
|
||||
this.body[ 0 ].appendChild(this.snackBar);
|
||||
this.snackBar.classList.add('SnackBar-show');
|
||||
this.snackBarTimer = setTimeout(() => {
|
||||
if (this.snackBar) {
|
||||
if (document.getElementById('snack-bar')) {
|
||||
this.body[ 0 ].removeChild(this.snackBar);
|
||||
}
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes snackbar
|
||||
*/
|
||||
public removeSnackBar(): void {
|
||||
if (this.body && this.body.length > 0 && this.snackBar) {
|
||||
if (document.getElementById('snack-bar')) {
|
||||
this.body[ 0 ].removeChild(this.snackBar);
|
||||
if (this.snackBarTimer) {
|
||||
clearTimeout(this.snackBarTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the custom title bar
|
||||
*/
|
||||
public render(): string {
|
||||
return (
|
||||
`<div id='snack-bar' class='SnackBar'>
|
||||
<span >${i18n.t('Press ', SNACKBAR_NAMESPACE)()}</span>
|
||||
<span class='SnackBar-esc'>${i18n.t('esc', SNACKBAR_NAMESPACE)()}</span>
|
||||
<span >${i18n.t(' to exit full screen', SNACKBAR_NAMESPACE)()}</span>
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
|
||||
import Timer = NodeJS.Timer;
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
|
||||
interface IState {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const SNACKBAR_NAMESPACE = 'SnackBar';
|
||||
|
||||
export default class SnackBar extends React.Component<{}, IState> {
|
||||
private snackBarTimer: Timer | undefined;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: false,
|
||||
};
|
||||
this.showSnackBar = this.showSnackBar.bind(this);
|
||||
this.removeSnackBar = this.removeSnackBar.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
ipcRenderer.on('window-enter-full-screen', this.showSnackBar);
|
||||
ipcRenderer.on('window-leave-full-screen', this.removeSnackBar);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
ipcRenderer.removeListener('window-enter-full-screen', this.showSnackBar);
|
||||
ipcRenderer.removeListener('window-leave-full-screen', this.removeSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays snackbar for 3sec
|
||||
*/
|
||||
public showSnackBar(): void {
|
||||
this.setState({ show: true });
|
||||
this.snackBarTimer = setTimeout(() => {
|
||||
this.removeSnackBar();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes snackbar
|
||||
*/
|
||||
public removeSnackBar(): void {
|
||||
this.setState({ show: false });
|
||||
if (this.snackBarTimer) {
|
||||
clearTimeout(this.snackBarTimer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the custom title bar
|
||||
*/
|
||||
public render(): JSX.Element | null {
|
||||
const { show } = this.state;
|
||||
|
||||
return show ? (
|
||||
<div className='SnackBar SnackBar-show'>
|
||||
<span >{i18n.t('Press ', SNACKBAR_NAMESPACE)()}</span>
|
||||
<span className='SnackBar-esc'>{i18n.t('esc', SNACKBAR_NAMESPACE)()}</span>
|
||||
<span >{i18n.t(' to exit full screen', SNACKBAR_NAMESPACE)()}</span>
|
||||
</div>
|
||||
) : <div />;
|
||||
}
|
||||
}
|
@@ -65,17 +65,12 @@ ipcRenderer.on('page-load', (_event, { locale, resources, enableCustomTitleBar,
|
||||
});
|
||||
|
||||
// injects snack bar
|
||||
const snackBar = React.createElement(SnackBar);
|
||||
const snackBarContainer = document.createElement( 'div' );
|
||||
document.body.appendChild(snackBarContainer);
|
||||
ReactDOM.render(snackBar, snackBarContainer);
|
||||
const snackBar = new SnackBar();
|
||||
snackBar.initSnackBar();
|
||||
|
||||
// injects download manager contents
|
||||
const downloadManager = React.createElement(DownloadManager);
|
||||
const footerSFE = document.getElementById('footer');
|
||||
if (footerSFE) {
|
||||
ReactDOM.render(downloadManager, footerSFE);
|
||||
}
|
||||
const downloadManager = new DownloadManager();
|
||||
downloadManager.initDownloadManager();
|
||||
|
||||
if (isMainWindow) {
|
||||
setInterval(async () => {
|
||||
|
Reference in New Issue
Block a user