From 5db20ef74386412dd099c53efaef76e704fb6b10 Mon Sep 17 00:00:00 2001 From: Kiran Niranjan Date: Tue, 20 Nov 2018 21:52:35 +0530 Subject: [PATCH] Typescript - Add custom title bar --- src/browser/main-api-handler.ts | 46 +--- src/browser/window-handler.ts | 8 +- src/common/api-interface.ts | 41 +++ src/common/env.ts | 2 +- src/common/i18n.ts | 64 +++++ src/common/logger.ts | 4 +- src/locale/en-US.json | 155 +++++++++++ src/locale/ja-JP.json | 155 +++++++++++ src/renderer/about-app.tsx | 6 +- .../assets/symphony-title-bar-logo.png | Bin 0 -> 872 bytes src/renderer/loading-screen.tsx | 2 +- src/renderer/preload-component.ts | 2 +- src/renderer/preload-main.ts | 14 + src/renderer/styles/title-bar.css | 107 ++++++++ src/renderer/windows-title-bar.tsx | 249 ++++++++++++++++++ 15 files changed, 802 insertions(+), 53 deletions(-) create mode 100644 src/common/api-interface.ts create mode 100644 src/common/i18n.ts create mode 100644 src/locale/en-US.json create mode 100644 src/locale/ja-JP.json create mode 100644 src/renderer/assets/symphony-title-bar-logo.png create mode 100644 src/renderer/styles/title-bar.css create mode 100644 src/renderer/windows-title-bar.tsx diff --git a/src/browser/main-api-handler.ts b/src/browser/main-api-handler.ts index 95a7b5d0..b8a1c437 100644 --- a/src/browser/main-api-handler.ts +++ b/src/browser/main-api-handler.ts @@ -1,48 +1,8 @@ import { ipcMain } from 'electron'; + +import { apiCmds, apiName, IApiArgs } from '../common/api-interface'; import { logger } from '../common/logger'; -export enum ApiCmds { - isOnline, - registerLogger, - setBadgeCount, - badgeDataUrl, - activate, - registerBoundsChange, - registerProtocolHandler, - registerActivityDetection, - showNotificationSettings, - sanitize, - bringToFront, - openScreenPickerWindow, - popupMenu, - optimizeMemoryConsumption, - optimizeMemoryRegister, - setIsInMeeting, - setLocale, - keyPress, -} - -export enum apiName { - symphonyApi = 'symphony-api', -} - -export interface IApiArgs { - cmd: ApiCmds; - isOnline: boolean; - count: number; - dataUrl: string; - windowName: string; - period: number; - reason: string; - sources: Electron.DesktopCapturerSource[]; - id: number; - memory: Electron.ProcessMemoryInfo; - cpuUsage: Electron.CPUUsage; - isInMeeting: boolean; - locale: string; - keyCode: number; -} - /** * Ensure events comes from a window that we have created. * @param {EventEmitter} event node emitter event to be tested @@ -108,7 +68,7 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => { case ApiCmds.registerBoundsChange: windowMgr.setBoundsChangeWindow(event.sender); break;*/ - case ApiCmds.registerLogger: + case apiCmds.registerLogger: // renderer window that has a registered logger from JS. logger.setLoggerWindow(event.sender); break; diff --git a/src/browser/window-handler.ts b/src/browser/window-handler.ts index 79bdde7a..39fbec2e 100644 --- a/src/browser/window-handler.ts +++ b/src/browser/window-handler.ts @@ -1,4 +1,5 @@ import { BrowserWindow, crashReporter } from 'electron'; +import * as fs from 'fs'; import * as path from 'path'; import * as url from 'url'; @@ -99,7 +100,12 @@ export class WindowHandler { this.loadingWindow.destroy(); this.loadingWindow = null; } - if (this.mainWindow) this.mainWindow.show(); + if (this.mainWindow) { + this.mainWindow.webContents.insertCSS( + fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/title-bar.css'), 'utf8').toString(), + ); + this.mainWindow.show(); + } this.createAboutAppWindow(); }); return this.mainWindow; diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts new file mode 100644 index 00000000..89923b0e --- /dev/null +++ b/src/common/api-interface.ts @@ -0,0 +1,41 @@ +export enum apiCmds { + isOnline, + registerLogger, + setBadgeCount, + badgeDataUrl, + activate, + registerBoundsChange, + registerProtocolHandler, + registerActivityDetection, + showNotificationSettings, + sanitize, + bringToFront, + openScreenPickerWindow, + popupMenu, + optimizeMemoryConsumption, + optimizeMemoryRegister, + setIsInMeeting, + setLocale, + keyPress, +} + +export enum apiName { + symphonyApi = 'symphony-api', +} + +export interface IApiArgs { + cmd: apiCmds; + isOnline: boolean; + count: number; + dataUrl: string; + windowName: string; + period: number; + reason: string; + sources: Electron.DesktopCapturerSource[]; + id: number; + memory: Electron.ProcessMemoryInfo; + cpuUsage: Electron.CPUUsage; + isInMeeting: boolean; + locale: string; + keyCode: number; +} \ No newline at end of file diff --git a/src/common/env.ts b/src/common/env.ts index 3397e484..d12a6f5f 100644 --- a/src/common/env.ts +++ b/src/common/env.ts @@ -1,6 +1,6 @@ export const isDevEnv = process.env.ELECTRON_DEV ? process.env.ELECTRON_DEV.trim().toLowerCase() === 'true' : false; -export const isElectronQA = process.env.ELECTRON_QA; +export const isElectronQA = !!process.env.ELECTRON_QA; export const isMac = (process.platform === 'darwin'); export const isWindowsOS = (process.platform === 'win32'); diff --git a/src/common/i18n.ts b/src/common/i18n.ts new file mode 100644 index 00000000..6ed464c7 --- /dev/null +++ b/src/common/i18n.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +// import { logger } from './logger'; +import { formatString } from './utils'; + +const localeCodeRegex = /^([a-z]{2})-([A-Z]{2})$/; + +export type localeType = 'en-US' | 'ja-JP'; + +class Translation { + private static translate = (value: string, resource) => resource[value]; + private locale: localeType = 'en-US'; + private loadedResource: object = {}; + + /** + * Apply the locale for translation + * @param locale + */ + public setLocale(locale: localeType): void { + const localeMatch: string[] | null = locale.match(localeCodeRegex); + if (!locale && (!localeMatch || localeMatch.length < 1)) { + // logger.error(`Translation: invalid locale ${locale} found`); + return; + } + + this.locale = locale; + // logger.info(`Translation: locale updated with ${locale}`); + } + + /** + * fetches and returns the translated value + * @param value {string} + * @param data {object} + */ + public t(value: string, data?: object): string { + console.log(process.type); + if (this.loadedResource && this.loadedResource[this.locale]) { + return formatString(Translation.translate(value, this.loadedResource[this.locale])); + } + const resource = this.loadResource(this.locale); + return formatString(resource ? resource[value] : value || value, data); + } + + /** + * Reads the resources dir and returns the data + * @param locale + */ + public loadResource(locale: localeType): object | null { + const resourcePath = path.resolve(__dirname, '..', 'locale', `${locale}.json`); + + if (!fs.existsSync(resourcePath)) { + // logger.error(`Translation: locale resource path does not exits ${resourcePath}`); + return null; + } + + return this.loadedResource[this.locale] = require(resourcePath); + } + +} + +const i18n = new Translation(); + +export { i18n }; \ No newline at end of file diff --git a/src/common/logger.ts b/src/common/logger.ts index 310c8f5b..5b77a4a0 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -197,6 +197,4 @@ export class Logger { const logger = new Logger(); -export { - logger, -}; \ No newline at end of file +export { logger }; \ No newline at end of file diff --git a/src/locale/en-US.json b/src/locale/en-US.json new file mode 100644 index 00000000..1c8750ee --- /dev/null +++ b/src/locale/en-US.json @@ -0,0 +1,155 @@ +{ + "About Symphony": "About Symphony", + "Actual Size": "Actual Size", + "Always on Top": "Always on Top", + "Auto Launch On Startup": "Auto Launch On Startup", + "BasicAuth": { + "Authentication Request": "Authentication Request", + "Cancel": "Cancel", + "hostname": "hostname", + "Invalid user name/password": "Invalid user name/password", + "Log In": "Log In", + "Password:": "Password:", + "Please provide your login credentials for:": "Please provide your login credentials for:", + "User name:": "User name:" + }, + "Bring All to Front": "Bring All to Front", + "Bring to Front on Notifications": "Bring to Front on Notifications", + "Certificate Error": "Certificate Error", + "Close": "Close", + "Cancel": "Cancel", + "ContextMenu": { + "Add to Dictionary": "Add to Dictionary", + "Copy": "Copy", + "Copy Email Address": "Copy Email Address", + "Copy Image": "Copy Image", + "Copy Image URL": "Copy Image URL", + "Copy Link": "Copy Link", + "Cut": "Cut", + "Inspect Element": "Inspect Element", + "Look Up {searchText}": "Look Up \"{searchText}\"", + "Open Link": "Open Link", + "Paste": "Paste", + "Reload": "Reload", + "Search with Google": "Search with Google" + }, + "DownloadManager": { + "Show in Folder": "Show in Folder", + "Reveal in Finder": "Reveal in Finder", + "Open": "Open", + "Downloaded": "Downloaded", + "File not Found": "File not Found", + "The file you are trying to open cannot be found in the specified path.": "The file you are trying to open cannot be found in the specified path." + }, + "Copy": "Copy", + "Custom": "Custom", + "Cut": "Cut", + "Delete": "Delete", + "Disable Hamburger menu": "Disable Hamburger menu", + "Dev Tools disabled": "Dev Tools disabled", + "Dev Tools has been disabled. Please contact your system administrator": "Dev Tools has been disabled. Please contact your system administrator", + "Edit": "Edit", + "Enable Hamburger menu": "Enable Hamburger menu", + "Error loading configuration": "Error loading configuration", + "Error loading URL": "Error loading URL", + "Error loading window": "Error loading window", + "Error setting AutoLaunch configuration": "Error setting AutoLaunch configuration", + "Failed!": "Failed!", + "Flash Notification in Taskbar": "Flash Notification in Taskbar", + "Help": "Help", + "Help Url": "https://support.symphony.com", + "Symphony Url": "https://symphony.com/en-US", + "Hide Others": "Hide Others", + "Hide Symphony": "Hide Symphony", + "Learn More": "Learn More", + "Loading Error": "Loading Error", + "Minimize": "Minimize", + "Minimize on Close": "Minimize on Close", + "Native": "Native", + "No crashes available to share": "No crashes available to share", + "No logs are available to share": "No logs are available to share", + "Not Allowed": "Not Allowed", + "Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the \"Alt\" key.", + "NotificationSettings": { + "Bottom Left": "Bottom Left", + "Bottom Right": "Bottom Right", + "CANCEL": "CANCEL", + "Monitor": "Monitor", + "Notification Settings": "Notification Settings", + "Notification shown on Monitor: ": "Notification shown on Monitor: ", + "OK": "OK", + "Position": "Position", + "Symphony - Configure Notification Position": "Symphony - Configure Notification Position", + "Top Left": "Top Left", + "Top Right": "Top Right" + }, + "Oops! Looks like we have had a crash.": "Oops! Looks like we have had a crash.", + "Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! Looks like we have had a crash. Please reload or close this window.", + "Paste": "Paste", + "Paste and Match Style": "Paste and Match Style", + "Permission Denied": "Permission Denied", + "Please contact your admin for help": "Please contact your admin for help", + "please contact your administrator for more details": "please contact your administrator for more details", + "Quit Symphony": "Quit Symphony", + "Redo": "Redo", + "Refresh app when idle": "Refresh app when idle", + "Clear cache and Reload": "Clear cache and Reload", + "Relaunch Application": "Relaunch Application", + "Relaunch": "Relaunch", + "Reload": "Reload", + "Renderer Process Crashed": "Renderer Process Crashed", + "ScreenPicker": { + "Applications": "Applications", + "Cancel": "Cancel", + "Choose what you'd like to share": "Choose what you'd like to share", + "No screens or applications are currently available.": "No screens or applications are currently available.", + "Screen Picker": "Screen Picker", + "Screens": "Screens", + "Select Application": "Select Application", + "Select Screen": "Select Screen", + "Share": "Share" + }, + "ScreenSnippet": { + "Done": "Done", + "Erase": "Erase", + "Highlight": "Highlight", + "Pen": "Pen", + "Snipping Tool": "Snipping Tool" + }, + "Select All": "Select All", + "Services": "Services", + "Show All": "Show All", + "Show crash dump in Explorer": "Show crash dump in Explorer", + "Show crash dump in Finder": "Show crash dump in Finder", + "Show Logs in Explorer": "Show Logs in Explorer", + "Show Logs in Finder": "Show Logs in Finder", + "SnackBar": { + " to exit full screen": " to exit full screen", + "esc": "esc", + "Press ": "Press " + }, + "Sorry, you are not allowed to access this website": "Sorry, you are not allowed to access this website", + "Speech": "Speech", + "Start Speaking": "Start Speaking", + "Stop Speaking": "Stop Speaking", + "Symphony Help": "Symphony Help", + "TitleBar": { + "Close": "Close", + "Maximize": "Maximize", + "Menu": "Menu", + "Minimize": "Minimize" + }, + "Title Bar Style": "Title Bar Style", + "Toggle Full Screen": "Toggle Full Screen", + "Troubleshooting": "Troubleshooting", + "Unable to generate crash reports due to ": "Unable to generate crash reports due to ", + "Unable to generate logs due to ": "Unable to generate logs due to ", + "Undo": "Undo", + "Updating Title bar style requires Symphony to relaunch.": "Updating Title bar style requires Symphony to relaunch.", + "View": "View", + "Window": "Window", + "Your administrator has disabled": "Your administrator has disabled", + "Zoom": "Zoom", + "Zoom In": "Zoom In", + "Zoom Out": "Zoom Out" +} diff --git a/src/locale/ja-JP.json b/src/locale/ja-JP.json new file mode 100644 index 00000000..0cb6a7c8 --- /dev/null +++ b/src/locale/ja-JP.json @@ -0,0 +1,155 @@ +{ + "About Symphony": "Symphonyについて", + "Actual Size": "実際のサイズ", + "Always on Top": "つねに前面に表示", + "Auto Launch On Startup": "スタートアップ時に自動起動", + "BasicAuth": { + "Authentication Request": "認証要求", + "Cancel": "キャンセル", + "hostname": "ホスト名", + "Invalid user name/password": "ユーザー名かパスワード、または両方が無効", + "Log In": "ログイン", + "Password:": "パスワード:", + "Please provide your login credentials for:": "あなたのログイン認証情報を入力してください。", + "User name:": "ユーザー名:" + }, + "Bring All to Front": "すべて前面に表示", + "Bring to Front on Notifications": "通知時に前面に表示", + "Certificate Error": "証明書のエラー", + "Close": "閉じる", + "Cancel": "キャンセル", + "ContextMenu": { + "Add to Dictionary": "辞書に追加", + "Copy": "コピー", + "Copy Email Address": "電子メールアドレスをコピー", + "Copy Image": "画像をコピー", + "Copy Image URL": "画像のURLをコピー", + "Copy Link": "リンクをコピー", + "Cut": "切り取り", + "Inspect Element": "要素を調査", + "Look Up {searchText}": "「{searchText}」を検索", + "Open Link": "リンクを開く", + "Paste": "貼り付け", + "Reload": "再読み込み", + "Search with Google": "Googleで検索" + }, + "DownloadManager": { + "Show in Folder": "フォルダで見て", + "Reveal in Finder": "Finderで明らかにする", + "Open": "開いた", + "Downloaded": "ダウンロード済み", + "File not Found": "ファイルが見つかりません", + "The file you are trying to open cannot be found in the specified path.": "開こうとしているファイルが指定されたパスに見つかりません." + }, + "Copy": "コピー", + "Custom": "カスタム", + "Cut": "切り取り", + "Delete": "削除", + "Dev Tools disabled": "開発ツールを無効にする", + "Dev Tools has been disabled. Please contact your system administrator": "Dev Toolsが無効になっています。システム管理者に連絡してください", + "Disable Hamburger menu": "ハンバーガーメニューを無効にする", + "Edit": "編集", + "Enable Hamburger menu": "ハンバーガーメニューを有効にする", + "Error loading configuration": "構成の読み込みエラー", + "Error loading URL": "URLの読み込みエラー", + "Error loading window": "ウィンドウを読み込みエラー", + "Error setting AutoLaunch configuration": "自動起動の構成の設定エラー", + "Failed!": "問題が起きました!", + "Flash Notification in Taskbar": "タスクバーの通知を点滅", + "Help": "ヘルプ", + "Help Url": "https://support.symphony.com/hc/ja", + "Symphony Url": "https://symphony.com/ja", + "Hide Others": "他を隠す", + "Hide Symphony": "Symphonyを隠す", + "Learn More": "詳細", + "Loading Error": "読み込みエラー", + "Minimize": "最小化", + "Minimize on Close": "閉じるで最小化", + "Native": "Native", + "No crashes available to share": "共有できるクラッシュはありません", + "No logs are available to share": "共有できるログはありません", + "Not Allowed": "許可されていませ。", + "Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.": "注:ハンバーガーメニューが無効になっている場合、「Alt」キーを押してメインメニューをトリガーすることができます。", + "NotificationSettings": { + "Bottom Left": "左下", + "Bottom Right": "右下", + "CANCEL": "キャンセル", + "Monitor": "モニター", + "Notification Settings": "通知設定", + "Notification shown on Monitor: ": "モニターに表示する通知:", + "OK": "OK", + "Position": "位置", + "Symphony - Configure Notification Position": "Symphony - 通知位置の設定", + "Top Left": "左上", + "Top Right": "右上" + }, + "Oops! Looks like we have had a crash.": "おっと!クラッシュしたようです。", + "Oops! Looks like we have had a crash. Please reload or close this window.": "おっと!クラッシュしたようです。このウィンドウを再度読み込むか閉じてください。", + "Paste": "貼り付け", + "Paste and Match Style": "貼り付けでスタイルを合わせる", + "Permission Denied": "アクセス許可が拒否されています", + "Please contact your admin for help": "不明な点がある場合は、管理者にお問い合わせください", + "please contact your administrator for more details": "詳細は、管理者にお問い合わせください", + "Quit Symphony": "Symphonyを終了", + "Redo": "やり直し", + "Refresh app when idle": "アイドル時にアプリを再表示", + "Clear cache and Reload": "キャッシュをクリアしてリロードする", + "Relaunch Application": "アプリケーションの再起動", + "Relaunch": "「リスタート」", + "Reload": "再読み込み", + "Renderer Process Crashed": "レンダラープロセスがクラッシュしました", + "ScreenPicker": { + "Applications": "アプリケーション", + "Cancel": "キャンセル", + "Choose what you'd like to share": "共有するものを選択する", + "No screens or applications are currently available.": "現在利用できる画面およびアプリケーションはありません。", + "Screen Picker": "画面の選択", + "Screens": "画面", + "Select Application": "アプリケーションを選択", + "Select Screen": "画面を選択", + "Share": "共有" + }, + "ScreenSnippet": { + "Done": "完了", + "Erase": "消去", + "Highlight": "強調", + "Pen": "ペン", + "Snipping Tool": "切り取りツール" + }, + "Select All": "すべてを選択", + "Services": "サービス", + "Show All": "すべてを表示", + "Show crash dump in Explorer": "Explorerにクラッシュダンプを表示", + "Show crash dump in Finder": "ファインダーにクラッシュダンプを表示", + "Show Logs in Explorer": "Explorerにログを表示", + "Show Logs in Finder": "ファインダーにログを表示", + "SnackBar": { + " to exit full screen": " を押します", + "esc": "esc", + "Press ": "全画面表示を終了するには " + }, + "Sorry, you are not allowed to access this website": "申し訳ありませんが、このウェブサイトへのアクセスは許可されていません", + "Speech": "スピーチ", + "Start Speaking": "スピーチを開始", + "Stop Speaking": "スピーチを終了", + "Symphony Help": "Symphonyのヘルプ", + "TitleBar": { + "Close": "閉じる", + "Maximize": "最大化する", + "Menu": "メニュー", + "Minimize": "最小化する" + }, + "Title Bar Style": "タイトルバーのスタイル", + "Toggle Full Screen": "画面表示を切り替え", + "Troubleshooting": "トラブルシューティング", + "Unable to generate crash reports due to ": "クラッシュレポートを生成できません。理由: ", + "Unable to generate logs due to ": "ログを生成できません。理由:", + "Undo": "元に戻す", + "Updating Title bar style requires Symphony to relaunch.": "タイトルバーのスタイルを更新するには、Symphonyが再起動する必要があります。", + "View": "ビュー", + "Window": "ウインドウ", + "Your administrator has disabled": "管理者によて無効にされています", + "Zoom": "ズーム", + "Zoom In": "ズームイン", + "Zoom Out": "ズームアウト" +} diff --git a/src/renderer/about-app.tsx b/src/renderer/about-app.tsx index 08b6b4c9..fbc54c6a 100644 --- a/src/renderer/about-app.tsx +++ b/src/renderer/about-app.tsx @@ -26,7 +26,7 @@ export default class AboutApp extends React.Component<{}, IState> { /** * main render function */ - public render() { + public render(): JSX.Element { const { clientVersion, version, buildNumber } = this.state; const appName = remote.app.getName() || 'Symphony'; const versionString = `Version ${clientVersion}-${version} (${buildNumber})`; @@ -45,7 +45,7 @@ export default class AboutApp extends React.Component<{}, IState> { ipcRenderer.on('about-app-data', this.updateState.bind(this)); } - public componentWillUnmount() { + public componentWillUnmount(): void { ipcRenderer.removeListener('open-file-reply', this.updateState); } @@ -54,7 +54,7 @@ export default class AboutApp extends React.Component<{}, IState> { * @param _event * @param data {Object} { buildNumber, clientVersion, version } */ - private updateState(_event, data) { + private updateState(_event, data): void { this.setState(data as IState); } } \ No newline at end of file diff --git a/src/renderer/assets/symphony-title-bar-logo.png b/src/renderer/assets/symphony-title-bar-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b61197620064677d3d0eb621979687fec4d4ffdf GIT binary patch literal 872 zcmV-u1DE`XP)| zC}#-Eh0vo#Aq7sGwvN=Ug^ILl<1C^t(3wQS7CIKW2t*}pJ4&ClXod(kf{q($V=7rG zH1?v@H}C6q?sH~R85wk0Uifo{`_B8l|GD?vI{^FR-yck=gmVu%lMKg&SeDIC>8MfE z@Ob1_YI{Cg3TT^V+A&RVEDMP1(kT_yj0zRY=Su=DYk_qfU^#9>Jnuz@Vd8rNqPw(< z<>p}=x^y{iXyR6fUM#<4$-=Rtz?8}05|_g163{rjQ~r*?>W)abwkJBrDpJx{dlDE~ z@?dmLC)>NWY)0L~7lAFnU}H3aif&^z0D(dx-IN@#ZyT%)!yHEMeFi}q#;J|RWZupi zEuUX?r%v6wp$Z2NOi6x!KtAM$%)~U9E4u!vf57j7`d_NxkU{)tcvCwCsl%pCE&{e` zB#)eLO)QNq0w)M9g=e-6b~ZcY!3JN4fM@1_W5Sy{nYL4y&G(b*&==OhHr8kiW1O4m zd8gs-#_}a2w6E^j+bDvP2vkwv=`XaNmQ0B2fVI8`mhl_oswdM}^*#WEq)*?utQnaf zX|?(SkoBxazGi@3)`7QNkd}VJ>ZP;Tump1JRNUrcmGb~+G+vpm^)+v}q(Pl>A)O5sJ|cnP6W+li~)W3wYSQ@cLP3+=He zC(3kvE^fd?3ZzaGHLZ`;JzcQ*ar{a<;Y9^YR?w|Lr$#>xTyNaW_o~ literal 0 HcmV?d00001 diff --git a/src/renderer/loading-screen.tsx b/src/renderer/loading-screen.tsx index f3bd395c..017fde65 100644 --- a/src/renderer/loading-screen.tsx +++ b/src/renderer/loading-screen.tsx @@ -9,7 +9,7 @@ export default class LoadingScreen extends React.PureComponent { /** * main render function */ - public render() { + public render(): JSX.Element { const appName = remote.app.getName() || 'Symphony'; return (
diff --git a/src/renderer/preload-component.ts b/src/renderer/preload-component.ts index 0deb8d9b..4e720fc2 100644 --- a/src/renderer/preload-component.ts +++ b/src/renderer/preload-component.ts @@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', load); /** * Loads the appropriate component */ -export function load() { +function load() { const query = new URL(window.location.href).searchParams; const componentName = query.get('componentName'); diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index e69de29b..c996f468 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import WindowsTitleBar from '../renderer/windows-title-bar'; + +document.addEventListener('DOMContentLoaded', load); + +/** + * Injects custom title bar to the document body + */ +function load() { + const element = React.createElement(WindowsTitleBar); + ReactDOM.render(element, document.body); +} \ No newline at end of file diff --git a/src/renderer/styles/title-bar.css b/src/renderer/styles/title-bar.css new file mode 100644 index 00000000..4848bdfb --- /dev/null +++ b/src/renderer/styles/title-bar.css @@ -0,0 +1,107 @@ +.title-bar { + display: flex; + position: fixed; + background: rgba(74,74,74,1); + top: 0; + left: 0; + width: 100%; + height: 32px; + padding-left: 0; + justify-content: center; + align-items: center; + -webkit-app-region: drag; + -webkit-user-select: none; + box-sizing: content-box; + z-index: 1000; +} + +.hamburger-menu-button { + color: rgba(255,255,255,1); + text-align: center; + width: 40px; + height: 32px; + background: none; + border: none; + border-image: initial; + display: inline-grid; + border-radius: 0; + padding: 11px; + box-sizing: border-box; + cursor: default; +} + +.hamburger-menu-button:focus { + outline: none; +} + +.title-container { + height: 32px; + flex: 1; + display: flex; + justify-content: flex-start; + align-items: center; + white-space: nowrap; + overflow: hidden; +} + +.title-bar-title { + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + color: white; + margin: 0; + padding-left: 10px; + font-size: 13px; +} + +.title-bar-button-container { + justify-content: center; + align-items: center; + right: 0; + color: rgba(255,255,255,1); + -webkit-app-region: no-drag; + text-align: center; + width: 45px; + height: 32px; + background: rgba(74,74,74,1); + margin: 0; + box-sizing: border-box !important; + cursor: default; +} + +.title-bar-button { + color: rgba(255,255,255,1); + text-align: center; + width: 45px; + height: 32px; + background: none; + border: none; + border-image: initial; + display: inline-grid; + border-radius: 0; + padding: 10px 15px; + cursor: default; +} + +.title-bar-button:hover { + background-color: rgba(51,51,51,1); +} + +.title-bar-button:focus { + outline: none; +} + +.title-bar-button-container:hover { + background-color: rgba(51,51,51,1); +} + +.window-border { + border-left: 1px solid rgba(74,74,74,1); + border-right: 1px solid rgba(74,74,74,1); +} + +.bottom-window-border { + position: fixed; + border-bottom: 1px solid rgba(74,74,74,1); + width: 100%; + z-index: 3000; + bottom: 0; +} \ No newline at end of file diff --git a/src/renderer/windows-title-bar.tsx b/src/renderer/windows-title-bar.tsx new file mode 100644 index 00000000..3444d95f --- /dev/null +++ b/src/renderer/windows-title-bar.tsx @@ -0,0 +1,249 @@ +import { ipcRenderer, remote } from 'electron'; +import * as React from 'react'; + +import { apiCmds, apiName } from '../common/api-interface'; +import { i18n } from '../common/i18n'; + +interface IState { + isMaximized: boolean; + isFullScreen: boolean; + titleBarHeight: string; +} + +export default class WindowsTitleBar extends React.Component<{}, IState> { + private window: Electron.BrowserWindow; + private readonly eventHandlers = { + onClose: () => this.close(), + onMaximize: () => this.maximize(), + onMinimize: () => this.minimize(), + onShowMenu: () => this.showMenu(), + onUnmaximize: () => this.unmaximize(), + }; + + constructor(props) { + super(props); + this.window = remote.getCurrentWindow(); + this.state = { + isFullScreen: this.window.isFullScreen(), + isMaximized: this.window.isMaximized(), + titleBarHeight: '32px', + }; + // Adds borders to the window + this.addWindowBorders(); + + this.renderMaximizeButtons = this.renderMaximizeButtons.bind(this); + // Event to capture and update icons + this.window.on('maximize', () => this.updateState({ isMaximized: true })); + this.window.on('unmaximize', () => this.updateState({ isMaximized: false })); + this.window.on('enter-full-screen', () => this.updateState({ isFullScreen: true })); + this.window.on('leave-full-screen', () => this.updateState({ isFullScreen: false })); + } + + public componentDidMount() { + const contentWrapper = document.getElementById('content-wrapper'); + if (contentWrapper) { + if (this.state.isFullScreen) { + contentWrapper.style.marginTop = '0px'; + document.body.style.removeProperty('margin-top'); + } else { + contentWrapper.style.marginTop = this.state.titleBarHeight; + } + } + } + + public componentWillMount() { + this.window.removeListener('maximize', this.updateState); + this.window.removeListener('unmaximize', this.updateState); + this.window.removeListener('enter-full-screen', this.updateState); + this.window.removeListener('leave-full-screen', this.updateState); + } + + /** + * Renders the custom title bar + */ + public render(): JSX.Element | null { + + const { isFullScreen } = this.state; + const style = { display: isFullScreen ? 'none' : 'flex' }; + + return ( +
+
+ +
+
+ +

