Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/pyoncord/Bunny
Browse files Browse the repository at this point in the history
  • Loading branch information
pylixonly committed May 18, 2024
2 parents 73ebfbc + a0d1808 commit a7e7566
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 12 deletions.
17 changes: 14 additions & 3 deletions src/core/i18n/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,31 @@
"DEBUG": "Debug",
"DEBUGGER_URL": "Debugger URL",
"DELETE": "Delete",
"DESC_EXTRACT_FONTS_FROM_THEME": "Looks out for \"fonts\" field in your currently applied theme and install it.",
"DEVELOPER": "Developer",
"DEVELOPER_SETTINGS": "Developer Settings",
"DISABLE_THEME": "Disable Theme",
"DISABLE_UPDATES": "Disable updates",
"DISCORD_SERVER": "Discord Server",
"DONE": "Done",
"ENABLE_EVAL_COMMAND": "Enable /eval command",
"ENABLE_EVAL_COMMAND_DESC": "Evaluate JavaScript directly from command. Be cautious when using this command as it may pose a security risk. Make sure to know what you are doing.",
"ENABLE_UPDATES": "Enable updates",
"ERROR_BOUNDARY_TOOLS_LABEL": "ErrorBoundary Tools",
"EXTRACT": "Extract",
"FONT_NAME": "Font Name",
"FONTS": "Fonts",
"GENERAL": "General",
"GITHUB": "GitHub",
"HOLD_UP": "Hold Up",
"INFO": "Info",
"INSTALL": "Install",
"INSTALL_ADDON": "Install Add-on",
"INSTALL_PLUGIN": "Install Plugin",
"INSTALL_ADDON": "Install an add-on",
"INSTALL_FONT": "Install a font",
"INSTALL_PLUGIN": "Install a plugin",
"INSTALL_REACT_DEVTOOLS": "Install React DevTools",
"INSTALL_THEME": "Install Theme",
"LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme",
"LINKS": "Links",
"LOAD_FROM_CUSTOM_URL": "Load from custom URL",
"LOAD_FROM_CUSTOM_URL_DEC": "Load Bunny from a custom endpoint.",
Expand Down Expand Up @@ -77,17 +84,21 @@
"RELOAD_IN_NORMAL_MODE_DESC": "This will reload Discord normally",
"RELOAD_IN_SAFE_MODE": "Reload in Safe Mode",
"RELOAD_IN_SAFE_MODE_DESC": "This will reload Discord without loading addons",
"RESTART_REQUIRED_TO_TAKE_EFFECT": "Restart required to take effect",
"REMOVE": "Remove",
"RESTART_REQUIRED_TO_TAKE_EFFECT": "Restart is required to take effect",
"RETRY": "Retry",
"RETRY_RENDER": "Retry Render",
"SAFE_MODE": "Safe Mode",
"SAFE_MODE_NOTICE_FONTS": "You are in Safe Mode, meaning fonts have been temporarily disabled. {enabled, select, true {If a font appears to be causing the issue, you can press below to disable it persistently.} other {}}",
"SAFE_MODE_NOTICE_PLUGINS": "You are in Safe Mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page.",
"SAFE_MODE_NOTICE_THEMES": "You are in Safe Mode, meaning themes have been temporarily disabled. {enabled, select, true {If a theme appears to be causing the issue, you can press below to disable it persistently.} other {}}",
"SEARCH": "Search",
"SEPARATOR": ", ",
"SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS": "Activate Discord Experiments",
"SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC": "Warning: Messing with this feature may lead to account termination. We are not responsible for what you do with this feature.",
"STACK_TRACE": "Stack Trace",
"SUCCESSFULLY_INSTALLED": "Successfully installed",
"THEME_EXTRACTOR_DESC": "This pack overrides the following: {fonts}",
"THEME_REFETCH_FAILED": "Failed to refetch theme!",
"THEME_REFETCH_SUCCESSFUL": "Successfully refetched theme.",
"THEMES": "Themes",
Expand Down
21 changes: 15 additions & 6 deletions src/core/ui/components/AddonPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,30 @@ import { clipboard } from "@metro/common";
import { showInputAlert } from "@ui/alerts";
import { ErrorBoundary, Search } from "@ui/components";
import fuzzysort from "fuzzysort";
import { createContext } from "react";
import { FlatList, View } from "react-native";

