195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
import * as electron from "electron";
|
|
import {NativeImage} from "electron";
|
|
import * as loader from "tc-loader";
|
|
import {Stage} from "tc-loader";
|
|
|
|
import {
|
|
ClientIcon,
|
|
spriteEntries as kClientSpriteEntries,
|
|
spriteHeight as kClientSpriteHeight,
|
|
spriteUrl as kClientSpriteUrl,
|
|
spriteWidth as kClientSpriteWidth
|
|
} from "svg-sprites/client-icons";
|
|
import {RemoteIcon} from "tc-shared/file/Icons";
|
|
import {LogCategory, logError} from "tc-shared/log";
|
|
|
|
let nativeSprite: NativeImage;
|
|
|
|
export function clientIconClassToImage(klass: string) : NativeImage {
|
|
const sprite = kClientSpriteEntries.find(e => e.className === klass);
|
|
if(!sprite) return undefined;
|
|
|
|
return nativeSprite.crop({
|
|
height: sprite.height,
|
|
width: sprite.width,
|
|
x: sprite.xOffset,
|
|
y: sprite.yOffset
|
|
});
|
|
}
|
|
|
|
export class RemoteIconDatafier {
|
|
private cachedIcons: {[key: string]:{ refCount: number, icon: RemoteIconWrapper }} = {};
|
|
private cleanupTimer;
|
|
|
|
constructor() { }
|
|
|
|
destroy() {
|
|
clearTimeout(this.cleanupTimer);
|
|
}
|
|
|
|
resolveIcon(icon: RemoteIcon) : RemoteIconWrapper {
|
|
const uniqueId = icon.iconId + "-" + icon.serverUniqueId;
|
|
if(!this.cachedIcons[uniqueId]) {
|
|
this.cachedIcons[uniqueId] = {
|
|
refCount: 0,
|
|
icon: new RemoteIconWrapper(uniqueId, icon)
|
|
}
|
|
}
|
|
|
|
const cache = this.cachedIcons[uniqueId];
|
|
cache.refCount++;
|
|
return cache.icon;
|
|
}
|
|
|
|
unrefIcon(icon: RemoteIconWrapper) {
|
|
const cache = this.cachedIcons[icon.uniqueId];
|
|
if(!cache) { return; }
|
|
|
|
cache.refCount--;
|
|
if(cache.refCount <= 0) {
|
|
if(this.cleanupTimer) {
|
|
clearTimeout(this.cleanupTimer);
|
|
}
|
|
this.cleanupTimer = setTimeout(() => this.cleanupIcons(), 10 * 1000);
|
|
}
|
|
}
|
|
|
|
private cleanupIcons() {
|
|
this.cleanupTimer = undefined;
|
|
for(const key of Object.keys(this.cachedIcons)) {
|
|
if(this.cachedIcons[key].refCount <= 0) {
|
|
this.cachedIcons[key].icon.destroy();
|
|
delete this.cachedIcons[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
export const remoteIconDatafier = new RemoteIconDatafier();
|
|
|
|
export class RemoteIconWrapper {
|
|
readonly callbackUpdated: ((newUrl: string) => void)[] = [];
|
|
readonly uniqueId: string;
|
|
|
|
private readonly icon: RemoteIcon;
|
|
private readonly callbackStateChanged: () => void;
|
|
private dataUrl: string | undefined;
|
|
private currentImageUrl: string;
|
|
|
|
constructor(uniqueId: string, icon: RemoteIcon) {
|
|
this.icon = icon;
|
|
this.uniqueId = uniqueId;
|
|
this.callbackStateChanged = this.handleIconStateChanged.bind(this);
|
|
|
|
this.icon.events.on("notify_state_changed", this.callbackStateChanged);
|
|
this.handleIconStateChanged();
|
|
}
|
|
|
|
destroy() {
|
|
this.icon.events.off("notify_state_changed", this.callbackStateChanged);
|
|
this.currentImageUrl = undefined;
|
|
}
|
|
|
|
getDataUrl() : string | undefined { return this.dataUrl; }
|
|
|
|
onDataUrlChange(callback: (newUrl: string) => void) : () => void {
|
|
this.callbackUpdated.push(callback);
|
|
return () => {
|
|
const index = this.callbackUpdated.indexOf(callback);
|
|
if(index !== -1) { this.callbackUpdated.splice(index, 1); }
|
|
}
|
|
}
|
|
|
|
private async handleIconStateChanged() {
|
|
if(this.icon.getState() === "loaded") {
|
|
let imageUrl, dataUrl;
|
|
try {
|
|
if(this.icon.iconId >= 0 && this.icon.iconId <= 1000) {
|
|
imageUrl = "local-" + this.icon.iconId;
|
|
dataUrl = clientIconClassToImage(ClientIcon["Group_" + this.icon.iconId]).toDataURL();
|
|
} else {
|
|
imageUrl = this.icon.getImageUrl();
|
|
this.currentImageUrl = imageUrl;
|
|
|
|
const image = new Image();
|
|
image.src = imageUrl;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
image.onload = resolve;
|
|
image.onerror = reject;
|
|
});
|
|
|
|
if(this.currentImageUrl !== imageUrl) { return; }
|
|
|
|
const canvas = document.createElement("canvas");
|
|
if(image.naturalWidth > 1000 || image.naturalHeight > 1000) {
|
|
throw "image dimensions are too large";
|
|
} else {
|
|
canvas.width = image.naturalWidth;
|
|
canvas.height = image.naturalHeight;
|
|
}
|
|
|
|
canvas.getContext("2d").drawImage(image, 0, 0);
|
|
dataUrl = canvas.toDataURL();
|
|
|
|
/* We need to reset the current image URL in order to fire a changed event */
|
|
this.currentImageUrl = undefined;
|
|
}
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to render remote icon %s-%d: %o"), this.icon.serverUniqueId, this.icon.iconId, error);
|
|
|
|
imageUrl = "--error--" + Date.now();
|
|
dataUrl = clientIconClassToImage(ClientIcon.Error).toDataURL();
|
|
}
|
|
|
|
this.setDataUrl(imageUrl, dataUrl);
|
|
} else if(this.icon.getState() === "error") {
|
|
this.setDataUrl(undefined, clientIconClassToImage(ClientIcon.Error).toDataURL());
|
|
} else {
|
|
this.setDataUrl(undefined, undefined);
|
|
}
|
|
}
|
|
|
|
private setDataUrl(sourceImageUrl: string | undefined, dataUrl: string) {
|
|
if(sourceImageUrl && this.currentImageUrl === sourceImageUrl) { return; }
|
|
this.currentImageUrl = undefined; /* no image is loading any more */
|
|
|
|
if(this.dataUrl === dataUrl) { return; }
|
|
|
|
this.dataUrl = dataUrl;
|
|
this.callbackUpdated.forEach(callback => callback(dataUrl));
|
|
}
|
|
}
|
|
|
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|
priority: 100,
|
|
name: "native icon sprite loader",
|
|
function: async () => {
|
|
const image = new Image();
|
|
image.src = loader.config.baseUrl + kClientSpriteUrl;
|
|
await new Promise((resolve, reject) => {
|
|
image.onload = resolve;
|
|
image.onerror = () => reject("failed to load client icon sprite");
|
|
});
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = kClientSpriteWidth;
|
|
canvas.height = kClientSpriteHeight;
|
|
canvas.getContext("2d").drawImage(image, 0, 0);
|
|
|
|
nativeSprite = electron.remote.nativeImage.createFromDataURL(canvas.toDataURL());
|
|
}
|
|
})
|
|
|
|
export function finalize() {
|
|
nativeSprite = undefined;
|
|
} |