import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions"; import React, {useContext, useEffect, useMemo, useRef, useState} from "react"; import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; import {IpcRegistryDescription, Registry} from "tc-events"; import { IconUploadProgress, ModalIconViewerEvents, ModalIconViewerVariables, RemoteIconList } from "tc-shared/ui/modal/icon-viewer/Definitions"; import {UiVariableConsumer} from "tc-shared/ui/utils/Variable"; import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable"; import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab"; import {joinClassList} from "tc-shared/ui/react-elements/Helper"; import {Button} from "tc-shared/ui/react-elements/Button"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {getIconManager} from "tc-shared/file/Icons"; import {IconEmpty, IconError, IconLoading, IconUrl} from "tc-shared/ui/react-elements/Icon"; import {tra} from "tc-shared/i18n/localize"; import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons"; import {ClientIcon} from "svg-sprites/client-icons"; import {spawnContextMenu} from "tc-shared/ui/ContextMenu"; import {guid} from "tc-shared/crypto/uid"; import {LogCategory, logError} from "tc-shared/log"; import {ImageType, imageType2MediaType, responseImageType} from "tc-shared/file/ImageCache"; import {Crc32} from "tc-shared/crypto/crc32"; import {downloadUrl, promptFile} from "tc-shared/file/Utils"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; import {Tooltip} from "tc-shared/ui/react-elements/Tooltip"; import {ignorePromise} from "tc-shared/proto"; const cssStyle = require("./Renderer.scss"); const HandlerIdContext = React.createContext(undefined); const EventContext = React.createContext>(undefined); const VariablesContext = React.createContext>(undefined); const kLocalIconIds = [ "100", "200", "300", "500", "600" ]; const LocalIcon = React.memo((props: { iconId: string, selected: boolean }) => { const variables = useContext(VariablesContext); const events = useContext(EventContext); return (
variables.setVariable("selectedIconId", undefined, props.iconId)} onDoubleClick={() => events.fire("action_select", { targetIcon: props.iconId })} >
); }); const LocalIconTab = React.memo(() => { const variables = useContext(VariablesContext); const selectedIconId = variables.useReadOnly("selectedIconId", undefined, undefined); return (
{kLocalIconIds.map(iconId => ( ))}
Local icons are icons which are defined by your icon pack.
Everybody has the same set of local icons which only differ in their appearance.
); }); const ProgressRing = React.memo((props: { stroke, progress }) => { const radius = 100; const { stroke, progress } = props; const normalizedRadius = radius - stroke / 2; const circumference = normalizedRadius * 2 * Math.PI; const strokeDashoffset = circumference - progress / 100 * circumference; return ( ); }); const RemoteIconLiveRenderer = React.memo((props: { iconId: string, remoteIconId: number, selected: boolean }) => { const variables = useContext(VariablesContext); const handlerId = useContext(HandlerIdContext); const events = useContext(EventContext); const icon = useMemo(() => getIconManager().resolveIcon(props.remoteIconId, undefined, handlerId), [ props.remoteIconId ]); const [ , setRevision ] = useState(0); icon.events.reactUse("notify_state_changed", () => { setRevision(Date.now()); }); let iconBody, iconUrl; switch (icon.getState()) { case "destroyed": case "empty": iconBody = ( ); break; case "loading": iconBody = ( ); break; case "error": iconBody = ( ); break; case "loaded": iconUrl = icon.getImageUrl(); iconBody = ( ); break; } return (
events.fire("action_select", { targetIcon: props.iconId })} onClick={() => variables.setVariable("selectedIconId", undefined, props.iconId)} onContextMenu={event => { event.preventDefault(); variables.setVariable("selectedIconId", undefined, props.iconId); spawnContextMenu({ pageY: event.pageY, pageX: event.pageX }, [ { type: "normal", icon: ClientIcon.Download, label: tr("Download"), click: () => downloadUrl(iconUrl, "icon_" + props.remoteIconId), visible: !!iconUrl }, { type: "normal", icon: ClientIcon.Delete, label: tr("Delete"), click: () => events.fire("action_delete", { iconId: props.iconId }) } ]); }} > {iconBody}
); }); const UploadingTooltipRenderer = React.memo((props: { process: IconUploadProgress }) => { switch (props.process.state) { case "failed": return ( {props.process.message} ); case "transferring": return ( {props.process.process * 100} ); case "pre-process": return ( Preprocessing icon for upload ); case "pending": return ( Icon upload pending and will start soon ); case "initializing": return ( Icon upload initializing ); default: return ( Unknown upload state ); } }); const RemoteIconUploadingRenderer = React.memo((props: { iconId: string, selected: boolean, progress: IconUploadProgress }) => { const events = useContext(EventContext); const variables = useContext(VariablesContext); const iconBuffer = variables.useReadOnly("uploadingIconPayload", props.iconId, undefined); const iconUrl = useMemo(() => { if(!iconBuffer) { return undefined; } const imageType = responseImageType(iconBuffer); if(imageType === ImageType.UNKNOWN) { return URL.createObjectURL(new Blob([ iconBuffer ])); } else { const media = imageType2MediaType(imageType); return URL.createObjectURL(new Blob([ iconBuffer ], { type: media })); } }, [ iconBuffer ]); useEffect(() => () => URL.revokeObjectURL(iconUrl), [ iconUrl ]); let icon; if(iconUrl) { icon = ; } else if(props.progress.state === "failed") { icon = ; } else { icon = ; } let progress: number; switch (props.progress.state) { default: case "failed": progress = 100; break; case "pre-process": progress = 10; break; case "pending": progress = 20; break; case "initializing": progress = 30; break; case "transferring": progress = 30 + 70 * props.progress.process; break; } return ( }>
{ event.preventDefault(); if(props.progress.state === "failed") { spawnContextMenu({ pageX: event.pageX, pageY: event.pageY }, [ { type: "normal", label: tr("Clear failed icons"), icon: ClientIcon.Delete, click: () => events.fire("action_clear_failed") } ]) } }} >
{icon}
); }) const RemoteIcon = React.memo((props: { iconId: string, selected: boolean }) => { const variables = useContext(VariablesContext); const iconInfo = variables.useReadOnly("remoteIconInfo", props.iconId); if(iconInfo.status === "loading") { return (
); } switch (iconInfo.value.status) { case "live": return ( ); case "uploading": return ( ); case "unknown": default: return null; } }); const RemoteIconListRenderer = React.memo((props: { status: RemoteIconList }) => { const variables = useContext(VariablesContext); const selectedIconId = variables.useReadOnly("selectedIconId", undefined, undefined); switch (props.status.status) { case "loading": return (
Loading
); case "no-permission": return (
{props.status.failedPermission}
); case "loaded": return ( {props.status.icons.map(iconId => ( ))} ); case "error": default: return (
{props.status.status === "error" ? props.status.message : tr("Invalid state")}
); } }); async function doExecuteIconUpload(uploadId: string, file: File, events: Registry) { /* TODO: Upload */ if(file.size > 16 * 1024 * 1024) { throw tr("Icon file too large"); } /* Only check the type here if given else we'll check the content type later */ if(file.type) { if(!file.type.startsWith("image/")) { throw tra("Icon isn't an image ({})", file.type); } } let buffer: ArrayBuffer; try { buffer = await file.arrayBuffer(); } catch (error) { logError(LogCategory.GENERAL, tr("Failed to read icon file as array buffer: %o"), error); throw tr("Failed to read file"); } const contentType = responseImageType(buffer); switch (contentType) { case ImageType.BITMAP: case ImageType.GIF: case ImageType.JPEG: case ImageType.PNG: case ImageType.SVG: break; case ImageType.UNKNOWN: default: throw tr("File content isn't an image"); } let iconId: number; { const crc = new Crc32(); crc.update(buffer); iconId = parseInt(crc.digest(10)) >>> 0; } events.fire("action_upload", { buffer: buffer, iconId: iconId, uploadId: uploadId }); } async function executeIconUploads(files: File[], events: Registry) { const uploads = files.map<[string, File]>(file => [ guid(), file ]); for(const [ uploadId, ] of uploads) { events.fire("action_initialize_upload", { uploadId: uploadId }); } for(const [ uploadId, file ] of uploads) { await doExecuteIconUpload(uploadId, file, events).catch(error => { let message; if(typeof error === "string") { message = error; } else { logError(LogCategory.GENERAL, tr("Failed to run icon upload: %o"), error); message = tr("lookup the console"); } events.fire("action_fail_upload", { uploadId: uploadId, message: message }); }); } } const ButtonDelete = React.memo(() => { return ( ); }); const ButtonRefresh = React.memo(() => { const events = useContext(EventContext); const variables = useContext(VariablesContext); const iconIds = variables.useReadOnly("remoteIconList", undefined, { status: "loading" }); const [ renderTimestamp, setRenderTimestamp ] = useState(Date.now()); useEffect(() => { if(iconIds.status === "loading") { return; } if(renderTimestamp >= iconIds.refreshTimestamp) { return; } const id = setTimeout(() => setRenderTimestamp(Date.now()), iconIds.refreshTimestamp - Date.now()); return () => clearTimeout(id); }, [ iconIds ]); return ( ); }); const RemoteIconTab = React.memo(() => { const events = useContext(EventContext); const variables = useContext(VariablesContext); const iconIds = variables.useReadOnly("remoteIconList", undefined, { status: "loading" }); const [ dragOverCount, setDragOverCount ] = useState(0); const dragOverTimeout = useRef(0); return (
{ clearTimeout(dragOverTimeout.current); dragOverTimeout.current = 0; const files = [...event.dataTransfer.items].filter(item => item.kind === "file"); if(files.length === 0) { /* No files */ return; } /* Allow drop */ event.preventDefault(); setDragOverCount(files.length); }} onDragLeave={() => { if(dragOverTimeout.current) { clearTimeout(dragOverTimeout.current); return; } dragOverTimeout.current = setTimeout(() => setDragOverCount(0), 250); }} onDrop={event => { event.preventDefault(); clearTimeout(dragOverTimeout.current); dragOverTimeout.current = 0; setDragOverCount(0); const files = [...event.dataTransfer.items] .filter(item => item.kind === "file") .map(file => file.getAsFile()) .filter(file => !!file); if(files.length === 0) { /* No files */ return; } ignorePromise(executeIconUploads(files, events)); }} >
{dragOverCount}
); }); const IconTabs = React.memo(() => { const variables = useContext(VariablesContext); const selectedTab = variables.useVariable("selectedTab", undefined, "remote"); return ( selectedTab.setValue(newTab as any)} className={cssStyle.tab} bodyClassName={cssStyle.tabBody} > Remote Local ); }); const SelectButtons = React.memo(() => { const events = useContext(EventContext); const variables = useContext(VariablesContext); const selectedIcon = variables.useReadOnly("selectedIconId", undefined, undefined); return (
); }); class Modal extends AbstractModal { private readonly handlerId: string; private readonly events: Registry; private readonly variables: UiVariableConsumer; private readonly isIconSelect: boolean; constructor(handlerId: string, events: IpcRegistryDescription, variables: IpcVariableDescriptor, isIconSelect: boolean) { super(); this.handlerId = handlerId; this.events = Registry.fromIpcDescription(events); this.variables = createIpcUiVariableConsumer(variables); this.isIconSelect = isIconSelect; this.events.on("notify_delete_error", event => { switch (event.status) { case "no-permissions": createErrorModal(tr("Failed to delete icon"), tra("Failed to delete icon (No permissions):\n{}", event.failedPermission)).open(); break; case "not-found": createErrorModal(tr("Failed to delete icon"), tra("Failed to delete icon (Icon not found)")).open(); break; case "error": default: createErrorModal(tr("Failed to delete icon"), tra("Failed to delete icon:\n{}", event.message || tr("Unknown/invalid error status"))).open(); break; } }); } protected onDestroy() { super.onDestroy(); this.events.destroy(); this.variables.destroy(); } renderBody(): React.ReactElement { return (
{this.isIconSelect ? ( ) : undefined}
); } renderTitle(): React.ReactNode { return ( Icon manager ); } } export default Modal;