mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Rework the image updating injectables to use the updater state
- Move cache up a level and add a new option to createTryIcon - Make useDarkColors and updateAvailable fully observable, computed, and injectable - Add locking to remove race conditions within async code Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
1fc2008b2f
commit
c5edceca67
104
src/common/utils/hash-map.ts
Normal file
104
src/common/utils/hash-map.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator<T> {
|
||||
(iterator as IterableIterator<T>)[Symbol.iterator] = () => iterator as IterableIterator<T>;
|
||||
|
||||
return iterator as IterableIterator<T>;
|
||||
}
|
||||
|
||||
export class HashMap<K, V> implements Map<K, V> {
|
||||
#hashmap: Map<string, { key: K; value: V }>;
|
||||
|
||||
constructor(protected hasher: (key: K) => string, initialValues?: Iterable<readonly [K, V]>) {
|
||||
this.#hashmap = new Map();
|
||||
|
||||
if (initialValues) {
|
||||
for (const [key, value] of initialValues) {
|
||||
this.#hashmap.set(this.hasher(key), { key, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#hashmap.clear();
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
return this.#hashmap.delete(this.hasher(key));
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
||||
this.#hashmap.forEach(entry => callbackfn(entry.value, entry.key, thisArg ?? this));
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return this.#hashmap.get(this.hasher(key))?.value;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.#hashmap.has(this.hasher(key));
|
||||
}
|
||||
|
||||
set(key: K, value: V): this {
|
||||
this.#hashmap.set(this.hasher(key), { key, value });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#hashmap.size;
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[K, V]> {
|
||||
let nextIndex = 0;
|
||||
const keys = Array.from(this.keys());
|
||||
const values = Array.from(this.values());
|
||||
|
||||
return makeIterableIterator<[K, V]>({
|
||||
next() {
|
||||
const index = nextIndex++;
|
||||
|
||||
return index < values.length
|
||||
? { value: [keys[index], values[index]], done: false }
|
||||
: { done: true, value: undefined };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
keys(): IterableIterator<K> {
|
||||
let nextIndex = 0;
|
||||
const observableValues = Array.from(this.#hashmap.values());
|
||||
|
||||
return makeIterableIterator<K>({
|
||||
next: () => {
|
||||
return nextIndex < observableValues.length
|
||||
? { value: observableValues[nextIndex++].key, done: false }
|
||||
: { done: true, value: undefined };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
let nextIndex = 0;
|
||||
const observableValues = Array.from(this.#hashmap.values());
|
||||
|
||||
return makeIterableIterator<V>({
|
||||
next: () => {
|
||||
return nextIndex < observableValues.length
|
||||
? { value: observableValues[nextIndex++].value, done: false }
|
||||
: { done: true, value: undefined };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this.entries();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return "Map";
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
import type { IInterceptable, IInterceptor, IListenable, ISetWillChange, ObservableMap } from "mobx";
|
||||
import { action, observable, ObservableSet } from "mobx";
|
||||
|
||||
export function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator<T> {
|
||||
function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator<T> {
|
||||
(iterator as IterableIterator<T>)[Symbol.iterator] = () => iterator as IterableIterator<T>;
|
||||
|
||||
return iterator as IterableIterator<T>;
|
||||
|
||||
@ -26,6 +26,7 @@ export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./formatDuration";
|
||||
export * from "./getRandId";
|
||||
export * from "./hash-map";
|
||||
export * from "./hash-set";
|
||||
export * from "./n-fircate";
|
||||
export * from "./objects";
|
||||
|
||||
@ -14,6 +14,8 @@ import { once } from "lodash";
|
||||
import { ipcMain } from "electron";
|
||||
import { nextUpdateChannel } from "./utils/update-channel";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import appUpdaterStateInjectable from "./app-updater/state.injectable";
|
||||
|
||||
let installVersion: null | string = null;
|
||||
|
||||
@ -52,6 +54,8 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
|
||||
}
|
||||
|
||||
const userStore = UserStore.getInstance();
|
||||
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.main);
|
||||
const state = di.inject(appUpdaterStateInjectable);
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
@ -94,6 +98,9 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
|
||||
});
|
||||
logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: info.version });
|
||||
broadcastMessage(UpdateAvailableChannel, backchannel, info);
|
||||
state.set({
|
||||
status: "update-install-ready",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
|
||||
installVersion = undefined;
|
||||
|
||||
21
src/main/app-updater/state.injectable.ts
Normal file
21
src/main/app-updater/state.injectable.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { observable } from "mobx";
|
||||
|
||||
export type AppUpdaterState = {
|
||||
status: "idle";
|
||||
} | {
|
||||
status: "update-install-ready";
|
||||
};
|
||||
|
||||
const appUpdaterStateInjectable = getInjectable({
|
||||
id: "app-updater-state",
|
||||
instantiate: () => observable.box<AppUpdaterState>({
|
||||
status: "idle",
|
||||
}),
|
||||
});
|
||||
|
||||
export default appUpdaterStateInjectable;
|
||||
18
src/main/app-updater/update-available.injectable.ts
Normal file
18
src/main/app-updater/update-available.injectable.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import appUpdaterStateInjectable from "./state.injectable";
|
||||
|
||||
const updateAvailableInjectable = getInjectable({
|
||||
id: "update-available",
|
||||
instantiate: (di) => {
|
||||
const appUpdaterState = di.inject(appUpdaterStateInjectable);
|
||||
|
||||
return computed(() => appUpdaterState.get().status === "update-install-ready");
|
||||
},
|
||||
});
|
||||
|
||||
export default updateAvailableInjectable;
|
||||
21
src/main/electron/use-dark-colors.injectable.ts
Normal file
21
src/main/electron/use-dark-colors.injectable.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { observable } from "mobx";
|
||||
import nativeThemeInjectable from "./native-theme.injectable";
|
||||
|
||||
const useDarkColorsInjectable = getInjectable({
|
||||
id: "use-dark-colors",
|
||||
instantiate: (di) => {
|
||||
const nativeTheme = di.inject(nativeThemeInjectable);
|
||||
const state = observable.box(nativeTheme.shouldUseDarkColors);
|
||||
|
||||
nativeTheme.on("updated", () => state.set(nativeTheme.shouldUseDarkColors));
|
||||
|
||||
return state;
|
||||
},
|
||||
});
|
||||
|
||||
export default useDarkColorsInjectable;
|
||||
67
src/main/tray/computed-tray-icon.injectable.ts
Normal file
67
src/main/tray/computed-tray-icon.injectable.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import AwaitLock from "await-lock";
|
||||
import type { NativeImage, Tray } from "electron";
|
||||
import { comparer, reaction } from "mobx";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import type { Disposer } from "../../common/utils";
|
||||
import { getOrInsertWithAsync, HashMap } from "../../common/utils";
|
||||
import updateAvailableInjectable from "../app-updater/update-available.injectable";
|
||||
import useDarkColorsInjectable from "../electron/use-dark-colors.injectable";
|
||||
import { createTrayIcon } from "./create-tray-icon";
|
||||
|
||||
export interface ComputedTrayIcon {
|
||||
getCurrent(): Promise<NativeImage>;
|
||||
subscribe(tray: Tray): Disposer;
|
||||
}
|
||||
|
||||
interface NativeImageCacheKey {
|
||||
updateAvailable: boolean;
|
||||
useDarkColors: boolean;
|
||||
}
|
||||
|
||||
const computedTrayIconInjectable = getInjectable({
|
||||
id: "computed-tray-icon",
|
||||
instantiate: (di): ComputedTrayIcon => {
|
||||
const useDarkColors = di.inject(useDarkColorsInjectable);
|
||||
const updateAvailable = di.inject(updateAvailableInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const lock = new AwaitLock();
|
||||
const cache = new HashMap<NativeImageCacheKey, NativeImage>(
|
||||
(key) => `${key.updateAvailable ? "updateAvailable" : ""}:${key.useDarkColors ? "shouldUseDarkColors" : ""}`,
|
||||
);
|
||||
|
||||
const computedCurrent = (key: NativeImageCacheKey) => getOrInsertWithAsync(cache, key, () => createTrayIcon({
|
||||
size: 16,
|
||||
...key,
|
||||
}));
|
||||
|
||||
return {
|
||||
getCurrent: () => computedCurrent({
|
||||
updateAvailable: updateAvailable.get(),
|
||||
useDarkColors: useDarkColors.get(),
|
||||
}),
|
||||
subscribe: (tray) => reaction(
|
||||
() => ({
|
||||
updateAvailable: updateAvailable.get(),
|
||||
useDarkColors: useDarkColors.get(),
|
||||
}),
|
||||
(key) => {
|
||||
lock.acquireAsync()
|
||||
.then(() => computedCurrent(key))
|
||||
.then(img => tray.setImage(img))
|
||||
.catch((error) => logger.warn("[TRAY]: failed to update image after changing state", { key, error }))
|
||||
.finally(() => lock.release());
|
||||
},
|
||||
{
|
||||
equals: (a, b) => comparer.structural(a, b),
|
||||
},
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default computedTrayIconInjectable;
|
||||
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import nativeThemeInjectable from "../electron/native-theme.injectable";
|
||||
import { createTrayIcon } from "./create-tray-icon";
|
||||
|
||||
const createCurrentTrayIconInjectable = getInjectable({
|
||||
id: "create-current-tray-icon",
|
||||
instantiate: (di) => {
|
||||
const nativeTheme = di.inject(nativeThemeInjectable);
|
||||
|
||||
return () => createTrayIcon({
|
||||
shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
|
||||
size: 16,
|
||||
updateIsAvailable: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default createCurrentTrayIconInjectable;
|
||||
@ -4,25 +4,22 @@
|
||||
*/
|
||||
import { nativeImage } from "electron";
|
||||
import type { NativeImage } from "electron";
|
||||
import { base64, getOrInsertWithAsync } from "../../common/utils";
|
||||
import { base64 } from "../../common/utils";
|
||||
import sharp from "sharp";
|
||||
import { JSDOM } from "jsdom";
|
||||
import LogoLens from "../../renderer/components/icon/logo-lens.svg";
|
||||
import Notice from "../../renderer/components/icon/notice.svg";
|
||||
|
||||
export interface CreateTrayIconArgs {
|
||||
shouldUseDarkColors: boolean;
|
||||
useDarkColors: boolean;
|
||||
size: number;
|
||||
updateIsAvailable: boolean;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
const trayIcons = new Map<boolean, NativeImage>();
|
||||
|
||||
export async function createTrayIcon({ shouldUseDarkColors, size, updateIsAvailable }: CreateTrayIconArgs): Promise<NativeImage> {
|
||||
return getOrInsertWithAsync(trayIcons, shouldUseDarkColors, async () => {
|
||||
const trayIconColor = shouldUseDarkColors ? "white" : "black"; // Invert to show contrast
|
||||
const trayBackgroundColor = shouldUseDarkColors ? "black" : "white";
|
||||
const styleTag = `
|
||||
export async function createTrayIcon({ useDarkColors, size, updateAvailable }: CreateTrayIconArgs): Promise<NativeImage> {
|
||||
const trayIconColor = useDarkColors ? "white" : "black"; // Invert to show contrast
|
||||
const trayBackgroundColor = useDarkColors ? "black" : "white";
|
||||
const styleTag = `
|
||||
<style>
|
||||
ellipse {
|
||||
stroke: ${trayIconColor} !important;
|
||||
@ -35,41 +32,40 @@ export async function createTrayIcon({ shouldUseDarkColors, size, updateIsAvaila
|
||||
</style>
|
||||
`;
|
||||
|
||||
const overlayImages: sharp.OverlayOptions[] = [];
|
||||
const parsedLogoSvg = base64.decode(LogoLens.split("base64,")[1]);
|
||||
const logoSvgRoot = new JSDOM(parsedLogoSvg).window.document.getElementsByTagName("svg")[0];
|
||||
const overlayImages: sharp.OverlayOptions[] = [];
|
||||
const parsedLogoSvg = base64.decode(LogoLens.split("base64,")[1]);
|
||||
const logoSvgRoot = new JSDOM(parsedLogoSvg).window.document.getElementsByTagName("svg")[0];
|
||||
|
||||
logoSvgRoot.innerHTML += styleTag;
|
||||
logoSvgRoot.innerHTML += styleTag;
|
||||
|
||||
if (updateIsAvailable) {
|
||||
// This adds some contrast between the notice icon and the logo
|
||||
logoSvgRoot.innerHTML += `<ellipse ry="192" rx="192" cy="352" cx="352" />`;
|
||||
if (updateAvailable) {
|
||||
// This adds some contrast between the notice icon and the logo
|
||||
logoSvgRoot.innerHTML += `<ellipse ry="192" rx="192" cy="352" cx="352" />`;
|
||||
|
||||
const parsedNoticeSvg = base64.decode(Notice.split("base64,")[1]);
|
||||
const noticeSvgRoot = new JSDOM(parsedNoticeSvg).window.document.getElementsByTagName("svg")[0];
|
||||
const parsedNoticeSvg = base64.decode(Notice.split("base64,")[1]);
|
||||
const noticeSvgRoot = new JSDOM(parsedNoticeSvg).window.document.getElementsByTagName("svg")[0];
|
||||
|
||||
noticeSvgRoot.innerHTML += styleTag;
|
||||
noticeSvgRoot.innerHTML += styleTag;
|
||||
|
||||
const noticeImage = await sharp(Buffer.from(noticeSvgRoot.outerHTML))
|
||||
.resize({
|
||||
width: Math.floor(size/1.5),
|
||||
height: Math.floor(size/1.5),
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
overlayImages.push({
|
||||
input: noticeImage,
|
||||
top: Math.floor(size/2.5),
|
||||
left: Math.floor(size/2.5),
|
||||
});
|
||||
}
|
||||
|
||||
const iconBuffer = await sharp(Buffer.from(logoSvgRoot.outerHTML))
|
||||
.composite(overlayImages)
|
||||
.resize({ width: size, height: size })
|
||||
.png()
|
||||
const noticeImage = await sharp(Buffer.from(noticeSvgRoot.outerHTML))
|
||||
.resize({
|
||||
width: Math.floor(size/1.5),
|
||||
height: Math.floor(size/1.5),
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
return nativeImage.createFromBuffer(iconBuffer);
|
||||
});
|
||||
overlayImages.push({
|
||||
input: noticeImage,
|
||||
top: Math.floor(size/2.5),
|
||||
left: Math.floor(size/2.5),
|
||||
});
|
||||
}
|
||||
|
||||
const iconBuffer = await sharp(Buffer.from(logoSvgRoot.outerHTML))
|
||||
.composite(overlayImages)
|
||||
.resize({ width: size, height: size })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
return nativeImage.createFromBuffer(iconBuffer);
|
||||
}
|
||||
|
||||
@ -19,8 +19,7 @@ import { getInjectable } from "@ogre-tools/injectable";
|
||||
import windowManagerInjectable from "../window-manager.injectable";
|
||||
import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
|
||||
import trayMenuItemsInjectable from "./tray-menu-items.injectable";
|
||||
import createCurrentTrayIconInjectable from "./create-current-tray-icon.injectable";
|
||||
import trayIconUpdaterInjectable from "./tray-icon-updater.injectable";
|
||||
import computedTrayIconInjectable from "./computed-tray-icon.injectable";
|
||||
|
||||
const initTrayInjectable = getInjectable({
|
||||
id: "init-tray",
|
||||
@ -28,11 +27,10 @@ const initTrayInjectable = getInjectable({
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
|
||||
const createCurrentTrayIcon = di.inject(createCurrentTrayIconInjectable);
|
||||
const trayIconUpdater = di.inject(trayIconUpdaterInjectable);
|
||||
const computedTrayIcon = di.inject(computedTrayIconInjectable);
|
||||
|
||||
return async (): Promise<Disposer> => {
|
||||
const tray = new Tray(await createCurrentTrayIcon());
|
||||
const tray = new Tray(await computedTrayIcon.getCurrent());
|
||||
|
||||
tray.setToolTip(packageInfo.description);
|
||||
tray.setIgnoreDoubleClickEvents(true);
|
||||
@ -46,7 +44,7 @@ const initTrayInjectable = getInjectable({
|
||||
}
|
||||
|
||||
return disposer(
|
||||
trayIconUpdater(tray),
|
||||
computedTrayIcon.subscribe(tray),
|
||||
autorun(() => {
|
||||
try {
|
||||
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences);
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Tray } from "electron";
|
||||
import type { Disposer } from "../../common/utils";
|
||||
import nativeThemeInjectable from "../electron/native-theme.injectable";
|
||||
import createCurrentTrayIconInjectable from "./create-current-tray-icon.injectable";
|
||||
|
||||
export type TrayIconUpdater = (tray: Tray) => Disposer;
|
||||
|
||||
const trayIconUpdaterInjectable = getInjectable({
|
||||
id: "tray-icon-updater",
|
||||
instantiate: (di): TrayIconUpdater => {
|
||||
const nativeTheme = di.inject(nativeThemeInjectable);
|
||||
const createCurrentTrayIcon = di.inject(createCurrentTrayIconInjectable);
|
||||
|
||||
return (tray) => {
|
||||
let prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
|
||||
const onUpdated = () => {
|
||||
if (prevShouldUseDarkColors !== nativeTheme.shouldUseDarkColors) {
|
||||
const localShouldUseDarkColors = prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
|
||||
|
||||
createCurrentTrayIcon()
|
||||
.then(img => {
|
||||
// This guards against rapid changes back and forth
|
||||
if (localShouldUseDarkColors === prevShouldUseDarkColors) {
|
||||
tray.setImage(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
nativeTheme.on("updated", onUpdated);
|
||||
|
||||
return () => {
|
||||
nativeTheme.off("updated", onUpdated);
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default trayIconUpdaterInjectable;
|
||||
Loading…
Reference in New Issue
Block a user