{document.title || 'Symphony'}

+
+
+ +
+
+ {this.renderMaximizeButtons()} +
+
+ +
+
+ ); + } + + /** + * Renders maximize or minimize buttons based on fullscreen state + */ + public renderMaximizeButtons(): JSX.Element { + const { isMaximized } = this.state; + + if (isMaximized) { + return ( + + ); + } else { + return ( + + ); + } + } + + /** + * Method that closes the browser window + */ + public close(): void { + if (this.isValidWindow()) { + this.window.close(); + } + } + + /** + * Method that minimizes the browser window + */ + public minimize(): void { + if (this.isValidWindow()) { + this.window.minimize(); + } + } + + /** + * Method that maximize the browser window + */ + public maximize(): void { + if (this.isValidWindow()) { + console.log(this.window.maximize()); + this.window.maximize(); + this.setState({ isMaximized: true }); + } + } + + /** + * Method that unmaximize the browser window + */ + public unmaximize(): void { + if (this.isValidWindow()) { + this.window.isFullScreen() ? this.window.setFullScreen(false) : this.window.unmaximize(); + } + } + + /** + * Method that popup the application menu + */ + public showMenu(): void { + if (this.isValidWindow()) { + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.popupMenu, + }); + } + } + + /** + * verifies if the this.window is valid and is not destroyed + */ + public isValidWindow(): boolean { + return (this.window && !this.window.isDestroyed()); + } + + /** + * Prevent default to make sure buttons don't take focus + * @param e + */ + private handleMouseDown(e) { + e.preventDefault(); + } + + /** + * Adds borders to the edges of the window chrome + */ + private addWindowBorders() { + const borderBottom = document.createElement('div'); + borderBottom.className = 'bottom-window-border'; + + document.body.appendChild(borderBottom); + document.body.classList.add('window-border'); + } + + /** + * Updates the state with the give value + * @param state + */ + private updateState(state: Partial) { + this.setState((s) => Object.assign(s, state)); + } +}