mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
reorganize some code
This commit is contained in:
1 parent
7011d4d20f
commit
b298874f0d
24 files changed
+5
-5
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import type { MetadataAccessor } from "@blitzkit/closed";
|
||||
import type { Strings } from "@blitzkit/i18n";
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import type { Avatar } from "@protos/blitzkit/avatar";
|
||||
import type { AvatarList } from "@protos/blitzkit/avatar_list";
|
||||
import type { Avatars } from "@protos/blitzkit/avatars";
|
||||
import type { Background } from "@protos/blitzkit/background";
|
||||
import type { Backgrounds } from "@protos/blitzkit/backgrounds";
|
||||
import type { PopularTanks } from "@protos/blitzkit/popular_tanks";
|
||||
import type { Tank } from "@protos/blitzkit/tank";
|
||||
import type { TankList } from "@protos/blitzkit/tank_list";
|
||||
import type { Tanks } from "@protos/blitzkit/tanks";
|
||||
import type { Tiers } from "@protos/blitzkit/tiers";
|
||||
import type { TankUpgradePricePresetComponent } from "@protos/game/proto/legacy/blitz_static_tank_upgrade_price_preset_component";
|
||||
|
||||
export abstract class AbstractAPI {
|
||||
abstract metadata: MetadataAccessor;
|
||||
|
||||
abstract tankList(): Promise<TankList>;
|
||||
abstract tank(id: string): Promise<Tank>;
|
||||
abstract tanks(): Promise<Tanks>;
|
||||
abstract popularTanks(): Promise<PopularTanks>;
|
||||
abstract tankUpgradePreset(
|
||||
name: string,
|
||||
): Promise<TankUpgradePricePresetComponent>;
|
||||
|
||||
abstract tiers(): Promise<Tiers>;
|
||||
|
||||
abstract strings(locale: string): Promise<Strings>;
|
||||
abstract gameStrings(locale: string): Promise<Record<string, string>>;
|
||||
protected abstract _groupedGameStrings(
|
||||
locale: string,
|
||||
group: string,
|
||||
): Promise<Record<string, string>>;
|
||||
abstract gameStringGroups(): Promise<string[]>;
|
||||
|
||||
abstract avatar(id: string): Promise<Avatar>;
|
||||
abstract avatars(): Promise<Avatars>;
|
||||
abstract avatarList(): Promise<AvatarList>;
|
||||
|
||||
abstract background(id: string): Promise<Background>;
|
||||
abstract backgrounds(): Promise<Backgrounds>;
|
||||
|
||||
abstract currencyIcon(id: string): Promise<string>;
|
||||
|
||||
private assertLocale(locale: string) {
|
||||
const supported = locales.supported.find(
|
||||
(supported) => supported.locale === locale,
|
||||
);
|
||||
|
||||
if (supported === undefined) {
|
||||
throw new Error(`Unsupported locale: ${locale}`);
|
||||
}
|
||||
|
||||
return supported;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async groupedGameStrings(locale: string, group: string, prefix: boolean) {
|
||||
this.assertLocale(locale);
|
||||
|
||||
const gameStrings = await this._groupedGameStrings(locale, group);
|
||||
|
||||
if (!prefix) return gameStrings;
|
||||
|
||||
const strings: Record<string, string> = {};
|
||||
|
||||
for (const key in gameStrings) {
|
||||
strings[`${group}__${key}`] = gameStrings[key];
|
||||
}
|
||||
|
||||
return strings;
|
||||
}
|
||||
}
|
||||
|
||||
export function Cache<Arguments extends unknown[]>(disableInDev = false) {
|
||||
const cache = new WeakMap<object, Map<string, Promise<unknown>>>();
|
||||
|
||||
return function (
|
||||
_target: unknown,
|
||||
_propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const original = descriptor.value;
|
||||
|
||||
descriptor.value = function (...args: Arguments) {
|
||||
const key = args.join("-");
|
||||
|
||||
let thisCache = cache.get(this);
|
||||
|
||||
if (!thisCache) {
|
||||
thisCache = new Map();
|
||||
cache.set(this, thisCache);
|
||||
}
|
||||
|
||||
const disabled = disableInDev && import.meta.env.DEV;
|
||||
|
||||
if (thisCache.has(key) && !disabled) {
|
||||
return thisCache.get(key);
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
try {
|
||||
return await original.apply(this, args);
|
||||
} catch (error) {
|
||||
thisCache!.delete(key);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
thisCache.set(key, promise);
|
||||
|
||||
return promise;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { fetchPB } from "@blitzkit/core";
|
||||
import type { Strings } from "@blitzkit/i18n";
|
||||
import { Avatars } from "@protos/blitzkit/avatars";
|
||||
import { PopularTanks } from "@protos/blitzkit/popular_tanks";
|
||||
import { TankList } from "@protos/blitzkit/tank_list";
|
||||
import { Tanks } from "@protos/blitzkit/tanks";
|
||||
import { Tiers } from "@protos/blitzkit/tiers";
|
||||
import { TankUpgradePricePresetComponent } from "@protos/game/proto/legacy/blitz_static_tank_upgrade_price_preset_component";
|
||||
import { AbstractAPI, Cache } from "./abstract";
|
||||
|
||||
const rejected = Promise.reject("Not implemented on client");
|
||||
|
||||
export class ClientAPI extends AbstractAPI {
|
||||
get metadata(): never {
|
||||
throw new Error("MetadataAccessor is inaccessible on the client");
|
||||
}
|
||||
|
||||
@Cache()
|
||||
tankList() {
|
||||
return fetchPB("/api/tanks/list.pb", TankList);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async tank(id: string) {
|
||||
const tanks = await this.tanks();
|
||||
return tanks.tanks[id];
|
||||
}
|
||||
|
||||
@Cache()
|
||||
tanks() {
|
||||
return fetchPB("/api/tanks/all.pb", Tanks);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
popularTanks() {
|
||||
return fetchPB("/api/tanks/popular.pb", PopularTanks);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
tankUpgradePreset(name: string) {
|
||||
return fetchPB(
|
||||
`/api/tanks/presets/upgrade/${name}.pb`,
|
||||
TankUpgradePricePresetComponent,
|
||||
);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
tiers() {
|
||||
return fetchPB("/api/tiers.pb", Tiers);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async strings(locale: string) {
|
||||
const response = await fetch(`/api/strings/${locale}.json`);
|
||||
const json = (await response.json()) as Strings;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
@UnimplementedOnClient
|
||||
gameStrings() {
|
||||
return rejected;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
protected async _groupedGameStrings(locale: string, group: string) {
|
||||
const response = await fetch(`/api/game-strings/${locale}/${group}.json`);
|
||||
const json = (await response.json()) as Record<string, string>;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
@UnimplementedOnClient
|
||||
gameStringGroups() {
|
||||
return rejected;
|
||||
}
|
||||
|
||||
@UnimplementedOnClient
|
||||
avatar() {
|
||||
return rejected;
|
||||
}
|
||||
|
||||
avatars() {
|
||||
return fetchPB("/api/avatars/all.pb", Avatars);
|
||||
}
|
||||
|
||||
@UnimplementedOnClient
|
||||
avatarList() {
|
||||
return rejected;
|
||||
}
|
||||
|
||||
@UnimplementedOnClient
|
||||
background() {
|
||||
return rejected;
|
||||
}
|
||||
|
||||
@UnimplementedOnClient
|
||||
backgrounds() {
|
||||
return rejected;
|
||||
}
|
||||
}
|
||||
|
||||
function UnimplementedOnClient(
|
||||
_target: unknown,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
descriptor.value = function () {
|
||||
throw new Error(`${propertyKey} is not implemented on the client`);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { assertSecret } from "@blitzkit/core";
|
||||
import type { AbstractAPI } from "./abstract";
|
||||
|
||||
let dynamicAPI: AbstractAPI;
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
const { ProxyClient } = await import("@blitzkit/closed");
|
||||
const { ServerAPI } = await import("./server");
|
||||
|
||||
const proxyClient = await new ProxyClient(
|
||||
assertSecret(import.meta.env.WOTB_SERVER),
|
||||
).handshake(assertSecret(import.meta.env.WOTB_VERSION));
|
||||
const metadata = await proxyClient.metadata();
|
||||
|
||||
dynamicAPI = new ServerAPI(metadata);
|
||||
} else {
|
||||
const { ClientAPI } = await import("./client");
|
||||
|
||||
dynamicAPI = new ClientAPI();
|
||||
}
|
||||
|
||||
export const api = dynamicAPI;
|
||||
@@ -0,0 +1,23 @@
|
||||
import sharp from "sharp";
|
||||
|
||||
export async function imageProxy(url: string) {
|
||||
if (import.meta.env.DEV) {
|
||||
return Response.redirect(url);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(url);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch image proxy ${url}:`, error);
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
const image = sharp(buffer, { animated: true });
|
||||
const webp = await image.webp().toBuffer();
|
||||
const array = new Uint8Array(webp);
|
||||
|
||||
return new Response(array);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const clientUnmountedResponse = new Response(null, { status: 503 });
|
||||
|
||||
export const notFoundResponse = new Response(null, { status: 404 });
|
||||
@@ -0,0 +1,377 @@
|
||||
import type { MetadataAccessor } from "@blitzkit/closed";
|
||||
import { assertSecret, sluggify } from "@blitzkit/core";
|
||||
import type { Strings } from "@blitzkit/i18n";
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import type { Avatar, DeepPartial } from "@protos/blitzkit/avatar";
|
||||
import type { Background } from "@protos/blitzkit/background";
|
||||
import type { PopularTanks } from "@protos/blitzkit/popular_tanks";
|
||||
import type { Tank } from "@protos/blitzkit/tank";
|
||||
import type { TankListEntry } from "@protos/blitzkit/tank_list";
|
||||
import type { Tanks } from "@protos/blitzkit/tanks";
|
||||
import type { Tiers } from "@protos/blitzkit/tiers";
|
||||
import type { RemoteStorageComponent } from "@protos/game/proto/legacy/blitz_static_remote_storage_component";
|
||||
import { existsSync } from "fs";
|
||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||
import { google } from "googleapis";
|
||||
import { merge } from "lodash-es";
|
||||
import { parse } from "yaml";
|
||||
import { AbstractAPI, Cache } from "./abstract";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
throw new Error("ServerAPI is being evaluated in the browser");
|
||||
}
|
||||
|
||||
const globbedStrings = import.meta.glob("../../../../i18n/strings/*.json", {
|
||||
import: "default",
|
||||
});
|
||||
const tankSlugPattern = /^(\/\w+)?\/tanks\/([a-z0-9-]+)\/$/;
|
||||
|
||||
interface LocalizationConfig {
|
||||
namespaces: string[];
|
||||
}
|
||||
|
||||
export class ServerAPI extends AbstractAPI {
|
||||
constructor(public metadata: MetadataAccessor) {
|
||||
super();
|
||||
}
|
||||
|
||||
resolveRemoteUrl(remoteUrls: string[]) {
|
||||
let remoteStorage: RemoteStorageComponent | undefined = undefined;
|
||||
|
||||
for (const remoteUrl of remoteUrls) {
|
||||
const item = this.metadata.item(remoteUrl);
|
||||
const candidate = item.RemoteStorage();
|
||||
|
||||
if (
|
||||
remoteStorage === undefined ||
|
||||
candidate.download_speed_weight > remoteStorage.download_speed_weight
|
||||
) {
|
||||
remoteStorage = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteStorage === undefined) {
|
||||
throw new Error("No suitable production remote storage found");
|
||||
}
|
||||
|
||||
return remoteStorage;
|
||||
}
|
||||
|
||||
resolveClientConfig() {
|
||||
const group = this.metadata.group("ClientConfigsEntity");
|
||||
|
||||
if (group.length !== 1) {
|
||||
throw new RangeError(
|
||||
`Don't know how to handle ${group.length} ClientConfigsEntities`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.metadata.item(group[0].id);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async tankList() {
|
||||
const group = this.metadata.group("TankEntity");
|
||||
const list: TankListEntry[] = [];
|
||||
const strings = await this.groupedGameStrings("en", "TankEntity", true);
|
||||
|
||||
for (const item of group) {
|
||||
const tankCatalog = item.TankCatalog();
|
||||
|
||||
if (!tankCatalog.name) continue;
|
||||
|
||||
const name = strings[tankCatalog.name.value];
|
||||
|
||||
if (!name) continue;
|
||||
|
||||
const id = item.name;
|
||||
const slug = sluggify(name);
|
||||
|
||||
list.push({ id, slug });
|
||||
}
|
||||
|
||||
return { list };
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async tank(id: string) {
|
||||
const tankList = await this.tankList();
|
||||
const tankListEntry = tankList.list.find((tank) => tank.id === id);
|
||||
|
||||
if (!tankListEntry) {
|
||||
throw new Error(`Tank with id ${id} not found`);
|
||||
}
|
||||
|
||||
const { slug } = tankListEntry;
|
||||
const item = this.metadata.item(`TankEntity.${id}`);
|
||||
const tank = item.TankCatalog();
|
||||
const compensation = item.Compensation();
|
||||
|
||||
return { id, slug, tank, compensation } satisfies Tank;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async tanks() {
|
||||
const tankList = await this.tankList();
|
||||
const data: Tanks = { tanks: {} };
|
||||
|
||||
for (const { id } of tankList.list) {
|
||||
data.tanks[id] = await this.tank(id);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async popularTanks() {
|
||||
if (!existsSync("../../temp/popular.json")) {
|
||||
const { tanks } = await this.tanks();
|
||||
const tanksArray = Object.values(tanks);
|
||||
|
||||
const auth = await google.auth.getClient({
|
||||
keyFile: assertSecret(import.meta.env.GOOGLE_APPLICATION_CREDENTIALS),
|
||||
scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
|
||||
});
|
||||
const analytics = google.analyticsdata({ version: "v1beta", auth });
|
||||
const report = await analytics.properties.runReport({
|
||||
property: `properties/${assertSecret(
|
||||
import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID,
|
||||
)}`,
|
||||
requestBody: {
|
||||
dateRanges: [{ startDate: "7daysAgo", endDate: "today" }],
|
||||
dimensions: [{ name: "pagePath" }],
|
||||
metrics: [{ name: "screenPageViews" }],
|
||||
orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }],
|
||||
dimensionFilter: {
|
||||
filter: {
|
||||
fieldName: "pagePath",
|
||||
stringFilter: {
|
||||
matchType: "BEGINS_WITH",
|
||||
value: "/tanks/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const views: Record<string, number> = {};
|
||||
|
||||
if (!report.data.rows) {
|
||||
throw new Error("No rows in report");
|
||||
}
|
||||
|
||||
for (const row of report.data.rows) {
|
||||
if (
|
||||
!row.dimensionValues ||
|
||||
!row.dimensionValues[0].value ||
|
||||
!row.metricValues ||
|
||||
!row.metricValues[0].value
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const matches = row.dimensionValues[0].value.match(tankSlugPattern);
|
||||
|
||||
if (!matches) continue;
|
||||
|
||||
const slug = matches[2];
|
||||
const tank = tanksArray.find((tank) => tank.slug === slug);
|
||||
|
||||
if (!tank) continue;
|
||||
|
||||
if (tank.id in views) {
|
||||
views[tank.id] += Number(row.metricValues[0].value);
|
||||
} else {
|
||||
views[tank.id] = Number(row.metricValues[0].value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in tanks) {
|
||||
if (!(id in views)) {
|
||||
views[id] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const popularTanks = {
|
||||
tanks: Object.entries(views)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([id, views]) => ({ id, views })),
|
||||
} satisfies PopularTanks;
|
||||
|
||||
await mkdir("../../temp", { recursive: true });
|
||||
await writeFile("../../temp/popular.json", JSON.stringify(popularTanks));
|
||||
|
||||
return popularTanks;
|
||||
}
|
||||
|
||||
const data = await readFile("../../temp/popular.json", "utf-8");
|
||||
return JSON.parse(data) as PopularTanks;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async tankUpgradePreset(id: string) {
|
||||
const item = this.metadata.item(`TankUpgradePricePresetEntity.${id}`);
|
||||
return item.TankUpgradePricePreset();
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async tiers() {
|
||||
const group = this.metadata.group("TankTierEntity");
|
||||
const tiers: Tiers = { tiers: {} };
|
||||
|
||||
for (const item of group) {
|
||||
tiers.tiers[item.name] = item.TankTier();
|
||||
}
|
||||
|
||||
return tiers;
|
||||
}
|
||||
|
||||
@Cache(true)
|
||||
async strings(locale: string) {
|
||||
const localized = (await globbedStrings[
|
||||
`../../../../i18n/strings/${locale}.json`
|
||||
]()) as DeepPartial<Strings>;
|
||||
const defaults = (await globbedStrings[
|
||||
`../../../../i18n/strings/${locales.default}.json`
|
||||
]()) as Strings;
|
||||
const strings = merge({}, defaults, localized);
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async gameStrings(locale: string) {
|
||||
const clientConfig = this.resolveClientConfig();
|
||||
const localizationResources = clientConfig.LocalizationResources();
|
||||
const strings: Record<string, string> = {};
|
||||
|
||||
for (const remoteStorageSettings of localizationResources.remote_storages) {
|
||||
const remoteStorage = this.resolveRemoteUrl(
|
||||
remoteStorageSettings.remote_urls,
|
||||
);
|
||||
const configPath = `${remoteStorage.url}${remoteStorage.relative_path}/config.yaml`;
|
||||
const config = await fetch(configPath)
|
||||
.then((response) => response.text())
|
||||
.then((text) => parse(text) as LocalizationConfig);
|
||||
|
||||
for (const namespace of config.namespaces) {
|
||||
const url = `${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml`;
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
const namespaceStrings: Record<string, string> = {};
|
||||
const parsed = parse(text) as Record<string, string>;
|
||||
|
||||
for (const key in parsed) {
|
||||
strings[key] = parsed[key].replaceAll('\\"', '"');
|
||||
}
|
||||
|
||||
Object.assign(strings, namespaceStrings);
|
||||
}
|
||||
}
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
protected async _groupedGameStrings(locale: string, group: string) {
|
||||
const strings = await this.gameStrings(locale);
|
||||
const filtered: Record<string, string> = {};
|
||||
|
||||
for (const key in strings) {
|
||||
if (key.startsWith(`${group}__`)) {
|
||||
const trimmed = key.substring(group.length + 2);
|
||||
filtered[trimmed] = strings[key];
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async gameStringGroups() {
|
||||
const defaultGameStrings = await this.gameStrings(locales.default);
|
||||
const groups = new Set<string>();
|
||||
|
||||
for (const key in defaultGameStrings) {
|
||||
const [group] = key.split("__");
|
||||
groups.add(group);
|
||||
}
|
||||
|
||||
return Array.from(groups.values());
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async avatar(id: string) {
|
||||
const avatar = this.metadata.item(`ProfileAvatarEntity.${id}`);
|
||||
const name = avatar.name;
|
||||
const stuff_ui = avatar.StuffUI("UIComponent");
|
||||
const profile_avatar = avatar.ProfileAvatar();
|
||||
const sellable = avatar.components.sellableComponent
|
||||
? avatar.Sellable()
|
||||
: undefined;
|
||||
|
||||
return { name, stuff_ui, profile_avatar, sellable };
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async avatars() {
|
||||
const avatars: Avatar[] = [];
|
||||
|
||||
for (const item of this.metadata.group("ProfileAvatarEntity")) {
|
||||
avatars.push(await this.avatar(item.name));
|
||||
}
|
||||
|
||||
return { avatars };
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async avatarList() {
|
||||
const avatars = await this.avatars();
|
||||
const list = avatars.avatars.map((avatar) => avatar.name);
|
||||
|
||||
return { list };
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async background(id: string) {
|
||||
const background = this.metadata.item(`ProfileBackgroundEntity.${id}`);
|
||||
const name = background.name;
|
||||
const stuff_ui = background.StuffUI("UIComponent");
|
||||
const profile_background = background.ProfileBackground();
|
||||
const sellable = background.components.sellableComponent
|
||||
? background.Sellable()
|
||||
: undefined;
|
||||
|
||||
return { name, stuff_ui, profile_background, sellable };
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async backgrounds() {
|
||||
const backgrounds: Background[] = [];
|
||||
|
||||
for (const item of this.metadata.group("ProfileBackgroundEntity")) {
|
||||
backgrounds.push(await this.background(item.name));
|
||||
}
|
||||
|
||||
return { backgrounds };
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async currencyIcon(id: string) {
|
||||
const item = this.metadata.item(`CurrencyEntity.${id}`);
|
||||
const stuffUI = item.StuffUI("UIComponent");
|
||||
const config = this.resolveClientConfig();
|
||||
const metroResources = config.MetroResources();
|
||||
const remoteStorage = this.resolveRemoteUrl(
|
||||
metroResources.remote_storage!.remote_urls,
|
||||
);
|
||||
|
||||
const iconPathHack = stuffUI.icon.replace("\n", "");
|
||||
|
||||
if (!stuffUI.icon.includes("\n")) {
|
||||
throw new Error("Icon path doesn't include newline, can't apply hack");
|
||||
}
|
||||
|
||||
return `${remoteStorage.url}${remoteStorage.relative_path}${iconPathHack}`;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user