Merge branch 'master' into SDA-2651b
71
.vscode/launch.json
vendored
@ -10,7 +10,9 @@
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
|
||||
},
|
||||
"args": ["."],
|
||||
"args": [
|
||||
"."
|
||||
],
|
||||
"env": {
|
||||
"ELECTRON_DEBUGGING": "true",
|
||||
"ELECTRON_DEV": "true"
|
||||
@ -30,7 +32,10 @@
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
|
||||
},
|
||||
"args": [".", "--url=https://corporate.symphony.com"],
|
||||
"args": [
|
||||
".",
|
||||
"--url=https://corporate.symphony.com"
|
||||
],
|
||||
"env": {
|
||||
"ELECTRON_DEBUGGING": "true",
|
||||
"ELECTRON_DEV": "true"
|
||||
@ -42,7 +47,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mana",
|
||||
"name": "build corp",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@ -50,7 +55,11 @@
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
|
||||
},
|
||||
"args": [".", "--url=https://local-dev.symphony.com:9090"],
|
||||
"preLaunchTask": "build",
|
||||
"args": [
|
||||
".",
|
||||
"--url=https://corporate.symphony.com"
|
||||
],
|
||||
"env": {
|
||||
"ELECTRON_DEBUGGING": "true",
|
||||
"ELECTRON_DEV": "true"
|
||||
@ -60,7 +69,54 @@
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/lib/**/*.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mana local",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
|
||||
},
|
||||
"args": [
|
||||
".",
|
||||
"--url=https://local-dev.symphony.com:9090"
|
||||
],
|
||||
"env": {
|
||||
"ELECTRON_DEBUGGING": "true",
|
||||
"ELECTRON_DEV": "true"
|
||||
},
|
||||
"outputCapture": "std",
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/lib/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "build mana local",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
|
||||
},
|
||||
"preLaunchTask": "build",
|
||||
"args": [
|
||||
".",
|
||||
"--url=https://local-dev.symphony.com:9090"
|
||||
],
|
||||
"env": {
|
||||
"ELECTRON_DEBUGGING": "true",
|
||||
"ELECTRON_DEV": "true"
|
||||
},
|
||||
"outputCapture": "std",
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/lib/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "demo",
|
||||
"type": "node",
|
||||
@ -70,7 +126,10 @@
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
|
||||
},
|
||||
"args": [".", "--url=${workspaceFolder}/src/demo/index.html"],
|
||||
"args": [
|
||||
".",
|
||||
"--url=file://${workspaceFolder}/src/demo/index.html"
|
||||
],
|
||||
"env": {
|
||||
"ELECTRON_DEBUGGING": "true",
|
||||
"ELECTRON_DEV": "true"
|
||||
|
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"npm.packageManager": "npm",
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true,
|
||||
"jestrunner.configPath": "jest-config.json",
|
||||
}
|
10
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"type": "shell",
|
||||
"command": "npx run-s compile browserify"
|
||||
}
|
||||
]
|
||||
}
|
@ -170,6 +170,7 @@ namespace Symphony
|
||||
this.cancel.Text = "[WixUICancel]";
|
||||
this.cancel.UseVisualStyleBackColor = true;
|
||||
this.cancel.Click += new System.EventHandler(this.cancel_Click);
|
||||
this.cancel.Enabled = false;
|
||||
//
|
||||
// bottomBorder
|
||||
//
|
||||
|
@ -150,6 +150,7 @@
|
||||
"typescript": "3.9.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lazy-brush": "^1.0.0",
|
||||
"archiver": "3.1.1",
|
||||
"async.map": "0.5.2",
|
||||
"classnames": "2.2.6",
|
||||
@ -159,6 +160,8 @@
|
||||
"electron-spellchecker": "git+https://github.com/symphonyoss/electron-spellchecker.git#v2.0.4",
|
||||
"ffi-napi": "3.0.0",
|
||||
"filesize": "6.1.0",
|
||||
"image-size": "^0.9.3",
|
||||
"lazy-brush": "^1.0.1",
|
||||
"react": "16.13.0",
|
||||
"react-dom": "16.13.0",
|
||||
"ref-napi": "1.4.3",
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
NODE_REQUIRED_VERSION=v12.13.1
|
||||
SNYK_ORG=sda
|
||||
SNYK_PROJECT_NAME="Symphony Desktop Application"
|
||||
|
||||
if ! [ -x "$(command -v git)" ]; then
|
||||
echo 'GIT does not exist! Please set it up before running this script!' >&2
|
||||
@ -69,7 +70,8 @@ npm install
|
||||
|
||||
# Run Snyk Security Tests
|
||||
echo "Running snyk security tests"
|
||||
snyk test --file=package.json --org="$SNYK_ORG"
|
||||
snyk test --file=package-lock.json --org="$SNYK_ORG"
|
||||
snyk monitor --file=package-lock.json --org="$SNYK_ORG" --project-name="$SNYK_PROJECT_NAME"
|
||||
|
||||
# replace url in config
|
||||
echo "Setting default pod url to https://corporate.symphony.com"
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
NODE_REQUIRED_VERSION=v12.13.1
|
||||
SNYK_ORG=sda
|
||||
SNYK_PROJECT_NAME="Symphony Desktop Application"
|
||||
|
||||
# Check basic dependencies
|
||||
if ! [ -x "$(command -v git)" ]; then
|
||||
@ -85,7 +86,8 @@ codesign --force --options runtime -s "Developer ID Application: Symphony Commun
|
||||
|
||||
# Run Snyk Security Tests
|
||||
echo "Running snyk security tests"
|
||||
snyk test --file=package.json --org="$SNYK_ORG"
|
||||
snyk test --file=package-lock.json --org="$SNYK_ORG"
|
||||
snyk monitor --file=package-lock.json --org="$SNYK_ORG" --project-name="$SNYK_PROJECT_NAME"
|
||||
|
||||
# Replace url in config
|
||||
echo "Setting default pod url to https://my.symphony.com"
|
||||
|
@ -7,6 +7,7 @@ echo %PATH%
|
||||
set DISABLE_REBUILD=true
|
||||
set NODE_REQUIRED_VERSION=12.13.1
|
||||
set SNYK_ORG=sda
|
||||
set SNYK_PROJECT_NAME="Symphony Desktop Application"
|
||||
|
||||
set PATH=%PATH%;C:\Program Files\nodejs\;C:\Program Files\Git\cmd
|
||||
echo %PATH%
|
||||
@ -56,7 +57,8 @@ call npm install
|
||||
|
||||
# Run Snyk Security Tests
|
||||
echo "Running snyk security tests"
|
||||
call snyk test --file=package.json --org=%SNYK_ORG%
|
||||
call snyk test --file=package-lock.json --org=%SNYK_ORG%
|
||||
call snyk monitor --file=package-lock.json --org=%SNYK_ORG% --project-name=%SNYK_PROJECT_NAME%
|
||||
|
||||
:: Set expiry if required
|
||||
IF "%EXPIRY_PERIOD%"=="" (
|
||||
|
@ -7,6 +7,7 @@ echo %PATH%
|
||||
set DISABLE_REBUILD=true
|
||||
set NODE_REQUIRED_VERSION=12.13.1
|
||||
set SNYK_ORG=sda
|
||||
set SNYK_PROJECT_NAME="Symphony Desktop Application"
|
||||
|
||||
set PATH=%PATH%;C:\Program Files\nodejs\;C:\Program Files\Git\cmd
|
||||
echo %PATH%
|
||||
@ -44,7 +45,8 @@ call npm install
|
||||
|
||||
# Run Snyk Security Tests
|
||||
echo "Running snyk security tests"
|
||||
call snyk test --file=package.json --org=%SNYK_ORG%
|
||||
call snyk test --file=package-lock.json --org=%SNYK_ORG%
|
||||
call snyk monitor --file=package-lock.json --org=%SNYK_ORG% --project-name=%SNYK_PROJECT_NAME%
|
||||
|
||||
:: Set expiry if required
|
||||
IF "%EXPIRY_PERIOD%"=="" (
|
||||
|
73
spec/__snapshots__/snippingTool.spec.ts.snap
Normal file
@ -0,0 +1,73 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Snipping Tool should render correctly 1`] = `
|
||||
<div
|
||||
className="SnippingTool"
|
||||
lang="en-US"
|
||||
>
|
||||
<header>
|
||||
<div
|
||||
className="DrawActions"
|
||||
>
|
||||
<button
|
||||
className="ActionButton"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"border": "2px solid rgba(0, 142, 255, 1)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="../renderer/assets/snip-draw.svg"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="ActionButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<img
|
||||
src="../renderer/assets/snip-highlight.svg"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="ActionButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<img
|
||||
src="../renderer/assets/snip-erase.svg"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="ClearActions"
|
||||
>
|
||||
<button
|
||||
className="ClearButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
;
|
||||
<main>
|
||||
<div>
|
||||
<img
|
||||
alt="Screen snippet"
|
||||
className="SnippetImage"
|
||||
height={600}
|
||||
src="Screen-Snippet"
|
||||
width={800}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
20
spec/snippingTool.spec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import SnippingTool from '../src/renderer/components/snipping-tool';
|
||||
import { ipcRenderer } from './__mocks__/electron';
|
||||
|
||||
describe('Snipping Tool', () => {
|
||||
const snippingToolLabel = 'snipping-tool-data';
|
||||
const onLabelEvent = 'on';
|
||||
|
||||
it('should render correctly', () => {
|
||||
const wrapper = shallow(React.createElement(SnippingTool));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call `snipping-tool-data` event when component is mounted', () => {
|
||||
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
|
||||
shallow(React.createElement(SnippingTool));
|
||||
expect(spy).toBeCalledWith(snippingToolLabel, expect.any(Function));
|
||||
});
|
||||
});
|
@ -65,7 +65,7 @@ let {
|
||||
let initialAnalyticsSent = false;
|
||||
|
||||
const menuItemsArray = Object.keys(menuSections)
|
||||
.map((key) => menuSections[ key ])
|
||||
.map((key) => menuSections[key])
|
||||
.filter((value) => isMac ?
|
||||
true : value !== menuSections.about);
|
||||
|
||||
@ -82,7 +82,7 @@ export class AppMenu {
|
||||
constructor() {
|
||||
this.menuList = [];
|
||||
this.locale = i18n.getLocale();
|
||||
this.menuItemConfigFields = [ 'minimizeOnClose', 'launchOnStartup', 'alwaysOnTop', 'bringToFront', 'memoryRefresh', 'isCustomTitleBar', 'devToolsEnabled' ];
|
||||
this.menuItemConfigFields = ['minimizeOnClose', 'launchOnStartup', 'alwaysOnTop', 'bringToFront', 'memoryRefresh', 'isCustomTitleBar', 'devToolsEnabled'];
|
||||
this.cloudConfig = config.getFilteredCloudConfigFields(this.menuItemConfigFields);
|
||||
this.disableGpu = config.getConfigFields(['disableGpu']).disableGpu;
|
||||
this.enableRendererLogs = config.getConfigFields(['enableRendererLogs']).enableRendererLogs;
|
||||
@ -107,12 +107,12 @@ export class AppMenu {
|
||||
this.updateGlobals();
|
||||
|
||||
this.menuList = menuItemsArray.reduce((map: Electron.MenuItemConstructorOptions, key: string) => {
|
||||
map[ key ] = this.buildMenuKey(key);
|
||||
map[key] = this.buildMenuKey(key);
|
||||
return map;
|
||||
}, this.menuList || {});
|
||||
|
||||
const template = Object.keys(this.menuList)
|
||||
.map((key) => this.menuList[ key ]);
|
||||
.map((key) => this.menuList[key]);
|
||||
|
||||
this.menu = Menu.buildFromTemplate(template);
|
||||
logger.info(`app-menu: built menu from the provided template`);
|
||||
@ -262,17 +262,17 @@ export class AppMenu {
|
||||
logger.info(`app-menu: building view menu`);
|
||||
return {
|
||||
label: i18n.t('View')(),
|
||||
submenu: [ {
|
||||
submenu: [{
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click: (_item, focusedWindow) => focusedWindow ? reloadWindow(focusedWindow as ICustomBrowserWindow) : null,
|
||||
label: i18n.t('Reload')(),
|
||||
},
|
||||
this.buildSeparator(),
|
||||
this.assignRoleOrLabel({ role: 'resetZoom', label: i18n.t('Actual Size')() }),
|
||||
this.assignRoleOrLabel({ role: 'zoomIn', label: i18n.t('Zoom In')() }),
|
||||
this.assignRoleOrLabel({ role: 'zoomOut', label: i18n.t('Zoom Out')() }),
|
||||
this.buildSeparator(),
|
||||
this.assignRoleOrLabel({ role: 'togglefullscreen', label: i18n.t('Toggle Full Screen')() }),
|
||||
this.buildSeparator(),
|
||||
this.assignRoleOrLabel({ role: 'resetZoom', label: i18n.t('Actual Size')() }),
|
||||
this.assignRoleOrLabel({ role: 'zoomIn', label: i18n.t('Zoom In')() }),
|
||||
this.assignRoleOrLabel({ role: 'zoomOut', label: i18n.t('Zoom Out')() }),
|
||||
this.buildSeparator(),
|
||||
this.assignRoleOrLabel({ role: 'togglefullscreen', label: i18n.t('Toggle Full Screen')() }),
|
||||
],
|
||||
};
|
||||
}
|
||||
@ -378,7 +378,7 @@ export class AppMenu {
|
||||
const defaultSession = session.defaultSession;
|
||||
if (defaultSession) {
|
||||
await defaultSession.clearCache();
|
||||
focusedWindow.reload();
|
||||
reloadWindow(focusedWindow as ICustomBrowserWindow);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -430,7 +430,7 @@ export class AppMenu {
|
||||
label: i18n.t('Help')(),
|
||||
role: 'help',
|
||||
submenu:
|
||||
[ {
|
||||
[{
|
||||
click: () => shell.openExternal(i18n.t('Help Url')()),
|
||||
label: i18n.t('Symphony Help')(),
|
||||
}, {
|
||||
@ -438,7 +438,7 @@ export class AppMenu {
|
||||
label: i18n.t('Learn More')(),
|
||||
}, {
|
||||
label: i18n.t('Troubleshooting')(),
|
||||
submenu: [ {
|
||||
submenu: [{
|
||||
click: async () => exportLogs(),
|
||||
label: showLogsLabel,
|
||||
}, {
|
||||
@ -464,7 +464,7 @@ export class AppMenu {
|
||||
: i18n.t('Disable GPU')(),
|
||||
click: () => {
|
||||
gpuRestartDialog(!this.disableGpu);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.t('Enable Renderer Logs')(),
|
||||
@ -482,7 +482,7 @@ export class AppMenu {
|
||||
await config.updateUserConfig({ enableRendererLogs });
|
||||
logger.info('New value for enableRendererLogs: ' + this.enableRendererLogs);
|
||||
},
|
||||
} ],
|
||||
}],
|
||||
}, {
|
||||
label: i18n.t('About Symphony')(),
|
||||
visible: isWindowsOS || isLinux,
|
||||
@ -490,7 +490,7 @@ export class AppMenu {
|
||||
const windowName = focusedWindow ? (focusedWindow as ICustomBrowserWindow).winName : '';
|
||||
windowHandler.createAboutAppWindow(windowName);
|
||||
},
|
||||
} ],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
@ -517,13 +517,13 @@ export class AppMenu {
|
||||
}
|
||||
|
||||
if (isMac) {
|
||||
return label ? { role, label, accelerator: role ? macAccelerator[ role ] : '' }
|
||||
: { role, accelerator: role ? macAccelerator[ role ] : '' };
|
||||
return label ? { role, label, accelerator: role ? macAccelerator[role] : '' }
|
||||
: { role, accelerator: role ? macAccelerator[role] : '' };
|
||||
}
|
||||
|
||||
if (isWindowsOS) {
|
||||
return label ? { role, label, accelerator: role ? windowsAccelerator[ role ] : '' }
|
||||
: { role, accelerator: role ? windowsAccelerator[ role ] : '' };
|
||||
return label ? { role, label, accelerator: role ? windowsAccelerator[role] : '' }
|
||||
: { role, accelerator: role ? windowsAccelerator[role] : '' };
|
||||
}
|
||||
|
||||
return label ? { role, label } : { role };
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import sizeOf from 'image-size';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ChildProcess, ExecException, execFile } from 'child_process';
|
||||
import * as util from 'util';
|
||||
import { IScreenSnippet } from '../common/api-interface';
|
||||
import { isDevEnv, isElectronQA, isLinux, isMac, isWindowsOS } from '../common/env';
|
||||
import {
|
||||
isDevEnv,
|
||||
isElectronQA,
|
||||
isLinux,
|
||||
isMac,
|
||||
isWindowsOS,
|
||||
} from '../common/env';
|
||||
import { i18n } from '../common/i18n';
|
||||
import { logger } from '../common/logger';
|
||||
import { updateAlwaysOnTop } from './window-actions';
|
||||
@ -16,187 +23,279 @@ import { windowExists } from './window-utils';
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
|
||||
class ScreenSnippet {
|
||||
private readonly tempDir: string;
|
||||
private readonly captureUtil: string;
|
||||
private outputFileName: string | undefined;
|
||||
private captureUtilArgs: ReadonlyArray<string> | undefined;
|
||||
private child: ChildProcess | undefined;
|
||||
private focusedWindow: BrowserWindow | null = null;
|
||||
private shouldUpdateAlwaysOnTop: boolean = false;
|
||||
private readonly tempDir: string;
|
||||
private readonly captureUtil: string;
|
||||
private outputFileName: string | undefined;
|
||||
private captureUtilArgs: ReadonlyArray<string> | undefined;
|
||||
private child: ChildProcess | undefined;
|
||||
private focusedWindow: BrowserWindow | null = null;
|
||||
private shouldUpdateAlwaysOnTop: boolean = false;
|
||||
|
||||
constructor() {
|
||||
if (isElectronQA) {
|
||||
this.tempDir = os.tmpdir();
|
||||
} else {
|
||||
this.tempDir = path.join(app.getPath('userData'), 'temp');
|
||||
if (!fs.existsSync(this.tempDir)) {
|
||||
fs.mkdirSync(this.tempDir);
|
||||
}
|
||||
}
|
||||
this.captureUtil = isMac ? '/usr/sbin/screencapture' : isDevEnv
|
||||
? path.join(__dirname,
|
||||
'../../../node_modules/screen-snippet/ScreenSnippet.exe')
|
||||
: path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe');
|
||||
|
||||
if (isLinux) {
|
||||
this.captureUtil = '/usr/bin/gnome-screenshot';
|
||||
}
|
||||
constructor() {
|
||||
if (isElectronQA) {
|
||||
this.tempDir = os.tmpdir();
|
||||
} else {
|
||||
this.tempDir = path.join(app.getPath('userData'), 'temp');
|
||||
if (!fs.existsSync(this.tempDir)) {
|
||||
fs.mkdirSync(this.tempDir);
|
||||
}
|
||||
}
|
||||
this.captureUtil = isMac
|
||||
? '/usr/sbin/screencapture'
|
||||
: isDevEnv
|
||||
? path.join(
|
||||
__dirname,
|
||||
'../../../node_modules/screen-snippet/ScreenSnippet.exe',
|
||||
)
|
||||
: path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe');
|
||||
|
||||
/**
|
||||
* Captures a user selected portion of the monitor and returns jpeg image
|
||||
* encoded in base64 format.
|
||||
*
|
||||
* @param webContents {Electron.webContents}
|
||||
*/
|
||||
public async capture(webContents: Electron.webContents) {
|
||||
const mainWindow = windowHandler.getMainWindow();
|
||||
if (mainWindow && windowExists(mainWindow) && isWindowsOS) {
|
||||
this.shouldUpdateAlwaysOnTop = mainWindow.isAlwaysOnTop();
|
||||
if (this.shouldUpdateAlwaysOnTop) {
|
||||
await updateAlwaysOnTop(false, false, false);
|
||||
}
|
||||
}
|
||||
logger.info(`screen-snippet-handler: Starting screen capture!`);
|
||||
this.outputFileName = path.join(this.tempDir, 'symphonyImage-' + Date.now() + '.png');
|
||||
if (isMac) {
|
||||
this.captureUtilArgs = [ '-i', '-s', '-t', 'png', this.outputFileName ];
|
||||
} else if (isWindowsOS) {
|
||||
if (windowHandler.isMana) {
|
||||
this.captureUtilArgs = [ '--no-annotate', this.outputFileName, i18n.getLocale() ];
|
||||
} else {
|
||||
this.captureUtilArgs = [ this.outputFileName, i18n.getLocale() ];
|
||||
}
|
||||
} else if (isLinux) {
|
||||
this.captureUtilArgs = ['-a', '-f', this.outputFileName];
|
||||
} else {
|
||||
this.captureUtilArgs = [];
|
||||
}
|
||||
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
logger.info(`screen-snippet-handler: Capturing snippet with file ${this.outputFileName} and args ${this.captureUtilArgs}!`);
|
||||
|
||||
// only allow one screen capture at a time.
|
||||
if (this.child) {
|
||||
logger.info(`screen-snippet-handler: Child screen capture exists, killing it and keeping only 1 instance!`);
|
||||
this.killChildProcess();
|
||||
}
|
||||
try {
|
||||
await this.execCmd(this.captureUtil, this.captureUtilArgs);
|
||||
const { message, data, type }: IScreenSnippet = await this.convertFileToData();
|
||||
logger.info(`screen-snippet-handler: Snippet captured! Sending data to SFE`);
|
||||
webContents.send('screen-snippet-data', { message, data, type });
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
} catch (error) {
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
logger.error(`screen-snippet-handler: screen capture failed with error: ${error}!`);
|
||||
}
|
||||
if (isLinux) {
|
||||
this.captureUtil = '/usr/bin/gnome-screenshot';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a screen capture and closes the snippet window
|
||||
*/
|
||||
public async cancelCapture() {
|
||||
if (!isWindowsOS) {
|
||||
return;
|
||||
}
|
||||
logger.info(`screen-snippet-handler: Cancel screen capture!`);
|
||||
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
try {
|
||||
await this.execCmd(this.captureUtil, []);
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
} catch (error) {
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
logger.error(`screen-snippet-handler: screen capture cancel failed with error: ${error}!`);
|
||||
}
|
||||
/**
|
||||
* Captures a user selected portion of the monitor and returns jpeg image
|
||||
* encoded in base64 format.
|
||||
*
|
||||
* @param webContents {Electron.webContents}
|
||||
*/
|
||||
public async capture(webContents: Electron.webContents) {
|
||||
const mainWindow = windowHandler.getMainWindow();
|
||||
if (mainWindow && windowExists(mainWindow) && isWindowsOS) {
|
||||
this.shouldUpdateAlwaysOnTop = mainWindow.isAlwaysOnTop();
|
||||
if (this.shouldUpdateAlwaysOnTop) {
|
||||
await updateAlwaysOnTop(false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills the child process when the application is reloaded
|
||||
*/
|
||||
public killChildProcess(): void {
|
||||
if (this.child && typeof this.child.kill === 'function') {
|
||||
this.child.kill();
|
||||
}
|
||||
logger.info(`screen-snippet-handler: Starting screen capture!`);
|
||||
this.outputFileName = path.join(
|
||||
this.tempDir,
|
||||
'symphonyImage-' + Date.now() + '.png',
|
||||
);
|
||||
if (isMac) {
|
||||
this.captureUtilArgs = ['-i', '-s', '-t', 'png', this.outputFileName];
|
||||
} else if (isWindowsOS) {
|
||||
if (windowHandler.isMana) {
|
||||
this.captureUtilArgs = [
|
||||
'--no-annotate',
|
||||
this.outputFileName,
|
||||
i18n.getLocale(),
|
||||
];
|
||||
} else {
|
||||
this.captureUtilArgs = [this.outputFileName, i18n.getLocale()];
|
||||
}
|
||||
} else if (isLinux) {
|
||||
this.captureUtilArgs = ['-a', '-f', this.outputFileName];
|
||||
} else {
|
||||
this.captureUtilArgs = [];
|
||||
}
|
||||
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
/**
|
||||
* Executes the given command via a child process
|
||||
*
|
||||
* Windows: uses custom built windows screen capture tool
|
||||
* Mac OSX: uses built-in screencapture tool which has been
|
||||
* available since OSX ver 10.2.
|
||||
*
|
||||
* @param captureUtil {string}
|
||||
* @param captureUtilArgs {captureUtilArgs}
|
||||
* @example execCmd('-i -s', '/user/desktop/symphonyImage-1544025391698.png')
|
||||
*/
|
||||
private execCmd(captureUtil: string, captureUtilArgs: ReadonlyArray<string>): Promise<ChildProcess> {
|
||||
logger.info(`screen-snippet-handlers: execCmd ${captureUtil} ${captureUtilArgs}`);
|
||||
return new Promise<ChildProcess>((resolve, reject) => {
|
||||
return this.child = execFile(captureUtil, captureUtilArgs, (error: ExecException | null) => {
|
||||
if (error && error.killed) {
|
||||
// processs was killed, just resolve with no data.
|
||||
return reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
logger.info(
|
||||
`screen-snippet-handler: Capturing snippet with file ${this.outputFileName} and args ${this.captureUtilArgs}!`,
|
||||
);
|
||||
|
||||
// only allow one screen capture at a time.
|
||||
if (this.child) {
|
||||
logger.info(
|
||||
`screen-snippet-handler: Child screen capture exists, killing it and keeping only 1 instance!`,
|
||||
);
|
||||
this.killChildProcess();
|
||||
}
|
||||
try {
|
||||
await this.execCmd(this.captureUtil, this.captureUtilArgs);
|
||||
if (windowHandler.isMana) {
|
||||
windowHandler.closeSnippingToolWindow();
|
||||
const dimensions = this.getImageSize();
|
||||
windowHandler.createSnippingToolWindow(this.outputFileName, dimensions);
|
||||
this.uploadSnippet(webContents);
|
||||
return;
|
||||
}
|
||||
const {
|
||||
message,
|
||||
data,
|
||||
type,
|
||||
}: IScreenSnippet = await this.convertFileToData();
|
||||
logger.info(
|
||||
`screen-snippet-handler: Snippet captured! Sending data to SFE`,
|
||||
);
|
||||
webContents.send('screen-snippet-data', { message, data, type });
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
} catch (error) {
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
logger.error(
|
||||
`screen-snippet-handler: screen capture failed with error: ${error}!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a screen capture and closes the snippet window
|
||||
*/
|
||||
public async cancelCapture() {
|
||||
if (!isWindowsOS) {
|
||||
return;
|
||||
}
|
||||
logger.info(`screen-snippet-handler: Cancel screen capture!`);
|
||||
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
try {
|
||||
await this.execCmd(this.captureUtil, []);
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
} catch (error) {
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
logger.error(
|
||||
`screen-snippet-handler: screen capture cancel failed with error: ${error}!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills the child process when the application is reloaded
|
||||
*/
|
||||
public killChildProcess(): void {
|
||||
if (this.child && typeof this.child.kill === 'function') {
|
||||
this.child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given command via a child process
|
||||
*
|
||||
* Windows: uses custom built windows screen capture tool
|
||||
* Mac OSX: uses built-in screencapture tool which has been
|
||||
* available since OSX ver 10.2.
|
||||
*
|
||||
* @param captureUtil {string}
|
||||
* @param captureUtilArgs {captureUtilArgs}
|
||||
* @example execCmd('-i -s', '/user/desktop/symphonyImage-1544025391698.png')
|
||||
*/
|
||||
private execCmd(
|
||||
captureUtil: string,
|
||||
captureUtilArgs: ReadonlyArray<string>,
|
||||
): Promise<ChildProcess> {
|
||||
logger.info(
|
||||
`screen-snippet-handlers: execCmd ${captureUtil} ${captureUtilArgs}`,
|
||||
);
|
||||
return new Promise<ChildProcess>((resolve, reject) => {
|
||||
return (this.child = execFile(
|
||||
captureUtil,
|
||||
captureUtilArgs,
|
||||
(error: ExecException | null) => {
|
||||
if (error && error.killed) {
|
||||
// processs was killed, just resolve with no data.
|
||||
return reject(error);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the temporary stored file into base64
|
||||
* and removes the temp file
|
||||
*
|
||||
* @return Promise<IScreenSnippet> { message, data, type }
|
||||
*/
|
||||
private async convertFileToData(): Promise<IScreenSnippet> {
|
||||
try {
|
||||
if (!this.outputFileName) {
|
||||
logger.info(
|
||||
`screen-snippet-handler: screen capture failed! output file doesn't exist!`,
|
||||
);
|
||||
return { message: 'output file name is required', type: 'ERROR' };
|
||||
}
|
||||
const data = await readFile(this.outputFileName);
|
||||
if (!data) {
|
||||
logger.info(
|
||||
`screen-snippet-handler: screen capture failed! data doesn't exist!`,
|
||||
);
|
||||
return { message: `no file data provided`, type: 'ERROR' };
|
||||
}
|
||||
// convert binary data to base64 encoded string
|
||||
const output = Buffer.from(data).toString('base64');
|
||||
return { message: 'success', data: output, type: 'image/png;base64' };
|
||||
} catch (error) {
|
||||
// no such file exists or user likely aborted
|
||||
// creating snippet. also include any error when
|
||||
// creating child process.
|
||||
return error && error.code === 'ENOENT'
|
||||
? { message: `file does not exist`, type: 'ERROR' }
|
||||
: { message: `${error}`, type: 'ERROR' };
|
||||
} finally {
|
||||
if (this.focusedWindow && windowExists(this.focusedWindow)) {
|
||||
this.focusedWindow.moveTop();
|
||||
}
|
||||
// remove tmp file (async)
|
||||
if (this.outputFileName) {
|
||||
fs.unlink(this.outputFileName, (removeErr) => {
|
||||
logger.info(
|
||||
`screen-snippet-handler: cleaning up temp snippet file: ${this.outputFileName}!`,
|
||||
);
|
||||
if (removeErr) {
|
||||
logger.error(
|
||||
`screen-snippet-handler: error removing temp snippet file: ${this.outputFileName}, err: ${removeErr}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify and updates always on top
|
||||
*/
|
||||
private async verifyAndUpdateAlwaysOnTop(): Promise<void> {
|
||||
if (this.shouldUpdateAlwaysOnTop) {
|
||||
await updateAlwaysOnTop(true, false, false);
|
||||
this.shouldUpdateAlwaysOnTop = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height & width of an image
|
||||
*/
|
||||
private getImageSize():
|
||||
| {
|
||||
height: number | undefined;
|
||||
width: number | undefined;
|
||||
}
|
||||
| undefined {
|
||||
if (!this.outputFileName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the temporary stored file into base64
|
||||
* and removes the temp file
|
||||
*
|
||||
* @return Promise<IScreenSnippet> { message, data, type }
|
||||
*/
|
||||
private async convertFileToData(): Promise<IScreenSnippet> {
|
||||
try {
|
||||
if (!this.outputFileName) {
|
||||
logger.info(`screen-snippet-handler: screen capture failed! output file doesn't exist!`);
|
||||
return { message: 'output file name is required', type: 'ERROR' };
|
||||
}
|
||||
const data = await readFile(this.outputFileName);
|
||||
if (!data) {
|
||||
logger.info(`screen-snippet-handler: screen capture failed! data doesn't exist!`);
|
||||
return { message: `no file data provided`, type: 'ERROR' };
|
||||
}
|
||||
// convert binary data to base64 encoded string
|
||||
const output = Buffer.from(data).toString('base64');
|
||||
return { message: 'success', data: output, type: 'image/png;base64' };
|
||||
} catch (error) {
|
||||
// no such file exists or user likely aborted
|
||||
// creating snippet. also include any error when
|
||||
// creating child process.
|
||||
return error && error.code === 'ENOENT'
|
||||
? { message: `file does not exist`, type: 'ERROR' }
|
||||
: { message: `${error}`, type: 'ERROR' };
|
||||
} finally {
|
||||
if (this.focusedWindow && windowExists(this.focusedWindow)) {
|
||||
this.focusedWindow.moveTop();
|
||||
}
|
||||
// remove tmp file (async)
|
||||
if (this.outputFileName) {
|
||||
fs.unlink(this.outputFileName, (removeErr) => {
|
||||
logger.info(`screen-snippet-handler: cleaning up temp snippet file: ${this.outputFileName}!`);
|
||||
if (removeErr) {
|
||||
logger.error(`screen-snippet-handler: error removing temp snippet file: ${this.outputFileName}, err: ${removeErr}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const dimensions = sizeOf(this.outputFileName);
|
||||
return {
|
||||
height: dimensions.height,
|
||||
width: dimensions.width,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify and updates always on top
|
||||
*/
|
||||
private async verifyAndUpdateAlwaysOnTop(): Promise<void> {
|
||||
if (this.shouldUpdateAlwaysOnTop) {
|
||||
await updateAlwaysOnTop(true, false, false);
|
||||
this.shouldUpdateAlwaysOnTop = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Uploads a screen snippet
|
||||
* @param webContents A browser window's web contents object
|
||||
*/
|
||||
private uploadSnippet(webContents: Electron.webContents) {
|
||||
ipcMain.once('upload-snippet', async (_event, snipImage: string) => {
|
||||
windowHandler.closeSnippingToolWindow();
|
||||
if (snipImage) {
|
||||
this.outputFileName = snipImage;
|
||||
}
|
||||
const {
|
||||
message,
|
||||
data,
|
||||
type,
|
||||
}: IScreenSnippet = await this.convertFileToData();
|
||||
logger.info(
|
||||
`screen-snippet-handler: Snippet captured! Sending data to SFE`,
|
||||
);
|
||||
webContents.send('screen-snippet-data', { message, data, type });
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const screenSnippet = new ScreenSnippet();
|
||||
|
@ -32,7 +32,7 @@ enum styleNames {
|
||||
}
|
||||
|
||||
const checkValidWindow = true;
|
||||
const { ctWhitelist } = config.getConfigFields([ 'ctWhitelist' ]);
|
||||
const { ctWhitelist } = config.getConfigFields(['ctWhitelist']);
|
||||
|
||||
// Network status check variables
|
||||
const networkStatusCheckInterval = 10 * 1000;
|
||||
@ -76,7 +76,7 @@ export const preventWindowNavigation = (browserWindow: BrowserWindow, isPopOutWi
|
||||
if (browserWindow && windowExists(browserWindow)) {
|
||||
const response = await electron.dialog.showMessageBox(browserWindow, {
|
||||
type: 'warning',
|
||||
buttons: [ 'OK' ],
|
||||
buttons: ['OK'],
|
||||
title: i18n.t('Not Allowed')(),
|
||||
message: `${i18n.t(`Sorry, you are not allowed to access this website`)()} (${winUrl}), ${i18n.t('please contact your administrator for more details')()}`,
|
||||
});
|
||||
@ -317,7 +317,7 @@ export const getBounds = (winPos: ICustomRectangle | Electron.Rectangle | undefi
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
|
||||
for (let i = 0, len = displays.length; i < len; i++) {
|
||||
const bounds = displays[ i ].bounds;
|
||||
const bounds = displays[i].bounds;
|
||||
logger.info('window-utils: getBounds, bounds: ' + JSON.stringify(bounds));
|
||||
if (winPos.x >= bounds.x && winPos.y >= bounds.y &&
|
||||
((winPos.x + winPos.width) <= (bounds.x + bounds.width)) &&
|
||||
@ -570,6 +570,8 @@ export const reloadWindow = (browserWindow: ICustomBrowserWindow) => {
|
||||
logger.info(`window-utils: reloading the main window`);
|
||||
browserWindow.reload();
|
||||
|
||||
windowHandler.closeAllWindow();
|
||||
|
||||
windowHandler.execCmd(windowHandler.screenShareIndicatorFrameUtil, []);
|
||||
|
||||
return;
|
||||
@ -659,11 +661,11 @@ export const monitorNetworkInterception = (url: string) => {
|
||||
logger.info('window-utils: monitoring network interception for url', podUrl);
|
||||
|
||||
// Filter applied w.r.t pod url
|
||||
const filter = { urls: [ podUrl + '*' ] };
|
||||
const filter = { urls: [podUrl + '*'] };
|
||||
|
||||
if (mainWindow && windowExists(mainWindow)) {
|
||||
isNetworkMonitorInitialized = true;
|
||||
mainWindow.webContents.session.webRequest.onErrorOccurred(filter,async (details) => {
|
||||
mainWindow.webContents.session.webRequest.onErrorOccurred(filter, async (details) => {
|
||||
if (!mainWindow || !windowExists(mainWindow)) {
|
||||
return;
|
||||
}
|
||||
@ -671,8 +673,8 @@ export const monitorNetworkInterception = (url: string) => {
|
||||
const isMana = manaUrl && manaUrl.includes('client-bff');
|
||||
if (!isMana && windowHandler.isWebPageLoading
|
||||
&& (details.error === 'net::ERR_INTERNET_DISCONNECTED'
|
||||
|| details.error === 'net::ERR_NETWORK_CHANGED'
|
||||
|| details.error === 'net::ERR_NAME_NOT_RESOLVED')) {
|
||||
|| details.error === 'net::ERR_NETWORK_CHANGED'
|
||||
|| details.error === 'net::ERR_NAME_NOT_RESOLVED')) {
|
||||
|
||||
logger.error(`window-utils: URL failed to load`, details);
|
||||
mainWindow.webContents.send('show-banner', { show: true, bannerType: 'error', url: podUrl });
|
||||
|
@ -158,7 +158,8 @@
|
||||
"Erase": "Erase",
|
||||
"Highlight": "Highlight",
|
||||
"Pen": "Pen",
|
||||
"Snipping Tool": "Snipping Tool"
|
||||
"Snipping Tool": "Snipping Tool",
|
||||
"Clear": "Clear"
|
||||
},
|
||||
"Select All": "Select All",
|
||||
"Services": "Services",
|
||||
|
@ -158,7 +158,8 @@
|
||||
"Erase": "Erase",
|
||||
"Highlight": "Highlight",
|
||||
"Pen": "Pen",
|
||||
"Snipping Tool": "Snipping Tool"
|
||||
"Snipping Tool": "Snipping Tool",
|
||||
"Clear": "Clear"
|
||||
},
|
||||
"Select All": "Select All",
|
||||
"Services": "Services",
|
||||
|
@ -31,7 +31,6 @@
|
||||
"Build expired": "Construit expiré",
|
||||
"Cancel": "Annuler",
|
||||
"Certificate Error": "Erreur de certificat",
|
||||
"Changing GPU settings requires Symphony to relaunch.": "La modification des paramètres du GPU nécessite la relance de Symphony.",
|
||||
"Clear cache and Reload": "Vider le cache et rafraîchir Symphony",
|
||||
"Close": "Fermer",
|
||||
"ContextMenu": {
|
||||
@ -159,7 +158,8 @@
|
||||
"Erase": "Effacer",
|
||||
"Highlight": "Surligner",
|
||||
"Pen": "Stylo",
|
||||
"Snipping Tool": "Outil Capture"
|
||||
"Snipping Tool": "Outil Capture",
|
||||
"Clear": "Claire"
|
||||
},
|
||||
"Select All": "Tout sélectionner",
|
||||
"Services": "Services",
|
||||
|
@ -158,7 +158,8 @@
|
||||
"Erase": "Effacer",
|
||||
"Highlight": "Surligner",
|
||||
"Pen": "Stylo",
|
||||
"Snipping Tool": "Outil Capture"
|
||||
"Snipping Tool": "Outil Capture",
|
||||
"Clear": "Claire"
|
||||
},
|
||||
"Select All": "Tout sélectionner",
|
||||
"Services": "Services",
|
||||
|
@ -158,7 +158,8 @@
|
||||
"Erase": "消去",
|
||||
"Highlight": "強調",
|
||||
"Pen": "ペン",
|
||||
"Snipping Tool": "切り取りツール"
|
||||
"Snipping Tool": "切り取りツール",
|
||||
"Clear": "晴れ"
|
||||
},
|
||||
"Select All": "すべてを選択",
|
||||
"Services": "サービス",
|
||||
|
@ -158,7 +158,8 @@
|
||||
"Erase": "消去",
|
||||
"Highlight": "強調",
|
||||
"Pen": "ペン",
|
||||
"Snipping Tool": "切り取りツール"
|
||||
"Snipping Tool": "切り取りツール",
|
||||
"Clear": "晴れ"
|
||||
},
|
||||
"Select All": "すべてを選択",
|
||||
"Services": "サービス",
|
||||
|
4
src/renderer/assets/snip-draw.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 12V16H10V10L5 0L0 10V16H2V12H8ZM7.5 10L5.54186 6H4.45814L2.5 10H7.5Z" fill="#525760"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 12V16H10V10L5 0L0 10V16H2V12H8ZM7.5 10L5.54186 6H4.45814L2.5 10H7.5Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 399 B |
4
src/renderer/assets/snip-erase.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 5V16H2V10H8V16H10V5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5ZM8 8V6H2V8H8Z" fill="#525760"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 5V16H2V10H8V16H10V5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5ZM8 8V6H2V8H8Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 427 B |
4
src/renderer/assets/snip-highlight.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 1.5L2.95814 5H2.90657L0 10V16H2V12H10V16H12V10L8.62966 5.07316L8 0L3.5 1.5ZM10 10L7.75 7H3.75L2 10H10Z" fill="#525760"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 1.5L2.95814 5H2.90657L0 10V16H2V12H10V16H12V10L8.62966 5.07316L8 0L3.5 1.5ZM10 10L7.75 7H3.75L2 10H10Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 471 B |
BIN
src/renderer/assets/snip-redo.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
4
src/renderer/assets/snip-redo.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.7071 6.70706C16.0976 6.31654 16.0976 5.68337 15.7071 5.29285L10.7071 0.29289C10.3166 -0.0976323 9.68341 -0.0976299 9.29289 0.292896C8.90237 0.683422 8.90237 1.31659 9.2929 1.70711L12.5858 5.00001H5.5C2.46243 5.00001 0 7.46245 0 10.5C0 13.5376 2.46243 16 5.5 16H7C7.55228 16 8 15.5523 8 15C8 14.4477 7.55228 14 7 14H5.5C3.567 14 2 12.433 2 10.5C2 8.56701 3.567 7.00001 5.5 7.00001H12.5858L9.29289 10.2929C8.90237 10.6834 8.90237 11.3166 9.2929 11.7071C9.68342 12.0976 10.3166 12.0976 10.7071 11.7071L15.7071 6.70706Z" fill="#525760"/>
|
||||
<path d="M15.7071 6.70706C16.0976 6.31654 16.0976 5.68337 15.7071 5.29285L10.7071 0.29289C10.3166 -0.0976323 9.68341 -0.0976299 9.29289 0.292896C8.90237 0.683422 8.90237 1.31659 9.2929 1.70711L12.5858 5.00001H5.5C2.46243 5.00001 0 7.46245 0 10.5C0 13.5376 2.46243 16 5.5 16H7C7.55228 16 8 15.5523 8 15C8 14.4477 7.55228 14 7 14H5.5C3.567 14 2 12.433 2 10.5C2 8.56701 3.567 7.00001 5.5 7.00001H12.5858L9.29289 10.2929C8.90237 10.6834 8.90237 11.3166 9.2929 11.7071C9.68342 12.0976 10.3166 12.0976 10.7071 11.7071L15.7071 6.70706Z" fill="white" fill-opacity="0.72"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/renderer/assets/snip-undo.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
4
src/renderer/assets/snip-undo.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.292891 6.70706C-0.0976315 6.31654 -0.0976276 5.68337 0.292897 5.29285L5.2929 0.29289C5.68342 -0.0976323 6.31659 -0.0976299 6.70711 0.292896C7.09763 0.683422 7.09763 1.31659 6.7071 1.70711L3.41418 5.00001H10.5C13.5376 5.00001 16 7.46245 16 10.5C16 13.5376 13.5376 16 10.5 16H9C8.44772 16 8 15.5523 8 15C8 14.4477 8.44772 14 9 14H10.5C12.433 14 14 12.433 14 10.5C14 8.56701 12.433 7.00001 10.5 7.00001H3.41425L6.70711 10.2929C7.09763 10.6834 7.09763 11.3166 6.7071 11.7071C6.31658 12.0976 5.68341 12.0976 5.29289 11.7071L0.292891 6.70706Z" fill="#525760"/>
|
||||
<path d="M0.292891 6.70706C-0.0976315 6.31654 -0.0976276 5.68337 0.292897 5.29285L5.2929 0.29289C5.68342 -0.0976323 6.31659 -0.0976299 6.70711 0.292896C7.09763 0.683422 7.09763 1.31659 6.7071 1.70711L3.41418 5.00001H10.5C13.5376 5.00001 16 7.46245 16 10.5C16 13.5376 13.5376 16 10.5 16H9C8.44772 16 8 15.5523 8 15C8 14.4477 8.44772 14 9 14H10.5C12.433 14 14 12.433 14 10.5C14 8.56701 12.433 7.00001 10.5 7.00001H3.41425L6.70711 10.2929C7.09763 10.6834 7.09763 11.3166 6.7071 11.7071C6.31658 12.0976 5.68341 12.0976 5.29289 11.7071L0.292891 6.70706Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -133,12 +133,12 @@ export default class AboutApp extends React.Component<{}, IState> {
|
||||
public copy(): void {
|
||||
const data = this.state;
|
||||
if (data) {
|
||||
remote.clipboard.write({ text: JSON.stringify(data, null, 4) }, 'clipboard' );
|
||||
remote.clipboard.write({ text: JSON.stringify(data, null, 4) }, 'clipboard');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object} { buildNumber, clientVersion, version }
|
||||
|
@ -55,18 +55,18 @@ export default class BasicAuth extends React.Component<{}, IState> {
|
||||
<form id='basicAuth' name='Basic Auth' action='Login' onSubmit={this.eventHandlers.onSubmit}>
|
||||
<table className='form'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id='username-text'>{i18n.t('User name:', BASIC_AUTH_NAMESPACE)()}</td>
|
||||
<td>
|
||||
<input id='username' name='username' title='Username' onChange={this.eventHandlers.onChange} required/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id='password-text'>{i18n.t('Password:', BASIC_AUTH_NAMESPACE)()}</td>
|
||||
<td>
|
||||
<input name='password' id='password' type='password' title='Password' onChange={this.eventHandlers.onChange} required/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id='username-text'>{i18n.t('User name:', BASIC_AUTH_NAMESPACE)()}</td>
|
||||
<td>
|
||||
<input id='username' name='username' title='Username' onChange={this.eventHandlers.onChange} required />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id='password-text'>{i18n.t('Password:', BASIC_AUTH_NAMESPACE)()}</td>
|
||||
<td>
|
||||
<input name='password' id='password' type='password' title='Password' onChange={this.eventHandlers.onChange} required />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className='footer'>
|
||||
@ -89,7 +89,7 @@ export default class BasicAuth extends React.Component<{}, IState> {
|
||||
*/
|
||||
private change(event): void {
|
||||
this.setState({
|
||||
[ (event.target as any).id ]: (event.target as any).value,
|
||||
[(event.target as any).id]: (event.target as any).value,
|
||||
} as IState);
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ export default class BasicAuth extends React.Component<{}, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object} { hostname, isValidCredentials }
|
||||
|
66
src/renderer/components/color-picker-pill.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IColorPickerPillProps {
|
||||
availableColors: IColor[];
|
||||
onChange: (color: string) => void;
|
||||
}
|
||||
|
||||
export interface IColor {
|
||||
rgbaColor: string; // Should be provided as a rgba string i.e. 'rgba(255, 0, 0, 0.3)'
|
||||
outline?: string; // Should be provided as a rgba string i.e. 'rgba(255, 0, 0, 0.3)'
|
||||
chosen?: boolean;
|
||||
}
|
||||
|
||||
const ColorPickerPill = (props: IColorPickerPillProps) => {
|
||||
const getChosenColor = (colors: IColor[]) => {
|
||||
return colors.find((color) => color.chosen === true);
|
||||
};
|
||||
const chosenColor = getChosenColor(props.availableColors);
|
||||
|
||||
const ColorDot = (color: IColor) => {
|
||||
const isChosenColor = color === chosenColor;
|
||||
const hasOutline = !!color.outline;
|
||||
const border = 'solid 1px ' + color.outline;
|
||||
|
||||
const getWidthAndHeight = () => {
|
||||
if (isChosenColor) {
|
||||
return hasOutline ? '22px' : '24px';
|
||||
}
|
||||
return hasOutline ? '6px' : '8px';
|
||||
};
|
||||
|
||||
const widthAndHeight = getWidthAndHeight();
|
||||
|
||||
const chooseColor = () => {
|
||||
props.onChange(color.rgbaColor);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={color.rgbaColor}
|
||||
className='enclosingCircle'
|
||||
onClick={chooseColor}
|
||||
data-testid={'colorDot ' + color.rgbaColor}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: color.rgbaColor,
|
||||
width: widthAndHeight,
|
||||
height: widthAndHeight,
|
||||
cursor: 'pointer',
|
||||
border: hasOutline ? border : undefined,
|
||||
}}
|
||||
className='colorDot'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='colorPicker'>
|
||||
{props.availableColors.map((color) => ColorDot(color))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorPickerPill;
|
@ -50,11 +50,11 @@ export default class LoadingScreen extends React.Component<{}, IState> {
|
||||
|
||||
return (
|
||||
<div className='LoadingScreen'>
|
||||
<img className='LoadingScreen-logo' src={this.getImage(error)}/>
|
||||
<img className='LoadingScreen-logo' src={this.getImage(error)} />
|
||||
<span className='LoadingScreen-name'>{appName}</span>
|
||||
<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 200' preserveAspectRatio='xMidYMid'>
|
||||
<circle cx='50' cy='50' fill='none' ng-attr-stroke='{{config.color}}' stroke='#ffffff' strokeWidth='10' r='35' strokeDasharray='164.93361431346415 56.97787143782138' transform='rotate(59.6808 50 50)'>
|
||||
<animateTransform attributeName='transform' type='rotate' calcMode='linear' values='0 50 50;360 50 50' keyTimes='0;1' dur='1s' begin='0s' repeatCount='indefinite'/>
|
||||
<animateTransform attributeName='transform' type='rotate' calcMode='linear' values='0 50 50;360 50 50' keyTimes='0;1' dur='1s' begin='0s' repeatCount='indefinite' />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
@ -78,7 +78,7 @@ export default class LoadingScreen extends React.Component<{}, IState> {
|
||||
private renderErrorContent(error: string): JSX.Element {
|
||||
return (
|
||||
<div className='LoadingScreen'>
|
||||
<img className='LoadingScreen-logo' src={this.getImage(error)}/>
|
||||
<img className='LoadingScreen-logo' src={this.getImage(error)} />
|
||||
<span className='LoadingScreen-name'>{i18n.t('Problem connecting to Symphony')()}</span>
|
||||
<div id='error-code' className='LoadingScreen-error-code'>{error}</div>
|
||||
<div>
|
||||
@ -104,7 +104,7 @@ export default class LoadingScreen extends React.Component<{}, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object}
|
||||
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
|
||||
const whiteColorRegExp = new RegExp(/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i);
|
||||
const darkTheme = [ '#e23030', '#b5616a', '#ab8ead', '#ebc875', '#a3be77', '#58c6ff', '#ebab58' ];
|
||||
const darkTheme = ['#e23030', '#b5616a', '#ab8ead', '#ebc875', '#a3be77', '#58c6ff', '#ebab58'];
|
||||
type Theme = '' | 'light' | 'dark';
|
||||
|
||||
interface IState {
|
||||
@ -77,29 +77,29 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
|
||||
return (
|
||||
<div className='container'
|
||||
role='alert'
|
||||
style={bgColor}
|
||||
onContextMenu={this.eventHandlers.onContextMenu}
|
||||
onClick={this.eventHandlers.onClick(id)}
|
||||
onMouseEnter={this.eventHandlers.onMouseEnter(id)}
|
||||
onMouseLeave={this.eventHandlers.onMouseLeave(id)}
|
||||
role='alert'
|
||||
style={bgColor}
|
||||
onContextMenu={this.eventHandlers.onContextMenu}
|
||||
onClick={this.eventHandlers.onClick(id)}
|
||||
onMouseEnter={this.eventHandlers.onMouseEnter(id)}
|
||||
onMouseLeave={this.eventHandlers.onMouseLeave(id)}
|
||||
>
|
||||
{isExternal ? <div className='ext-border'/> : null}
|
||||
{isExternal ? <div className='ext-border' /> : null}
|
||||
<div className='logo-container'>
|
||||
<div className='logo'>
|
||||
<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z' fill='#000028'/>
|
||||
<path d='M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z' fill='url(#paint0_linear)'/>
|
||||
<path d='M28 17.1029V13.4094C28 12.6528 27.56 11.9425 26.8467 11.5534C25.7833 10.9728 23.48 10 20 10C16.52 10 14.2167 10.9728 13.1533 11.5565C12.44 11.9425 12 12.6528 12 13.4094V18.9559L24.6667 22.3529V24.8235C24.6667 25.1571 24.44 25.3918 24.0533 25.5678L20 27.4485L15.9233 25.5585C15.56 25.3918 15.3333 25.1571 15.3333 24.8235V22.9706L12 22.0441V24.8235C12 26.3491 12.9433 27.6462 14.4433 28.3225L20 31L25.5333 28.3349C27.0567 27.6462 28 26.3491 28 24.8235V20.1912L15.3333 16.7941V13.9746C16.24 13.57 17.78 13.0882 20 13.0882C22.22 13.0882 23.76 13.57 24.6667 13.9746V16.1765L28 17.1029Z' fill='#0098FF'/>
|
||||
<path d='M28 17.1029V13.4094C28 12.6528 27.56 11.9425 26.8467 11.5534C25.7833 10.9728 23.48 10 20 10C16.52 10 14.2167 10.9728 13.1533 11.5565C12.44 11.9425 12 12.6528 12 13.4094V18.9559L24.6667 22.3529V24.8235C24.6667 25.1571 24.44 25.3918 24.0533 25.5678L20 27.4485L15.9233 25.5585C15.56 25.3918 15.3333 25.1571 15.3333 24.8235V22.9706L12 22.0441V24.8235C12 26.3491 12.9433 27.6462 14.4433 28.3225L20 31L25.5333 28.3349C27.0567 27.6462 28 26.3491 28 24.8235V20.1912L15.3333 16.7941V13.9746C16.24 13.57 17.78 13.0882 20 13.0882C22.22 13.0882 23.76 13.57 24.6667 13.9746V16.1765L28 17.1029Z' fill='url(#paint1_radial)'/>
|
||||
<path d='M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z' fill='#000028' />
|
||||
<path d='M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z' fill='url(#paint0_linear)' />
|
||||
<path d='M28 17.1029V13.4094C28 12.6528 27.56 11.9425 26.8467 11.5534C25.7833 10.9728 23.48 10 20 10C16.52 10 14.2167 10.9728 13.1533 11.5565C12.44 11.9425 12 12.6528 12 13.4094V18.9559L24.6667 22.3529V24.8235C24.6667 25.1571 24.44 25.3918 24.0533 25.5678L20 27.4485L15.9233 25.5585C15.56 25.3918 15.3333 25.1571 15.3333 24.8235V22.9706L12 22.0441V24.8235C12 26.3491 12.9433 27.6462 14.4433 28.3225L20 31L25.5333 28.3349C27.0567 27.6462 28 26.3491 28 24.8235V20.1912L15.3333 16.7941V13.9746C16.24 13.57 17.78 13.0882 20 13.0882C22.22 13.0882 23.76 13.57 24.6667 13.9746V16.1765L28 17.1029Z' fill='#0098FF' />
|
||||
<path d='M28 17.1029V13.4094C28 12.6528 27.56 11.9425 26.8467 11.5534C25.7833 10.9728 23.48 10 20 10C16.52 10 14.2167 10.9728 13.1533 11.5565C12.44 11.9425 12 12.6528 12 13.4094V18.9559L24.6667 22.3529V24.8235C24.6667 25.1571 24.44 25.3918 24.0533 25.5678L20 27.4485L15.9233 25.5585C15.56 25.3918 15.3333 25.1571 15.3333 24.8235V22.9706L12 22.0441V24.8235C12 26.3491 12.9433 27.6462 14.4433 28.3225L20 31L25.5333 28.3349C27.0567 27.6462 28 26.3491 28 24.8235V20.1912L15.3333 16.7941V13.9746C16.24 13.57 17.78 13.0882 20 13.0882C22.22 13.0882 23.76 13.57 24.6667 13.9746V16.1765L28 17.1029Z' fill='url(#paint1_radial)' />
|
||||
<defs>
|
||||
<linearGradient id='paint0_linear' x1='20' y1='0' x2='20' y2='40' gradientUnits='userSpaceOnUse'>
|
||||
<stop stopColor='white' stopOpacity='0.2'/>
|
||||
<stop offset='1' stopColor='white' stopOpacity='0'/>
|
||||
<stop stopColor='white' stopOpacity='0.2' />
|
||||
<stop offset='1' stopColor='white' stopOpacity='0' />
|
||||
</linearGradient>
|
||||
<radialGradient id='paint1_radial' cx='0' cy='0' r='1' gradientUnits='userSpaceOnUse' gradientTransform='translate(20.0278 10) rotate(90) scale(14.2187 20.1481)'>
|
||||
<stop stopColor='white' stopOpacity='0.4'/>
|
||||
<stop offset='1' stopColor='white' stopOpacity='0'/>
|
||||
<stop stopColor='white' stopOpacity='0.4' />
|
||||
<stop offset='1' stopColor='white' stopOpacity='0' />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
@ -115,8 +115,8 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
{this.renderProfile(icon, image, title)}
|
||||
<div className='close' title={i18n.t('Close')()} onClick={this.eventHandlers.onClose(id)}>
|
||||
<svg width='8' height='8' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M1.35355 0.646447C1.15829 0.451184 0.841709 0.451184 0.646447 0.646447C0.451184 0.841709 0.451184 1.15829 0.646447 1.35355L3.29289 4L0.646447 6.64645C0.451185 6.84171 0.451185 7.15829 0.646447 7.35356C0.841709 7.54882 1.15829 7.54882 1.35355 7.35356L4 4.70711L6.64645 7.35355C6.84171 7.54882 7.15829 7.54882 7.35355 7.35355C7.54882 7.15829 7.54882 6.84171 7.35355 6.64645L4.70711 4L7.35355 1.35356C7.54882 1.15829 7.54882 0.84171 7.35355 0.646448C7.15829 0.451186 6.84171 0.451186 6.64645 0.646448L4 3.29289L1.35355 0.646447Z' fill='#525760'/>
|
||||
<path d='M1.35355 0.646447C1.15829 0.451184 0.841709 0.451184 0.646447 0.646447C0.451184 0.841709 0.451184 1.15829 0.646447 1.35355L3.29289 4L0.646447 6.64645C0.451185 6.84171 0.451185 7.15829 0.646447 7.35356C0.841709 7.54882 1.15829 7.54882 1.35355 7.35356L4 4.70711L6.64645 7.35355C6.84171 7.54882 7.15829 7.54882 7.35355 7.35355C7.54882 7.15829 7.54882 6.84171 7.35355 6.64645L4.70711 4L7.35355 1.35356C7.54882 1.15829 7.54882 0.84171 7.35355 0.646448C7.15829 0.451186 6.84171 0.451186 6.64645 0.646448L4 3.29289L1.35355 0.646447Z' fill='white' fill-opacity='0.96'/>
|
||||
<path d='M1.35355 0.646447C1.15829 0.451184 0.841709 0.451184 0.646447 0.646447C0.451184 0.841709 0.451184 1.15829 0.646447 1.35355L3.29289 4L0.646447 6.64645C0.451185 6.84171 0.451185 7.15829 0.646447 7.35356C0.841709 7.54882 1.15829 7.54882 1.35355 7.35356L4 4.70711L6.64645 7.35355C6.84171 7.54882 7.15829 7.54882 7.35355 7.35355C7.54882 7.15829 7.54882 6.84171 7.35355 6.64645L4.70711 4L7.35355 1.35356C7.54882 1.15829 7.54882 0.84171 7.35355 0.646448C7.15829 0.451186 6.84171 0.451186 6.64645 0.646448L4 3.29289L1.35355 0.646447Z' fill='#525760' />
|
||||
<path d='M1.35355 0.646447C1.15829 0.451184 0.841709 0.451184 0.646447 0.646447C0.451184 0.841709 0.451184 1.15829 0.646447 1.35355L3.29289 4L0.646447 6.64645C0.451185 6.84171 0.451185 7.15829 0.646447 7.35356C0.841709 7.54882 1.15829 7.54882 1.35355 7.35356L4 4.70711L6.64645 7.35355C6.84171 7.54882 7.15829 7.54882 7.35355 7.35355C7.54882 7.15829 7.54882 6.84171 7.35355 6.64645L4.70711 4L7.35355 1.35356C7.54882 1.15829 7.54882 0.84171 7.35355 0.646448C7.15829 0.451186 6.84171 0.451186 6.64645 0.646448L4 3.29289L1.35355 0.646447Z' fill='white' fill-opacity='0.96' />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,10 +134,10 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
return (
|
||||
<div className='ext-badge-container'>
|
||||
<svg width='32' height='16' viewBox='0 0 32 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<rect width='32' height='16' rx='8' fill='#F6B202'/>
|
||||
<rect width='32' height='16' rx='8' fill='white' fill-opacity='0.24'/>
|
||||
<path d='M11.4414 13H6.72461V4.59766H11.2539V5.78125H8.11914V8.16016H11.0078V9.33789H8.11914V11.8223H11.4414V13ZM19.3574 13H17.6875L15.9648 9.91797C15.9141 9.82422 15.8574 9.69141 15.7949 9.51953H15.7715C15.7363 9.60547 15.6777 9.73828 15.5957 9.91797L13.8203 13H12.1387L14.8926 8.77539L12.3613 4.59766H14.0664L15.584 7.43359C15.6816 7.62109 15.7695 7.80859 15.8477 7.99609H15.8652C15.9785 7.75 16.0762 7.55469 16.1582 7.41016L17.7344 4.59766H19.3047L16.7148 8.76367L19.3574 13ZM26.1191 5.78125H23.7051V13H22.3105V5.78125H19.9023V4.59766H26.1191V5.78125Z' fill='#525760'/>
|
||||
<path d='M11.4414 13H6.72461V4.59766H11.2539V5.78125H8.11914V8.16016H11.0078V9.33789H8.11914V11.8223H11.4414V13ZM19.3574 13H17.6875L15.9648 9.91797C15.9141 9.82422 15.8574 9.69141 15.7949 9.51953H15.7715C15.7363 9.60547 15.6777 9.73828 15.5957 9.91797L13.8203 13H12.1387L14.8926 8.77539L12.3613 4.59766H14.0664L15.584 7.43359C15.6816 7.62109 15.7695 7.80859 15.8477 7.99609H15.8652C15.9785 7.75 16.0762 7.55469 16.1582 7.41016L17.7344 4.59766H19.3047L16.7148 8.76367L19.3574 13ZM26.1191 5.78125H23.7051V13H22.3105V5.78125H19.9023V4.59766H26.1191V5.78125Z' fill='black' fill-opacity='0.24'/>
|
||||
<rect width='32' height='16' rx='8' fill='#F6B202' />
|
||||
<rect width='32' height='16' rx='8' fill='white' fill-opacity='0.24' />
|
||||
<path d='M11.4414 13H6.72461V4.59766H11.2539V5.78125H8.11914V8.16016H11.0078V9.33789H8.11914V11.8223H11.4414V13ZM19.3574 13H17.6875L15.9648 9.91797C15.9141 9.82422 15.8574 9.69141 15.7949 9.51953H15.7715C15.7363 9.60547 15.6777 9.73828 15.5957 9.91797L13.8203 13H12.1387L14.8926 8.77539L12.3613 4.59766H14.0664L15.584 7.43359C15.6816 7.62109 15.7695 7.80859 15.8477 7.99609H15.8652C15.9785 7.75 16.0762 7.55469 16.1582 7.41016L17.7344 4.59766H19.3047L16.7148 8.76367L19.3574 13ZM26.1191 5.78125H23.7051V13H22.3105V5.78125H19.9023V4.59766H26.1191V5.78125Z' fill='#525760' />
|
||||
<path d='M11.4414 13H6.72461V4.59766H11.2539V5.78125H8.11914V8.16016H11.0078V9.33789H8.11914V11.8223H11.4414V13ZM19.3574 13H17.6875L15.9648 9.91797C15.9141 9.82422 15.8574 9.69141 15.7949 9.51953H15.7715C15.7363 9.60547 15.6777 9.73828 15.5957 9.91797L13.8203 13H12.1387L14.8926 8.77539L12.3613 4.59766H14.0664L15.584 7.43359C15.6816 7.62109 15.7695 7.80859 15.8477 7.99609H15.8652C15.9785 7.75 16.0762 7.55469 16.1582 7.41016L17.7344 4.59766H19.3047L16.7148 8.76367L19.3574 13ZM26.1191 5.78125H23.7051V13H22.3105V5.78125H19.9023V4.59766H26.1191V5.78125Z' fill='black' fill-opacity='0.24' />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
@ -155,7 +155,7 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
if (icon || image) {
|
||||
return (
|
||||
<div className='user-profile-pic-container'>
|
||||
<img src={image || icon || '../renderer/assets/symphony-default-profile-pic.png'} className='user-profile-pic' alt='user profile picture'/>
|
||||
<img src={image || icon || '../renderer/assets/symphony-default-profile-pic.png'} className='user-profile-pic' alt='user profile picture' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -223,7 +223,7 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object}
|
||||
|
@ -118,7 +118,7 @@ export default class NotificationSettings extends React.Component<{}, IState> {
|
||||
type='radio'
|
||||
name='position'
|
||||
checked={this.state.position === id}
|
||||
value={id}/>
|
||||
value={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -174,7 +174,7 @@ export default class NotificationSettings extends React.Component<{}, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object} { buildNumber, clientVersion, version }
|
||||
|
@ -148,7 +148,7 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
id={source.id}
|
||||
onClick={() => this.eventHandlers.onSelect(source)}>
|
||||
<div className='ScreenPicker-screen-section-box'>
|
||||
<img className='ScreenPicker-img-wrapper' src={source.thumbnail as any} alt='thumbnail image'/>
|
||||
<img className='ScreenPicker-img-wrapper' src={source.thumbnail as any} alt='thumbnail image' />
|
||||
</div>
|
||||
<div className='ScreenPicker-screen-source-title'>{screenName}</div>
|
||||
</div>,
|
||||
@ -161,7 +161,7 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
id={source.id}
|
||||
onClick={() => this.eventHandlers.onSelect(source)}>
|
||||
<div className='ScreenPicker-screen-section-box'>
|
||||
<img className='ScreenPicker-img-wrapper' src={source.thumbnail as any} alt='thumbnail image'/>
|
||||
<img className='ScreenPicker-img-wrapper' src={source.thumbnail as any} alt='thumbnail image' />
|
||||
</div>
|
||||
<div className='ScreenPicker-screen-source-title'>{source.name}</div>
|
||||
</div>,
|
||||
@ -173,8 +173,8 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
if (!this.isScreensAvailable && !this.isApplicationsAvailable) {
|
||||
return (
|
||||
<div className='ScreenPicker-error-content'>
|
||||
<span className='error-message'>
|
||||
{i18n.t('No screens or applications are currently available.', SCREEN_PICKER_NAMESPACE)()}
|
||||
<span className='error-message'>
|
||||
{i18n.t('No screens or applications are currently available.', SCREEN_PICKER_NAMESPACE)()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@ -205,7 +205,7 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
onChange={this.eventHandlers.onToggle('screens')}
|
||||
/>,
|
||||
<label className={classNames('screens', { hidden: !this.isScreensAvailable })}
|
||||
htmlFor='screen-tab'
|
||||
htmlFor='screen-tab'
|
||||
>
|
||||
{i18n.t('Screens', SCREEN_PICKER_NAMESPACE)()}
|
||||
</label>,
|
||||
@ -218,7 +218,7 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
onChange={this.eventHandlers.onToggle('applications')}
|
||||
/>,
|
||||
<label className={classNames('applications', { hidden: !this.isApplicationsAvailable })}
|
||||
htmlFor='application-tab'
|
||||
htmlFor='application-tab'
|
||||
>
|
||||
{i18n.t('Applications', SCREEN_PICKER_NAMESPACE)()}
|
||||
</label>,
|
||||
@ -235,7 +235,7 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
onChange={this.eventHandlers.onToggle('screens')}
|
||||
/>,
|
||||
<label className={classNames('screens', { hidden: !this.isScreensAvailable })}
|
||||
htmlFor='screen-tab'
|
||||
htmlFor='screen-tab'
|
||||
>
|
||||
{i18n.t('Screens', SCREEN_PICKER_NAMESPACE)()}
|
||||
</label>,
|
||||
@ -252,7 +252,7 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
onChange={this.eventHandlers.onToggle('applications')}
|
||||
/>,
|
||||
<label className={classNames('applications', { hidden: !this.isApplicationsAvailable })}
|
||||
htmlFor='application-tab'
|
||||
htmlFor='application-tab'
|
||||
>
|
||||
{i18n.t('Applications', SCREEN_PICKER_NAMESPACE)()}
|
||||
</label>,
|
||||
@ -372,14 +372,14 @@ export default class ScreenPicker extends React.Component<{}, IState> {
|
||||
|
||||
// Find the next item to be selected
|
||||
const nextIndex = (this.currentIndex + index + sources.length) % sources.length;
|
||||
if (sources[ nextIndex ] && sources[ nextIndex ].id) {
|
||||
if (sources[nextIndex] && sources[nextIndex].id) {
|
||||
// Updates the selected source
|
||||
this.setState({ selectedSource: sources[ nextIndex ] });
|
||||
this.setState({ selectedSource: sources[nextIndex] });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { ipcRenderer, remote } from 'electron';
|
||||
import { ipcRenderer, remote } from 'electron';
|
||||
import * as React from 'react';
|
||||
|
||||
import { apiCmds, apiName } from '../../common/api-interface';
|
||||
@ -40,7 +40,7 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState>
|
||||
|
||||
return (
|
||||
<div className={classNames('ScreenSharingIndicator', { mac: isMac })}>
|
||||
<span className='text-label'>{(i18n.t(`You are sharing your screen on {appName}`, namespace)({ appName: remote.app.getName()})).replace(remote.app.getName(), '')}
|
||||
<span className='text-label'>{(i18n.t(`You are sharing your screen on {appName}`, namespace)({ appName: remote.app.getName() })).replace(remote.app.getName(), '')}
|
||||
<span className='text-label2'> {remote.app.getName()}</span>
|
||||
</span>
|
||||
<span className='buttons'>
|
||||
@ -82,7 +82,7 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the About app state
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object} { buildNumber, clientVersion, version }
|
||||
|
282
src/renderer/components/snipping-tool.tsx
Normal file
@ -0,0 +1,282 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
import ColorPickerPill, { IColor } from './color-picker-pill';
|
||||
|
||||
const { useState, useCallback, useRef, useEffect } = React;
|
||||
|
||||
enum Tool {
|
||||
pen = 'PEN',
|
||||
highlight = 'HIGHLIGHT',
|
||||
eraser = 'ERASER',
|
||||
}
|
||||
|
||||
export interface IPath {
|
||||
points: IPoint[];
|
||||
color: string;
|
||||
strokeWidth: number;
|
||||
shouldShow: boolean;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface IPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ISvgPath {
|
||||
svgPath: string;
|
||||
key: string;
|
||||
strokeWidth: number;
|
||||
color: string;
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
const availablePenColors: IColor[] = [
|
||||
{ rgbaColor: 'rgba(0, 0, 40, 1)' },
|
||||
{ rgbaColor: 'rgba(0, 142, 255, 1)' },
|
||||
{ rgbaColor: 'rgba(38, 196, 58, 1)' },
|
||||
{ rgbaColor: 'rgba(246, 178, 2, 1)' },
|
||||
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' },
|
||||
];
|
||||
const availableHighlightColors: IColor[] = [
|
||||
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
|
||||
{ rgbaColor: 'rgba(38, 196, 58, 0.64)' },
|
||||
{ rgbaColor: 'rgba(246, 178, 2, 0.64)' },
|
||||
{ rgbaColor: 'rgba(233, 0, 0, 0.64)' },
|
||||
];
|
||||
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
|
||||
|
||||
const SnippingTool = () => {
|
||||
// State and ref preparation functions
|
||||
|
||||
const [screenSnippet, setScreenSnippet] = useState('Screen-Snippet');
|
||||
const [imageDimensions, setImageDimensions] = useState({
|
||||
height: 600,
|
||||
width: 800,
|
||||
});
|
||||
const [paths, setPaths] = useState<IPath[]>([]);
|
||||
const [chosenTool, setChosenTool] = useState(Tool.pen);
|
||||
const [annotateAreaLocation, setAnnotateAreaLocation] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
});
|
||||
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)');
|
||||
const [highlightColor, setHighlightColor] = useState(
|
||||
'rgba(0, 142, 255, 0.64)',
|
||||
);
|
||||
const [
|
||||
shouldRenderHighlightColorPicker,
|
||||
setShouldRenderHighlightColorPicker,
|
||||
] = useState(false);
|
||||
const [shouldRenderPenColorPicker, setShouldRenderPenColorPicker] = useState(
|
||||
false,
|
||||
);
|
||||
|
||||
const getSnipImageData = ({ }, { snipImage, height, width }) => {
|
||||
setScreenSnippet(snipImage);
|
||||
setImageDimensions({ height, width });
|
||||
};
|
||||
|
||||
ipcRenderer.on('snipping-tool-data', getSnipImageData);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
ipcRenderer.removeListener('snipping-tool-data', getSnipImageData);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const annotateRef = useCallback((domNode) => {
|
||||
if (domNode) {
|
||||
setAnnotateAreaLocation(domNode.getBoundingClientRect());
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Hook that alerts clicks outside of the passed ref
|
||||
const useClickOutsideExaminer = (
|
||||
colorPickerRf: React.RefObject<HTMLDivElement>,
|
||||
penRf: React.RefObject<HTMLButtonElement>,
|
||||
highlightRf: React.RefObject<HTMLButtonElement>,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
!colorPickerRf?.current?.contains(event.target as Node) &&
|
||||
!penRf?.current?.contains(event.target as Node) &&
|
||||
!highlightRf?.current?.contains(event.target as Node)
|
||||
) {
|
||||
setShouldRenderHighlightColorPicker(false);
|
||||
setShouldRenderPenColorPicker(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [colorPickerRf, penRf, highlightRf]);
|
||||
};
|
||||
|
||||
const colorPickerRef = useRef<HTMLDivElement>(null);
|
||||
const penRef = useRef<HTMLButtonElement>(null);
|
||||
const highlightRef = useRef<HTMLButtonElement>(null);
|
||||
useClickOutsideExaminer(colorPickerRef, penRef, highlightRef);
|
||||
|
||||
// State mutating functions
|
||||
|
||||
const penColorChosen = (color: string) => {
|
||||
setPenColor(color);
|
||||
setShouldRenderPenColorPicker(false);
|
||||
};
|
||||
|
||||
const highlightColorChosen = (color: string) => {
|
||||
setHighlightColor(color);
|
||||
setShouldRenderHighlightColorPicker(false);
|
||||
};
|
||||
|
||||
const usePen = () => {
|
||||
setChosenTool(Tool.pen);
|
||||
setShouldRenderPenColorPicker(!shouldRenderPenColorPicker);
|
||||
setShouldRenderHighlightColorPicker(false);
|
||||
};
|
||||
|
||||
const useHighlight = () => {
|
||||
setChosenTool(Tool.highlight);
|
||||
setShouldRenderHighlightColorPicker(!shouldRenderHighlightColorPicker);
|
||||
setShouldRenderPenColorPicker(false);
|
||||
};
|
||||
|
||||
const useEraser = () => {
|
||||
setChosenTool(Tool.eraser);
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
const updPaths = [...paths];
|
||||
updPaths.map((p) => {
|
||||
p.shouldShow = false;
|
||||
return p;
|
||||
});
|
||||
setPaths(updPaths);
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
|
||||
const getMousePosition = (e: React.MouseEvent) => {
|
||||
return {
|
||||
x: e.pageX - annotateAreaLocation.left,
|
||||
y: e.pageY - annotateAreaLocation.top,
|
||||
};
|
||||
};
|
||||
|
||||
const markChosenColor = (colors: IColor[], chosenColor: string) => {
|
||||
return colors.map((color) => {
|
||||
if (color.rgbaColor === chosenColor) {
|
||||
return { ...color, chosen: true };
|
||||
} else {
|
||||
return color;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getBorderStyle = (tool: Tool) => {
|
||||
if (chosenTool !== tool) {
|
||||
return undefined;
|
||||
}
|
||||
if (chosenTool === Tool.pen) {
|
||||
return { border: '2px solid ' + penColor };
|
||||
} else if (chosenTool === Tool.highlight) {
|
||||
return { border: '2px solid ' + highlightColor };
|
||||
} else if (chosenTool === Tool.eraser) {
|
||||
return { border: '2px solid #008EFF' };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const done = (e) => {
|
||||
getMousePosition(e);
|
||||
ipcRenderer.send('upload-snippet', screenSnippet);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='SnippingTool' lang={i18n.getLocale()}>
|
||||
<header>
|
||||
<div className='DrawActions'>
|
||||
<button
|
||||
style={getBorderStyle(Tool.pen)}
|
||||
className='ActionButton'
|
||||
onClick={usePen}
|
||||
>
|
||||
<img src='../renderer/assets/snip-draw.svg' />
|
||||
</button>
|
||||
<button
|
||||
style={getBorderStyle(Tool.highlight)}
|
||||
className='ActionButton'
|
||||
onClick={useHighlight}
|
||||
>
|
||||
<img src='../renderer/assets/snip-highlight.svg' />
|
||||
</button>
|
||||
<button
|
||||
style={getBorderStyle(Tool.eraser)}
|
||||
className='ActionButton'
|
||||
onClick={useEraser}
|
||||
>
|
||||
<img src='../renderer/assets/snip-erase.svg' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='ClearActions'>
|
||||
<button className='ClearButton' onClick={clear}>
|
||||
{i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
|
||||
</button>
|
||||
</div>
|
||||
</header>;
|
||||
|
||||
{
|
||||
shouldRenderPenColorPicker && (
|
||||
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
|
||||
<div style={{ position: 'relative', left: '-50%' }}>
|
||||
<ColorPickerPill
|
||||
availableColors={markChosenColor(availablePenColors, penColor)}
|
||||
onChange={penColorChosen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
shouldRenderHighlightColorPicker && (
|
||||
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
|
||||
<div style={{ position: 'relative', left: '-50%' }}>
|
||||
<ColorPickerPill
|
||||
availableColors={markChosenColor(
|
||||
availableHighlightColors,
|
||||
highlightColor,
|
||||
)}
|
||||
onChange={highlightColorChosen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<main>
|
||||
<div ref={annotateRef}>
|
||||
<img
|
||||
src={screenSnippet}
|
||||
width={imageDimensions.width}
|
||||
height={imageDimensions.height}
|
||||
className='SnippetImage'
|
||||
alt={i18n.t('Screen snippet', SNIPPING_TOOL_NAMESPACE)()}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<button onClick={done}>
|
||||
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
|
||||
</button>
|
||||
</footer>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default SnippingTool;
|
@ -10,6 +10,7 @@ import NotificationSettings from './components/notification-settings';
|
||||
import ScreenPicker from './components/screen-picker';
|
||||
import ScreenSharingFrame from './components/screen-sharing-frame';
|
||||
import ScreenSharingIndicator from './components/screen-sharing-indicator';
|
||||
import SnippingTool from './components/snipping-tool';
|
||||
import Welcome from './components/welcome';
|
||||
|
||||
const enum components {
|
||||
@ -21,6 +22,7 @@ const enum components {
|
||||
notification = 'notification-comp',
|
||||
notificationSettings = 'notification-settings',
|
||||
welcome = 'welcome',
|
||||
snippingTool = 'snipping-tool',
|
||||
}
|
||||
|
||||
const loadStyle = (style) => {
|
||||
@ -47,26 +49,31 @@ const load = () => {
|
||||
break;
|
||||
case components.screenPicker:
|
||||
loadStyle(components.screenPicker);
|
||||
document.title = 'Screen Picker - Symphony';
|
||||
document.title = i18n.t('Screen Picker - Symphony')();
|
||||
component = ScreenPicker;
|
||||
break;
|
||||
case components.screenSharingIndicator:
|
||||
loadStyle(components.screenSharingIndicator);
|
||||
document.title = 'Screen Sharing Indicator - Symphony';
|
||||
document.title = i18n.t('Screen Sharing Indicator - Symphony')();
|
||||
component = ScreenSharingIndicator;
|
||||
break;
|
||||
case components.screenSharingFrame:
|
||||
loadStyle(components.screenSharingFrame);
|
||||
component = ScreenSharingFrame;
|
||||
break;
|
||||
case components.snippingTool:
|
||||
loadStyle(components.snippingTool);
|
||||
document.title = i18n.t('Symphony')();
|
||||
component = SnippingTool;
|
||||
break;
|
||||
case components.basicAuth:
|
||||
loadStyle(components.basicAuth);
|
||||
document.title = 'Basic Authentication - Symphony';
|
||||
document.title = i18n.t('Basic Authentication - Symphony')();
|
||||
component = BasicAuth;
|
||||
break;
|
||||
case components.notification:
|
||||
loadStyle(components.notification);
|
||||
document.title = 'Notification - Symphony';
|
||||
document.title = i18n.t('Notification - Symphony')();
|
||||
component = NotificationComp;
|
||||
break;
|
||||
case components.notificationSettings:
|
||||
|
207
src/renderer/styles/snipping-tool.less
Normal file
@ -0,0 +1,207 @@
|
||||
@import 'theme';
|
||||
|
||||
@white: rgb(255, 255, 255, 1);
|
||||
@version-text-color: rgb(47, 47, 47, 1);
|
||||
@text-padding: 10px;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.SnippingTool:lang(ja-JP) {
|
||||
font-family: @font-family-ja;
|
||||
|
||||
h4 {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.SnippingTool-symphony-section {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.SnippingTool:lang(fr-FR) {
|
||||
.SnippingTool-symphony-section {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.SnippingTool {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: @font-family;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
max-height: 48px;
|
||||
|
||||
.ActionButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 15px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px solid white;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ClearButton {
|
||||
display: block;
|
||||
padding: 4px 10px;
|
||||
border: 2px solid #7c7f86;
|
||||
border-radius: 16px;
|
||||
color: #7c7f86;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-left: 24px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.DrawActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ClearActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
align-items: center;
|
||||
|
||||
.ActionButton {
|
||||
img {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0.96),
|
||||
rgba(255, 255, 255, 0.96)
|
||||
),
|
||||
#525760;
|
||||
|
||||
.SnippetImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 2px 32px 4px 0;
|
||||
max-height: 72px;
|
||||
|
||||
button {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 8px 24px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
line-height: 16px;
|
||||
background-color: #008eff;
|
||||
color: rgba(255, 255, 255, 0.96);
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
margin: 10px 30px 4px 0;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 10px rgba(61, 162, 253, 1);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 400px) {
|
||||
header {
|
||||
.ClearActions {
|
||||
position: relative;
|
||||
right: auto;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colorPicker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
height: 40px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 4px 16px rgba(15, 27, 36, 0.14), 0px 4px 8px rgba(15, 27, 36, 0.26);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.enclosingCircle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
margin: 8px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.colorDot {
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #000028;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
@font-family: "Segoe UI", "Helvetica Neue", "Verdana", "Arial", sans-serif;
|
||||
@font-family-ja: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif;
|
||||
@text-color-primary: #FFFFFF;
|
||||
@text-color-primary-dark: #2F3237;
|
||||
@text-color-secondary: #6A707C;
|
||||
@font-family: 'Segoe UI', 'Helvetica Neue', 'Verdana', 'Arial', sans-serif;
|
||||
@font-family-ja: 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo,
|
||||
'MS Pゴシック', 'MS PGothic', sans-serif;
|
||||
@text-color-primary: #ffffff;
|
||||
@text-color-primary-dark: #2f3237;
|
||||
@text-color-secondary: #6a707c;
|
||||
|
@ -26,7 +26,7 @@
|
||||
"no-trailing-whitespace": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-var-keyword": true,
|
||||
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"],
|
||||
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"],
|
||||
"no-empty": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
|