This commit is contained in:
cocobo1 2025-11-07 22:12:37 +02:00
commit 5b5de50cab
4 changed files with 159 additions and 21 deletions

View file

@ -1,30 +1,70 @@
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
import { iterateAssets } from "@lib/api/assets";
import { iterateAssets, findAssetId } from "@lib/api/assets";
import { Text } from "@metro/common/components";
import { ErrorBoundary, Search } from "@ui/components";
import { useMemo } from "react";
import { FlatList, View } from "react-native";
import { FlatList, View, ScrollView, TouchableOpacity, Image } from "react-native";
const displayable = new Set(['png', 'jpg', 'svg']);
export default function AssetBrowser() {
const [search, setSearch] = React.useState("");
const [showNonImages, setShowNonImages] = React.useState(false);
const all = useMemo(() => Array.from(iterateAssets()), []);
const filteredData = useMemo(() => {
let result = all.filter(a => a.name.includes(search) || a.id.toString() === search);
if (!showNonImages) {
result = result.filter(a => displayable.has(a.type));
}
return result;
}, [all, search, showNonImages]);
return (
<ErrorBoundary>
<View style={{ flex: 1 }}>
<Search
style={{ margin: 10 }}
onChangeText={(v: string) => setSearch(v)}
/>
<View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}>
<Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text>
<FlatList
data={all.filter(a => a.name.includes(search) || a.id.toString() === search)}
renderItem={({ item }: any) => <AssetDisplay asset={item} />}
contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
keyExtractor={a => a.name}
<View style={{ flexDirection: 'row', alignItems: 'center', margin: 10 }}>
<View style={{ flex: 1, marginRight: 10 }}>
<Search
onChangeText={(v: string) => setSearch(v)}
/>
</View>
<TouchableOpacity
style={{
padding: 12,
backgroundColor: showNonImages ? '#0f1013' : '#303139',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
minWidth: 44,
minHeight: 44,
}}
onPress={() => setShowNonImages(!showNonImages)}
>
<Image
style={{ width: 20, height: 20 }}
source={findAssetId("ic_image")!}
/>
</TouchableOpacity>
</View>
<ScrollView>
<View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}>
{showNonImages && (
<Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text>
)}
<FlatList
data={filteredData}
renderItem={({ item }: any) => <AssetDisplay asset={item} showNonImages={showNonImages} />}
contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
scrollEnabled={false}
keyExtractor={a => a.id.toString()}
/>
</View>
</ScrollView>
</View>
</ErrorBoundary>
);

View file

@ -90,7 +90,6 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini
manifest.rawColors = draft;
}
return {
spec: 2,
reference: resolveType(),
@ -100,9 +99,97 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini
};
}
if (manifest.spec === 1 || (manifest.theme_color_map && !manifest.spec)) {
const semanticDefinitions: InternalColorDefinition["semantic"] = {};
const rawDefinitions: Record<string, string> = {};
const themeIndex = resolveType() === "light" ? 1 : 0;
if (manifest.theme_color_map) {
for (const [key, colorArray] of Object.entries(manifest.theme_color_map)) {
if (!Array.isArray(colorArray) || colorArray.length < 2) {
console.warn(`[Parser] Invalid color array for ${key}:`, colorArray);
continue;
}
const colorValue = colorArray[themeIndex];
if (!colorValue) {
console.warn(`[Parser] Missing color value for ${key} at index ${themeIndex}`);
continue;
}
const normalized = normalizeToHex(colorValue);
if (normalized) {
semanticDefinitions[key] = {
value: normalized,
opacity: 1
};
}
}
}
const rawColors = manifest.colours || manifest.colors;
if (rawColors) {
for (const [key, colorValue] of Object.entries(rawColors)) {
if (typeof colorValue !== "string") continue;
const normalized = normalizeToHex(colorValue);
if (normalized) {
const discordKey = convertEnmityToDiscordRawKey(key);
rawDefinitions[discordKey] = normalized;
if (discordKey !== key) {
rawDefinitions[key] = normalized;
}
}
}
}
if (manifest.unsafe_colors) {
for (const [key, colorValue] of Object.entries(manifest.unsafe_colors)) {
if (typeof colorValue !== "string") continue;
const normalized = normalizeToHex(colorValue);
if (normalized) {
rawDefinitions[key] = normalized;
}
}
}
if (Platform.OS === "android") applyAndroidAlphaKeys(rawDefinitions);
return {
spec: 2,
reference: resolveType(),
semantic: semanticDefinitions,
raw: rawDefinitions,
background: manifest.background
};
}
throw new Error("Invalid theme spec");
}
function convertEnmityToDiscordRawKey(enmityKey: string): string {
const conversions: Record<string, string> = {
"PRIMARY_DARK": "PRIMARY_100",
"PRIMARY_DARK_100": "PRIMARY_100",
"PRIMARY_DARK_200": "PRIMARY_200",
"PRIMARY_DARK_300": "PRIMARY_300",
"PRIMARY_DARK_360": "PRIMARY_360",
"PRIMARY_DARK_400": "PRIMARY_400",
"PRIMARY_DARK_500": "PRIMARY_500",
"PRIMARY_DARK_600": "PRIMARY_600",
"PRIMARY_DARK_630": "PRIMARY_630",
"PRIMARY_DARK_700": "PRIMARY_700",
"PRIMARY_DARK_800": "PRIMARY_800",
"PRIMARY_DARK_900": "PRIMARY_900",
};
return conversions[enmityKey] || enmityKey;
}
export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) {
if (!rawColors) return;
@ -129,6 +216,11 @@ export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) {
export function normalizeToHex(colorString: string | undefined): string | undefined {
if (colorString === undefined) return undefined;
if (colorString.toLowerCase() === "transparent") {
return "#00000000";
}
if (chroma.valid(colorString)) return chroma(colorString).hex();
const color = Number(processColor(colorString));
@ -140,4 +232,3 @@ export function normalizeToHex(colorString: string | undefined): string | undefi
color >> 24 & 0xff // alpha
).hex();
}

View file

@ -39,7 +39,7 @@ export interface VendettaThemeManifest {
/** @internal */
export interface InternalColorDefinition {
spec: 2 | 3;
spec: 1 | 2 | 3;
reference: "darker" | "light";
semantic: Record<string, {
value: string;

View file

@ -53,19 +53,26 @@ function processData(data: VendettaThemeManifest) {
if (Platform.OS === "android") applyAndroidAlphaKeys(rawColors);
}
// this field is required by the spec but vd seems to ignore this
// so are most vd themes
data.spec ??= 2;
if (data.spec === undefined) {
if (!('theme_color_map' in data)) {
data.spec = 2;
}
//If not then Enmity theme and no spec
}
return data;
}
function validateTheme(themeJSON: any): boolean {
if (typeof themeJSON !== "object" || themeJSON === null) return false;
if (themeJSON.spec !== 2 && themeJSON.spec !== 3) return false;
if (themeJSON.spec === 3 && !themeJSON.main) return false;
return true;
if (themeJSON.spec === 2) return true;
if (themeJSON.theme_color_map) return true;
return themeJSON.spec === 2 || themeJSON.spec === 3;
}
export async function fetchTheme(url: string, selected = false) {