From d7be92ff495ae3f157cd34f40071a32ef8a60e42 Mon Sep 17 00:00:00 2001 From: pylixonly <82711525+pylixonly@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:35:02 +0800 Subject: [PATCH 01/22] refactor: port theme engine from dev --- .../ui/settings/pages/Themes/ThemeCard.tsx | 7 +- src/index.ts | 3 +- src/lib/addons/themes/colors/index.ts | 19 + src/lib/addons/themes/colors/parser.ts | 133 +++++++ .../themes/colors/patches/background.tsx | 53 +++ .../addons/themes/colors/patches/resolver.ts | 71 ++++ .../addons/themes/colors/patches/storage.ts | 38 ++ src/lib/addons/themes/colors/types.ts | 48 +++ src/lib/addons/themes/colors/updater.ts | 59 ++++ src/lib/addons/themes/index.ts | 329 ++---------------- src/lib/addons/types.ts | 13 +- src/lib/ui/color.ts | 5 +- 12 files changed, 464 insertions(+), 314 deletions(-) create mode 100644 src/lib/addons/themes/colors/index.ts create mode 100644 src/lib/addons/themes/colors/parser.ts create mode 100644 src/lib/addons/themes/colors/patches/background.tsx create mode 100644 src/lib/addons/themes/colors/patches/resolver.ts create mode 100644 src/lib/addons/themes/colors/patches/storage.ts create mode 100644 src/lib/addons/themes/colors/types.ts create mode 100644 src/lib/addons/themes/colors/updater.ts diff --git a/src/core/ui/settings/pages/Themes/ThemeCard.tsx b/src/core/ui/settings/pages/Themes/ThemeCard.tsx index 4934d20..3f296ec 100644 --- a/src/core/ui/settings/pages/Themes/ThemeCard.tsx +++ b/src/core/ui/settings/pages/Themes/ThemeCard.tsx @@ -2,22 +2,21 @@ import { formatString, Strings } from "@core/i18n"; import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard"; import { showConfirmationAlert } from "@core/vendetta/alerts"; import { useProxy } from "@core/vendetta/storage"; -import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/addons/themes"; +import { fetchTheme, removeTheme, selectTheme, themes, VdThemeInfo } from "@lib/addons/themes"; import { findAssetId } from "@lib/api/assets"; import { settings } from "@lib/api/settings"; import { clipboard } from "@metro/common"; import { showToast } from "@ui/toasts"; -function selectAndApply(value: boolean, theme: Theme) { +function selectAndApply(value: boolean, theme: VdThemeInfo) { try { selectTheme(value ? theme : null); - applyTheme(value ? theme : null); } catch (e: any) { console.error("Error while selectAndApply,", e); } } -export default function ThemeCard({ item: theme }: CardWrapper) { +export default function ThemeCard({ item: theme }: CardWrapper) { useProxy(theme); const [removed, setRemoved] = React.useState(false); diff --git a/src/index.ts b/src/index.ts index 6751865..2845790 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { initVendettaObject } from "@core/vendetta/api"; import { VdPluginManager } from "@core/vendetta/plugins"; import { updateFonts } from "@lib/addons/fonts"; import { initPlugins, updatePlugins } from "@lib/addons/plugins"; -import { initThemes, patchChatBackground } from "@lib/addons/themes"; +import { initThemes } from "@lib/addons/themes"; import { patchCommands } from "@lib/api/commands"; import { patchLogHook } from "@lib/api/debug"; import { injectFluxInterceptor } from "@lib/api/flux"; @@ -40,7 +40,6 @@ export default async () => { patchSettings(), patchLogHook(), patchCommands(), - patchChatBackground(), patchJsx(), initVendettaObject(), initFetchI18nStrings(), diff --git a/src/lib/addons/themes/colors/index.ts b/src/lib/addons/themes/colors/index.ts new file mode 100644 index 0000000..435fde0 --- /dev/null +++ b/src/lib/addons/themes/colors/index.ts @@ -0,0 +1,19 @@ + +import patchChatBackground from "./patches/background"; +import patchDefinitionAndResolver from "./patches/resolver"; +import patchStorage from "./patches/storage"; +import { ColorManifest } from "./types"; +import { updateBunnyColor } from "./updater"; + +/** @internal */ +export default function initColors(manifest: ColorManifest | null) { + const patches = [ + patchStorage(), + patchDefinitionAndResolver(), + patchChatBackground() + ]; + + if (manifest) updateBunnyColor(manifest, { update: false }); + + return () => patches.forEach(p => p()); +} diff --git a/src/lib/addons/themes/colors/parser.ts b/src/lib/addons/themes/colors/parser.ts new file mode 100644 index 0000000..4e25978 --- /dev/null +++ b/src/lib/addons/themes/colors/parser.ts @@ -0,0 +1,133 @@ +import { findByProps } from "@metro"; +import chroma from "chroma-js"; +import { omit } from "es-toolkit"; +import { Platform, processColor } from "react-native"; + +import { ColorManifest, InternalColorDefinition } from "./types"; + +const tokenRef = findByProps("SemanticColor"); + +export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition { + const resolveType = (type = "dark") => /* (ColorManager.preferences.type ?? type) */ type === "dark" ? "darker" : "light"; + + if (manifest.spec === 3) { + const semanticColorDefinitions: InternalColorDefinition["semantic"] = {}; + + for (const [semanticColorKey, semanticColorValue] of Object.entries(manifest.semantic ?? {})) { + if (typeof semanticColorValue === "object") { + const { type, value, opacity: semanticColorOpacity } = semanticColorValue; + + if (type === "raw") { + semanticColorDefinitions[semanticColorKey] = { + value, + opacity: semanticColorOpacity ?? 1, + }; + } else { + const rawColorValue = tokenRef.RawColor[value]; + semanticColorDefinitions[semanticColorKey] = { + value: rawColorValue, + opacity: semanticColorOpacity ?? 1, + }; + } + } else if (typeof semanticColorValue === "string") { + if (semanticColorValue.startsWith("#")) { + semanticColorDefinitions[semanticColorKey] = { + value: chroma.hex(semanticColorValue).hex(), + opacity: 1, + }; + } else { + semanticColorDefinitions[semanticColorKey] = { + value: tokenRef.RawColor[semanticColorValue], + opacity: 1, + }; + } + } else { + throw new Error(`Invalid semantic definitions: ${semanticColorValue}`); + } + } + + return { + reference: resolveType(manifest.type), + semantic: semanticColorDefinitions, + raw: manifest.raw ?? {}, + background: manifest.background, + }; + } else if (manifest.spec === 2) { // is Vendetta theme + const semanticDefinitions: InternalColorDefinition["semantic"] = {}; + const background: InternalColorDefinition["background"] | undefined = manifest.background ? { + ...omit(manifest.background, ["alpha"]), + opacity: manifest.background.alpha + } : undefined; + + if (manifest.semanticColors) { + for (const key in manifest.semanticColors) { + const values = manifest.semanticColors[key].map(c => c || undefined).slice(0, 2); + if (!values[0]) continue; + + semanticDefinitions[key] = { + value: normalizeToHex(values[resolveType() === "light" ? 1 : 0])!, + opacity: 1 + }; + } + } + + if (manifest.rawColors) { + for (const key in manifest.rawColors) { + const value = manifest.rawColors[key]; + if (!value) continue; + manifest.rawColors[key] = normalizeToHex(value)!; + } + + if (Platform.OS === "android") applyAndroidAlphaKeys(manifest.rawColors); + } + + + return { + reference: resolveType(), + semantic: semanticDefinitions, + raw: manifest.rawColors ?? {}, + background + }; + } + + throw new Error("Invalid theme spec"); +} + +export function applyAndroidAlphaKeys(rawColors?: Record) { + if (!rawColors) return; + + // these are native Discord Android keys + const alphaMap: Record = { + "BLACK_ALPHA_60": ["BLACK", 0.6], + "BRAND_NEW_360_ALPHA_20": ["BRAND_360", 0.2], + "BRAND_NEW_360_ALPHA_25": ["BRAND_360", 0.25], + "BRAND_NEW_500_ALPHA_20": ["BRAND_500", 0.2], + "PRIMARY_DARK_500_ALPHA_20": ["PRIMARY_500", 0.2], + "PRIMARY_DARK_700_ALPHA_60": ["PRIMARY_700", 0.6], + "STATUS_GREEN_500_ALPHA_20": ["GREEN_500", 0.2], + "STATUS_RED_500_ALPHA_20": ["RED_500", 0.2], + }; + + for (const key in alphaMap) { + const [colorKey, alpha] = alphaMap[key]; + if (!rawColors[colorKey]) continue; + rawColors[key] = chroma(rawColors[colorKey]).alpha(alpha).hex(); + } + + return rawColors; +} + +export function normalizeToHex(colorString: string | undefined): string | undefined { + if (colorString === undefined) return undefined; + if (chroma.valid(colorString)) return chroma(colorString).hex(); + + const color = Number(processColor(colorString)); + + return chroma.rgb( + color >> 16 & 0xff, // red + color >> 8 & 0xff, // green + color & 0xff, // blue + color >> 24 & 0xff // alpha + ).hex(); +} + diff --git a/src/lib/addons/themes/colors/patches/background.tsx b/src/lib/addons/themes/colors/patches/background.tsx new file mode 100644 index 0000000..fd809eb --- /dev/null +++ b/src/lib/addons/themes/colors/patches/background.tsx @@ -0,0 +1,53 @@ +import { _colorRef } from "@lib/addons/themes/colors/updater"; +import { after } from "@lib/api/patcher"; +import { findInReactTree } from "@lib/utils"; +import { lazyDestructure } from "@lib/utils/lazy"; +import { findByNameLazy, findByProps } from "@metro"; +import chroma from "chroma-js"; +import { ImageBackground } from "react-native"; + +const MessagesWrapperConnected = findByNameLazy("MessagesWrapperConnected", false); +const { MessagesWrapper } = lazyDestructure(() => findByProps("MessagesWrapper")); + +export default function patchChatBackground() { + const patches = [ + after("default", MessagesWrapperConnected, (_, ret) => { + // TODO: support custom preferences + // useObservable([ColorManager.preferences]); + // if (!_colorRef.current || ColorManager.preferences.customBackground === "hidden") return ret; + + if (!_colorRef.current || !_colorRef.current.background?.url) return ret; + + return + {ret} + ; + } + ), + after("render", MessagesWrapper.prototype, (_, ret) => { + if (!_colorRef.current || !_colorRef.current.background?.url) return; + const messagesComponent = findInReactTree( + ret, + x => x && "HACK_fixModalInteraction" in x.props && x?.props?.style + ); + + if (messagesComponent) { + const backgroundColor = chroma( + messagesComponent.props.style.backgroundColor || "black" + ).alpha( + 1 - (_colorRef.current.background?.opacity ?? 1) + ); + + messagesComponent.props.style = [ + messagesComponent.props.style, + { backgroundColor } + ]; + } + }) + ]; + + return () => patches.forEach(x => x()); +} diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts new file mode 100644 index 0000000..e5ef8b1 --- /dev/null +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -0,0 +1,71 @@ +import { _colorRef } from "@lib/addons/themes/colors/updater"; +import { ThemeManager } from "@lib/api/native/modules"; +import { before, instead } from "@lib/api/patcher"; +import { findByProps } from "@metro"; +import { byMutableProp } from "@metro/filters"; +import { createLazyModule } from "@metro/lazy"; +import chroma from "chroma-js"; + +const tokenReference = findByProps("SemanticColor"); +const isThemeModule = createLazyModule(byMutableProp("isThemeDark")); + +export default function patchDefinitionAndResolver() { + const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0; + + Object.keys(tokenReference.RawColor).forEach(key => { + Object.defineProperty(tokenReference.RawColor, key, { + configurable: true, + enumerable: true, + get: () => { + const ret = _colorRef.current?.raw[key]; + return ret || _colorRef.origRaw[key]; + } + }); + }); + + const unpatches = [ + before("isThemeDark", isThemeModule, callback), + before("isThemeLight", isThemeModule, callback), + before("updateTheme", ThemeManager, callback), + instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => { + if (!_colorRef.current) return orig(...args); + if (args[0] !== _colorRef.key) return orig(...args); + + args[0] = _colorRef.current.reference; + + const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]); + + const semanticDef = _colorRef.current.semantic[name]; + if (semanticDef?.value) { + return chroma(semanticDef.value).alpha(semanticDef.opacity).hex(); + } + + const rawValue = _colorRef.current.raw[colorDef.raw]; + if (rawValue) { + // Set opacity if needed + return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); + } + + // Fallback to default + return orig(...args); + }), + () => { + // Not the actual module but.. yeah. + Object.defineProperty(tokenReference, "RawColor", { + configurable: true, + writable: true, + value: _colorRef.origRaw + }); + } + ]; + + return () => unpatches.forEach(p => p()); +} + +function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] { + // @ts-ignore - assigning to extractInfo._sym + const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]]; + const colorDef = tokenReference.SemanticColor[propName]; + + return [propName, colorDef[themeName]]; +} diff --git a/src/lib/addons/themes/colors/patches/storage.ts b/src/lib/addons/themes/colors/patches/storage.ts new file mode 100644 index 0000000..ddf14ad --- /dev/null +++ b/src/lib/addons/themes/colors/patches/storage.ts @@ -0,0 +1,38 @@ +import { _colorRef } from "@lib/addons/themes/colors/updater"; +import { after, before } from "@lib/api/patcher"; +import { findInTree } from "@lib/utils"; +import { proxyLazy } from "@lib/utils/lazy"; +import { findByProps } from "@metro"; + +const mmkvStorage = proxyLazy(() => { + const newModule = findByProps("impl"); + if (typeof newModule?.impl === "object") return newModule.impl; + return findByProps("storage"); +}); + +export default function patchStorage() { + const patchedKeys = new Set(["ThemeStore", "SelectivelySyncedUserSettingsStore"]); + + const patches = [ + after("get", mmkvStorage, ([key], ret) => { + if (!_colorRef.current || !patchedKeys.has(key)) return; + + const state = findInTree(ret._state, s => typeof s.theme === "string"); + if (state) state.theme = _colorRef.key; + }), + before("set", mmkvStorage, ([key, value]) => { + if (!patchedKeys.has(key)) return; + + const json = JSON.stringify(value); + const lastSetDiscordTheme = _colorRef.lastSetDiscordTheme ?? "darker"; + const replaced = json.replace( + /"theme":"bn-theme-\d+"/, + `"theme":${JSON.stringify(lastSetDiscordTheme)}` + ); + + return [key, JSON.parse(replaced)]; + }) + ]; + + return () => patches.forEach(p => p()); +} diff --git a/src/lib/addons/themes/colors/types.ts b/src/lib/addons/themes/colors/types.ts new file mode 100644 index 0000000..03a3801 --- /dev/null +++ b/src/lib/addons/themes/colors/types.ts @@ -0,0 +1,48 @@ +import { Author, BunnyManifest } from "@lib/addons/types"; + +interface SemanticReference { + type: "color" | "raw"; + value: string; + opacity?: number +} + +interface BackgroundDefinition { + url: string; + blur?: number; + opacity?: number; +} + +export interface BunnyColorManifest extends BunnyManifest { + spec: 3; + type: "dark" | "light"; + semantic?: Record; + raw?: Record; + background?: BackgroundDefinition; +} + +export interface VendettaThemeManifest { + spec: 2; + name: string; + description?: string; + authors?: Author[]; + semanticColors?: Record; + rawColors?: Record; + background?: { + url: string; + blur?: number; + alpha?: number; + }; +} + +/** @internal */ +export interface InternalColorDefinition { + reference: "darker" | "light"; + semantic: Record; + raw: Record; + background?: BackgroundDefinition; +} + +export type ColorManifest = BunnyColorManifest | VendettaThemeManifest; diff --git a/src/lib/addons/themes/colors/updater.ts b/src/lib/addons/themes/colors/updater.ts new file mode 100644 index 0000000..7721628 --- /dev/null +++ b/src/lib/addons/themes/colors/updater.ts @@ -0,0 +1,59 @@ +import { settings } from "@lib/api/settings"; +import { findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro"; + +import { parseColorManifest } from "./parser"; +import { ColorManifest, InternalColorDefinition } from "./types"; + +const tokenRef = findByProps("SemanticColor"); +const origRawColor = { ...tokenRef.RawColor }; +const AppearanceManager = findByPropsLazy("updateTheme"); +const ThemeStore = findByStoreNameLazy("ThemeStore"); +const FormDivider = findByPropsLazy("DIVIDER_COLORS"); + +let _inc = 1; + +interface InternalColorRef { + key: `bn-theme-${string}`; + current: InternalColorDefinition | null; + readonly origRaw: Record; + lastSetDiscordTheme: string; +} + +/** @internal */ +export const _colorRef: InternalColorRef = { + current: null, + key: `bn-theme-${_inc}`, + origRaw: origRawColor, + lastSetDiscordTheme: "darker" +}; + +export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) { + if (settings.safeMode?.enabled) return; + + const internalDef = colorManifest ? parseColorManifest(colorManifest) : null; + const ref = Object.assign(_colorRef, { + current: internalDef, + key: `bn-theme-${++_inc}`, + lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-") + ? ThemeStore.theme + : _colorRef.lastSetDiscordTheme + }); + + if (internalDef != null) { + tokenRef.Theme[ref.key.toUpperCase()] = ref.key; + FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference]; + + Object.keys(tokenRef.Shadow).forEach(k => tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference]); + Object.keys(tokenRef.SemanticColor).forEach(k => { + tokenRef.SemanticColor[k][ref.key] = { + ...tokenRef.SemanticColor[k][ref.current!.reference] + }; + }); + } + + if (update) { + AppearanceManager.setShouldSyncAppearanceSettings(false); + AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme); + } +} + diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index b610474..fd5502d 100644 --- a/src/lib/addons/themes/index.ts +++ b/src/lib/addons/themes/index.ts @@ -1,77 +1,22 @@ -/** - * Theming system in Bunny is currently a prototype, expect an unreadable theme implementation below - */ - import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage"; -import { Author } from "@lib/addons/types"; import { getStoredTheme, getThemeFilePath } from "@lib/api/native/loader"; -import { ThemeManager } from "@lib/api/native/modules"; -import { after, before, instead } from "@lib/api/patcher"; -import { findInReactTree, safeFetch } from "@lib/utils"; -import { lazyDestructure, proxyLazy } from "@lib/utils/lazy"; -import { byMutableProp } from "@metro/filters"; -import { createLazyModule } from "@metro/lazy"; -import { findByNameLazy, findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro/wrappers"; -import chroma from "chroma-js"; -import { ImageBackground, Platform, processColor } from "react-native"; +import { safeFetch } from "@lib/utils"; +import { Platform } from "react-native"; -export interface ThemeData { - name: string; - description?: string; - authors?: Author[]; - spec: number; - semanticColors?: Record; - rawColors?: Record; - background?: { - url: string; - blur?: number; - /** - * The alpha value of the background. - * `CHAT_BACKGROUND` of semanticColors alpha value will be ignored when this is specified - */ - alpha?: number; - }; -} +import initColors from "./colors"; +import { applyAndroidAlphaKeys, normalizeToHex } from "./colors/parser"; +import { VendettaThemeManifest } from "./colors/types"; +import { updateBunnyColor } from "./colors/updater"; -export interface Theme { +export interface VdThemeInfo { id: string; selected: boolean; - data: ThemeData; + data: VendettaThemeManifest; } -//! As of 173.10, early-finding this does not work. -// Somehow, this is late enough, though? -export const color = findByPropsLazy("SemanticColor"); - -const mmkvStorage = proxyLazy(() => { - const newModule = findByProps("impl"); - if (typeof newModule?.impl === "object") return newModule.impl; - return findByProps("storage"); -}); +export const themes = wrapSync(createStorage>(createMMKVBackend("VENDETTA_THEMES"))); -const appearanceManager = findByPropsLazy("updateTheme"); -const ThemeStore = findByStoreNameLazy("ThemeStore"); -const formDividerModule = findByPropsLazy("DIVIDER_COLORS"); -const MessagesWrapperConnected = findByNameLazy("MessagesWrapperConnected", false); -const { MessagesWrapper } = lazyDestructure(() => findByProps("MessagesWrapper")); -const isThemeModule = createLazyModule(byMutableProp("isThemeDark")); - -export const themes = wrapSync(createStorage>(createMMKVBackend("VENDETTA_THEMES"))); - -const semanticAlternativeMap: Record = { - "BG_BACKDROP": "BACKGROUND_FLOATING", - "BG_BASE_PRIMARY": "BACKGROUND_PRIMARY", - "BG_BASE_SECONDARY": "BACKGROUND_SECONDARY", - "BG_BASE_TERTIARY": "BACKGROUND_SECONDARY_ALT", - "BG_MOD_FAINT": "BACKGROUND_MODIFIER_ACCENT", - "BG_MOD_STRONG": "BACKGROUND_MODIFIER_ACCENT", - "BG_MOD_SUBTLE": "BACKGROUND_MODIFIER_ACCENT", - "BG_SURFACE_OVERLAY": "BACKGROUND_FLOATING", - "BG_SURFACE_OVERLAY_TMP": "BACKGROUND_FLOATING", - "BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY" -}; - -async function writeTheme(theme: Theme | {}) { +async function writeTheme(theme: VdThemeInfo | {}) { if (typeof theme !== "object") throw new Error("Theme must be an object"); // Save the current theme as current-theme.json. When supported by loader, @@ -79,59 +24,14 @@ async function writeTheme(theme: Theme | {}) { await createFileBackend(getThemeFilePath() || "theme.json").set(theme); } -/** - * @internal - */ -export function patchChatBackground() { - const patches = [ - after("default", MessagesWrapperConnected, (_, ret) => enabled ? React.createElement(ImageBackground, { - style: { flex: 1, height: "100%" }, - source: currentTheme?.data?.background?.url && { uri: currentTheme.data.background.url } || 0, - blurRadius: typeof currentTheme?.data?.background?.blur === "number" ? currentTheme?.data?.background?.blur : 0, - children: ret, - }) : ret), - after("render", MessagesWrapper.prototype, (_, ret) => { - if (!enabled || !currentTheme?.data?.background?.url) return; - - // HORRIBLE - const Messages = findInReactTree(ret, x => x && "HACK_fixModalInteraction" in x.props && x?.props?.style); - if (Messages) { - Messages.props.style = [ - Messages.props.style, - { - backgroundColor: chroma(Messages.props.style.backgroundColor || "black") - .alpha(1 - (currentTheme?.data.background?.alpha ?? 1)).hex() - }, - ]; - } - else console.error("Didn't find Messages when patching MessagesWrapper!"); - }) - ]; - - return () => patches.forEach(x => x()); -} - -function normalizeToHex(colorString: string): string { - if (chroma.valid(colorString)) return chroma(colorString).hex(); - - const color = Number(processColor(colorString)); - - return chroma.rgb( - color >> 16 & 0xff, // red - color >> 8 & 0xff, // green - color & 0xff, // blue - color >> 24 & 0xff // alpha - ).hex(); -} - // Process data for some compatiblity with native side -function processData(data: ThemeData) { +function processData(data: VendettaThemeManifest) { if (data.semanticColors) { const { semanticColors } = data; for (const key in semanticColors) { for (const index in semanticColors[key]) { - semanticColors[key][index] &&= normalizeToHex(semanticColors[key][index] as string); + semanticColors[key][index] &&= normalizeToHex(semanticColors[key][index] as string) || false; } } } @@ -140,7 +40,8 @@ function processData(data: ThemeData) { const { rawColors } = data; for (const key in rawColors) { - data.rawColors[key] = normalizeToHex(rawColors[key]); + const normalized = normalizeToHex(rawColors[key]); + if (normalized) data.rawColors[key] = normalized; } if (Platform.OS === "android") applyAndroidAlphaKeys(rawColors); @@ -153,26 +54,6 @@ function processData(data: ThemeData) { return data; } -function applyAndroidAlphaKeys(rawColors: Record) { - // these are native Discord Android keys - const alphaMap: Record = { - "BLACK_ALPHA_60": ["BLACK", 0.6], - "BRAND_NEW_360_ALPHA_20": ["BRAND_360", 0.2], - "BRAND_NEW_360_ALPHA_25": ["BRAND_360", 0.25], - "BRAND_NEW_500_ALPHA_20": ["BRAND_500", 0.2], - "PRIMARY_DARK_500_ALPHA_20": ["PRIMARY_500", 0.2], - "PRIMARY_DARK_700_ALPHA_60": ["PRIMARY_700", 0.6], - "STATUS_GREEN_500_ALPHA_20": ["GREEN_500", 0.2], - "STATUS_RED_500_ALPHA_20": ["RED_500", 0.2], - }; - - for (const key in alphaMap) { - const [colorKey, alpha] = alphaMap[key]; - if (!rawColors[colorKey]) continue; - rawColors[key] = chroma(rawColors[colorKey]).alpha(alpha).hex(); - } -} - export async function fetchTheme(id: string, selected = false) { let themeJSON: any; @@ -188,10 +69,9 @@ export async function fetchTheme(id: string, selected = false) { data: processData(themeJSON), }; - // TODO: Should we prompt when the selected theme is updated? if (selected) { writeTheme(themes[id]); - applyTheme(themes[id], vdThemeFallback); + updateBunnyColor(themes[id].data, { update: true }); } } @@ -200,15 +80,17 @@ export async function installTheme(id: string) { await fetchTheme(id); } -export function selectTheme(theme: Theme | null, write = true) { +export function selectTheme(theme: VdThemeInfo | null, write = true) { if (theme) theme.selected = true; Object.keys(themes).forEach( k => themes[k].selected = themes[k].id === theme?.id ); if (theme == null && write) { + updateBunnyColor(null, { update: true }); return writeTheme({}); } else if (theme) { + updateBunnyColor(theme.data, { update: true }); return writeTheme(theme); } } @@ -221,13 +103,6 @@ export async function removeTheme(id: string) { return theme.selected; } -/** - * @internal - */ -export function getThemeFromLoader(): Theme | null { - return getStoredTheme(); -} - export async function updateThemes() { await awaitStorage(themes); const currentTheme = getThemeFromLoader(); @@ -235,157 +110,14 @@ export async function updateThemes() { } export function getCurrentTheme() { - return currentTheme; -} - -const origRawColor = { ...color.RawColor }; - -let inc = 0; -let vdKey = "vd-theme"; - -let vdThemeFallback = "darker"; -let enabled = false; -let currentTheme: Theme | null; -let storageResolved = false; - -const discordThemes = new Set(["darker", "midnight", "dark", "light"]); -function isDiscordTheme(name: string) { - return discordThemes.has(name); + return Object.values(themes).find(t => t.selected) ?? null; } -function patchColor() { - const callback = ([theme]: any[]) => theme === vdKey ? [vdThemeFallback] : void 0; - - Object.keys(color.RawColor).forEach(k => { - Object.defineProperty(color.RawColor, k, { - configurable: true, - enumerable: true, - get: () => { - return enabled ? currentTheme?.data?.rawColors?.[k] ?? origRawColor[k] : origRawColor[k]; - } - }); - }); - - before("isThemeDark", isThemeModule, callback); - before("isThemeLight", isThemeModule, callback); - before("updateTheme", ThemeManager, callback); - - after("get", mmkvStorage, ([a], ret) => { - if (a === "SelectivelySyncedUserSettingsStore") { - storageResolved = true; - if (ret?._state?.appearance?.settings?.theme && enabled) { - vdThemeFallback = ret._state.appearance.settings.theme; - ret._state.appearance.settings.theme = vdKey; - } - } else if (a === "ThemeStore") { - storageResolved = true; - if (ret?._state?.theme && enabled) { - vdThemeFallback = ret._state.theme; - ret._state.theme = vdKey; - } - } - }); - - // Prevent setting to real Discord settings - before("set", mmkvStorage, args => { - if (!args[1]) return; - - const key = args[0]; - const value = JSON.parse(JSON.stringify(args[1])); - - const interceptors: Record void> = ({ - SelectivelySyncedUserSettingsStore: () => { - if (value._state?.appearance?.settings?.theme) { - const { theme } = value._state?.appearance?.settings ?? {}; - if (isDiscordTheme(theme)) { - vdThemeFallback = theme; - } else { - value._state.appearance.settings.theme = vdThemeFallback; - } - } - }, - ThemeStore: () => { - if (value._state?.theme) { - const { theme } = value._state; - if (isDiscordTheme(theme)) { - vdThemeFallback = theme; - } else { - value._state.theme = vdThemeFallback; - } - } - } - }); - - if (!(key in interceptors)) return args; - - interceptors[key](); - return [key, value]; - }); - - instead("resolveSemanticColor", color.default.meta ?? color.default.internal, (args, orig) => { - if (!enabled || !currentTheme) return orig(...args); - if (args[0] !== vdKey) return orig(...args); - - args[0] = vdThemeFallback; - - const [name, colorDef] = extractInfo(vdThemeFallback, args[1]); - - const themeIndex = vdThemeFallback === "midnight" ? 2 : vdThemeFallback === "light" ? 1 : 0; - - //! As of 192.7, Tabs v2 uses BG_ semantic colors instead of BACKGROUND_ ones - const alternativeName = semanticAlternativeMap[name] ?? name; - - const semanticColorVal = (currentTheme.data?.semanticColors?.[name] ?? currentTheme.data?.semanticColors?.[alternativeName])?.[themeIndex]; - - if (semanticColorVal) return semanticColorVal; - - const rawValue = currentTheme.data?.rawColors?.[colorDef.raw]; - if (rawValue) { - // Set opacity if needed - return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); - } - - // Fallback to default - return orig(...args); - }); -} - -function getDefaultFallbackTheme(fallback: string = vdThemeFallback) { - const theme = ThemeStore.theme.toLowerCase() as string; - - if (isDiscordTheme(theme)) { - return theme; - } else { - return fallback; - } -} - -export function applyTheme(appliedTheme: Theme | null, fallbackTheme?: string) { - if (!fallbackTheme) fallbackTheme = getDefaultFallbackTheme(); - - currentTheme = appliedTheme; - enabled = !!currentTheme; - vdThemeFallback = fallbackTheme!!; - vdKey = `vd-theme-${inc++}-${fallbackTheme}`; - - if (appliedTheme) { - color.Theme[vdKey.toUpperCase()] = vdKey; - - formDividerModule.DIVIDER_COLORS[vdKey] = formDividerModule.DIVIDER_COLORS[vdThemeFallback]; - - Object.keys(color.Shadow).forEach(k => color.Shadow[k][vdKey] = color.Shadow[k][vdThemeFallback]); - Object.keys(color.SemanticColor).forEach(k => { - color.SemanticColor[k][vdKey] = { - ...color.SemanticColor[k][vdThemeFallback], - override: appliedTheme?.data?.semanticColors?.[k]?.[0] - }; - }); - } - - if (storageResolved) { - appearanceManager.setShouldSyncAppearanceSettings(false); - appearanceManager.updateTheme(appliedTheme ? vdKey : fallbackTheme); - } +/** + * @internal + */ +export function getThemeFromLoader(): VdThemeInfo | null { + return getStoredTheme(); } /** @@ -393,18 +125,7 @@ export function applyTheme(appliedTheme: Theme | null, fallbackTheme?: string) { */ export function initThemes() { const currentTheme = getThemeFromLoader(); - enabled = Boolean(currentTheme); - - patchColor(); - applyTheme(currentTheme, vdThemeFallback); + initColors(currentTheme?.data ?? null); updateThemes().catch(e => console.error("Failed to update themes", e)); } - -function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] { - // @ts-ignore - assigning to extractInfo._sym - const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]]; - const colorDef = color.SemanticColor[propName]; - - return [propName, colorDef[themeName]]; -} diff --git a/src/lib/addons/types.ts b/src/lib/addons/types.ts index 29065ea..8ebc62f 100644 --- a/src/lib/addons/types.ts +++ b/src/lib/addons/types.ts @@ -1,4 +1,13 @@ -export interface Author { - name: string; +export type Author = { name: string, id?: `${bigint}`; }; + +export interface BunnyManifest { id: string; + display: { + name: string; + description?: string; + authors?: Author[]; + }; + extras?: { + [key: string]: any; + }; } diff --git a/src/lib/ui/color.ts b/src/lib/ui/color.ts index a22576a..8b1e7cc 100644 --- a/src/lib/ui/color.ts +++ b/src/lib/ui/color.ts @@ -1,6 +1,5 @@ -import { color } from "@lib/addons/themes"; import { constants } from "@metro/common"; -import { findByStoreNameLazy } from "@metro/wrappers"; +import { findByProps, findByStoreNameLazy } from "@metro/wrappers"; //! This module is only found on 165.0+, under the assumption that iOS 165.0 is the same as Android 165.0. //* In 167.1, most if not all traces of the old color modules were removed. @@ -10,6 +9,8 @@ import { findByStoreNameLazy } from "@metro/wrappers"; // ? These comments are preserved for historical purposes. // const colorModule = findByPropsProxy("colors", "meta"); +const color = findByProps("SemanticColor"); + // ? SemanticColor and default.colors are effectively ThemeColorMap export const semanticColors = (color?.default?.colors ?? constants?.ThemeColorMap) as Record; From 0fd2107473c35334cc8ff2a548021469dadbbeb4 Mon Sep 17 00:00:00 2001 From: pylixonly <82711525+pylixonly@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:17:23 +0800 Subject: [PATCH 02/22] fix: readd semantic fallback for spec 2 themes --- src/lib/addons/themes/colors/parser.ts | 2 ++ .../addons/themes/colors/patches/resolver.ts | 19 ++++++++++++++++++- src/lib/addons/themes/colors/types.ts | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/lib/addons/themes/colors/parser.ts b/src/lib/addons/themes/colors/parser.ts index 4e25978..c8c1bb4 100644 --- a/src/lib/addons/themes/colors/parser.ts +++ b/src/lib/addons/themes/colors/parser.ts @@ -47,6 +47,7 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini } return { + spec: 3, reference: resolveType(manifest.type), semantic: semanticColorDefinitions, raw: manifest.raw ?? {}, @@ -83,6 +84,7 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini return { + spec: 2, reference: resolveType(), semantic: semanticDefinitions, raw: manifest.rawColors ?? {}, diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index e5ef8b1..7af7940 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -9,6 +9,19 @@ import chroma from "chroma-js"; const tokenReference = findByProps("SemanticColor"); const isThemeModule = createLazyModule(byMutableProp("isThemeDark")); +const SEMANTIC_FALLBACK_MAP: Record = { + "BG_BACKDROP": "BACKGROUND_FLOATING", + "BG_BASE_PRIMARY": "BACKGROUND_PRIMARY", + "BG_BASE_SECONDARY": "BACKGROUND_SECONDARY", + "BG_BASE_TERTIARY": "BACKGROUND_SECONDARY_ALT", + "BG_MOD_FAINT": "BACKGROUND_MODIFIER_ACCENT", + "BG_MOD_STRONG": "BACKGROUND_MODIFIER_ACCENT", + "BG_MOD_SUBTLE": "BACKGROUND_MODIFIER_ACCENT", + "BG_SURFACE_OVERLAY": "BACKGROUND_FLOATING", + "BG_SURFACE_OVERLAY_TMP": "BACKGROUND_FLOATING", + "BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY" +}; + export default function patchDefinitionAndResolver() { const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0; @@ -35,7 +48,11 @@ export default function patchDefinitionAndResolver() { const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]); - const semanticDef = _colorRef.current.semantic[name]; + let semanticDef = _colorRef.current.semantic[name]; + if (!semanticDef && _colorRef.current?.spec === 2 && name in SEMANTIC_FALLBACK_MAP) { + semanticDef = _colorRef.current.semantic[SEMANTIC_FALLBACK_MAP[name]]; + } + if (semanticDef?.value) { return chroma(semanticDef.value).alpha(semanticDef.opacity).hex(); } diff --git a/src/lib/addons/themes/colors/types.ts b/src/lib/addons/themes/colors/types.ts index 03a3801..be87a53 100644 --- a/src/lib/addons/themes/colors/types.ts +++ b/src/lib/addons/themes/colors/types.ts @@ -3,7 +3,7 @@ import { Author, BunnyManifest } from "@lib/addons/types"; interface SemanticReference { type: "color" | "raw"; value: string; - opacity?: number + opacity?: number; } interface BackgroundDefinition { @@ -36,6 +36,7 @@ export interface VendettaThemeManifest { /** @internal */ export interface InternalColorDefinition { + spec: 2 | 3; reference: "darker" | "light"; semantic: Record Date: Mon, 21 Oct 2024 22:46:26 +0800 Subject: [PATCH 03/22] refactor: introduce colors preferences and some cleanups --- src/core/vendetta/storage.ts | 6 ++++- src/index.ts | 18 +------------- src/lib/addons/themes/colors/parser.ts | 3 ++- .../themes/colors/patches/background.tsx | 12 ++++------ .../addons/themes/colors/patches/resolver.ts | 2 +- src/lib/addons/themes/colors/preferences.ts | 18 ++++++++++++++ src/lib/addons/themes/index.ts | 24 +++++++++++++++---- 7 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 src/lib/addons/themes/colors/preferences.ts diff --git a/src/core/vendetta/storage.ts b/src/core/vendetta/storage.ts index 19ce87c..f49451b 100644 --- a/src/core/vendetta/storage.ts +++ b/src/core/vendetta/storage.ts @@ -142,7 +142,8 @@ export function wrapSync>(store: T): Awaited { export function awaitStorage(...stores: any[]) { return Promise.all( - stores.map(store => new Promise(res => store[syncAwaitSymbol](res))) + stores.map(store => + new Promise(res => store[syncAwaitSymbol](res))) ); } @@ -150,11 +151,14 @@ export interface StorageBackend { get: () => unknown | Promise; set: (data: unknown) => void | Promise; } + const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g; + const filePathFixer = (file: string): string => Platform.select({ default: file, ios: FileManager.saveFileToGallery ? file : `Documents/${file}`, }); + const getMMKVPath = (name: string): string => { if (ILLEGAL_CHARS_REGEX.test(name)) { // Replace forbidden characters with hyphens diff --git a/src/index.ts b/src/index.ts index 2845790..55ec53b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,32 +10,16 @@ import { initThemes } from "@lib/addons/themes"; import { patchCommands } from "@lib/api/commands"; import { patchLogHook } from "@lib/api/debug"; import { injectFluxInterceptor } from "@lib/api/flux"; -import { writeFile } from "@lib/api/native/fs"; -import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader"; import { patchJsx } from "@lib/api/react/jsx"; import { logger } from "@lib/utils/logger"; import { patchSettings } from "@ui/settings"; import * as lib from "./lib"; -function maybeLoadThemes() { - if (!isThemeSupported()) return; - - try { - if (isPyonLoader()) { - writeFile("../vendetta_theme.json", "null"); - } - initThemes(); - } catch (e) { - console.error("Failed to initialize themes", e); - } -} - export default async () => { - maybeLoadThemes(); - // Load everything in parallel await Promise.all([ + initThemes(), injectFluxInterceptor(), patchSettings(), patchLogHook(), diff --git a/src/lib/addons/themes/colors/parser.ts b/src/lib/addons/themes/colors/parser.ts index c8c1bb4..fd32653 100644 --- a/src/lib/addons/themes/colors/parser.ts +++ b/src/lib/addons/themes/colors/parser.ts @@ -3,12 +3,13 @@ import chroma from "chroma-js"; import { omit } from "es-toolkit"; import { Platform, processColor } from "react-native"; +import { colorsPref } from "./preferences"; import { ColorManifest, InternalColorDefinition } from "./types"; const tokenRef = findByProps("SemanticColor"); export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition { - const resolveType = (type = "dark") => /* (ColorManager.preferences.type ?? type) */ type === "dark" ? "darker" : "light"; + const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light"; if (manifest.spec === 3) { const semanticColorDefinitions: InternalColorDefinition["semantic"] = {}; diff --git a/src/lib/addons/themes/colors/patches/background.tsx b/src/lib/addons/themes/colors/patches/background.tsx index fd809eb..ee62986 100644 --- a/src/lib/addons/themes/colors/patches/background.tsx +++ b/src/lib/addons/themes/colors/patches/background.tsx @@ -1,5 +1,7 @@ +import { colorsPref } from "@lib/addons/themes/colors/preferences"; import { _colorRef } from "@lib/addons/themes/colors/updater"; import { after } from "@lib/api/patcher"; +import { useObservable } from "@lib/api/storage"; import { findInReactTree } from "@lib/utils"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByNameLazy, findByProps } from "@metro"; @@ -12,11 +14,8 @@ const { MessagesWrapper } = lazyDestructure(() => findByProps("MessagesWrapper") export default function patchChatBackground() { const patches = [ after("default", MessagesWrapperConnected, (_, ret) => { - // TODO: support custom preferences - // useObservable([ColorManager.preferences]); - // if (!_colorRef.current || ColorManager.preferences.customBackground === "hidden") return ret; - - if (!_colorRef.current || !_colorRef.current.background?.url) return ret; + useObservable([colorsPref]); + if (!_colorRef.current || !_colorRef.current.background?.url || colorsPref.customBackground === "hidden") return ret; return {ret} ; - } - ), + }), after("render", MessagesWrapper.prototype, (_, ret) => { if (!_colorRef.current || !_colorRef.current.background?.url) return; const messagesComponent = findInReactTree( diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index 7af7940..17d541c 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -49,7 +49,7 @@ export default function patchDefinitionAndResolver() { const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]); let semanticDef = _colorRef.current.semantic[name]; - if (!semanticDef && _colorRef.current?.spec === 2 && name in SEMANTIC_FALLBACK_MAP) { + if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) { semanticDef = _colorRef.current.semantic[SEMANTIC_FALLBACK_MAP[name]]; } diff --git a/src/lib/addons/themes/colors/preferences.ts b/src/lib/addons/themes/colors/preferences.ts new file mode 100644 index 0000000..ba5eb1c --- /dev/null +++ b/src/lib/addons/themes/colors/preferences.ts @@ -0,0 +1,18 @@ +import { createStorage } from "@lib/api/storage"; + +interface BunnyColorPreferencesStorage { + selected: string | null; + type?: "dark" | "light" | null; + customBackground: "hidden" | null; + per?: Record; +} + +export const colorsPref = createStorage( + "themes/colors/preferences.json", + { + dflt: { + selected: null, + customBackground: null + } + } +); diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index fd5502d..dcae18d 100644 --- a/src/lib/addons/themes/index.ts +++ b/src/lib/addons/themes/index.ts @@ -1,10 +1,12 @@ import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage"; -import { getStoredTheme, getThemeFilePath } from "@lib/api/native/loader"; +import { writeFile } from "@lib/api/native/fs"; +import { getStoredTheme, getThemeFilePath, isPyonLoader, isThemeSupported } from "@lib/api/native/loader"; import { safeFetch } from "@lib/utils"; import { Platform } from "react-native"; import initColors from "./colors"; import { applyAndroidAlphaKeys, normalizeToHex } from "./colors/parser"; +import { colorsPref } from "./colors/preferences"; import { VendettaThemeManifest } from "./colors/types"; import { updateBunnyColor } from "./colors/updater"; @@ -123,9 +125,21 @@ export function getThemeFromLoader(): VdThemeInfo | null { /** * @internal */ -export function initThemes() { - const currentTheme = getThemeFromLoader(); - initColors(currentTheme?.data ?? null); +export async function initThemes() { + if (!isThemeSupported()) return; + + try { + if (isPyonLoader()) { + writeFile("../vendetta_theme.json", "null"); + } - updateThemes().catch(e => console.error("Failed to update themes", e)); + await awaitStorage(colorsPref); + + const currentTheme = getThemeFromLoader(); + initColors(currentTheme?.data ?? null); + + updateThemes().catch(e => console.error("Failed to update themes", e)); + } catch (e) { + console.error("Failed to initialize themes", e); + } } From 0eb28e41b7243fcae952ded5a7c44c274103d753 Mon Sep 17 00:00:00 2001 From: pylixonly <82711525+pylixonly@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:05:59 +0800 Subject: [PATCH 04/22] feat(themes): port more stuff from 'dev' --- src/core/ui/components/AddonPage.tsx | 53 +++++++----- src/core/ui/settings/pages/Themes/index.tsx | 50 +++++++++-- src/lib/addons/themes/index.ts | 3 +- src/lib/ui/alerts.ts | 4 + src/lib/ui/index.ts | 1 + src/lib/ui/types.ts | 3 +- src/metro/common/components.ts | 29 +++++-- src/metro/common/types/components.ts | 91 +++++++++++++++++++-- 8 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 src/lib/ui/alerts.ts diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx index 7122e39..84956ec 100644 --- a/src/core/ui/components/AddonPage.tsx +++ b/src/core/ui/components/AddonPage.tsx @@ -1,28 +1,28 @@ import { CardWrapper } from "@core/ui/components/AddonCard"; -import { useProxy } from "@core/vendetta/storage"; import { findAssetId } from "@lib/api/assets"; import { settings } from "@lib/api/settings"; -import AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal"; +import { dismissAlert, openAlert } from "@lib/ui/alerts"; +import { showSheet } from "@lib/ui/sheets"; import isValidHttpUrl from "@lib/utils/isValidHttpUrl"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByProps } from "@metro"; -import { clipboard } from "@metro/common"; -import { Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput } from "@metro/common/components"; +import { clipboard, NavigationNative } from "@metro/common"; +import { AlertActionButton, AlertModal, Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; +import { isNotNil } from "es-toolkit"; import fuzzysort from "fuzzysort"; -import { ComponentType, ReactNode, useCallback, useMemo } from "react"; +import { ComponentType, ReactNode, useCallback, useEffect, useMemo } from "react"; import { Image, ScrollView, View } from "react-native"; const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet")); -const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert")); -type SearchKeywords = Array string)>; +type SearchKeywords = Array string)>; interface AddonPageProps { title: string; items: I[]; - searchKeywords: SearchKeywords; - sortOptions?: Record number>; + searchKeywords: SearchKeywords; + sortOptions?: Record number>; resolveItem?: (value: I) => T | undefined; safeModeHint?: { message?: string; @@ -34,6 +34,9 @@ interface AddonPageProps { fetchFn?: (url: string) => Promise; onPress?: () => void; }; + + OptionsActionSheetComponent?: ComponentType; + CardComponent: ComponentType>; ListHeaderComponent?: ComponentType; ListFooterComponent?: ComponentType; @@ -55,7 +58,7 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise Promise({ CardComponent, ...props }: AddonPageProps) { - useProxy(settings); - const [search, setSearch] = React.useState(""); - const [sortFn, setSortFn] = React.useState<((a: unknown, b: unknown) => number) | null>(() => null); + const [sortFn, setSortFn] = React.useState<((a: T, b: T) => number) | null>(() => null); + const navigation = NavigationNative.useNavigation(); + + useEffect(() => { + if (props.OptionsActionSheetComponent) { + navigation.setOptions({ + headerRight: () => showSheet("AddonMoreSheet", props.OptionsActionSheetComponent!)} + /> + }); + } + }, [navigation]); const results = useMemo(() => { let values = props.items; - if (props.resolveItem) values = values.map(props.resolveItem); - const items = values.filter(i => i && typeof i === "object"); + if (props.resolveItem) values = values.map(props.resolveItem).filter(isNotNil); + const items = values.filter(i => isNotNil(i) && typeof i === "object"); if (!search && sortFn) items.sort(sortFn); return fuzzysort.go(search, items, { keys: props.searchKeywords, all: true }); @@ -134,7 +149,7 @@ export default function AddonPage({ CardComponent, ...props }: if (results.length === 0 && !search) { return - + Oops! Nothing to see here… yet! @@ -175,7 +190,7 @@ export default function AddonPage({ CardComponent, ...props }: })} />} - {props.ListHeaderComponent && !search && } + {props.ListHeaderComponent && } ); @@ -187,12 +202,12 @@ export default function AddonPage({ CardComponent, ...props }: estimatedItemSize={136} ListHeaderComponent={headerElement} ListEmptyComponent={() => - + Hmmm... could not find that! } - contentContainerStyle={{ padding: 8, paddingHorizontal: 12 }} + contentContainerStyle={{ padding: 8, paddingHorizontal: 12, paddingBottom: 90 }} ItemSeparatorComponent={() => } ListFooterComponent={props.ListFooterComponent} renderItem={({ item }: any) => } diff --git a/src/core/ui/settings/pages/Themes/index.tsx b/src/core/ui/settings/pages/Themes/index.tsx index 8bdb1cf..5595edb 100644 --- a/src/core/ui/settings/pages/Themes/index.tsx +++ b/src/core/ui/settings/pages/Themes/index.tsx @@ -2,26 +2,31 @@ import { formatString, Strings } from "@core/i18n"; import AddonPage from "@core/ui/components/AddonPage"; import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard"; import { useProxy } from "@core/vendetta/storage"; -import { installTheme, Theme, themes } from "@lib/addons/themes"; +import { getCurrentTheme, installTheme, themes, VdThemeInfo } from "@lib/addons/themes"; +import { colorsPref } from "@lib/addons/themes/colors/preferences"; +import { updateBunnyColor } from "@lib/addons/themes/colors/updater"; import { Author } from "@lib/addons/types"; +import { findAssetId } from "@lib/api/assets"; import { settings } from "@lib/api/settings"; -import { Button } from "@metro/common/components"; +import { useObservable } from "@lib/api/storage"; +import { ActionSheet, BottomSheetTitleHeader, Button, TableRadioGroup, TableRadioRow, TableRowIcon } from "@metro/common/components"; +import { View } from "react-native"; export default function Themes() { useProxy(settings); useProxy(themes); return ( - + title={Strings.THEMES} searchKeywords={[ "data.name", "data.description", - p => p.data.authors?.map((a: Author) => a.name).join(", ") + p => p.data.authors?.map((a: Author) => a.name).join(", ") ?? "" ]} sortOptions={{ - "Name (A-Z)": (a, b) => a.name.localeCompare(b.name), - "Name (Z-A)": (a, b) => b.name.localeCompare(a.name) + "Name (A-Z)": (a, b) => a.data.name.localeCompare(b.data.name), + "Name (Z-A)": (a, b) => b.data.name.localeCompare(a.data.name) }} installAction={{ label: "Install a theme", @@ -38,6 +43,39 @@ export default function Themes() { /> }} CardComponent={ThemeCard} + OptionsActionSheetComponent={() => { + useObservable([colorsPref]); + + return + + + { + colorsPref.type = type !== "auto" ? type as "dark" | "light" : undefined; + getCurrentTheme()?.data && updateBunnyColor(getCurrentTheme()!.data!, { update: true }); + }} + > + } label="Auto" value="auto" /> + } label="Dark" value="dark" /> + } label="Light" value="light" /> + + { + colorsPref.customBackground = type !== "shown" ? type as "hidden" : null; + }} + > + } label="Show" value={"shown"} /> + } label="Hide" value={"hidden"} /> + + + ; + }} /> ); } diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index dcae18d..e898926 100644 --- a/src/lib/addons/themes/index.ts +++ b/src/lib/addons/themes/index.ts @@ -1,6 +1,7 @@ import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage"; import { writeFile } from "@lib/api/native/fs"; import { getStoredTheme, getThemeFilePath, isPyonLoader, isThemeSupported } from "@lib/api/native/loader"; +import { awaitStorage as newAwaitStorage } from "@lib/api/storage"; import { safeFetch } from "@lib/utils"; import { Platform } from "react-native"; @@ -133,7 +134,7 @@ export async function initThemes() { writeFile("../vendetta_theme.json", "null"); } - await awaitStorage(colorsPref); + await newAwaitStorage(colorsPref); const currentTheme = getThemeFromLoader(); initColors(currentTheme?.data ?? null); diff --git a/src/lib/ui/alerts.ts b/src/lib/ui/alerts.ts new file mode 100644 index 0000000..f80fe38 --- /dev/null +++ b/src/lib/ui/alerts.ts @@ -0,0 +1,4 @@ +import { lazyDestructure } from "@lib/utils/lazy"; +import { findByProps } from "@metro"; + +export const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert")); diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts index f4c027e..6417d63 100644 --- a/src/lib/ui/index.ts +++ b/src/lib/ui/index.ts @@ -1,4 +1,5 @@ +export * as alerts from "./alerts"; export * as components from "./components"; export * as settings from "./settings"; export * as sheets from "./sheets"; diff --git a/src/lib/ui/types.ts b/src/lib/ui/types.ts index 33dc3b9..0daa132 100644 --- a/src/lib/ui/types.ts +++ b/src/lib/ui/types.ts @@ -1,3 +1,4 @@ import { LiteralUnion } from "type-fest"; -export type DiscordTextStyles = LiteralUnion<"heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg", string>; +export type TextStyles = LiteralUnion<"heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg", string>; +export type ThemeColors = LiteralUnion<"action-sheet-gradient-bg" | "activity-card-background" | "activity-card-icon-overlay" | "alert-bg" | "android-navigation-bar-background" | "android-navigation-scrim-background" | "android-ripple" | "autocomplete-bg" | "background-accent" | "background-floating" | "background-mentioned" | "background-mentioned-hover" | "background-message-automod" | "background-message-automod-hover" | "background-message-highlight" | "background-message-highlight-hover" | "background-message-hover" | "background-mobile-primary" | "background-mobile-secondary" | "background-modifier-accent" | "background-modifier-accent-2" | "background-modifier-active" | "background-modifier-hover" | "background-modifier-selected" | "background-nested-floating" | "background-primary" | "background-secondary" | "background-secondary-alt" | "background-tertiary" | "badge-brand-bg" | "badge-brand-text" | "bg-backdrop" | "bg-backdrop-no-opacity" | "bg-base-primary" | "bg-base-secondary" | "bg-base-tertiary" | "bg-brand" | "bg-mod-faint" | "bg-mod-strong" | "bg-mod-subtle" | "bg-surface-overlay" | "bg-surface-overlay-tmp" | "bg-surface-raised" | "black" | "blur-fallback" | "blur-fallback-pressed" | "border-faint" | "border-strong" | "border-subtle" | "bug-reporter-modal-submitting-background" | "button-creator-revenue-background" | "button-danger-background" | "button-danger-background-active" | "button-danger-background-disabled" | "button-danger-background-hover" | "button-outline-brand-background" | "button-outline-brand-background-active" | "button-outline-brand-background-hover" | "button-outline-brand-border" | "button-outline-brand-border-active" | "button-outline-brand-border-hover" | "button-outline-brand-text" | "button-outline-brand-text-active" | "button-outline-brand-text-hover" | "button-outline-danger-background" | "button-outline-danger-background-active" | "button-outline-danger-background-hover" | "button-outline-danger-border" | "button-outline-danger-border-active" | "button-outline-danger-border-hover" | "button-outline-danger-text" | "button-outline-danger-text-active" | "button-outline-danger-text-hover" | "button-outline-positive-background" | "button-outline-positive-background-active" | "button-outline-positive-background-hover" | "button-outline-positive-border" | "button-outline-positive-border-active" | "button-outline-positive-border-hover" | "button-outline-positive-text" | "button-outline-positive-text-active" | "button-outline-positive-text-hover" | "button-outline-primary-background" | "button-outline-primary-background-active" | "button-outline-primary-background-hover" | "button-outline-primary-border" | "button-outline-primary-border-active" | "button-outline-primary-border-hover" | "button-outline-primary-text" | "button-outline-primary-text-active" | "button-outline-primary-text-hover" | "button-positive-background" | "button-positive-background-active" | "button-positive-background-disabled" | "button-positive-background-hover" | "button-secondary-background" | "button-secondary-background-active" | "button-secondary-background-disabled" | "button-secondary-background-hover" | "card-gradient-bg" | "card-gradient-pressed-bg" | "card-primary-bg" | "card-primary-pressed-bg" | "card-secondary-bg" | "card-secondary-pressed-bg" | "channel-icon" | "channel-text-area-placeholder" | "channels-default" | "channeltextarea-background" | "chat-background" | "chat-banner-bg" | "chat-border" | "chat-input-container-background" | "chat-swipe-to-reply-background" | "chat-swipe-to-reply-gradient-background" | "coachmark-bg" | "content-inventory-media-seekbar-container" | "content-inventory-overlay-text-primary" | "content-inventory-overlay-text-secondary" | "content-inventory-overlay-ui-mod" | "content-inventory-overlay-ui-mod-bg" | "context-menu-backdrop-background" | "control-brand-foreground" | "control-brand-foreground-new" | "creator-revenue-icon-gradient-end" | "creator-revenue-icon-gradient-start" | "creator-revenue-info-box-background" | "creator-revenue-info-box-border" | "creator-revenue-locked-channel-icon" | "creator-revenue-progress-bar" | "deprecated-card-bg" | "deprecated-card-editable-bg" | "deprecated-quickswitcher-input-background" | "deprecated-quickswitcher-input-placeholder" | "deprecated-store-bg" | "deprecated-text-input-bg" | "deprecated-text-input-border" | "deprecated-text-input-border-disabled" | "deprecated-text-input-border-hover" | "deprecated-text-input-prefix" | "display-banner-overflow-background" | "divider-strong" | "divider-subtle" | "embed-background" | "embed-background-alternate" | "embed-title" | "expression-picker-bg" | "focus-primary" | "forum-post-extra-media-count-container-background" | "forum-post-tag-background" | "guild-icon-inactive-bg" | "guild-icon-inactive-nested-bg" | "guild-notifications-bottom-sheet-pill-background" | "halo-positive" | "header-muted" | "header-primary" | "header-secondary" | "home-background" | "home-card-resting-border" | "icon-muted" | "icon-primary" | "icon-secondary" | "icon-transparent" | "info-box-background" | "info-danger-background" | "info-danger-foreground" | "info-danger-text" | "info-help-background" | "info-help-foreground" | "info-help-text" | "info-positive-background" | "info-positive-foreground" | "info-positive-text" | "info-warning-background" | "info-warning-foreground" | "info-warning-text" | "input-background" | "input-focused-border" | "input-placeholder-text" | "interactive-active" | "interactive-hover" | "interactive-muted" | "interactive-normal" | "legacy-android-blur-overlay-default" | "legacy-android-blur-overlay-ultra-thin" | "legacy-blur-fallback-default" | "legacy-blur-fallback-ultra-thin" | "live-stage-tile-border" | "logo-primary" | "mention-background" | "mention-foreground" | "menu-item-danger-active-bg" | "menu-item-danger-hover-bg" | "menu-item-default-active-bg" | "menu-item-default-hover-bg" | "modal-background" | "modal-footer-background" | "navigator-header-tint" | "panel-bg" | "polls-normal-fill-hover" | "polls-normal-image-background" | "polls-victor-fill" | "polls-voted-fill" | "premium-nitro-pink-text" | "profile-gradient-card-background" | "profile-gradient-message-input-border" | "profile-gradient-note-background" | "profile-gradient-overlay" | "profile-gradient-overlay-synced-with-user-theme" | "profile-gradient-profile-body-background-hover" | "profile-gradient-role-pill-background" | "profile-gradient-role-pill-border" | "profile-gradient-section-box" | "redesign-activity-card-background" | "redesign-activity-card-background-pressed" | "redesign-activity-card-badge-icon" | "redesign-activity-card-border" | "redesign-activity-card-overflow-background" | "redesign-button-active-background" | "redesign-button-active-pressed-background" | "redesign-button-active-text" | "redesign-button-danger-background" | "redesign-button-danger-pressed-background" | "redesign-button-danger-text" | "redesign-button-destructive-background" | "redesign-button-destructive-pressed-background" | "redesign-button-destructive-text" | "redesign-button-overlay-alpha-background" | "redesign-button-overlay-alpha-pressed-background" | "redesign-button-overlay-alpha-text" | "redesign-button-overlay-background" | "redesign-button-overlay-pressed-background" | "redesign-button-overlay-text" | "redesign-button-positive-background" | "redesign-button-positive-pressed-background" | "redesign-button-positive-text" | "redesign-button-primary-alt-background" | "redesign-button-primary-alt-border" | "redesign-button-primary-alt-on-blurple-background" | "redesign-button-primary-alt-on-blurple-border" | "redesign-button-primary-alt-on-blurple-pressed-background" | "redesign-button-primary-alt-on-blurple-pressed-border" | "redesign-button-primary-alt-on-blurple-text" | "redesign-button-primary-alt-pressed-background" | "redesign-button-primary-alt-pressed-border" | "redesign-button-primary-alt-pressed-text" | "redesign-button-primary-alt-text" | "redesign-button-primary-background" | "redesign-button-primary-on-blurple-pressed-text" | "redesign-button-primary-overlay-background" | "redesign-button-primary-overlay-pressed-background" | "redesign-button-primary-overlay-text" | "redesign-button-primary-pressed-background" | "redesign-button-primary-text" | "redesign-button-secondary-background" | "redesign-button-secondary-border" | "redesign-button-secondary-overlay-background" | "redesign-button-secondary-overlay-pressed-background" | "redesign-button-secondary-overlay-text" | "redesign-button-secondary-pressed-background" | "redesign-button-secondary-pressed-border" | "redesign-button-secondary-text" | "redesign-button-selected-background" | "redesign-button-selected-pressed-background" | "redesign-button-selected-text" | "redesign-button-tertiary-background" | "redesign-button-tertiary-pressed-background" | "redesign-button-tertiary-pressed-text" | "redesign-button-tertiary-text" | "redesign-channel-category-name-text" | "redesign-channel-message-preview-text" | "redesign-channel-name-muted-text" | "redesign-channel-name-text" | "redesign-chat-input-background" | "redesign-image-button-pressed-background" | "redesign-input-control-active-bg" | "redesign-input-control-selected" | "redesign-only-background-active" | "redesign-only-background-default" | "redesign-only-background-overlay" | "redesign-only-background-raised" | "redesign-only-background-sunken" | "scrollbar-auto-scrollbar-color-thumb" | "scrollbar-auto-scrollbar-color-track" | "scrollbar-auto-thumb" | "scrollbar-auto-track" | "scrollbar-thin-thumb" | "scrollbar-thin-track" | "spoiler-hidden-background" | "spoiler-revealed-background" | "stage-card-pill-bg" | "status-danger" | "status-danger-background" | "status-danger-text" | "status-dnd" | "status-idle" | "status-offline" | "status-online" | "status-positive" | "status-positive-background" | "status-positive-text" | "status-speaking" | "status-warning" | "status-warning-background" | "status-warning-text" | "text-brand" | "text-danger" | "text-link" | "text-link-low-saturation" | "text-low-contrast" | "text-message-preview-low-sat" | "text-muted" | "text-muted-on-default" | "text-normal" | "text-positive" | "text-primary" | "text-secondary" | "text-warning" | "textbox-markdown-syntax" | "theme-locked-blur-fallback" | "thread-channel-spine" | "toast-bg" | "typing-indicator-bg" | "user-profile-header-overflow-background" | "voice-video-video-tile-background" | "voice-video-video-tile-blur-fallback" | "white" | "you-bar-bg", string>; diff --git a/src/metro/common/components.ts b/src/metro/common/components.ts index 49e69c6..514ffb9 100644 --- a/src/metro/common/components.ts +++ b/src/metro/common/components.ts @@ -12,7 +12,7 @@ const bySingularProp = createFilterDefinition<[string]>( ); const findSingular = (prop: string) => proxyLazy(() => findExports(bySingularProp(prop))?.[prop]); -const findProp = (prop: string) => proxyLazy(() => findByProps(prop)[prop]); +const findProp = (...props: string[]) => proxyLazy(() => findByProps(...props)[props[0]]); // Discord export const LegacyAlert = findByDisplayNameLazy("FluxContainer(Alert)"); @@ -27,18 +27,22 @@ export const ActionSheetRow = findProp("ActionSheetRow"); // Buttons export const Button = findSingular("Button") as t.Button; -export const TwinButtons = findProp("TwinButtons"); +export const TwinButtons = findProp("TwinButtons") as t.TwinButtons; export const IconButton = findSingular("IconButton") as t.IconButton; export const RowButton = findProp("RowButton") as t.RowButton; export const PressableScale = findProp("PressableScale"); // Tables -export const TableRow = findProp("TableRow"); -export const TableRowIcon = findProp("TableRowIcon"); -export const TableRowTrailingText = findProp("TableRowTrailingText"); -export const TableRowGroup = findProp("TableRowGroup"); -export const TableSwitchRow = findProp("TableSwitchRow"); +export const TableRow = findProp("TableRow") as t.TableRow; +export const TableRowIcon = findProp("TableRowIcon") as t.TableRowIcon; +export const TableRowTrailingText = findProp("TableRowTrailingText") as t.TableRowTrailingText; +export const TableRowGroup = findProp("TableRowGroup") as t.TableRowGroup; +export const TableRadioGroup = findProp("TableRadioGroup") as t.TableRadioGroup; +export const TableRadioRow = findProp("TableRadioRow") as t.TableRadioRow; +export const TableSwitchRow = findProp("TableSwitchRow") as t.TableSwitchRow; +export const TableCheckboxRow = findProp("TableCheckboxRow") as t.TableCheckboxRow; + export const TableSwitch = findSingular("FormSwitch"); export const TableRadio = findSingular("FormRadio"); export const TableCheckbox = findSingular("FormCheckbox"); @@ -51,11 +55,22 @@ export const FormCheckbox = findSingular("FormCheckbox"); export const Card = findProp("Card"); export const RedesignCompat = proxyLazy(() => findByProps("RedesignCompat").RedesignCompat); +// Alert +export const AlertModal = findProp("AlertModal"); +export const AlertActionButton = findProp("AlertActionButton"); +export const AlertActions = findProp("AlertActions"); + +// Pile +export const AvatarPile = findSingular("AvatarPile"); + // Misc. export const Stack = findProp("Stack") as t.Stack; +export const Avatar = findProp("default", "AvatarSizes", "getStatusSize"); + // Inputs export const TextInput = findSingular("TextInput") as t.TextInput; +export const TextArea = findSingular("TextArea"); // SegmentedControl export const SegmentedControl = findProp("SegmentedControl") as t.SegmentedControl; diff --git a/src/metro/common/types/components.ts b/src/metro/common/types/components.ts index 1a8bbde..b801625 100644 --- a/src/metro/common/types/components.ts +++ b/src/metro/common/types/components.ts @@ -1,5 +1,6 @@ -import { DiscordTextStyles } from "@ui/types"; -import { MutableRefObject, ReactNode, RefObject } from "react"; +import { TextStyles, ThemeColors } from "@lib/ui/types"; +import { Falsey } from "lodash"; +import { FC, MutableRefObject, PropsWithoutRef, ReactNode, RefObject } from "react"; import type * as RN from "react-native"; import { ImageSourcePropType, PressableProps } from "react-native"; import { SharedValue } from "react-native-reanimated"; @@ -24,11 +25,16 @@ interface ButtonProps { scaleAmountInPx?: number; icon?: ImageSourcePropType | ReactNode; style?: Style; - grow?: boolean; } export type Button = React.ForwardRefExoticComponent; +interface TwinButtonsProps { + children: ReactNode; +} + +export type TwinButtons = React.ComponentType; + // Segmented Control interface SegmentedControlItem { id: string; @@ -103,7 +109,7 @@ interface TextInputProps extends Omit; trailingPressableProps?: PressableProps; trailingText?: string; - value?: string | RN.Falsy; + value?: string | Falsey; } export type TextInput = React.FC; @@ -144,8 +150,8 @@ interface ActionSheetProps { export type ActionSheet = React.FC>; type TextProps = React.ComponentProps & { - variant?: DiscordTextStyles; - color?: string; // TODO: type this + variant?: TextStyles; + color?: ThemeColors; lineClamp?: number; maxFontSizeMultiplier?: number; style?: RN.TextStyle; @@ -163,3 +169,76 @@ interface IconButtonProps { } export type IconButton = React.FC; + +export type PressableScale = React.FC>; + +interface TableRowBaseProps { + arrow?: boolean; + label: string | ReactNode; + subLabel?: string | ReactNode; + variant?: LiteralUnion<"danger", string>; + icon?: JSX.Element | Falsey; + disabled?: boolean; + trailing?: ReactNode | React.ComponentType; +} + +interface TableRowProps extends TableRowBaseProps { + onPress?: () => void; +} + +export type TableRow = React.FC & { + Icon: TableRowIcon; + TrailingText: TableRowTrailingText; + Arrow: FC<{}>; +}; + +interface TableRowTrailingTextProps { + text: string; +} + +export type TableRowTrailingText = FC; + +interface TableRowIconProps { + style?: RN.ImageStyle; + variant?: LiteralUnion<"danger", string>, + source: ImageSourcePropType | undefined; +} + +export type TableRowIcon = React.FC; + +interface TableRowGroupProps { + title: string; + children: ReactNode; +} + +export type TableRowGroup = React.FC; + +interface TableRadioGroupProps { + title: string; + value: string; + hasIcons?: boolean; + onChange: (type: T) => void; + children: ReactNode; +} + +export type TableRadioGroup = FC; + +interface TableRadioRowProps extends TableRowBaseProps { + value: string; +} + +export type TableRadioRow = FC; + +interface TableSwitchRowProps extends TableRowBaseProps { + value: boolean; + onValueChange: (value: boolean) => void; +} + +export type TableSwitchRow = FC; + +interface TableCheckboxRowProps extends TableRowBaseProps { + checked: boolean; + onPress: () => void; +} + +export type TableCheckboxRow = FC; From 292d0ab5e6d07048333c686288268788836cae0e Mon Sep 17 00:00:00 2001 From: pylixonly <82711525+pylixonly@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:38:24 +0800 Subject: [PATCH 05/22] refactor: port wintry minor ui polish --- src/core/debug/safeMode.ts | 17 ++++++ src/core/ui/settings/pages/General/index.tsx | 58 +++++++++++++------- src/core/ui/settings/pages/Plugins/index.tsx | 18 +++++- src/lib/addons/themes/index.ts | 11 ++-- src/metro/common/types/components.ts | 1 + 5 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 src/core/debug/safeMode.ts diff --git a/src/core/debug/safeMode.ts b/src/core/debug/safeMode.ts new file mode 100644 index 0000000..f44bf06 --- /dev/null +++ b/src/core/debug/safeMode.ts @@ -0,0 +1,17 @@ +import { getCurrentTheme, writeThemeToNative } from "@lib/addons/themes"; +import { BundleUpdaterManager } from "@lib/api/native/modules"; +import { settings } from "@lib/api/settings"; + +export function isSafeMode() { + return settings.safeMode?.enabled === true; +} + +export async function toggleSafeMode({ + to = !isSafeMode(), + reload = true +} = {}) { + const enabled = (settings.safeMode ??= { enabled: to }).enabled = to; + const currentColor = getCurrentTheme(); + await writeThemeToNative(enabled ? {} : currentColor?.data ?? {}); + if (reload) setTimeout(() => BundleUpdaterManager.reload(), 500); +} diff --git a/src/core/ui/settings/pages/General/index.tsx b/src/core/ui/settings/pages/General/index.tsx index 6535325..c021a85 100644 --- a/src/core/ui/settings/pages/General/index.tsx +++ b/src/core/ui/settings/pages/General/index.tsx @@ -1,14 +1,17 @@ +import { isSafeMode, toggleSafeMode } from "@core/debug/safeMode"; import { Strings } from "@core/i18n"; import { PyoncordIcon } from "@core/ui/settings"; import About from "@core/ui/settings/pages/General/About"; import { useProxy } from "@core/vendetta/storage"; import { findAssetId } from "@lib/api/assets"; -import { getDebugInfo, toggleSafeMode } from "@lib/api/debug"; +import { getDebugInfo } from "@lib/api/debug"; +import { BundleUpdaterManager } from "@lib/api/native/modules"; import { settings } from "@lib/api/settings"; +import { openAlert } from "@lib/ui/alerts"; import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants"; import { NavigationNative, url } from "@metro/common"; -import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components"; -import { NativeModules, ScrollView } from "react-native"; +import { AlertActionButton, AlertActions, AlertModal, Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components"; +import { ScrollView } from "react-native"; export default function General() { useProxy(settings); @@ -27,14 +30,13 @@ export default function General() { /> } + icon={} trailing={} /> } - trailing={TableRow.Arrow} + icon={} onPress={() => navigation.push("BUNNY_CUSTOM_PAGE", { title: Strings.ABOUT, render: () => , @@ -43,33 +45,51 @@ export default function General() { } - trailing={TableRow.Arrow} + icon={} onPress={() => url.openDeeplink(DISCORD_SERVER)} /> } - trailing={TableRow.Arrow} + icon={} onPress={() => url.openURL(GITHUB)} /> } - onPress={() => NativeModules.BundleUpdaterManager.reload()} + icon={} + onPress={() => BundleUpdaterManager.reload()} /> - } - onPress={toggleSafeMode} + } + value={isSafeMode()} + onValueChange={(to: boolean) => { + toggleSafeMode({ to, reload: false }); + openAlert( + "bunny-reload-safe-mode", + + BundleUpdaterManager.reload()} + /> + + } + /> + ); + }} /> } + icon={} value={settings.developerSettings} onValueChange={(v: boolean) => { settings.developerSettings = v; @@ -80,7 +100,7 @@ export default function General() { } + icon={} value={settings.enableDiscordDeveloperSettings} onValueChange={(v: boolean) => { settings.enableDiscordDeveloperSettings = v; diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 05f852e..da2c11c 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -13,7 +13,7 @@ import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByProps } from "@metro"; import { NavigationNative } from "@metro/common"; -import { Card, FlashList, IconButton, Text } from "@metro/common/components"; +import { Button, Card, FlashList, IconButton, Text } from "@metro/common/components"; import { ComponentProps } from "react"; import { View } from "react-native"; @@ -53,7 +53,7 @@ function PluginPage(props: PluginPageProps) { "description", p => p.authors?.map( (a: Author | string) => typeof a === "string" ? a : a.name - ).join() + ).join() || "" ]} sortOptions={{ "Name (A-Z)": (a, b) => a.name.localeCompare(b.name), @@ -119,6 +119,20 @@ export default function Plugins() { ; }} + ListFooterComponent={() => ( +