Skip to content
This repository has been archived by the owner on Dec 18, 2020. It is now read-only.

Implement About Menu and Version Dialog #100

Merged
merged 16 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"files": [
"package.json",
"dist/main.bundle.js",
"dist/renderer.bundle.js",
"dist/preload.js",
"dist/pages/*.html",
"dist/client",
"build/icon.*"
],
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"devDependencies": {
"@types/minimist": "1.2.0",
"@types/node": "13",
"@types/node-fetch": "^2.5.6",
"@typescript-eslint/eslint-plugin": "2.27.0",
"@typescript-eslint/parser": "2.27.0",
"asar": "3.0.3",
Expand Down Expand Up @@ -56,5 +57,9 @@
"test:other": "yarn prettier --ignore-path .gitignore --list-different",
"watch": "webpack -w"
},
"version": "0.0.1"
"version": "0.0.1",
"dependencies": {
"electron-store": "^5.1.1",
"html-webpack-plugin": "^4.2.0"
}
}
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const APP_NAME = "Excalidraw";
export const EXCALIDRAW_API = "https://excalidraw.com/version.json";
export const EXCALIDRAW_ASAR_SOURCE = "https://excalidraw.com/excalidraw.asar";
export const EXCALIDRAW_GITHUB_PACKAGE_JSON_URL =
"https://raw.githubusercontent.com/excalidraw/excalidraw-desktop/master/package.json";
32 changes: 24 additions & 8 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import {app, BrowserWindow} from "electron";
import * as minimist from "minimist";

import * as path from "path";
import * as url from "url";
import {setupMenu} from "./menu";
import checkVersion from "./util/checkVersion";
import {setMetadata, setAppName} from "./util/metadata";
import {APP_NAME} from "./constants";

const path = require("path");
const url = require("url");
const EXCALIDRAW_BUNDLE = path.join(__dirname, "client", "index.html");
let mainWindow: Electron.BrowserWindow;
const argv = minimist(process.argv.slice(1));
const EXCALIDRAW_BUNDLE = path.join(__dirname, "client", "index.html");
const APP_ICON_PATH = path.join(__dirname, "client", "logo-180x180.png");

