369 lines
15 KiB
TypeScript
369 lines
15 KiB
TypeScript
import * as loader from "tc-loader";
|
|
import {Stage} from "tc-loader";
|
|
import {ImageCache, ImageType, imageType2MediaType, responseImageType} from "tc-shared/file/ImageCache";
|
|
import {AbstractIconManager, kIPCIconChannel, RemoteIcon, RemoteIconState, setIconManager} from "tc-shared/file/Icons";
|
|
import {LogCategory, logDebug, logError, logWarn} from "tc-shared/log";
|
|
import {server_connections} from "tc-shared/ConnectionManager";
|
|
import {ConnectionEvents, ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
|
import {FileTransferState, ResponseTransferTarget, TransferProvider, TransferTargetType} from "tc-shared/file/Transfer";
|
|
import {tr} from "tc-shared/i18n/localize";
|
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
|
import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
|
import * as ipc from "tc-shared/ipc/BrowserIPC";
|
|
|
|
/* TODO: Retry icon download after some time */
|
|
/* TODO: Download icon when we're connected to the server were we want the icon from and update the icon */
|
|
|
|
async function responseToImageUrl(response: Response) : Promise<string> {
|
|
if(!response.headers.has('X-media-bytes')) {
|
|
throw "missing media bytes";
|
|
}
|
|
|
|
const type = responseImageType(response.headers.get('X-media-bytes'));
|
|
if(type === ImageType.UNKNOWN) {
|
|
throw "unknown image type";
|
|
}
|
|
|
|
const media = imageType2MediaType(type);
|
|
const blob = await response.blob();
|
|
if(blob.type !== "image/" + media) {
|
|
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
|
|
} else {
|
|
return URL.createObjectURL(blob)
|
|
}
|
|
}
|
|
|
|
class LocalRemoteIcon extends RemoteIcon {
|
|
constructor(serverUniqueId: string, iconId: number) {
|
|
super(serverUniqueId, iconId);
|
|
}
|
|
|
|
destroy() {
|
|
super.destroy();
|
|
if(this.imageUrl && "revokeObjectURL" in URL) {
|
|
URL.revokeObjectURL(this.imageUrl);
|
|
}
|
|
}
|
|
|
|
public setImageUrl(url: string) {
|
|
super.setImageUrl(url);
|
|
}
|
|
|
|
public setErrorMessage(message: string) {
|
|
super.setErrorMessage(message);
|
|
}
|
|
|
|
public setState(state: RemoteIconState) {
|
|
super.setState(state);
|
|
}
|
|
}
|
|
|
|
export let localIconCache: ImageCache;
|
|
class IconManager extends AbstractIconManager {
|
|
private cachedIcons: {[key: string]: LocalRemoteIcon} = {};
|
|
private connectionStateChangeListener: {[key: string]: (handlerId: string, event: ConnectionEvents["notify_connection_state_changed"]) => void} = {};
|
|
private ipcChannel: IPCChannel;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.ipcChannel = ipc.getIpcInstance().createChannel(kIPCIconChannel);
|
|
this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this);
|
|
|
|
server_connections.events().on("notify_handler_created", event => {
|
|
this.connectionStateChangeListener[event.handlerId] = this.handleHandlerStateChange.bind(this, event.handlerId);
|
|
event.handler.events().on("notify_connection_state_changed", this.connectionStateChangeListener[event.handlerId] as any);
|
|
});
|
|
|
|
server_connections.events().on("notify_handler_deleted", event => {
|
|
if(this.connectionStateChangeListener[event.handlerId]) {
|
|
event.handler.events().off("notify_connection_state_changed", this.connectionStateChangeListener[event.handlerId] as any);
|
|
delete this.connectionStateChangeListener[event.handlerId];
|
|
}
|
|
});
|
|
}
|
|
|
|
destroy() {
|
|
Object.values(this.cachedIcons).forEach(icon => icon.destroy());
|
|
this.cachedIcons = {};
|
|
|
|
/* TODO: Unregister server handler events */
|
|
}
|
|
|
|
private handleHandlerStateChange(handlerId: string, event: ConnectionEvents["notify_connection_state_changed"]) {
|
|
const connection = server_connections.findConnection(handlerId);
|
|
if(!connection) {
|
|
logWarn(LogCategory.CLIENT, tr("Received handler state changed event for invalid handler id %s"), handlerId);
|
|
return;
|
|
}
|
|
|
|
if(event.newState !== ConnectionState.CONNECTED) {
|
|
return;
|
|
}
|
|
|
|
/* update all empty icons */
|
|
Object.values(this.cachedIcons).forEach((icon: LocalRemoteIcon) => {
|
|
if(icon.serverUniqueId !== connection.getCurrentServerUniqueId()) {
|
|
return;
|
|
}
|
|
|
|
if(icon.getState() === "empty") {
|
|
this.wrapIconDownload(icon, connection, IconManager.iconUniqueKey(icon.iconId, icon.serverUniqueId)).then(() => {});
|
|
}
|
|
});
|
|
}
|
|
|
|
private handleIconStateChanged(icon: RemoteIcon) {
|
|
this.sendIconStateChange(icon);
|
|
}
|
|
|
|
private sendIconStateChange(icon: RemoteIcon, remoteId?: string) {
|
|
let data = {} as any;
|
|
|
|
data.iconUniqueId = IconManager.iconUniqueKey(icon.iconId, icon.serverUniqueId);
|
|
data.status = icon.getState();
|
|
|
|
switch (icon.getState()) {
|
|
case "loaded":
|
|
data.url = icon.hasImageUrl() ? icon.getImageUrl() : undefined;
|
|
break;
|
|
|
|
case "error":
|
|
data.errorMessage = icon.getErrorMessage();
|
|
break;
|
|
}
|
|
|
|
this.ipcChannel.sendMessage("notify-icon-status", data, remoteId);
|
|
}
|
|
|
|
|
|
private handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
|
if(broadcast) { return; }
|
|
if(message.type === "initialize") {
|
|
this.ipcChannel.sendMessage("initialized", {}, remoteId);
|
|
return;
|
|
} else if(message.type === "icon-resolve") {
|
|
this.sendIconStateChange(this.resolveIcon(message.data.iconId, message.data.serverUniqueId, message.data.handlerId), remoteId);
|
|
}
|
|
}
|
|
|
|
resolveIcon(iconId: number, serverUniqueId: string, handlerIdHint: string): RemoteIcon {
|
|
serverUniqueId = serverUniqueId || "";
|
|
|
|
/* just to ensure */
|
|
iconId = iconId >>> 0;
|
|
|
|
const iconUniqueId = IconManager.iconUniqueKey(iconId, serverUniqueId);
|
|
if(this.cachedIcons[iconUniqueId]) {
|
|
return this.cachedIcons[iconUniqueId];
|
|
}
|
|
|
|
let icon = new LocalRemoteIcon(serverUniqueId, iconId);
|
|
this.cachedIcons[iconUniqueId] = icon;
|
|
|
|
icon.events.on("notify_state_changed", () => this.handleIconStateChanged(icon));
|
|
|
|
if(iconId >= 0 && iconId <= 1000) {
|
|
icon.setState("loaded");
|
|
} else {
|
|
this.loadIcon(icon, iconUniqueId, handlerIdHint).catch(error => {
|
|
if(typeof error !== "string") {
|
|
logError(LogCategory.FILE_TRANSFER, tr("Failed to load icon %d (%s): %o"), iconId, serverUniqueId, error);
|
|
icon.setErrorMessage(tr("load error, lookup the console"));
|
|
} else{
|
|
icon.setErrorMessage(error);
|
|
icon.setState("error");
|
|
}
|
|
});
|
|
}
|
|
|
|
return icon;
|
|
}
|
|
|
|
private async loadIcon(icon: LocalRemoteIcon, iconUniqueId: string, handlerIdHint: string) {
|
|
/* try to load the icon from the local cache */
|
|
const localCache = await localIconCache.resolveCached(iconUniqueId);
|
|
if(localCache) {
|
|
try {
|
|
const url = await responseToImageUrl(localCache);
|
|
icon.setImageUrl(url);
|
|
icon.setState("loaded");
|
|
logDebug(LogCategory.FILE_TRANSFER, tr("Loaded icon %d (%s) from local cache."), icon.iconId, icon.serverUniqueId);
|
|
return;
|
|
} catch (error) {
|
|
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to decode locally cached icon %d (%s): %o. Invalidating cache."), icon.iconId, icon.serverUniqueId, error);
|
|
try {
|
|
await localIconCache.delete(iconUniqueId);
|
|
} catch (error) {
|
|
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to delete invalid key from icon cache (%s): %o"), iconUniqueId, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* try to fetch the icon from the server, if we're connected */
|
|
let handler = server_connections.findConnection(handlerIdHint);
|
|
if(handler) {
|
|
if(!handler.connected) {
|
|
logWarn(LogCategory.FILE_TRANSFER, tr("Received handler id hint for icon download, but handler %s is not connected. Trying others."), handlerIdHint);
|
|
handler = undefined;
|
|
} else if(icon.serverUniqueId && handler.channelTree.server.properties.virtualserver_unique_identifier !== icon.serverUniqueId) {
|
|
logWarn(LogCategory.FILE_TRANSFER,
|
|
tr("Received handler id hint for icon download, but handler %s is not connected to the expected server (%s <=> %s). Trying others."),
|
|
handlerIdHint, handler.channelTree.server.properties.virtualserver_unique_identifier, icon.serverUniqueId);
|
|
handler = undefined;
|
|
} else {
|
|
logDebug(LogCategory.FILE_TRANSFER, tr("Icon %s (%s) not found locally but the suggested handler is connected to the server. Downloading icon."), icon.iconId, icon.serverUniqueId);
|
|
}
|
|
|
|
/* we don't want any "handler not found" warning */
|
|
handlerIdHint = undefined;
|
|
}
|
|
|
|
if(!handler) {
|
|
if(handlerIdHint) {
|
|
logWarn(LogCategory.FILE_TRANSFER, tr("Received handler id hint for icon download, but handler %s does not exists. Trying others."), handlerIdHint);
|
|
}
|
|
|
|
const connections = server_connections.getAllConnectionHandlers()
|
|
.filter(handler => handler.connected)
|
|
.filter(handler => handler.channelTree.server.properties.virtualserver_unique_identifier === icon.serverUniqueId);
|
|
|
|
if(connections.length === 0) {
|
|
logDebug(LogCategory.FILE_TRANSFER, tr("Icon %s (%s) not found locally and we're currently not connected to the target server. Returning an empty result."), icon.iconId, icon.serverUniqueId);
|
|
icon.setState("empty");
|
|
return;
|
|
}
|
|
|
|
logDebug(LogCategory.FILE_TRANSFER, tr("Icon %s (%s) not found locally but we're connected to the server, using first available connection (%s). Downloading icon."), icon.iconId, icon.serverUniqueId, connections[0].handlerId);
|
|
handler = connections[0];
|
|
}
|
|
|
|
await this.wrapIconDownload(icon, handler, iconUniqueId);
|
|
}
|
|
|
|
private async wrapIconDownload(icon: LocalRemoteIcon, handler: ConnectionHandler, iconUniqueId: string) {
|
|
try {
|
|
await this.downloadIcon(icon, handler, iconUniqueId);
|
|
} catch (error) {
|
|
if(typeof error !== "string") {
|
|
logError(LogCategory.FILE_TRANSFER, tr("Failed to download icon %d (%s) from %s: %o"), icon.iconId, icon.serverUniqueId, handler.handlerId, error);
|
|
error = tr("download failed, lookup the console");
|
|
}
|
|
|
|
icon.setErrorMessage(error);
|
|
icon.setState("error");
|
|
return;
|
|
}
|
|
|
|
if(icon.getState() === "loading") {
|
|
icon.setErrorMessage(tr("unexpected loading state"));
|
|
icon.setState("error");
|
|
}
|
|
}
|
|
|
|
private async downloadIcon(icon: LocalRemoteIcon, handler: ConnectionHandler, iconUniqueId: string) {
|
|
const transfer = handler.fileManager.initializeFileDownload({
|
|
path: "",
|
|
name: "/icon_" + icon.iconId,
|
|
targetSupplier: async () => await TransferProvider.provider().createResponseTarget()
|
|
});
|
|
|
|
try {
|
|
await transfer.awaitFinished();
|
|
|
|
if(transfer.transferState() === FileTransferState.CANCELED) {
|
|
throw tr("download canceled");
|
|
} else if(transfer.transferState() === FileTransferState.ERRORED) {
|
|
throw transfer.currentError();
|
|
} else if(transfer.transferState() === FileTransferState.FINISHED) {
|
|
|
|
} else {
|
|
throw tr("Unknown transfer finished state");
|
|
}
|
|
} catch(error) {
|
|
if(error instanceof CommandResult) {
|
|
if(error.id === ErrorCode.FILE_NOT_FOUND) {
|
|
throw tr("Icon could not be found");
|
|
} else if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
|
throw tr("No permissions to download icon");
|
|
} else {
|
|
throw error.extra_message || error.message;
|
|
}
|
|
}
|
|
logError(LogCategory.FILE_TRANSFER, tr("Could not request download for icon %d: %o"), icon.iconId, error);
|
|
if(error === transfer.currentError()) {
|
|
throw transfer.currentErrorMessage();
|
|
}
|
|
|
|
throw typeof error === "string" ? error : tr("Failed to initialize icon download");
|
|
}
|
|
|
|
/* could only be tested here, because before we don't know which target we have */
|
|
if(transfer.target.type !== TransferTargetType.RESPONSE) {
|
|
throw "unsupported transfer target";
|
|
}
|
|
|
|
const response = transfer.target as ResponseTransferTarget;
|
|
if(!response.hasResponse()) {
|
|
throw tr("Transfer has no response");
|
|
}
|
|
|
|
try {
|
|
const url = await responseToImageUrl(response.getResponse().clone());
|
|
icon.setImageUrl(url);
|
|
icon.setState("loaded");
|
|
} catch (error) {
|
|
if(typeof error !== "string") {
|
|
logError(LogCategory.FILE_TRANSFER, tr("Failed to convert downloaded icon %d (%s) into an url: %o"), icon.iconId, icon.serverUniqueId, error);
|
|
error = tr("download failed, lookup the console");
|
|
}
|
|
|
|
icon.setErrorMessage(error);
|
|
icon.setState("error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resp = response.getResponse();
|
|
if(!resp.headers.has('X-media-bytes')) {
|
|
throw "missing media bytes";
|
|
}
|
|
|
|
const type = responseImageType(resp.headers.get('X-media-bytes'));
|
|
if(type === ImageType.UNKNOWN) {
|
|
throw "unknown image type";
|
|
}
|
|
|
|
const media = imageType2MediaType(type);
|
|
await localIconCache.putCache(iconUniqueId, response.getResponse(), "image/" + media);
|
|
} catch (error) {
|
|
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to save icon %s (%s) into local icon cache: %o"), icon.iconId, icon.serverUniqueId, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|
name: "icon init",
|
|
priority: 60,
|
|
function: async () => {
|
|
localIconCache = await ImageCache.load("icons");
|
|
setIconManager(new IconManager());
|
|
}
|
|
});
|
|
|
|
//TODO!
|
|
/*
|
|
window.addEventListener("beforeunload", () => {
|
|
icon_cache_loader.clear_memory_cache();
|
|
});
|
|
|
|
(window as any).flush_icon_cache = async () => {
|
|
icon_cache_loader.clear_memory_cache();
|
|
await icon_cache_loader.clear_cache();
|
|
|
|
server_connections.all_connections().forEach(e => {
|
|
e.fileManager.icons.flush_cache();
|
|
});
|
|
};
|
|
*/ |