export const RemoveModeContext = createContext(false);

interface AddonPageProps<T> {
title: string;
floatingButtonText: string;
fetchFunction: (url: string) => Promise<void>;
items: Record<string, T & { id: string; }>;
items: Record<string, T & ({ id: string; } | { name: string; })>;
safeModeMessage: string;
safeModeExtras?: JSX.Element | JSX.Element[];
card: React.ComponentType<CardWrapper<T>>;
isRemoveMode?: boolean;
headerComponent?: JSX.Element;
}

function getItemsByQuery<T extends { id?: string; }>(items: T[], query: string): T[] {
function getItemsByQuery<T extends AddonPageProps<unknown>["items"][string]>(items: T[], query: string): T[] {
if (!query) return items;

return fuzzysort.go(query, items, {
keys: [
"id",
"name",
"manifest.name",
"manifest.description",
"manifest.authors.0.name",
Expand All @@ -39,9 +45,9 @@ function getItemsByQuery<T extends { id?: string; }>(items: T[], query: string):
const reanimated = findByProps("useSharedValue");
const { FloatingActionButton } = findByProps("FloatingActionButton");

export default function AddonPage<T>({ floatingButtonText, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent }: AddonPageProps<T>) {
useProxy(settings);
export default function AddonPage<T>({ floatingButtonText, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent, isRemoveMode, headerComponent }: AddonPageProps<T>) {
useProxy(items);
useProxy(settings);

const collapseText = reanimated.useSharedValue(0);
const yOffset = React.useRef<number>(0);
Expand All @@ -61,6 +67,7 @@ export default function AddonPage<T>({ floatingButtonText, fetchFunction, items,
onChangeText={(v: string) => setSearch(v.toLowerCase())}
placeholder={Strings.SEARCH}
/>
{headerComponent}
</>}
onScroll={e => {
if (e.nativeEvent.contentOffset.y <= 0) return;
Expand All @@ -69,8 +76,10 @@ export default function AddonPage<T>({ floatingButtonText, fetchFunction, items,
}}
style={{ paddingHorizontal: 10, paddingTop: 10 }}
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
data={getItemsByQuery(Object.values(items), search)}
renderItem={({ item, index }) => <CardComponent item={item} index={index} />}
data={getItemsByQuery(Object.values(items).filter(i => typeof i === "object"), search)}
renderItem={({ item, index }) => <RemoveModeContext.Provider value={!!isRemoveMode}>
<CardComponent item={item} index={index} />
</RemoveModeContext.Provider>}
/>
<FloatingActionButton
text={floatingButtonText}
Expand Down
41 changes: 41 additions & 0 deletions src/core/ui/components/FontCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Strings } from "@core/i18n";
import { CardWrapper } from "@core/ui/components/Card";
import { getAssetIDByName } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, removeFont, selectFont } from "@lib/managers/fonts";
import { FormCheckbox, IconButton, TableRow, TableRowGroup } from "@lib/ui/components/discord/Redesign";
import { showToast } from "@lib/ui/toasts";
import { useContext } from "react";
import { Pressable, View } from "react-native";

import { RemoveModeContext } from "./AddonPage";

export default function FontCard({ item: font, index }: CardWrapper<FontDefinition>) {
useProxy(fonts);

const removeMode = useContext(RemoveModeContext);
const selected = fonts.__selected === font.name;

return <View key={index} style={{ marginVertical: 4 }}>
<TableRowGroup>
<TableRow
label={font.name}
subLabel={font.previewText ?? "Lorem ipsum is placeholder text commonly used in the graphic, print, and publishing industries for previewing layouts and visual mockups."}
trailing={
<Pressable onPress={() => {
selectFont(selected ? null : font.name).then(() => {
showToast(Strings.RESTART_REQUIRED_TO_TAKE_EFFECT, getAssetIDByName("WarningIcon"));
});
}}>
{!removeMode ? <FormCheckbox checked={selected} /> : <IconButton
size="sm"
variant="secondary"
icon={getAssetIDByName("TrashIcon")}
onPress={() => removeFont(font.name)}
/>}
</Pressable>
}
/>
</TableRowGroup>
</View>;
}
9 changes: 8 additions & 1 deletion src/core/ui/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Strings } from "@core/i18n";
import { getAssetIDByName } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { isFontSupported, isThemeSupported } from "@lib/api/native/loader";
import { useProxy } from "@lib/api/storage";
import { settings } from "@lib/settings";
import { registerSection } from "@lib/ui/settings";
Expand Down Expand Up @@ -38,6 +38,13 @@ export default function initSettings() {
render: () => import("@core/ui/settings/pages/Themes"),
usePredicate: () => isThemeSupported()
},
{
key: "BUNNY_FONTS",
title: () => Strings.FONTS,
icon: getAssetIDByName("ic_add_text"),
render: () => import("@core/ui/settings/pages/Fonts"),
usePredicate: () => isFontSupported()
},
{
key: "BUNNY_DEVELOPER",
title: () => Strings.DEVELOPER,
Expand Down
138 changes: 138 additions & 0 deletions src/core/ui/settings/pages/Fonts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { formatString, Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import FontCard from "@core/ui/components/FontCard";
import { getAssetIDByName } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, installFont, saveFont } from "@lib/managers/fonts";
import { getCurrentTheme } from "@lib/managers/themes";
import { findByProps } from "@lib/metro/filters";
import { settings } from "@lib/settings";
import { ErrorBoundary } from "@lib/ui/components";
import { FormText } from "@lib/ui/components/discord/Forms";
import { ActionSheet, BottomSheetTitleHeader, Button, RowButton, TableRow, Text, TextInput, useNavigation } from "@lib/ui/components/discord/Redesign";
import { useEffect, useState } from "react";
import { TouchableOpacity, View } from "react-native";

const actionSheet = findByProps("hideActionSheet");

function guessFontName(urls: string[]) {
const fileNames = urls.map(url => {
const { pathname } = new URL(url);
const fileName = pathname.replace(/\.[^/.]+$/, "");
return fileName.split("/").pop();
}).filter(Boolean) as string[];

const shortest = fileNames.reduce((shortest, name) => {
return name.length < shortest.length ? name : shortest;
}, fileNames[0] || "");

return shortest?.replace(/-[A-Za-z]*$/, "") || null;
}

function ExtractFontsComponent({ fonts }: { fonts: Record<string, string>; }) {
const [fontName, setFontName] = useState(guessFontName(Object.values(fonts)));
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | void>(undefined);

return <View style={{ padding: 8, paddingBottom: 16, gap: 12 }}>
<TextInput
size="md"
label={Strings.FONT_NAME}
value={fontName}
placeholder={fontName || "Whitney"}
onChange={setFontName}
errorMessage={error}
status={error ? "error" : void 0}
/>
<Text variant="text-xs/normal" color="text-muted">
{formatString("THEME_EXTRACTOR_DESC", {
fonts: Object.keys(fonts).join(Strings.SEPARATOR)
})}
</Text>
<Button
size="md"
variant="primary"
text={Strings.EXTRACT}
disabled={!fontName || saving}
loading={saving}
onPress={() => {
setSaving(true);
saveFont({
spec: 1,
name: fontName!.trim(),
main: fonts
})
.then(() => actionSheet.hideActionSheet())
.catch(e => setError(String(e)))
.finally(() => setSaving(false));
}}
/>
</View>;
}

function promptFontExtractor() {
const currentTheme = getCurrentTheme()?.data;
if (!currentTheme || !("fonts" in currentTheme)) return;

const fonts = currentTheme.fonts as Record<string, string>;

actionSheet.openLazy(
Promise.resolve({
default: () => (
<ErrorBoundary>
<ActionSheet>
<BottomSheetTitleHeader title={Strings.LABEL_EXTRACT_FONTS_FROM_THEME} />
<ExtractFontsComponent fonts={fonts} />
</ActionSheet>
</ErrorBoundary>
)
}),
"FontsFromThemeExtractorActionSheet"
);
}

export default function Plugins() {
useProxy(settings);
useProxy(fonts);

const [removeMode, setRemoveMode] = useState(false);

const navigation = useNavigation();

useEffect(() => {
const onPressCallback = () => {
setRemoveMode(x => !x);
};

navigation.setOptions({
headerRight: () => <TouchableOpacity onPress={onPressCallback}>
<FormText style={{ marginRight: 12 }}>
{removeMode ? Strings.DONE : Strings.REMOVE}
</FormText>
</TouchableOpacity>
});
}, [removeMode]);

return (
<AddonPage<FontDefinition>
title={Strings.FONTS}
floatingButtonText={Strings.INSTALL_FONT}
fetchFunction={installFont}
items={fonts as Record<string, FontDefinition>}
safeModeMessage={Strings.SAFE_MODE_NOTICE_FONTS}
isRemoveMode={removeMode}
card={FontCard}
headerComponent={<>
{/* @ts-ignore */}
{getCurrentTheme()?.data?.fonts && <View style={{ marginVertical: 8 }}>
<RowButton
label={Strings.LABEL_EXTRACT_FONTS_FROM_THEME}
subLabel={Strings.DESC_EXTRACT_FONTS_FROM_THEME}
icon={<TableRow.Icon source={getAssetIDByName("HammerIcon")} />}
onPress={promptFontExtractor}
/>
</View>}
</>}
/>
);
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { removeFile, writeFile } from "@lib/api/native/fs";
import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
import { FileManager } from "@lib/api/native/modules";
import { patchLogHook } from "@lib/debug";
import { updateFonts } from "@lib/managers/fonts";
import { initPlugins } from "@lib/managers/plugins";
import { initThemes, patchChatBackground } from "@lib/managers/themes";
import { patchSettings } from "@lib/ui/settings";
Expand Down Expand Up @@ -60,6 +61,9 @@ export default async () => {
// Once done, load plugins
lib.unload.push(await initPlugins());

// Update the fonts
updateFonts();

// We good :)
logger.log("Bunny is ready!");
};
11 changes: 10 additions & 1 deletion src/lib/api/native/fs.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { FileManager } from "./modules";

/**
* Removes all files in a directory from the path given
* @param path Path to the targeted directory
*/
export async function clearFolder(path: string, prefix = "pyoncord/") {
if (typeof FileManager.clearFolder !== "function") throw new Error("'fs.clearFolder' is not supported");
return void await FileManager.clearFolder("documents", `${prefix}${path}`);
}

/**
* Remove file from given path, currently no check for any failure
* @param path Path to the file
*/
export async function removeFile(path: string, prefix = "pyoncord/") {
if (typeof FileManager.removeFile !== "function") throw new Error("'fs.removeFile' is not supported");
return void await FileManager.removeFile("documents", `${prefix}${path}`);
} // TODO: handle errors from native side properly (discord does not use Promise properly apparently)
}

/**
* Check if the file or directory given by the path exists
Expand Down
6 changes: 6 additions & 0 deletions src/lib/api/native/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,9 @@ export function getLoaderConfigPath() {

return "loader.json";
}

export function isFontSupported() {
if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2;

return false;
}
Loading

0 comments on commit a7e7566

Please sign in to comment.