Skip to content

Commit

Permalink
Merge branch 'feat/themes-rewrite' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
pylixonly committed Oct 22, 2024
2 parents a0d467a + 0eb28e4 commit ada2480
Show file tree
Hide file tree
Showing 21 changed files with 717 additions and 374 deletions.
53 changes: 34 additions & 19 deletions src/core/ui/components/AddonPage.tsx
Original file line number Diff line number Diff line change
@@ -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 | ((obj: any & {}) => string)>;
type SearchKeywords<T> = Array<string | ((obj: T & {}) => string)>;

interface AddonPageProps<T extends object, I = any> {
title: string;
items: I[];
searchKeywords: SearchKeywords;
sortOptions?: Record<string, (a: I, b: I) => number>;
searchKeywords: SearchKeywords<T>;
sortOptions?: Record<string, (a: T, b: T) => number>;
resolveItem?: (value: I) => T | undefined;
safeModeHint?: {
message?: string;
Expand All @@ -34,6 +34,9 @@ interface AddonPageProps<T extends object, I = any> {
fetchFn?: (url: string) => Promise<void>;
onPress?: () => void;
};

OptionsActionSheetComponent?: ComponentType<any>;

CardComponent: ComponentType<CardWrapper<T>>;
ListHeaderComponent?: ComponentType<any>;
ListFooterComponent?: ComponentType<any>;
Expand All @@ -55,7 +58,7 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<vo

return <AlertModal
title={props.label}
content="Enter the URL of the source you want to install from:"
content="Type in the source URL you want to install from:"
extraContent={
<Stack style={{ marginTop: -12 }}>
<TextInput
Expand Down Expand Up @@ -107,15 +110,27 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<vo
}

export default function AddonPage<T extends object>({ CardComponent, ...props }: AddonPageProps<T>) {
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: () => <IconButton
size="sm"
variant="secondary"
icon={findAssetId("MoreHorizontalIcon")}
onPress={() => 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 });
Expand All @@ -134,7 +149,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
if (results.length === 0 && !search) {
return <View style={{ gap: 32, flexGrow: 1, justifyContent: "center", alignItems: "center" }}>
<View style={{ gap: 8, alignItems: "center" }}>
<Image source={findAssetId("empty_quick_switcher")} />
<Image source={findAssetId("empty_quick_switcher")!} />
<Text variant="text-lg/semibold" color="text-normal">
Oops! Nothing to see here… yet!
</Text>
Expand Down Expand Up @@ -175,7 +190,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
})}
/>}
</View>
{props.ListHeaderComponent && !search && <props.ListHeaderComponent />}
{props.ListHeaderComponent && <props.ListHeaderComponent />}
</View>
);

Expand All @@ -187,12 +202,12 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
estimatedItemSize={136}
ListHeaderComponent={headerElement}
ListEmptyComponent={() => <View style={{ gap: 12, padding: 12, alignItems: "center" }}>
<Image source={findAssetId("devices_not_found")} />
<Image source={findAssetId("devices_not_found")!} />
<Text variant="text-lg/semibold" color="text-normal">
Hmmm... could not find that!
</Text>
</View>}
contentContainerStyle={{ padding: 8, paddingHorizontal: 12 }}
contentContainerStyle={{ padding: 8, paddingHorizontal: 12, paddingBottom: 90 }}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
ListFooterComponent={props.ListFooterComponent}
renderItem={({ item }: any) => <CardComponent item={item.obj} result={item} />}
Expand Down
7 changes: 3 additions & 4 deletions src/core/ui/settings/pages/Themes/ThemeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Theme>) {
export default function ThemeCard({ item: theme }: CardWrapper<VdThemeInfo>) {
useProxy(theme);

const [removed, setRemoved] = React.useState(false);
Expand Down
50 changes: 44 additions & 6 deletions src/core/ui/settings/pages/Themes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<AddonPage<Theme>
<AddonPage<VdThemeInfo>
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",
Expand All @@ -38,6 +43,39 @@ export default function Themes() {
/>
}}
CardComponent={ThemeCard}
OptionsActionSheetComponent={() => {
useObservable([colorsPref]);

return <ActionSheet>
<BottomSheetTitleHeader title="Options" />
<View style={{ paddingVertical: 20, gap: 12 }}>
<TableRadioGroup
title="Override Theme Type"
value={colorsPref.type ?? "auto"}
hasIcons={true}
onChange={type => {
colorsPref.type = type !== "auto" ? type as "dark" | "light" : undefined;
getCurrentTheme()?.data && updateBunnyColor(getCurrentTheme()!.data!, { update: true });
}}
>
<TableRadioRow icon={<TableRowIcon source={findAssetId("RobotIcon")} />} label="Auto" value="auto" />
<TableRadioRow icon={<TableRowIcon source={findAssetId("ThemeDarkIcon")} />} label="Dark" value="dark" />
<TableRadioRow icon={<TableRowIcon source={findAssetId("ThemeLightIcon")} />} label="Light" value="light" />
</TableRadioGroup>
<TableRadioGroup
title="Chat Background"
value={colorsPref.customBackground ?? "shown"}
hasIcons={true}
onChange={type => {
colorsPref.customBackground = type !== "shown" ? type as "hidden" : null;
}}
>
<TableRadioRow icon={<TableRowIcon source={findAssetId("ImageIcon")} />} label="Show" value={"shown"} />
<TableRadioRow icon={<TableRowIcon source={findAssetId("DenyIcon")} />} label="Hide" value={"hidden"} />
</TableRadioGroup>
</View>
</ActionSheet>;
}}
/>
);
}
6 changes: 5 additions & 1 deletion src/core/vendetta/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,23 @@ export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {

export function awaitStorage(...stores: any[]) {
return Promise.all(
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
stores.map(store =>
new Promise<void>(res => store[syncAwaitSymbol](res)))
);
}

export interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}

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
Expand Down
21 changes: 2 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,24 @@ 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";
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(),
patchCommands(),
patchChatBackground(),
patchJsx(),
initVendettaObject(),
initFetchI18nStrings(),
Expand Down
19 changes: 19 additions & 0 deletions src/lib/addons/themes/colors/index.ts
Original file line number Diff line number Diff line change
@@ -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());
}
Loading

0 comments on commit ada2480

Please sign in to comment.