Typescript 🎉

This commit is contained in:
Kiran Niranjan 2018-10-05 17:34:19 +05:30
parent d4dcb933d1
commit 1fc1e29d1a
19 changed files with 979 additions and 3 deletions

View File

@ -1,14 +1,17 @@
{
"name": "Symphony",
"productName": "Symphony",
"productName": "Symphony-dev",
"version": "4.5.0",
"clientVersion": "1.55.0",
"buildNumber": "0",
"description": "Symphony desktop app (Foundation ODP)",
"author": "Symphony",
"main": "js/main.js",
"main": "src/browser/main.js",
"types": "src/browser/main.d.ts",
"scripts": {
"lint": "eslint --ext .js js/",
"tsc": "git clean -xdf ./lib && npm run lint && tsc",
"lint": "tslint --project tsconfig.json",
"start": "npm run tsc && electron .",
"prebuild": "npm run rebuild && npm run browserify-preload",
"browserify-preload": "browserify -o js/preload/_preloadMain.js -x electron --insert-global-vars=__filename,__dirname js/preload/preloadMain.js --exclude electron-spellchecker",
"rebuild": "electron-rebuild -f",
@ -76,6 +79,9 @@
"url": "https://support.symphony.com"
},
"devDependencies": {
"@types/auto-launch": "^5.0.0",
"@types/lodash.omit": "^4.5.4",
"@types/node": "10.11.4",
"babel-cli": "6.26.0",
"babel-eslint": "7.2.3",
"babel-plugin-transform-async-to-generator": "6.24.1",
@ -103,6 +109,9 @@
"ncp": "2.0.0",
"robotjs": "0.5.1",
"selenium-webdriver": "3.6.0",
"spectron": "5.0.0",
"tslint": "5.11.0",
"typescript": "3.1.1",
"wdio-selenium-standalone-service": "0.0.12"
},
"dependencies": {

View File

@ -0,0 +1,25 @@
import { app, session } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
// Cache check file path
const cacheCheckFilePath: string = path.join(app.getPath('userData'), 'CacheCheck');
/**
* Deletes app cache file if exists or clears
* the cache for the session
*/
export async function cleanUpAppCache(): Promise<void> {
if (fs.existsSync(cacheCheckFilePath)) {
await fs.unlinkSync(cacheCheckFilePath);
return;
}
await new Promise((resolve) => session.defaultSession ? session.defaultSession.clearCache(resolve) : null);
}
/**
* Creates a new file cache file on app exit
*/
export function createAppCacheFile(): void {
fs.writeFileSync(cacheCheckFilePath, '');
}

View File

@ -0,0 +1,87 @@
import AutoLaunch = require('auto-launch');
import { isMac } from '../common/mics';
import { config, IConfig } from './config-handler';
const { autoLaunchPath }: IConfig = config.getGlobalConfigFields([ 'autoLaunchPath' ]);
const props = isMac ? {
mac: {
useLaunchAgent: true,
},
name: 'Symphony',
path: process.execPath,
} : {
name: 'Symphony',
path: autoLaunchPath
? autoLaunchPath.replace(/\//g, '\\')
: null || process.execPath,
};
export interface IAutoLaunchOptions {
name: string;
path?: string;
isHidden?: boolean;
mac?: {
useLaunchAgent?: boolean;
};
}
class AutoLaunchController extends AutoLaunch {
constructor(opts: IAutoLaunchOptions) {
super(opts);
}
/**
* Enable auto launch
*
* @return {Promise<void>}
*/
public async enableAutoLaunch(): Promise<void> {
// log.send(logLevels.INFO, `Enabling auto launch!`);
return await this.enable();
}
/**
* Disable auto launch
*
* @return {Promise<void>}
*/
public async disableAutoLaunch(): Promise<void> {
// log.send(logLevels.INFO, `Disabling auto launch!`);
return await this.disable();
}
/**
* Checks if auto launch is enabled
*
* @return {Boolean}
*/
public async isAutoLaunchEnabled(): Promise<boolean> {
return await this.isEnabled();
}
/**
* Validates the user config and enables auto launch
*/
public async handleAutoLaunch(): Promise<void> {
const { launchOnStartup }: IConfig = config.getConfigFields([ 'launchOnStartup' ]);
if (typeof launchOnStartup === 'boolean' && launchOnStartup) {
if (await !this.isAutoLaunchEnabled()) {
await this.enableAutoLaunch();
}
} else {
if (await this.isAutoLaunchEnabled()) {
await this.disableAutoLaunch();
}
}
}
}
const autoLaunchInstance = new AutoLaunchController(props);
export {
autoLaunchInstance,
};

View File

@ -0,0 +1,64 @@
import { app } from 'electron';
import getCmdLineArg from '../common/get-command-line-args';
import { isDevEnv } from '../common/mics';
import { config, IConfig } from './config-handler';
export default function setChromeFlags() {
const { customFlags } = config.getGlobalConfigFields([ 'customFlags' ]) as IConfig;
const configFlags: object = {
'auth-negotiate-delegate-whitelist': customFlags.authServerWhitelist,
'auth-server-whitelist': customFlags.authNegotiateDelegateWhitelist,
'disable-background-timer-throttling': 'true',
'disable-d3d11': customFlags.disableGpu || null,
'disable-gpu': customFlags.disableGpu || null,
'disable-gpu-compositing': customFlags.disableGpu || null,
};
for (const key in configFlags) {
if (!Object.prototype.hasOwnProperty.call(configFlags, key)) {
continue;
}
if (key && configFlags[key]) {
app.commandLine.appendSwitch(key, configFlags[key]);
}
}
if (isDevEnv) {
const chromeFlagsFromCmd = getCmdLineArg(process.argv, '--chrome-flags=', false);
if (!chromeFlagsFromCmd) {
return;
}
const chromeFlagsArgs = chromeFlagsFromCmd.substr(15);
if (!chromeFlagsArgs) {
return;
}
const flags = chromeFlagsArgs.split(',');
if (!flags || !Array.isArray(flags)) {
return;
}
for (const key in flags) {
if (!Object.prototype.hasOwnProperty.call(flags, key)) {
continue;
}
if (!flags[key]) {
return;
}
const flagArray = flags[key].split(':');
if (flagArray && Array.isArray(flagArray) && flagArray.length > 0) {
const chromeFlagKey = flagArray[0];
const chromeFlagValue = flagArray[1];
// log.send(logLevels.INFO, `Setting chrome flag ${chromeFlagKey} to ${chromeFlagValue}`);
app.commandLine.appendSwitch(chromeFlagKey, chromeFlagValue);
}
}
}
}

View File

@ -0,0 +1,206 @@
import { app } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { omit } from 'lodash';
import compareSemVersions from '../common/compare-sem-versions';
import { isDevEnv, isMac } from '../common/mics';
import pick from '../common/pick';
const ignoreSettings = [
'minimizeOnClose',
'launchOnStartup',
'alwaysOnTop',
'url',
'memoryRefresh',
'bringToFront',
'isCustomTitleBar',
];
export interface IConfig {
url: string;
minimizeOnClose: boolean;
launchOnStartup: boolean;
alwaysOnTop: boolean;
bringToFront: boolean;
whitelistUrl: string;
isCustomTitleBar: boolean;
memoryRefresh: boolean;
devToolsEnabled: boolean;
ctWhitelist: string[];
configVersion: string;
autoLaunchPath: string;
notificationSettings: INotificationSetting;
permissions: IPermission;
customFlags: ICustomFlag;
crashReporter: ICrashReporter;
}
export interface IPermission {
media: boolean;
geolocation: boolean;
notifications: boolean;
midiSysex: boolean;
pointerLock: boolean;
fullscreen: boolean;
openExternal: boolean;
}
export interface ICustomFlag {
authServerWhitelist: string;
authNegotiateDelegateWhitelist: string;
disableGpu: boolean;
}
export interface ICrashReporter {
submitURL: string;
companyName: string;
uploadToServer: boolean;
}
export interface INotificationSetting {
position: string;
display: string;
}
class Config {
private userConfig: IConfig | {};
private globalConfig: IConfig | {};
private isFirstTime: boolean = true;
private readonly configFileName: string;
private readonly userConfigPath: string;
private readonly appPath: string;
private readonly globalConfigPath: string;
constructor() {
this.configFileName = 'Symphony.config';
this.userConfigPath = path.join(app.getPath('userData'), this.configFileName);
this.appPath = isDevEnv ? app.getAppPath() : path.dirname(app.getPath('exe'));
this.globalConfigPath = isDevEnv
? path.join(this.appPath, path.join('config', this.configFileName))
: path.join(this.appPath, isMac ? '..' : '', 'config', this.configFileName);
this.globalConfig = {};
this.userConfig = {};
this.readUserConfig();
this.readGlobalConfig();
this.checkFirstTimeLaunch();
}
/**
* Returns the specified fields from both user and global config file
* and keep values from user config as priority
*
* @param fields
*/
public getConfigFields(fields: string[]): IConfig {
return { ...this.getGlobalConfigFields(fields), ...this.getUserConfigFields(fields) } as IConfig;
}
/**
* Returns the specified fields from user config file
*
* @param fields {Array}
*/
public getUserConfigFields(fields: string[]): IConfig {
return pick(this.userConfig, fields) as IConfig;
}
/**
* Returns the specified fields from global config file
*
* @param fields {Array}
*/
public getGlobalConfigFields(fields: string[]): IConfig {
return pick(this.globalConfig, fields) as IConfig;
}
/**
* updates new data to the user config
*
* @param data {IConfig}
*/
public updateUserConfig(data: IConfig): void {
this.userConfig = { ...data, ...this.userConfig };
fs.writeFileSync(this.userConfigPath, JSON.stringify(this.userConfig), { encoding: 'utf8' });
}
public isFirstTimeLaunch(): boolean {
return this.isFirstTime;
}
/**
* Method that updates user config file
* by modifying the old config file
*/
public async setUpFirstTimeLaunch(): Promise<void> {
const filteredFields: IConfig = omit(this.userConfig, ignoreSettings) as IConfig;
await this.updateUserConfig(filteredFields);
}
/**
* Parses the config data string
*
* @param data
*/
private parseConfigData(data: string): object {
let parsedData;
if (!data) {
throw new Error('unable to read user config file');
}
try {
parsedData = JSON.parse(data);
} catch (e) {
throw new Error(e);
}
return parsedData;
}
/**
* Reads a stores the user config file
*
* If user config doesn't exits?
* this creates a new one with { configVersion: current_app_version }
*/
private readUserConfig() {
if (!fs.existsSync(this.userConfigPath)) {
this.updateUserConfig({ configVersion: app.getVersion().toString() } as IConfig);
}
this.userConfig = this.parseConfigData(fs.readFileSync(this.userConfigPath, 'utf8'));
}
/**
* Reads a stores the global config file
*/
private readGlobalConfig() {
this.globalConfig = this.parseConfigData(fs.readFileSync(this.globalConfigPath, 'utf8'));
}
/**
* Verifies if the application is launched for the first time
*/
private checkFirstTimeLaunch() {
const appVersionString = app.getVersion().toString();
const execPath = path.dirname(this.appPath);
const shouldUpdateUserConfig = execPath.indexOf('AppData\\Local\\Programs') !== -1 || isMac;
const userConfigVersion = this.userConfig && (this.userConfig as IConfig).configVersion || null;
if (!userConfigVersion) {
this.isFirstTime = true;
return;
}
if (!(userConfigVersion
&& typeof userConfigVersion === 'string'
&& (compareSemVersions(appVersionString, userConfigVersion) !== 1)) && shouldUpdateUserConfig) {
this.isFirstTime = true;
}
}
}
const config = new Config();
export {
config,
};

View File

@ -0,0 +1,26 @@
import { ipcMain } from 'electron';
export enum IApiCmds {
'isOnline',
'registerLogger',
'setBadgeCount',
'badgeDataUrl',
'activate',
'registerBoundsChange',
'registerProtocolHandler',
'registerActivityDetection',
'showNotificationSettings',
'sanitize',
'bringToFront',
'openScreenPickerWindow',
'popupMenu',
'optimizeMemoryConsumption',
'optimizeMemoryRegister',
'setIsInMeeting',
'setLocale',
'keyPress',
}
export enum apiName {
'symphonyapi',
}

54
src/browser/main.ts Normal file
View File

@ -0,0 +1,54 @@
import { app } from 'electron';
import getCmdLineArg from '../common/get-command-line-args';
import { isDevEnv } from '../common/mics';
import { cleanUpAppCache, createAppCacheFile } from './app-cache-handler';
import { autoLaunchInstance } from './auto-launch-controller';
import setChromeFlags from './chrome-flags';
import { config } from './config-handler';
import { windowHandler } from './window-handler';
const allowMultiInstance: string | boolean = getCmdLineArg(process.argv, '--multiInstance', true) || isDevEnv;
const singleInstanceLock: boolean = allowMultiInstance ? false : app.requestSingleInstanceLock();
if (!singleInstanceLock) {
app.quit();
} else {
main();
}
async function main() {
await appReady();
createAppCacheFile();
windowHandler.showLoadingScreen();
windowHandler.createApplication();
if (config.isFirstTimeLaunch()) {
await config.setUpFirstTimeLaunch();
/**
* Enables or disables auto launch base on user settings
*/
await autoLaunchInstance.handleAutoLaunch();
}
/**
* Sets chrome flags from global config
*/
setChromeFlags();
}
async function appReady(): Promise<any> {
await new Promise((res) => app.once('ready', res));
}
/**
* Is triggered when all the windows are closed
* In which case we quit the app
*/
app.on('window-all-closed', () => app.quit());
/**
* Creates a new empty cache file when the app is quit
*/
app.on('quit', () => cleanUpAppCache());

View File

@ -0,0 +1,115 @@
import * as url from 'url';
// import log from '../logs';
// import { LogLevels } from '../logs/interface';
import getCmdLineArg from '../common/get-command-line-args';
import { isMac } from '../common/mics';
import { windowHandler } from './window-handler';
let protocolWindow: Electron.WebContents;
let protocolUrl: string | undefined;
/**
* processes a protocol uri
* @param {String} uri - the uri opened in the format 'symphony://...'
*/
export function processProtocolUri(uri: string): void {
// log.send(LogLevels.INFO, `protocol action, uri ${uri}`);
if (!protocolWindow) {
// log.send(LogLevels.INFO, `protocol window not yet initialized, caching the uri ${uri}`);
setProtocolUrl(uri);
return;
}
if (uri && uri.startsWith('symphony://')) {
protocolWindow.send('protocol-action', uri);
}
}
/**
* processes protocol action for windows clients
* @param argv {Array} an array of command line arguments
* @param isAppAlreadyOpen {Boolean} whether the app is already open
*/
export function processProtocolArgv(argv: string[], isAppAlreadyOpen: boolean): void {
// In case of windows, we need to handle protocol handler
// manually because electron doesn't emit
// 'open-url' event on windows
if (!(process.platform === 'win32')) {
return;
}
const protocolUri = getCmdLineArg(argv, 'symphony://', false);
// log.send(LogLevels.INFO, `Trying to process a protocol action for uri ${protocolUri}`);
if (protocolUri) {
const parsedURL = url.parse(protocolUri);
if (!parsedURL.protocol || !parsedURL.slashes) {
return;
}
// log.send(LogLevels.INFO, `Parsing protocol url successful for ${parsedURL}`);
handleProtocolAction(protocolUri, isAppAlreadyOpen);
}
}
/**
* Handles a protocol action based on the current state of the app
* @param uri
* @param isAppAlreadyOpen {Boolean} whether the app is already open
*/
export function handleProtocolAction(uri: string, isAppAlreadyOpen: boolean): void {
if (!isAppAlreadyOpen) {
// log.send(LogLevels.INFO, `App started by protocol url ${uri}. We are caching this to be processed later!`);
// app is opened by the protocol url, cache the protocol url to be used later
setProtocolUrl(uri);
return;
}
// This is needed for mac OS as it brings pop-outs to foreground
// (if it has been previously focused) instead of main window
if (isMac) {
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
// windowMgr.activate(mainWindow.winName);
}
}
// app is already open, so, just trigger the protocol action method
// log.send(LogLevels.INFO, `App opened by protocol url ${uri}`);
processProtocolUri(uri);
}
/**
* sets the protocol window
* @param {Object} win - the renderer window
*/
export function setProtocolWindow(win: Electron.WebContents): void {
protocolWindow = win;
}
/**
* checks to see if the app was opened by a uri
*/
export function checkProtocolAction(): void {
// log.send(LogLevels.INFO, `checking if we have a cached protocol url`);
if (protocolUrl) {
// log.send(LogLevels.INFO, `found a cached protocol url, processing it!`);
processProtocolUri(protocolUrl);
protocolUrl = undefined;
}
}
/**
* caches the protocol uri
* @param {String} uri - the uri opened in the format 'symphony://...'
*/
export function setProtocolUrl(uri: string): void {
protocolUrl = uri;
}
/**
* gets the protocol url set against an instance
* @returns {*}
*/
export function getProtocolUrl(): string | undefined {
return protocolUrl;
}

View File

@ -0,0 +1,111 @@
import { BrowserWindow, crashReporter } from 'electron';
import * as path from 'path';
import * as url from 'url';
import getCmdLineArg from '../common/get-command-line-args';
import { config, IConfig } from './config-handler';
export class WindowHandler {
private static getMainWindowOpts() {
return {
alwaysOnTop: false,
frame: true,
minHeight: 300,
minWidth: 400,
show: false,
title: 'Symphony',
webPreferences: {
nativeWindowOpen: true,
nodeIntegration: false,
sandbox: false,
},
};
}
private static getLoadingWindowOpts() {
return {
alwaysOnTop: false,
center: true,
frame: false,
height: 200,
maximizable: false,
minimizable: false,
resizable: false,
show: false,
title: 'Symphony',
width: 400,
};
}
private readonly windowOpts: Electron.BrowserWindowConstructorOptions;
private readonly globalConfig: IConfig;
private mainWindow: Electron.BrowserWindow | null;
private loadingWindow: Electron.BrowserWindow | null;
constructor(opts?: Electron.BrowserViewConstructorOptions) {
this.windowOpts = { ... WindowHandler.getMainWindowOpts(), ...opts };
this.mainWindow = null;
this.loadingWindow = null;
this.globalConfig = config.getGlobalConfigFields([ 'url', 'crashReporter' ]);
try {
crashReporter.start({
companyName: this.globalConfig!.crashReporter!.companyName,
extra: {
podUrl: this.globalConfig.url,
process: 'main',
},
submitURL: this.globalConfig!.crashReporter!.submitURL,
uploadToServer: this.globalConfig!.crashReporter!.uploadToServer,
});
} catch (e) {
throw new Error('failed to init crash report');
}
}
public createApplication() {
this.mainWindow = new BrowserWindow(this.windowOpts);
const urlFromCmd = getCmdLineArg(process.argv, '--url=', false);
this.mainWindow.loadURL(urlFromCmd && urlFromCmd.substr(6) || this.validateURL(this.globalConfig.url));
this.mainWindow.webContents.on('did-finish-load', () => {
if (this.loadingWindow) {
this.loadingWindow.destroy();
}
if (this.mainWindow) this.mainWindow.show();
});
return this.mainWindow;
}
public getMainWindow(): Electron.BrowserWindow | null {
return this.mainWindow;
}
public validateURL(configURL: string): string {
const parsedUrl = url.parse(configURL);
if (!parsedUrl.protocol || parsedUrl.protocol !== 'https') {
parsedUrl.protocol = 'https:';
parsedUrl.slashes = true;
}
return url.format(parsedUrl);
}
/**
* Displays a loading window until the main
* application is loaded
*/
public showLoadingScreen() {
this.loadingWindow = new BrowserWindow(WindowHandler.getLoadingWindowOpts());
this.loadingWindow.once('ready-to-show', () => this.loadingWindow ? this.loadingWindow.show() : null);
this.loadingWindow.loadURL(`file://${path.join(__dirname, '../renderer/loading-screen.html')}`);
this.loadingWindow.setMenu(null as any);
this.loadingWindow.once('closed', () => this.loadingWindow = null);
}
}
const windowHandler = new WindowHandler();
export { windowHandler };

View File

@ -0,0 +1,81 @@
// regex match the semver (semantic version) this checks for the pattern X.Y.Z
// ex-valid v1.2.0, 1.2.0, 2.3.4-r51
const semver = /^v?(?:\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)?)?$/i;
const patch = /-([0-9A-Za-z-.]+)/;
/**
* This function splits the versions
* into major, minor and patch
* @param v
* @returns {String[]}
*/
function split(v: string): string[] {
const temp = v.replace(/^v/, '').split('.');
const arr = temp.splice(0, 2);
arr.push(temp.join('.'));
return arr;
}
function tryParse(v: string): string | number {
return Number.isNaN(Number(v)) ? v : Number(v);
}
/**
* This validates the version
* with the semver regex and returns
* -1 if not valid else 1
* @param version
* @returns {number}
*/
function validate(version: string): number {
if (!semver.test(version)) {
return -1;
}
return 1;
}
/**
* This function compares the v1 version
* with the v2 version for all major, minor, patch
* if v1 > v2 returns 1
* if v1 < v2 returns -1
* if v1 = v2 returns 0
* @param v1
* @param v2
* @returns {number}
*/
export default function check(v1: string, v2: string): number {
if (validate(v1) === -1 || validate(v2) === -1) {
return -1;
}
const s1 = split(v1);
const s2 = split(v2);
for (let i = 0; i < 3; i++) {
const n1 = parseInt(s1[i] || '0', 10);
const n2 = parseInt(s2[i] || '0', 10);
if (n1 > n2) return 1;
if (n2 > n1) return -1;
}
if ([ s1[2], s2[2] ].every(patch.test.bind(patch))) {
// @ts-ignore
const p1 = patch.exec(s1[2])[1].split('.').map(tryParse);
// @ts-ignore
const p2 = patch.exec(s2[2])[1].split('.').map(tryParse);
for (let k = 0; k < Math.max(p1.length, p2.length); k++) {
if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') return -1;
if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') return 1;
if (p1[k] > p2[k]) return 1;
if (p2[k] > p1[k]) return -1;
}
} else if ([ s1[2], s2[2] ].some(patch.test.bind(patch))) {
return patch.test(s1[2]) ? -1 : 1;
}
return 0;
}

View File

@ -0,0 +1,29 @@
// import log from '../logs';
// import { LogLevels } from '../logs/interface';
/**
* Search given argv for argName using exact match or starts with. Comparison is case insensitive
* @param {Array} argv Array of strings
* @param {String} argName Arg name to search for.
* @param {Boolean} exactMatch If true then look for exact match otherwise
* try finding arg that starts with argName.
* @return {String} If found, returns the arg, otherwise null.
*/
export default function getCmdLineArg(argv: string[], argName: string, exactMatch: boolean): string | null {
if (!Array.isArray(argv)) {
// log.send(LogLevels.WARN, `getCmdLineArg: TypeError invalid func arg, must be an array: ${argv}`);
return null;
}
const argNameToFind = argName.toLocaleLowerCase();
for (let i = 0, len = argv.length; i < len; i++) {
const arg = argv[i].toLocaleLowerCase();
if ((exactMatch && arg === argNameToFind) ||
(!exactMatch && arg.startsWith(argNameToFind))) {
return argv[i];
}
}
return null;
}

0
src/common/logger.ts Normal file
View File

7
src/common/mics.ts Normal file
View File

@ -0,0 +1,7 @@
export const isDevEnv = process.env.ELECTRON_DEV ?
process.env.ELECTRON_DEV.trim().toLowerCase() === 'true' : false;
export const isMac = (process.platform === 'darwin');
export const isWindowsOS = (process.platform === 'win32');
export const isNodeEnv = !!process.env.NODE_ENV;

9
src/common/pick.ts Normal file
View File

@ -0,0 +1,9 @@
export default function pick(object: object, fields: string[]) {
const obj = {};
for (const field of fields) {
if (object[field]) {
obj[field] = object[field];
}
}
return obj;
}

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>About Symphony</title>
<style>
html, body {
margin: 0;
height: 100%;
font-family: sans-serif;
}
.name {
flex: 1;
font-size: 1.3em;
padding: 10px;
font-weight: bold;
color: #ffffff;
}
.version-text {
flex: 1;
font-size: 1em;
color: #2f2f2f;
}
.copyright-text {
flex: 1;
padding: 10px;
font-size: 0.6em;
color: #7f7f7f;
}
.content {
text-align: center;
display: flex;
flex-direction: column;
padding-top: 20px;
background: url(symphony-background.jpg) no-repeat center center fixed;
background-size: cover;
}
.logo {
margin: auto;
}
</style>
</head>
<body>
<div class="content">
<img class="logo" src="symphony-logo.png">
<span id="app-name" class="name">Symphony</span>
<span id="version" class="version-text"></span>
<span id="copyright" class="copyright-text"></span>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

32
tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"compilerOptions": {
"module": "commonjs",
"pretty": true,
"target": "ES2016",
"lib": [
"es2016",
"dom"
],
"moduleResolution": "node",
"strict": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"declaration": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noImplicitThis": true,
"noUnusedParameters": true,
"typeRoots": [
"node_modules/@types"
]
},
"exclude": [
"node_modules",
"lib",
"tests"
]
}

66
tslint.json Normal file
View File

@ -0,0 +1,66 @@
{
"extends": [
"tslint:recommended"
],
"rules": {
"curly": false,
"eofline": false,
"align": [
true,
"parameters"
],
"class-name": true,
"indent": [
true,
"spaces"
],
"max-line-length": [
true,
650
],
"no-trailing-whitespace": true,
"no-duplicate-variable": true,
"no-var-keyword": true,
"no-empty": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-requires": true,
"no-console": [
false,
"log",
"error"
],
"one-line": [
true,
"check-else",
"check-whitespace",
"check-open-brace"
],
"quotemark": [
true,
"single",
"avoid-escape"
],
"semicolon": [
true,
"always"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-type"
]
}
}