Skip to content

Commit

Permalink
feat: add ability to change theme
Browse files Browse the repository at this point in the history
  • Loading branch information
icelam committed Aug 9, 2020
1 parent b114a9e commit 8ade8da
Show file tree
Hide file tree
Showing 20 changed files with 224 additions and 42 deletions.
6 changes: 5 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"plugin:@typescript-eslint/recommended",
"airbnb-base",
],
"env": {
"browser": true,
"node": true
},
"settings": {
"import/resolver": [
"node",
Expand Down Expand Up @@ -62,7 +66,7 @@
},
"overrides": [
{
"files": ["webpack/**/*.js", "scripts/**/*.js"],
"files": ["webpack/**/*.js", "scripts/**/*.js", "src/**/*.html"],
"parser": "espree",
"parserOptions": {},
"extends": [
Expand Down
7 changes: 7 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IpcRenderer } from 'electron';

declare global {
interface Window {
ipcRenderer: IpcRenderer;
}
}
25 changes: 20 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as path from 'path';
import * as url from 'url';
import {
app, BrowserWindow, Menu, ipcMain
app, BrowserWindow, Menu, ipcMain, nativeTheme
} from 'electron';
import { debounce } from '@utils';
import { debounce, listenToSystemThemeChange, getUserPreferedTheme } from '@utils';
import { getStoredWindowLocation, saveWindowPositionToStorage } from '@storage';
import { applicationMenu, settingMenu } from '@menus';
import { IS_DEVELOPEMENT, IS_LINUX, APP_ICON_PATH } from '@constants';
import { Position } from '@types';
import { Position, AppThemeOptions } from '@types';

const WINDOW_WIDTH = 396;
const WINDOW_HEIGHT = 190;
Expand All @@ -27,9 +27,11 @@ const createWindow = async () => {
x: WINDOW_POSITION?.x ?? undefined,
y: WINDOW_POSITION?.y ?? undefined,
resizable: false,
backgroundColor: '#ffffff', // TODO: dark mode
backgroundColor: nativeTheme.shouldUseDarkColors
? '#2d2d2d'
: '#ffffff',
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, './preload.js'),
devTools: IS_DEVELOPEMENT
},
// For app icon to be displayed correctly on linux AppImage
Expand Down Expand Up @@ -78,6 +80,19 @@ app.on('activate', () => {
// Application menu bar
Menu.setApplicationMenu(applicationMenu);

// It would be good to move setInitialTheme into menuItem.ts
// However, it would mean to make settingMenu as an async function which would be hard to import
// Another apporach is to move this to utils folder but it will breaks other menu items
// since introducing `settingMenu` would introduce circular self-reference
// TODO: Rethink the approach of initializing checked theme value
const setInitialTheme = async () => {
const userPreference = await getUserPreferedTheme();
const initialAppTheme: AppThemeOptions = userPreference ?? 'system';
settingMenu.getMenuItemById(`${initialAppTheme}-theme`).checked = true;
};
setInitialTheme();
listenToSystemThemeChange();

// Event Handlers
ipcMain.on('QUIT_APP', () => mainWindow?.close());

Expand Down
10 changes: 5 additions & 5 deletions src/menus/applicationMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import { IS_DEVELOPEMENT } from '@constants';

import {
appMenuItem,
copyFormatMenuItem,
themeMenuItem,
developerMenuItem
} from './menuItems';

/**
* Menu template for creating application menu displayed in macOS
*/
const applicationMenuTemplate: MenuItemConstructorOptions[] = [
appMenuItem,
copyFormatMenuItem,
themeMenuItem
appMenuItem
// Temporary disable `copyFormatMenuItem` and `themeMenuItem`
// Since importing `applicationMenu` and `settingMenu` at `setAppTheme()`
// breaks functions of other menu items
// TODO: Figure out a way to sync checked option value between two menus
];

if (IS_DEVELOPEMENT) {
Expand Down
24 changes: 19 additions & 5 deletions src/menus/menuItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
openIssuePage,
checkUpdates,
clearAppStorage,
openStorageFolder
openStorageFolder,
setAppTheme
} from '@utils';

/**
Expand Down Expand Up @@ -42,13 +43,26 @@ export const themeMenuItem: MenuItemConstructorOptions = {
{
label: translations.menus.lightTheme,
type: 'radio',
checked: true
// TODO: Set Light theme, share value between setting menu and app menu
id: 'light-theme',
click: (): void => {
setAppTheme('light');
}
},
{
label: translations.menus.darkTheme,
type: 'radio'
// TODO: Set Dark theme, share value between setting menu and app menu
type: 'radio',
id: 'dark-theme',
click: (): void => {
setAppTheme('dark');
}
},
{
label: translations.menus.useSystemTheme,
type: 'radio',
id: 'system-theme',
click: (): void => {
setAppTheme('system');
}
}
]
};
Expand Down
20 changes: 20 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ipcRenderer } from 'electron';
import { getUserPreferedTheme, getOsTheme } from '@utils/getThemes';
import { AppTheme } from '@types';

const setInitialAppTheme = async () => {
const userPreference = await getUserPreferedTheme();
const osTheme = getOsTheme();
const themeToUse: AppTheme = userPreference && userPreference !== 'system'
? userPreference
: osTheme;
window.document.documentElement.setAttribute('data-theme', themeToUse);
};

window.addEventListener('DOMContentLoaded', () => {
setInitialAppTheme();
});

// nodeIntegration is set to `false` for security reasons
// Inject functions needed for web contents here
window.ipcRenderer = ipcRenderer;
9 changes: 4 additions & 5 deletions src/renderer/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ipcRenderer } from 'electron';
import {
LitElement, html, css, CSSResult, property, customElement, TemplateResult
} from 'lit-element';
Expand Down Expand Up @@ -37,24 +36,24 @@ class GeneratorApp extends LitElement {
}

private closeFrame() {
ipcRenderer.send('QUIT_APP');
window.ipcRenderer.send('QUIT_APP');
}

private openSettingMenu(event: MouseEvent) {
ipcRenderer.send('OPEN_SETTING_MENU', {
window.ipcRenderer.send('OPEN_SETTING_MENU', {
x: event.clientX + 10,
y: event.clientY + 10
});
}

private minimizeFrame() {
ipcRenderer.send('MINIMIZE_APP');
window.ipcRenderer.send('MINIMIZE_APP');
}

private pinFrame() {
const newPinState = !this.shouldPinFrame;
this.shouldPinFrame = newPinState;
ipcRenderer.send('PIN_APP', newPinState);
window.ipcRenderer.send('PIN_APP', newPinState);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/renderer/assets/scss/_app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ body {
line-height: 1.5;
font-family: 'Source Code Pro', monospace, sans-serif;
-webkit-text-size-adjust: 100%;
background-color: var(--color-background);
color: var(--color-text);
}
31 changes: 30 additions & 1 deletion src/renderer/assets/scss/_colors.scss
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
:root {
/* Common colors */
--color-black: #000000;
--color-white: #ffffff;
--color-light-red: #ff5f57;
--color-dark-red: #83251e;
--color-yellow: #febc2e;
--color-dark-yellow: #90591d;
--color-grey-40: #666666;

/* Light mode */
--color-grey-96: #f4f4f4;
--color-grey-93: #eeeeee;
--color-grey-91: #e7e7e7;
--color-grey-67: #aaaaaa;
--color-grey-54: #8a8a8a;
--color-grey-40: #666666;

/* Dark mode */
--color-grey-18: #2d2d2d;
--color-grey-23: #3b3b3b;
--color-grey-28: #484848;
--color-grey-46: #747474;
--color-grey-60: #999999;
--color-grey-68: #afafaf;
}

:root,
[data-theme^='light'] {
--color-background: var(--color-white);
--color-text: var(--color-grey-40);
--color-frame-control-close: var(--color-light-red);
--color-frame-control-close-text: var(--color-dark-red);
--color-frame-control-minimize: var(--color-yellow);
Expand All @@ -21,3 +37,16 @@
--color-frame-header-icon: var(--color-grey-54);
--color-frame-header-icon-hover: var(--color-grey-40);
}

[data-theme^='dark'] {
--color-background: var(--color-grey-18);
--color-text: var(--color-grey-68);
--color-frame-control-close: var(--color-light-red);
--color-frame-control-close-text: var(--color-dark-red);
--color-frame-control-minimize: var(--color-yellow);
--color-frame-control-minimize-text: var(--color-dark-yellow);
--color-frame-header-background: var(--color-grey-28);
--color-frame-header-text: var(--color-grey-46);
--color-frame-header-icon: var(--color-grey-46);
--color-frame-header-icon-hover: var(--color-grey-60);
}
4 changes: 1 addition & 3 deletions src/renderer/components/Button/button.styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
css
} from 'lit-element';
import { css } from 'lit-element';

/**
* Shared styles for buttons.
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { AppTheme } from '@types';
import '@webcomponents/webcomponentsjs/webcomponents-bundle';
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter';

import '@styles/index.scss';
import './app';

// Event Handlers for handiing events sent from main process
window.ipcRenderer.on('CHANGE_RENDERER_THEME', (_, theme: AppTheme) => {
window.document.documentElement.setAttribute('data-theme', theme);
});
44 changes: 41 additions & 3 deletions src/storage/storage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
import * as storage from 'electron-json-storage';
import log from 'electron-log';
import { Position } from '@types';
import { Position, AppThemeOptions, AppThemeStorage } from '@types';

const POSITION_STORAGE_PATH = 'window.position';
const POSITION_STORAGE_PATH = 'windowPosition';
const THEME_STORAGE_PATH = 'appTheme';
// const PIN_STATUS_STORAGE_PATH = 'window.pin';

/**
* Get user defined theme preference stored in storage
* @returns {AppThemeStorage} theme preference stored as JSON
*/
export const getAppTheme = async (): Promise<AppThemeStorage> => {
try {
const appTheme = await new Promise((resolve, reject) => {
storage.get(THEME_STORAGE_PATH, (error: Error, data: Position) => {
if (error) { reject(error); }
resolve(data);
});
});
return appTheme as AppThemeStorage;
} catch (error) {
log.error(error?.message ?? 'Unknown error from getAppTheme()');
return {};
}
};

/**
* Save user defined theme preference to storage
* @param {AppThemeOptions} user defined theme
*/
export const saveAppTheme = (theme?: AppThemeOptions): void => {
try {
if (!theme) {
throw new Error('Theme not provided when trying to save theme');
}

storage.set(THEME_STORAGE_PATH, { theme }, (error) => {
if (error) { throw error; }
});
} catch (error) {
log.error(error?.message ?? 'Unknown error from saveAppTheme()');
}
};

/**
* Get window position stored in storage
* @returns {Position} window position stored when window is moved
Expand Down Expand Up @@ -32,7 +70,7 @@ export const getStoredWindowLocation = async (): Promise<Position> => {
export const saveWindowPositionToStorage = (x?: number, y?: number): void => {
try {
if (!x || !y) {
throw new Error('Missing X or Y in position');
throw new Error('Missing X or Y in position when trying to save window position');
}

const windowPositionData: Position = { x, y };
Expand Down
1 change: 1 addition & 0 deletions src/translations/menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const menusTranslations = {
theme: 'Theme',
lightTheme: 'Light',
darkTheme: 'Dark',
useSystemTheme: 'Use system setting',
about: 'About',
reportIssue: 'Report an issue',
checkUpdates: 'Check for updates',
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export type Position = {
x?: number; y?: number;
}

export type AppTheme = 'light' | 'dark';
export type AppThemeOptions = AppTheme | 'system';
export type AppThemeStorage = {
theme?: AppThemeOptions;
}
31 changes: 31 additions & 0 deletions src/utils/appTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { nativeTheme } from 'electron';
import { AppTheme, AppThemeOptions } from '@types';
import { saveAppTheme } from '@storage';
import { getOsTheme, getUserPreferedTheme } from './getThemes';
import getMainWindow from '../main';

/**
* Set app theme when user explicitly set it through theme preference menu
*/
export const setAppTheme = (theme: AppThemeOptions): void => {
const window = getMainWindow();
const osTheme = getOsTheme();
const themeToUse: AppTheme = theme !== 'system' ? theme : osTheme;
window.webContents.send('CHANGE_RENDERER_THEME', themeToUse);
saveAppTheme(theme);
};

/**
* Listen to theme change on system preferences
*/
export const listenToSystemThemeChange = (): void => {
nativeTheme.on('updated', async () => {
const window = getMainWindow();
const userPreference = await getUserPreferedTheme();
const osTheme = getOsTheme();
const themeToUse: AppTheme = userPreference && userPreference !== 'system'
? userPreference
: osTheme;
window.webContents.send('CHANGE_RENDERER_THEME', themeToUse);
});
};
Loading

0 comments on commit 8ade8da

Please sign in to comment.