Skip to content

Commit

Permalink
render tray icon "unread" overlay in main process
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Jun 29, 2018
1 parent fb4130e commit d4b176c
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 83 deletions.
4 changes: 3 additions & 1 deletion electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ directories:
files: [
"./app/**/*",
"./package.json",
"!node_modules/jimp/{browser,fonts}",
"!node_modules/jimp/browser",
"!node_modules/jimp/fonts/open-sans/open-sans-{8,16,32,128}-*",
"!node_modules/jimp/fonts/open-sans/open-sans-64-black",
"!node_modules/rxjs/{_esm5,_esm2015,src,bundles}",
"!app/electron-preload/browser-window-e2e.js",
# TODO sodium-native: include into the package only needed prebuilds for the platform is being built
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
"unionize": "https://github.com/vladimiry/unionize#add-tagprefix-option-lib",
"url-loader": "1.0.1",
"wait-on": "2.1.0",
"webpack": "4.13.0",
"webpack": "4.14.0",
"webpack-cli": "3.0.8",
"webpack-dev-server": "3.1.4",
"webpack-merge": "4.1.3",
Expand Down
Binary file added src/assets/dist/icons/tray-icon-overlay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions src/electron-main/ipc-main-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,17 @@ test.beforeEach(async (t) => {
},
openItem: sinon.spy(),
},
nativeImage: {
createFromPath: sinon.stub().returns({toPNG: sinon.spy}),
},
},
"jimp": {
read: sinon.stub().returns(Promise.resolve({
resize: (w: any, h: any, cb: any) => cb(),
bitmap: {width: 0, height: 0},
})),
loadFont: sinon.spy(),
_rewiremock_no_callThrough: true,
},
"keytar": {
_rewiremock_no_callThrough: true,
Expand All @@ -476,10 +487,10 @@ test.beforeEach(async (t) => {
.keys(t.context.mocks)
.forEach((key) => {
const mocks = t.context.mocks[key];
let mocked: any = mock(key);
let mocked = mock(key);

if (!mocks._rewiremock_no_callThrough) {
mocked = mocked.callThrough();
mocked = (mocked as any).callThrough();
}

mocked.with(mocks);
Expand Down
70 changes: 55 additions & 15 deletions src/electron-main/ipc-main-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import aboutWindow from "about-window";
import Jimp from "jimp";
import keytar from "keytar";
import {app, nativeImage, shell} from "electron";
import {app, nativeImage, NativeImage, shell} from "electron";
import {EMPTY, from} from "rxjs";
import {isWebUri} from "valid-url";
import {KeePassHttpClient} from "keepasshttp-client";
Expand All @@ -12,6 +12,7 @@ import {assert} from "_@shared/util";
import {BuildEnvironment} from "_@shared/model/common";
import {buildSettingsAdapter, handleKeePassRequestError, toggleBrowserWindow} from "./util";
import {Context} from "./model";
import {ElectronContextLocations} from "_@shared/model/electron";
import {Endpoints, IPC_MAIN_API} from "_@shared/api/main";
import {KEYTAR_MASTER_PASSWORD_ACCOUNT, KEYTAR_SERVICE_NAME} from "./constants";
import {StatusCode, StatusCodeError} from "_@shared/model/error";
Expand Down Expand Up @@ -242,30 +243,23 @@ export const initEndpoints = async (ctx: Context): Promise<Endpoints> => {
return await ctx.settingsStore.write(settings);
})()),
...(await (async () => {
const overlaySizeFactor = 0.6;
const native = nativeImage.createFromPath(ctx.locations.trayIcon);
const jimp = await Jimp.read(native.toPNG());
const main: { native: Electron.NativeImage; jimp: Jimp; w: number; h: number; } = {
native,
jimp,
w: jimp.bitmap.width,
h: jimp.bitmap.height,
};
const trayIconsService = await prepareTrayIcons(ctx.locations);
const result: Pick<Endpoints, "updateOverlayIcon"> = {
updateOverlayIcon: ({unread, dataURL}) => from((async () => {
updateOverlayIcon: ({unread}) => from((async () => {
const browserWindow = ctx.uiContext && ctx.uiContext.browserWindow;
const tray = ctx.uiContext && ctx.uiContext.tray;

if (!browserWindow || !tray) {
return EMPTY.toPromise();
}

if (unread > 0 && dataURL) {
const overlaySourceJimp = await Jimp.read(nativeImage.createFromDataURL(dataURL).toPNG());
const overlaySize = {w: Math.round(main.w * overlaySizeFactor), h: Math.round(main.h * overlaySizeFactor)};
const overlayJimp = await promisify(overlaySourceJimp.resize.bind(overlaySourceJimp))(overlaySize.w, overlaySize.h);
const {main, buildOverlay} = trayIconsService;

if (unread > 0) {
const overlayJimp = buildOverlay(unread);
const overlayBuffer = await promisify(overlayJimp.getBuffer.bind(overlayJimp))(Jimp.MIME_PNG);
const overlayNative = nativeImage.createFromBuffer(overlayBuffer);
const overlaySize = {w: overlayJimp.bitmap.width, h: overlayJimp.bitmap.height};
const composedJimp = main.jimp.composite(overlayJimp, main.w - overlaySize.w, main.h - overlaySize.h);
const composedBuffer = await promisify(composedJimp.getBuffer.bind(composedJimp))(Jimp.MIME_PNG);
const composedNative = nativeImage.createFromBuffer(composedBuffer);
Expand All @@ -291,3 +285,49 @@ export const initEndpoints = async (ctx: Context): Promise<Endpoints> => {

return endpoints;
};

export async function prepareTrayIcons(locations: ElectronContextLocations): Promise<{
main: { native: NativeImage, jimp: Jimp, w: number, h: number },
buildOverlay: (unread: number) => Jimp,
}> {
const main = await (async () => {
const native = nativeImage.createFromPath(locations.trayIcon);
const jimp = await Jimp.read(native.toPNG());

return Object.freeze({
native,
jimp,
w: jimp.bitmap.width,
h: jimp.bitmap.height,
});
})();
const buildOverlay = await (async () => {
const factors = {overlay: .75, text: .9};
const size = {w: Math.round(main.w * factors.overlay), h: Math.round(main.h * factors.overlay)};
const imageSource = await Jimp.read(nativeImage.createFromPath(locations.trayIconOverlay).toPNG());
const jimp = await promisify(imageSource.resize.bind(imageSource))(size.w, size.h);
// TODO there is no "loadFont" function signature provided by Jimp's declaration file
const font = await (Jimp as any).loadFont(Jimp.FONT_SANS_64_WHITE);
const fontSize = 64;
const printX = [
30,
8,
];
const printY = size.h / 2 - (fontSize / 2);

return (unread: number) => {
const index = String(unread).length - 1;

if (index < printX.length) {
return jimp.clone().print(font, printX[index], printY, String(unread), size.w * factors.text);
}

return jimp;
};
})();

return {
main,
buildOverlay,
};
}
1 change: 1 addition & 0 deletions src/electron-main/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function initLocations(runtimeEnvironment: RuntimeEnvironment, paths?: ContextIn
userData: userDataDir,
icon: buildAppPath(largeIcon),
trayIcon: buildAppPath(os.platform() === "darwin" ? "./assets/icons/mac/icon.png" : largeIcon),
trayIconOverlay: buildAppPath("./assets/icons/tray-icon-overlay.png"),
browserWindowPage: (process.env.NODE_ENV as BuildEnvironment) === "development" ? "http://localhost:8080/index.html"
: formatFileUrl(path.join(appDir, "./web/index.html")),
preload: {
Expand Down
2 changes: 1 addition & 1 deletion src/shared/api/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface Endpoints {
toggleBrowserWindow: ApiMethod<{ forcedState?: boolean }, never>;
toggleCompactLayout: ApiMethodNoArgument<Config>;
updateAccount: ApiMethod<AccountConfigPatch, Settings>;
updateOverlayIcon: ApiMethod<{ unread: number; dataURL?: string; }, never>;
updateOverlayIcon: ApiMethod<{ unread: number }, never>;
}

export const IPC_MAIN_API = new IpcMainApiService<Endpoints>({channel: `${process.env.APP_ENV_PACKAGE_NAME}:ipcMain-api`});
1 change: 1 addition & 0 deletions src/shared/model/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ElectronContextLocations {
readonly browserWindowPage: string;
readonly icon: string;
readonly trayIcon: string;
readonly trayIconOverlay: string;
readonly userData: string;
readonly preload: {
browserWindow: string;
Expand Down
30 changes: 1 addition & 29 deletions src/web/src/app/+accounts/accounts.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
takeUntil(this.unSubscribe$),
)
.subscribe((unread) => {
return this.store.dispatch(ACCOUNTS_ACTIONS.UpdateOverlayIcon({
count: unread, dataURL: unread > 0 ? createOverlayIconDataURL(unread) : undefined,
}));
return this.store.dispatch(ACCOUNTS_ACTIONS.UpdateOverlayIcon({count: unread}));
});
}

Expand Down Expand Up @@ -98,29 +96,3 @@ export class AccountsComponent implements OnInit, OnDestroy {
this.unSubscribe$.complete();
}
}

// TODO move overlay creating logic to backend (main process), send only {count: unread} then
function createOverlayIconDataURL(unread: number): string {
const canvas = document.createElement("canvas");

canvas.height = 128;
canvas.width = 128;
canvas.style.letterSpacing = "-5px";

const ctx = canvas.getContext("2d");

if (!ctx) {
throw new Error("Failed to get 2d canvas context");
}

ctx.fillStyle = "#DC3545";
ctx.beginPath();
ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI);
ctx.fill();
ctx.textAlign = "center";
ctx.fillStyle = "white";
ctx.font = "90px sans-serif";
ctx.fillText(String(Math.min(99, unread)), 64, 96);

return canvas.toDataURL();
}
2 changes: 1 addition & 1 deletion src/web/src/app/+accounts/accounts.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class AccountsEffects {
updateOverlayIcon$ = this.actions$.pipe(
filter(ACCOUNTS_ACTIONS.is.UpdateOverlayIcon),
switchMap(({payload}) => this.electronService
.callIpcMain("updateOverlayIcon")({unread: payload.count, dataURL: payload.dataURL})
.callIpcMain("updateOverlayIcon")({unread: payload.count})
.pipe(
mergeMap(() => []),
catchError((error) => this.effectsService.buildFailActionObservable(error)),
Expand Down
Loading

0 comments on commit d4b176c

Please sign in to comment.