diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index 8c461c9d..b9fba89b 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -696,12 +696,12 @@ export const webRoutes = [ "/instances/:id/view", "/mods", "/mods/mod-packs/:id/view", + "/mods/mod-packs/:id/icon-reference", "/users", "/users/:id/view", "/roles", "/roles/:id/view", "/plugins", "/plugins/:name/view", - "/icon-reference", "/error", ]; diff --git a/packages/web_ui/src/components/FactorioIcon.tsx b/packages/web_ui/src/components/FactorioIcon.tsx new file mode 100644 index 00000000..a7cd1487 --- /dev/null +++ b/packages/web_ui/src/components/FactorioIcon.tsx @@ -0,0 +1,11 @@ +import { PrototypeMetadataEntry } from "../store/export_prototype_metadata_store"; + +export default function FactorioIcon(props: { modPackId: number | undefined, prototype?: PrototypeMetadataEntry }) { + return ; +} diff --git a/packages/web_ui/src/components/IconReferencePage.tsx b/packages/web_ui/src/components/IconReferencePage.tsx index 800bfb0c..de5263b0 100644 --- a/packages/web_ui/src/components/IconReferencePage.tsx +++ b/packages/web_ui/src/components/IconReferencePage.tsx @@ -1,117 +1,56 @@ -import React, { useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Card, Input, Space, Table, Tag, Typography } from "antd"; -import { - useItemMetadata, - useRecipeMetadata, - useVirtualSignalMetadata, - useTechnologyMetadata, - useSpaceLocationMetadata, - useQualityMetadata, - useEntityMetadata, - useFluidMetadata, -} from "../model/item_metadata"; -import { useExportManifest } from "../model/export_manifest"; +import { useExportPrototypeMetadata } from "../model/export_prototype_metadata"; import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; +import FactorioIcon from "./FactorioIcon"; +import { useParams } from "react-router-dom"; +import { useModPack } from "../model/mod_pack"; +import { PrototypeMetadataEntry } from "../store/export_prototype_metadata_store"; const { Text } = Typography; -type IconEntry = { - key: string; - category: string; - name: string; - cssClass: string; - path?: string; -}; +function filterPrototypes( + prototypes: Map> | undefined, + filter: string +) { + const query = filter.trim().toLowerCase(); + return new Map( + [...prototypes ?? []].map(([baseType, entries]) => [ + baseType, + new Map([...entries.entries()].filter(([name, entry]) => ( + `${entry.base_type}.${name}`.includes(query) + || `${entry.type}.${name}`.includes(query) + ))), + ]), + ); +} -const CATEGORIES = [ - "item", "fluid", "recipe", "virtual-signal", "technology", "space-location", "quality", "entity", -] as const; - -type Category = typeof CATEGORIES[number]; - -const CATEGORY_LABELS: Record = { - item: "Item", - fluid: "Fluid", - recipe: "Recipe", - "virtual-signal": "Virtual Signal", - technology: "Technology", - "space-location": "Space Location", - quality: "Quality", - entity: "Entity", -}; - -function useAllIconEntries() { - const item = useItemMetadata(); - const fluid = useFluidMetadata(); - const recipe = useRecipeMetadata(); - const virtualSignal = useVirtualSignalMetadata(); - const technology = useTechnologyMetadata(); - const spaceLocation = useSpaceLocationMetadata(); - const quality = useQualityMetadata(); - const entity = useEntityMetadata(); - - const byCategory: Record> = { - item, - fluid, - recipe, - "virtual-signal": virtualSignal, - technology, - "space-location": spaceLocation, - quality, - entity, - }; - - return useMemo(() => { - const entries: IconEntry[] = []; - - for (const category of CATEGORIES) { - const names = [...byCategory[category].keys()].sort(); - for (const name of names) { - const meta = byCategory[category].get(name)!; - entries.push({ - key: `${category}:${name}`, - category, - name, - cssClass: `${category}-${CSS.escape(name)}`, - path: meta.icon?.path, - }); - } - } - - return entries; - }, [item, fluid, recipe, virtualSignal, technology, spaceLocation, quality, entity]); +function countPrototypes( + prototypes: Map> | undefined, +) { + return prototypes ? [...prototypes.values()].reduce((a, e) => a + e.size, 0) : 0; } export default function IconReferencePage() { + let params = useParams(); + let modPackId = Number(params.id); + const [filter, setFilter] = useState(""); const [expandedGroups, setExpandedGroups] = useState>(new Set()); - const allEntries = useAllIconEntries(); + const [modPack] = useModPack(modPackId); + const manifest = modPack?.exportManifest; + const prototypes = useExportPrototypeMetadata(modPack); + const filtered = useMemo(() => filterPrototypes(prototypes, filter), [prototypes, filter]); + const groups = useMemo(() => ( + [...filtered] + .map(([baseType, entries]) => ({ baseType, entries: [...entries.values()] })) + .filter(group => group.entries.length) + ), [filtered]); - const filtered = useMemo(() => { - const q = filter.trim().toLowerCase(); - if (!q) { - return allEntries; - } - return allEntries.filter(e => e.name.toLowerCase().includes(q) || e.cssClass.toLowerCase().includes(q) - ); - }, [allEntries, filter]); - - const groups = useMemo(() => { - const map = new Map(); - for (const cat of CATEGORIES) { - map.set(cat, []); - } - for (const entry of filtered) { - map.get(entry.category)!.push(entry); - } - return CATEGORIES.map(cat => ({ category: cat, entries: map.get(cat)! })) - .filter(g => g.entries.length > 0); - }, [filtered]); - - const visibleCount = filtered.length; - const totalCount = allEntries.length; + const visibleCount = countPrototypes(filtered); + const totalCount = countPrototypes(prototypes); const autoExpand = visibleCount <= 20; const isExpanded = (category: string) => autoExpand || expandedGroups.has(category); @@ -131,11 +70,10 @@ export default function IconReferencePage() { const columns = [ { title: "Icon", - dataIndex: "cssClass", key: "icon", width: 48, - render: (_: string, record: IconEntry) => ( -
+ render: (prototype: PrototypeMetadataEntry) => ( + ), }, { @@ -146,19 +84,9 @@ export default function IconReferencePage() { {name} ), }, - { - title: "Usage", - dataIndex: "cssClass", - key: "usage", - render: (cssClass: string) => ( - - {`
`} - - ), - }, { title: "Path", - dataIndex: "path", + dataIndex: ["icon", "path"], key: "path", render: (p: string | undefined) => (p ? {p} @@ -166,13 +94,17 @@ export default function IconReferencePage() { }, ]; - const manifest = useExportManifest(); - + let nav = [ + { name: "Mods", path: "/mods" }, + { name: "Mod Packs" }, + { name: modPack?.name ?? String(modPackId), path: `/mods/mod-packs/${modPackId}/view` }, + { name: "Icon Reference" }, + ]; return ( - +
- {!manifest && ( + {!modPack?.exportManifest && ( No export data available. Run export-data on an instance to generate icons. @@ -188,7 +120,7 @@ export default function IconReferencePage() {
)}
- Icons: + Prototypes: {totalCount}
@@ -196,7 +128,7 @@ export default function IconReferencePage() { setFilter(e.target.value)} allowClear @@ -208,13 +140,12 @@ export default function IconReferencePage() { : `${visibleCount} / ${totalCount} icons`}
- {groups.map(({ category, entries }) => { - const label = CATEGORY_LABELS[category as Category] ?? category; - const expanded = isExpanded(category); + {groups.map(({ baseType, entries }) => { + const expanded = isExpanded(baseType); return ( -
+
toggleGroup(category)} + onClick={() => toggleGroup(baseType)} style={{ display: "flex", alignItems: "center", @@ -230,13 +161,13 @@ export default function IconReferencePage() { {expanded ? "▾" : "►"} - {label} + {baseType} {entries.length}
{expanded && ( - + columns={columns} dataSource={entries} rowKey="key" diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index 8cccaf24..caea5202 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -27,6 +27,7 @@ import PageLayout from "./PageLayout"; import SectionHeader from "./SectionHeader"; import ModDetails from "./ModDetails"; import InputPartialVersion from "./InputPartialVersion"; +import Link from "./Link"; const { logger } = lib; const { Text } = Typography; @@ -1334,6 +1335,12 @@ export default function ModPackViewPage() { prototypes={prototypes} locale={locale} /> + + Inspect exported data + + + Icon reference +
void) => exportLocaleStore.subscribe(modPack?.id, localePath, callback), + [modPack?.id, localePath], + ); + return useSyncExternalStore(subscribe, () => exportLocaleStore.getSnapshot(modPack?.id)); +} diff --git a/packages/web_ui/src/model/export_manifest.ts b/packages/web_ui/src/model/export_manifest.ts deleted file mode 100644 index 5ec39b28..00000000 --- a/packages/web_ui/src/model/export_manifest.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useContext, useState } from "react"; - -import * as lib from "@clusterio/lib"; -import ControlContext from "../components/ControlContext"; - -let exportManifestCache: lib.ExportManifest|null = null; -let loaded = false; -export function useExportManifest(): lib.ExportManifest|null { - const control = useContext(ControlContext); - let [exportManifest, setExportManifest] = useState(exportManifestCache); - useEffect(() => { - async function load() { - let modPack = await control.send(new lib.ModPackGetDefaultRequest()); - if (modPack.exportManifest) { - exportManifestCache = modPack.exportManifest; - setExportManifest(exportManifestCache); - } - loaded = true; - } - - if (!loaded) { - load(); - } - }, []); - - return exportManifest; -} diff --git a/packages/web_ui/src/model/export_prototype_metadata.ts b/packages/web_ui/src/model/export_prototype_metadata.ts new file mode 100644 index 00000000..3b1e1fe1 --- /dev/null +++ b/packages/web_ui/src/model/export_prototype_metadata.ts @@ -0,0 +1,17 @@ +import { useCallback, useSyncExternalStore } from "react"; + +import { ModPack } from "@clusterio/lib"; +import { exportPrototypeMetadataStore } from "../store/export_prototype_metadata_store"; + + +/** + * Load the unified spritesheet metadata and inject CSS rules for every icon. + * Required for {@link FactorioIcon}. + */ +export function useExportPrototypeMetadata(modPack: ModPack | undefined) { + const subscribe = useCallback( + (callback: () => void) => exportPrototypeMetadataStore.subscribe(modPack, callback), + [modPack] + ); + return useSyncExternalStore(subscribe, () => exportPrototypeMetadataStore.getSnapshot(modPack?.id)); +} diff --git a/packages/web_ui/src/model/item_metadata.ts b/packages/web_ui/src/model/item_metadata.ts deleted file mode 100644 index e8e1bc05..00000000 --- a/packages/web_ui/src/model/item_metadata.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { useEffect, useState } from "react"; - -import { useExportManifest } from "./export_manifest"; -import { ExportMetadata, ExportMetadataEntry } from "@clusterio/lib"; - - -const metadataCaches = new Map>(); -let cssInjected = false; - -/** - * Load the unified spritesheet metadata and inject CSS rules for every icon. - * CSS class name: `.{category}-{CSS.escape(name)}` - * e.g. `.item-iron-plate`, `.recipe-iron-plate`, `.signal-signal-red`, `.planet-nauvis` - */ -function useSpriteMetadata(category: string): Map { - const exportManifest = useExportManifest(); - const [metadata, setMetadata] = useState>( - metadataCaches.get(category) ?? new Map() - ); - - useEffect(() => { - async function load() { - if ( - !exportManifest - || !exportManifest.assets["metadata"] - || !exportManifest.assets["spritesheet"] - ) { - return; - } - - const response = await fetch(`${staticRoot}static/${exportManifest.assets["metadata"]}`); - if (!response.ok) { - return; - } - - const data: ExportMetadata = await response.json(); - - if (!cssInjected) { - const sheetUrl = `${staticRoot}static/${exportManifest.assets["spritesheet"]}`; - const style = document.createElement("style"); - document.head.appendChild(style); - for (const [baseName, prototypes] of Object.entries(data)) { - for (const prototype of prototypes) { - const icon = prototype.icon; - if (!icon) { - continue; - } - style.sheet!.insertRule(`\ -.${baseName}-${CSS.escape(prototype.name)} { - background-image: url("${sheetUrl}"); - background-repeat: no-repeat; - background-position: -${icon.x}px -${icon.y}px; - height: ${icon.size}px; - width: ${icon.size}px; -}` - ); - } - } - cssInjected = true; - } - - const cache = new Map(); - for (const prototype of data[category] ?? []) { - cache.set(prototype.name, prototype); - } - metadataCaches.set(category, cache); - setMetadata(cache); - } - - if (!metadataCaches.has(category)) { - load(); - } - }, [exportManifest]); - - return metadata; -} - -export function useItemMetadata() { return useSpriteMetadata("item"); } -export function useFluidMetadata() { return useSpriteMetadata("fluid"); } -export function useRecipeMetadata() { return useSpriteMetadata("recipe"); } -export function useVirtualSignalMetadata() { return useSpriteMetadata("virtual-signal"); } -export function useTechnologyMetadata() { return useSpriteMetadata("technology"); } -export function useSpaceLocationMetadata() { return useSpriteMetadata("space-location"); } -export function useQualityMetadata() { return useSpriteMetadata("quality"); } -export function useEntityMetadata() { return useSpriteMetadata("entity"); } diff --git a/packages/web_ui/src/model/locale.ts b/packages/web_ui/src/model/locale.ts deleted file mode 100644 index 95b136ef..00000000 --- a/packages/web_ui/src/model/locale.ts +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useEffect, useState } from "react"; - -import { useExportManifest } from "./export_manifest"; - - -let localeCache: Map|null = null; -export function useLocale(): Map { - let exportManifest = useExportManifest(); - let [locale, setLocale] = useState>(localeCache || new Map()); - useEffect(() => { - async function load() { - if (!exportManifest?.assets["locale"]) { - return; - } - let response = await fetch(`${staticRoot}static/${exportManifest.assets["locale"]}`); - if (response.ok) { - let data = await response.json(); - localeCache = new Map(data); - setLocale(localeCache); - } - } - - if (!localeCache) { - load(); - } - }, [exportManifest]); - - return locale; -} diff --git a/packages/web_ui/src/model/mod_pack.ts b/packages/web_ui/src/model/mod_pack.ts index 927a9a75..a3b63bb2 100644 --- a/packages/web_ui/src/model/mod_pack.ts +++ b/packages/web_ui/src/model/mod_pack.ts @@ -1,5 +1,7 @@ import { useCallback, useContext, useSyncExternalStore } from "react"; import ControlContext from "../components/ControlContext"; +import { defaultModPackStore } from "../store/default_mod_pack_store"; + export function useModPack(id?: number) { const [modPacks, synced] = useModPacks(); @@ -11,3 +13,11 @@ export function useModPacks() { const subscribe = useCallback((callback: () => void) => control.modPacks.subscribe(callback), [control]); return useSyncExternalStore(subscribe, () => control.modPacks.getSnapshot()); } + +export function useDefaultModPack() { + const control = useContext(ControlContext); + const subscribe = useCallback( + (callback: () => void) => defaultModPackStore.subscribe(control, callback), [control], + ); + return useSyncExternalStore(subscribe, () => defaultModPackStore.getSnapshot()); +} diff --git a/packages/web_ui/src/pages.tsx b/packages/web_ui/src/pages.tsx index f3f36a35..f2187c45 100644 --- a/packages/web_ui/src/pages.tsx +++ b/packages/web_ui/src/pages.tsx @@ -18,6 +18,8 @@ import PluginViewPage from "./components/PluginViewPage"; import ErrorPage from "./components/ErrorPage"; import IconReferencePage from "./components/IconReferencePage"; +// When adding or editing page paths here the corresponding webRoutes entry +// in packages/controller/src/routes.ts also needs to be updated. export const pages: PluginPage[] = [ { path: "/controller", @@ -57,6 +59,11 @@ export const pages: PluginPage[] = [ sidebarPath: "/mods", content: , }, + { + path: "/mods/mod-packs/:id/icon-reference", + sidebarPath: "/mods", + content: , + }, { path: "/users", sidebarName: "Users", @@ -89,10 +96,6 @@ export const pages: PluginPage[] = [ sidebarPath: "/plugins", content: , }, - { - path: "/icon-reference", - content: , - }, { path: "/error", content: , diff --git a/packages/web_ui/src/store/default_mod_pack_store.ts b/packages/web_ui/src/store/default_mod_pack_store.ts new file mode 100644 index 00000000..4b92f30a --- /dev/null +++ b/packages/web_ui/src/store/default_mod_pack_store.ts @@ -0,0 +1,51 @@ +import { ModPackGetDefaultRequest, type ModPack, isDeepStrictEqual } from "@clusterio/lib"; +import type { Control } from "../util/websocket"; + +// TODO: replace this super duper expensive method with listening to a default mod pack id event. +class DefaultModPackStore { + refCount = 0; + interval: undefined | ReturnType; + modPack: undefined | ModPack; + callbacks: (() => void)[] = []; + + fetchDefaultModPack(control: Control) { + control.send(new ModPackGetDefaultRequest()).then(modPack => { + if (!isDeepStrictEqual(this.modPack, modPack)) { + this.modPack = modPack; + for (const callback of this.callbacks) { + callback(); + } + } + }).catch(() => { + if (this.modPack !== undefined) { + this.modPack = undefined; + for (const callback of this.callbacks) { + callback(); + } + } + }); + } + + subscribe(control: Control, callback: () => void) { + if (!this.refCount) { + const update = () => { this.fetchDefaultModPack(control); }; + update(); + this.interval = setInterval(update, 60e3); + } + this.callbacks.push(callback); + this.refCount += 1; + return () => { + this.callbacks.splice(this.callbacks.indexOf(callback), 1); + this.refCount -= 1; + if (!this.refCount) { + clearInterval(this.interval); + this.interval = undefined; + } + }; + } + + getSnapshot() { + return this.modPack; + } +}; +export const defaultModPackStore = new DefaultModPackStore(); diff --git a/packages/web_ui/src/store/export_locale_stores.ts b/packages/web_ui/src/store/export_locale_stores.ts new file mode 100644 index 00000000..ef07b4b7 --- /dev/null +++ b/packages/web_ui/src/store/export_locale_stores.ts @@ -0,0 +1,61 @@ +import { ExportLocale, logger } from "@clusterio/lib"; +import notify from "../util/notify"; + + +interface ExportLocaleCacheEntry { + callbacks: (() => void)[]; + localePath: string | undefined; + data: Map; +} + +class ExportLocaleStore { + cache = new Map(); + _emptyMap = new Map(); + + async loadLocale(modPackId: number, cacheEntry: ExportLocaleCacheEntry) { + let response = await fetch(`${staticRoot}static/${cacheEntry.localePath}`); + if (response.ok) { + cacheEntry.data = new Map(await response.json() as ExportLocale); + for (const callback of cacheEntry.callbacks) { + callback(); + } + } else { + notify( + `Failed to locale for mod pack ${modPackId}, server returned: ` + + `${response.status} ${response.statusText}` + ); + } + } + + subscribe(modPackId: number | undefined, localePath: string | undefined, callback: () => void) { + if (modPackId === undefined || !localePath) { + return () => {}; + } + + let cacheEntry = this.cache.get(modPackId); + if (!cacheEntry) { + cacheEntry = { + callbacks: [], + localePath: undefined, + data: new Map(), + }; + this.cache.set(modPackId, cacheEntry); + } + if (cacheEntry.localePath !== localePath) { + cacheEntry.localePath = localePath; + this.loadLocale(modPackId, cacheEntry).catch(logger.error); + } + cacheEntry.callbacks.push(callback); + return () => { + cacheEntry.callbacks.splice(cacheEntry.callbacks.indexOf(callback), 1); + }; + } + + getSnapshot(modPackId: number | undefined): ReadonlyMap { + if (modPackId === undefined) { + return this._emptyMap; + } + return this.cache.get(modPackId)?.data ?? this._emptyMap; + } +} +export const exportLocaleStore = new ExportLocaleStore(); diff --git a/packages/web_ui/src/store/export_prototype_metadata_store.ts b/packages/web_ui/src/store/export_prototype_metadata_store.ts new file mode 100644 index 00000000..17030032 --- /dev/null +++ b/packages/web_ui/src/store/export_prototype_metadata_store.ts @@ -0,0 +1,126 @@ +import { ExportMetadata, ExportMetadataEntry, logger, ModPack } from "@clusterio/lib"; +import unknownIconUrl from "../images/unknown-item.png"; +import notify from "../util/notify"; + + +export type PrototypeMetadataEntry = ExportMetadataEntry & { base_type: string }; + +interface MetadataCacheEntry { + callbacks: (() => void)[], + metadataPath: string | undefined, + spritesheetPath: string | undefined, + metadata: Map>, + styleSheet: CSSStyleSheet, +} + +class ExportPrototypeMetadataStore { + cache = new Map(); + async loadMetadata(modPackId: number, cacheEntry: MetadataCacheEntry) { + const response = await fetch(`${staticRoot}static/${cacheEntry.metadataPath}`); + if (!response.ok) { + notify( + `Failed to fetch prototype metadata for mod pack ${modPackId}, server returned: ` + + `${response.status} ${response.statusText}` + ); + // eslint-disable-next-line node/no-sync + cacheEntry.styleSheet.replaceSync(""); + cacheEntry.metadata = new Map(); + for (const callback of cacheEntry.callbacks) { + callback(); + } + return; + } + + const metadata = await response.json() as ExportMetadata; + + const spriteUrl = `${staticRoot}static/${cacheEntry.spritesheetPath}`; + const styleContent: string[] = []; + styleContent.push(`\ +.factorio-icon\ +[data-mod-pack="${modPackId}"] { + background-image: url("${spriteUrl}"); +}`); + for (const [baseName, prototypes] of Object.entries(metadata)) { + for (const prototype of prototypes) { + const icon = prototype.icon; + if (!icon) { + styleContent.push(`\ +.factorio-icon\ +[data-mod-pack="${modPackId}"]\ +[data-type="${baseName}"]\ +[data-name="${CSS.escape(prototype.name)}"] { + background-image: url("${unknownIconUrl}"); + height: 32px; + width: 32px; +}`); + continue; + } + styleContent.push(`\ +.factorio-icon\ +[data-mod-pack="${modPackId}"]\ +[data-type="${baseName}"]\ +[data-name="${CSS.escape(prototype.name)}"] { + background-position: -${icon.x}px -${icon.y}px; + height: ${icon.size}px; + width: ${icon.size}px; +}`); + } + } + + // eslint-disable-next-line node/no-sync + cacheEntry.styleSheet.replaceSync(styleContent.join("\n")); + cacheEntry.metadata = new Map( + Object.entries(metadata) + .map(([baseName, entries]) => [ + baseName, + new Map( + entries.map(entry => [entry.name, { base_type: baseName, ...entry }]) + ), + ]) + ); + for (const callback of cacheEntry.callbacks) { + callback(); + } + } + + subscribe(modPack: ModPack | undefined, callback: () => void) { + if (!modPack) { + return () => {}; + } + + const assets = modPack.exportManifest?.assets; + let cacheEntry = this.cache.get(modPack.id); + if (!cacheEntry) { + const styleSheet = new CSSStyleSheet(); + document.adoptedStyleSheets.push(styleSheet); + cacheEntry = { + callbacks: [], + spritesheetPath: undefined, + metadataPath: undefined, + metadata: new Map(), + styleSheet, + }; + this.cache.set(modPack.id, cacheEntry); + } + if ( + cacheEntry.metadataPath !== assets?.metadata + || cacheEntry.spritesheetPath !== assets?.spritesheet + ) { + cacheEntry.metadataPath = assets?.metadata; + cacheEntry.spritesheetPath = assets?.spritesheet; + this.loadMetadata(modPack.id, cacheEntry).catch(logger.error); + } + cacheEntry.callbacks.push(callback); + return () => { + cacheEntry.callbacks.splice(cacheEntry.callbacks.indexOf(callback), 1); + }; + } + + getSnapshot(modPackId: number | undefined) { + if (modPackId === undefined) { + return undefined; + } + return this.cache.get(modPackId)?.metadata; + } +} +export const exportPrototypeMetadataStore = new ExportPrototypeMetadataStore(); diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index 937c4cd9..59be1d2b 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -320,7 +320,14 @@ body { font-family: monospace; } -.factorio-icon.item-unknown-item { +.factorio-icon { + display: inline-block; + vertical-align: middle; + background-repeat: no-repeat; +} + +/* data-mod-pack might not be present, but we need the specificity to win over it if it is */ +.factorio-icon:is([data-mod-pack],*)[data-unknown="true"] { background: url("./images/unknown-item.png"); height: 32px; width: 32px; diff --git a/plugins/subspace_storage/web/index.tsx b/plugins/subspace_storage/web/index.tsx index 120d29da..63f669d7 100644 --- a/plugins/subspace_storage/web/index.tsx +++ b/plugins/subspace_storage/web/index.tsx @@ -1,10 +1,9 @@ -import React, { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { Input, Table, Typography } from "antd"; -import * as lib from "@clusterio/lib"; import { - BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, - notifyErrorHandler, useItemMetadata, useFluidMetadata, useLocale, + BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler, + useExportLocale, useExportPrototypeMetadata, useDefaultModPack, FactorioIcon, } from "@clusterio/web_ui"; import { GetStorageRequest, Item, SetStorageSubscriptionRequest, UpdateStorageEvent } from "../messages"; @@ -32,16 +31,18 @@ function useStorage(control: Control) { function StoragePage() { let control = useContext(ControlContext); - let locale = useLocale(); - let itemMetadata = useItemMetadata(); - let fluidMetadata = useFluidMetadata(); + const modPack = useDefaultModPack(); + let locale = useExportLocale(modPack); + const prototypes = useExportPrototypeMetadata(modPack); + let itemMetadata = prototypes?.get("item"); + let fluidMetadata = prototypes?.get("fluid"); let storage = useStorage(control); type ItemFilter = ([name, item]: [string, Item]) => boolean; let [filter, setFilter] = useState(null); function getLocaleName(itemName: string) { let localeName = itemName; - let meta = itemMetadata.get(itemName) ?? fluidMetadata.get(itemName); + let meta = itemMetadata?.get(itemName) ?? fluidMetadata?.get(itemName); if (meta && meta.localised_name) { // TODO: implement the locale to name conversion. if (typeof meta.localised_name === "string") { @@ -86,6 +87,7 @@ function StoragePage() { /> { let localeName = getLocaleName(item[1].name); - let type; - let name; - if (itemMetadata.has(item[1].name)) { - type = "item"; - name = item[1].name; - } else if (fluidMetadata.has(item[1].name)) { - type = "fluid"; - name = item[1].name; - } else { - type = "item"; - name = "unknown-item"; - } + let name = item[1].name; + const prototype = itemMetadata?.get(name) ?? fluidMetadata?.get(name); return <> - + {localeName} ; }, diff --git a/plugins/subspace_storage/web/style.css b/plugins/subspace_storage/web/style.css index d96377fc..09460697 100644 --- a/plugins/subspace_storage/web/style.css +++ b/plugins/subspace_storage/web/style.css @@ -1,5 +1,3 @@ -.factorio-icon { - display: inline-block; - vertical-align: middle; +.subspace-storage-storage .factorio-icon { margin-right: 0.5em; }