function createWindow() {
mainWindow = new BrowserWindow({
show: false,
height: 600,
width: 800,
webPreferences: {
contextIsolation: true, // protect against prototype pollution
preload: `${__dirname}/preload.js`,
},
});

if (argv.devtools) {
Expand All @@ -31,11 +39,19 @@ function createWindow() {
mainWindow = null;
});

mainWindow.on("show", async () => {
const {local: localVersion, needsUpdate} = await checkVersion();
// calling.show after this event, ensure there's no visual flash
mainWindow.once("ready-to-show", async () => {
const versions = await checkVersion();

console.info("Current version", versions.local);
console.info("Needs update", versions.needsUpdate);

setAppName(APP_NAME);
setMetadata("versions", versions);
setMetadata("appIconPath", APP_ICON_PATH);
setupMenu(mainWindow);

console.info("Current version", localVersion);
console.info("Needs update", needsUpdate);
mainWindow.show();
});
}

Expand Down
79 changes: 79 additions & 0 deletions src/menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {BrowserWindow, Menu, MenuItem} from "electron";
import * as path from "path";
import * as url from "url";
import {APP_NAME} from "./constants";
import {getAppVersions, getAppName, getMetadata} from "./util/metadata";

const ABOUT_PAGE_PATH = path.resolve(__dirname, "pages", "about.html");

const openAboutWindow = () => {
let aboutWindow = new BrowserWindow({
height: 320,
width: 320,
modal: true,
backgroundColor: "white",
show: false,
webPreferences: {
contextIsolation: true,
preload: `${__dirname}/preload.js`,
},
});

aboutWindow.loadURL(
url.format({
pathname: ABOUT_PAGE_PATH,
protocol: "file",
slashes: true,
}),
);

aboutWindow.setMenuBarVisibility(false);
aboutWindow.center();
aboutWindow.on("ready-to-show", () => aboutWindow.show());
aboutWindow.on("show", () => {
const aboutContent = {
appName: getAppName(),
iconPath: getMetadata("appIconPath"),
versions: getAppVersions(),
};

aboutWindow.webContents.send("show-about-contents", aboutContent);
});
};

export const setupMenu = (activeWindow: BrowserWindow, options = {}) => {
const isDarwin = process.platform === "darwin";
const defaultMenuItems: MenuItem[] = Menu.getApplicationMenu().items;
const menuTemplate = [];
if (isDarwin) {
defaultMenuItems.shift();
menuTemplate.push({
label: APP_NAME,
submenu: [
{
label: `About ${APP_NAME}`,
enabled: true,
click: () => openAboutWindow(),
},
],
});
menuTemplate.push(...defaultMenuItems);
} else {
defaultMenuItems.pop();
menuTemplate.push(...defaultMenuItems);
menuTemplate.push({
label: "Help",
submenu: [
{
label: `About ${APP_NAME}`,
enabled: true,
click: () => openAboutWindow(),
},
],
});
}

// TODO: Remove default menu items that aren't relevant
const appMenu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(appMenu);
};
88 changes: 88 additions & 0 deletions src/pages/about.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes"
/>
<meta
http-equiv="Content-Security-Policy"
content="block-all-mixed-content; child-src 'none'; connect-src https: wss: filesystem:; default-src 'self'; font-src 'self' data: https: filesystem:; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https:;"
/>
<title>About This App</title>
<style>
body,
html {
width: 100%;
height: 100%;
-webkit-user-select: none;
user-select: none;
-webkit-app-region: drag;
}

body {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #1d1d1d;
background-color: #fafafa;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

#app-icon {
-webkit-user-select: none;
user-select: none;
display: inline-block;
background: #ffffff;
border-radius: 50px;
box-shadow: 20px 20px 60px #d9d9d9, -20px -20px 60px #ffffff;
}

.versions {
margin: 0.3em;
}

.title {
margin-top: 0.6em;
margin-bottom: 0.6em;
}

.clickable {
cursor: pointer;
}

.description {
margin-bottom: 1em;
text-align: center;
}

.copyright {
color: #999;
margin-top: 1.5em;
}

.link {
cursor: pointer;
color: #80a0c2;
}

</style>
</head>
<body>
<img id="app-icon" alt="App icon" height="60" width="60" />
<h2 class="title"></h2>
<p id="app-version" class="versions"></p>
<p id="web-version" class="versions"></p>
<p class="copyright"></p>

<!-- https://github.com/electron/electron/issues/2863 -->
<script>
var exports = exports || {};
</script>
</body>
</html>
15 changes: 15 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {ipcRenderer, contextBridge, remote} from "electron";
import {IpcListener} from "./types";

contextBridge.exposeInMainWorld("ipcRenderer", {
send: (channel: string, data: any[]) => {
ipcRenderer.send(channel, data);
},
receive: (channel: string, func: IpcListener) => {
ipcRenderer.on(channel, (event, ...args: any[]) => func(event, ...args));
},
});

contextBridge.exposeInMainWorld("remote", {
getVersion: remote.app.getVersion,
});
30 changes: 30 additions & 0 deletions src/renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const rendererWindow: RendererWindow = window;

rendererWindow.ipcRenderer.receive("show-about-contents", (_, options: any) => {
const {appName, versions, iconPath} = options;
document.title = `About ${appName}`;

const $titleElement = document.querySelector(".title") as HTMLHeadingElement;
$titleElement.textContent = appName;

const $iconElement = document.getElementById("app-icon") as HTMLImageElement;
$iconElement.src = iconPath;

const $appVersionElement = document.getElementById(
"app-version",
) as HTMLParagraphElement;

$appVersionElement.textContent = `Version ${versions.app.remote}`;

const $webVersionElement = document.getElementById(
"web-version",
) as HTMLParagraphElement;

$webVersionElement.textContent = `${appName} for Web Version ${versions.web.remote}`;

const $copyRightElement = document.querySelector(
".copyright",
) as HTMLParagraphElement;

$copyRightElement.textContent = `Copyright (c) ${new Date().getFullYear()} ${appName}`;
});
18 changes: 18 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {IpcRendererEvent, IpcRenderer} from "electron";

export type IpcListener = (event: IpcRendererEvent, ...args: any[]) => void;
export type CustomIpcSender = (channel: string, ...args: any[]) => null;

export interface CustomIpcRenderer extends IpcRenderer {
send: (channel: string, ...args: any[]) => null;
receive: (channel: string, listener: IpcListener) => null;
}

declare global {
interface RendererWindow extends Window {
ipcRenderer?: CustomIpcRenderer;
remote?: {
getVersion: () => string;
};
}
}
20 changes: 14 additions & 6 deletions src/util/checkVersion.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
const fs = require("fs");
const path = require("path");
const fetch = require("node-fetch").default;
import * as fs from "fs";
import * as path from "path";
import fetch from "node-fetch";
import {EXCALIDRAW_API, EXCALIDRAW_GITHUB_PACKAGE_JSON_URL} from "../constants";

const LOCAL_VERSION_PATH = path.resolve(__dirname, "client", "version.json");

const EXCALIDRAW_API = "https://excalidraw.com/version.json";

interface CheckResponse {
local: string;
remote: string;
appVersion: string;
needsUpdate: boolean;
}

const _getLocalVersion = (): string => {
const raw = fs.readFileSync(LOCAL_VERSION_PATH);
const raw = fs.readFileSync(LOCAL_VERSION_PATH).toString();
const contents = JSON.parse(raw);
return contents.version;
};
Expand All @@ -24,13 +24,21 @@ const _getRemoteVersion = async (): Promise<string> => {
return contents.version;
};

const _getRemoteDesktopAppVersion = async (): Promise<string> => {
const raw = await fetch(EXCALIDRAW_GITHUB_PACKAGE_JSON_URL);
const contents = await raw.json();
return contents.version;
};

export default async function checkVersion(): Promise<CheckResponse> {
const localVersion = _getLocalVersion();
const remoteVersion = await _getRemoteVersion();
const appVersion = await _getRemoteDesktopAppVersion();

return {
local: localVersion,
remote: remoteVersion,
appVersion,
needsUpdate: localVersion < remoteVersion,
};
}