chore: merge dev to main (#53)
This commit is contained in:
commit
566feefc2a
6 changed files with 144 additions and 69 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,3 +1,18 @@
|
|||
# [1.3.0-dev.2](https://github.com/revenge-mod/revenge-bundle/compare/v1.3.0-dev.1...v1.3.0-dev.2) (2024-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* wrap `addonpage` in `safeareaview` ([724fc1e](https://github.com/revenge-mod/revenge-bundle/commit/724fc1e135850d9439abcba03df1cd288ca3593b))
|
||||
|
||||
# [1.3.0-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.2.0...v1.3.0-dev.1) (2024-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improve fonts management ux ([b30e33d](https://github.com/revenge-mod/revenge-bundle/commit/b30e33d2c6be9d5fefc26903fecbf8fcccc0df42))
|
||||
* show error when failing to import font entries ([9511f17](https://github.com/revenge-mod/revenge-bundle/commit/9511f174498dfa5758ad60a2e918220a685849e0))
|
||||
|
||||
# [1.2.0](https://github.com/revenge-mod/revenge-bundle/compare/v1.1.2...v1.2.0) (2024-10-20)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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 { Button, FlashList, FloatingActionButton, HelpMessage, IconButton, SafeAreaView, Stack, Text, TextInput } from "@metro/common/components";
|
||||
import { ErrorBoundary, Search } from "@ui/components";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { ComponentType, ReactNode, useCallback, useMemo } from "react";
|
||||
|
|
@ -181,26 +181,28 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<FlashList
|
||||
data={results}
|
||||
extraData={search}
|
||||
estimatedItemSize={136}
|
||||
ListHeaderComponent={headerElement}
|
||||
ListEmptyComponent={() => <View style={{ gap: 12, padding: 12, alignItems: "center" }}>
|
||||
<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 }}
|
||||
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
|
||||
ListFooterComponent={props.ListFooterComponent}
|
||||
renderItem={({ item }: any) => <CardComponent item={item.obj} result={item} />}
|
||||
/>
|
||||
{props.installAction && <FloatingActionButton
|
||||
icon={findAssetId("PlusLargeIcon")}
|
||||
onPress={onInstallPress}
|
||||
/>}
|
||||
<SafeAreaView>
|
||||
<FlashList
|
||||
data={results}
|
||||
extraData={search}
|
||||
estimatedItemSize={136}
|
||||
ListHeaderComponent={headerElement}
|
||||
ListEmptyComponent={() => <View style={{ gap: 12, padding: 12, alignItems: "center" }}>
|
||||
<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 }}
|
||||
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
|
||||
ListFooterComponent={props.ListFooterComponent}
|
||||
renderItem={({ item }: any) => <CardComponent item={item.obj} result={item} />}
|
||||
/>
|
||||
{props.installAction && <FloatingActionButton
|
||||
icon={findAssetId("PlusLargeIcon")}
|
||||
onPress={onInstallPress}
|
||||
/>}
|
||||
</SafeAreaView>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export default function FontCard({ item: font }: CardWrapper<FontDefinition>) {
|
|||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={selected}
|
||||
icon={findAssetId("PencilIcon")}
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,46 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { createProxy, useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/addons/fonts";
|
||||
import { FontDefinition, fonts, removeFont, saveFont, updateFont, validateFont } from "@lib/addons/fonts";
|
||||
import { getCurrentTheme } from "@lib/addons/themes";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { semanticColors } from "@lib/ui/color";
|
||||
import { createStyles, TextStyleSheet } from "@lib/ui/styles";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { ActionSheet, BottomSheetTitleHeader, Button, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput } from "@metro/common/components";
|
||||
import { findByPropsLazy } from "@metro/wrappers";
|
||||
import { findByProps, findByPropsLazy } from "@metro/wrappers";
|
||||
import { ErrorBoundary } from "@ui/components";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
|
||||
const useStyles = createStyles({
|
||||
errorText: {
|
||||
...TextStyleSheet["text-xs/medium"],
|
||||
color: semanticColors.TEXT_DANGER,
|
||||
},
|
||||
});
|
||||
|
||||
const actionSheet = findByPropsLazy("hideActionSheet");
|
||||
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
|
||||
|
||||
function promptDetachConfirmationForThen(fontName: string | undefined, cb: () => void) {
|
||||
if (fontName && fonts[fontName].source) openAlert("revenge-fonts-detach-source-confirmation", <AlertModal
|
||||
title="Detach font pack URL?"
|
||||
content="You need to detach the font pack URL from this font pack before you can manually edit its font entries. Do you want to detach the font pack URL?"
|
||||
actions={
|
||||
<Stack>
|
||||
<AlertActionButton text="Detach" variant="destructive" onPress={() => {
|
||||
delete fonts[fontName!].source;
|
||||
cb();
|
||||
}} />
|
||||
<AlertActionButton text={Strings.CANCEL} variant="secondary" />
|
||||
</Stack>
|
||||
}
|
||||
/>);
|
||||
else cb();
|
||||
};
|
||||
|
||||
function guessFontName(urls: string[]) {
|
||||
const fileNames = urls.map(url => {
|
||||
|
|
@ -121,7 +150,6 @@ function JsonFontImporter({ fonts, setName, setSource }: {
|
|||
.then(() => actionSheet.hideActionSheet())
|
||||
.catch(e => setError(String(e)))
|
||||
.finally(() => setSaving(false));
|
||||
|
||||
}}
|
||||
/>
|
||||
</View>;
|
||||
|
|
@ -130,6 +158,7 @@ function JsonFontImporter({ fonts, setName, setSource }: {
|
|||
function EntryEditorActionSheet(props: {
|
||||
fontEntries: Record<string, string>;
|
||||
name: string;
|
||||
onChange: () => void;
|
||||
}) {
|
||||
const [familyName, setFamilyName] = useState<string>(props.name);
|
||||
const [fontUrl, setFontUrl] = useState<string>(props.fontEntries[props.name]);
|
||||
|
|
@ -157,6 +186,8 @@ function EntryEditorActionSheet(props: {
|
|||
onPress={() => {
|
||||
delete props.fontEntries[props.name];
|
||||
props.fontEntries[familyName] = fontUrl;
|
||||
props.onChange();
|
||||
actionSheet.hideActionSheet();
|
||||
}}
|
||||
/>
|
||||
</View>;
|
||||
|
|
@ -182,7 +213,7 @@ function promptActionSheet(
|
|||
);
|
||||
}
|
||||
|
||||
function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
|
||||
function NewEntryRow({ fontName, fontEntry }: { fontName: string | undefined, fontEntry: Record<string, string>; }) {
|
||||
const nameRef = useRef<string>();
|
||||
const urlRef = useRef<string>();
|
||||
|
||||
|
|
@ -215,7 +246,7 @@ function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
|
|||
<IconButton
|
||||
size="md"
|
||||
variant="primary"
|
||||
onPress={() => {
|
||||
onPress={() => promptDetachConfirmationForThen(fontName, () => {
|
||||
if (!nameSet && nameRef.current) {
|
||||
setNameSet(true);
|
||||
} else if (nameSet && nameRef.current && urlRef.current) {
|
||||
|
|
@ -233,7 +264,7 @@ function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
|
|||
setError(String(e));
|
||||
}
|
||||
}
|
||||
}}
|
||||
})}
|
||||
icon={findAssetId(nameSet ? "PlusSmallIcon" : "ArrowLargeRightIcon")}
|
||||
/>
|
||||
</View>;
|
||||
|
|
@ -243,8 +274,11 @@ export default function FontEditor(props: {
|
|||
name?: string;
|
||||
}) {
|
||||
const [name, setName] = useState<string | undefined>(props.name);
|
||||
const [source, setSource] = useState<string>();
|
||||
const [source, setSource] = useState<string | undefined>(props.name && fonts[props.name].source);
|
||||
const [importing, setIsImporting] = useState<boolean>(false);
|
||||
const [errors, setErrors] = useState<Array<Error | undefined> | undefined>();
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
const memoEntry = useMemo(() => {
|
||||
return createProxy(props.name ? { ...fonts[props.name].main } : {}).proxy;
|
||||
|
|
@ -254,6 +288,8 @@ export default function FontEditor(props: {
|
|||
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), 0);
|
||||
|
||||
return <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
|
||||
<Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={12}>
|
||||
{!props.name
|
||||
|
|
@ -277,15 +313,14 @@ export default function FontEditor(props: {
|
|||
label={"Refetch fonts from source"}
|
||||
icon={<TableRow.Icon source={findAssetId("RetryIcon")} />}
|
||||
onPress={async () => {
|
||||
const ftCopy = { ...fonts[props.name!] };
|
||||
await removeFont(props.name!);
|
||||
await saveFont(ftCopy);
|
||||
await updateFont(fonts[props.name!]);
|
||||
navigation.goBack();
|
||||
}}
|
||||
/>
|
||||
<TableRow
|
||||
variant="danger"
|
||||
label={"Delete font pack"}
|
||||
icon={<TableRow.Icon source={findAssetId("TrashIcon")} />}
|
||||
icon={<TableRow.Icon variant="danger" source={findAssetId("TrashIcon")} />}
|
||||
onPress={() => removeFont(props.name!).then(() => navigation.goBack())}
|
||||
/>
|
||||
</TableRowGroup>}
|
||||
|
|
@ -296,32 +331,50 @@ export default function FontEditor(props: {
|
|||
placeholder={"Whitney"}
|
||||
onChange={setName}
|
||||
/>
|
||||
{props.name && fonts[props.name].source && <TextInput
|
||||
size="lg"
|
||||
value={source}
|
||||
label="Font Pack URL"
|
||||
onChange={setSource}
|
||||
/>}
|
||||
<TableRowGroup title="Font Entries">
|
||||
{Object.entries(fontEntries).map(([name, url]) => {
|
||||
{Object.entries(fontEntries).map(([name, url], index) => {
|
||||
const error = errors?.[index];
|
||||
|
||||
return <TableRow
|
||||
label={name}
|
||||
subLabel={url}
|
||||
trailing={<Stack spacing={2} direction="horizontal">
|
||||
subLabel={error ? <Text style={styles.errorText}>{error.message}</Text> : url}
|
||||
trailing={<Stack spacing={8} direction="horizontal">
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("PencilIcon")}
|
||||
onPress={() => promptActionSheet(EntryEditorActionSheet, fontEntries, {
|
||||
name,
|
||||
fontEntries,
|
||||
})}
|
||||
onPress={() => promptDetachConfirmationForThen(props.name, () =>
|
||||
promptActionSheet(EntryEditorActionSheet, fontEntries, {
|
||||
name,
|
||||
fontEntries,
|
||||
onChange: () => {
|
||||
setErrors(undefined);
|
||||
forceUpdate();
|
||||
}
|
||||
})
|
||||
)}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("TrashIcon")}
|
||||
onPress={() => delete fontEntries[name]}
|
||||
onPress={() => promptDetachConfirmationForThen(props.name, () => {
|
||||
delete fontEntries[name];
|
||||
setErrors(undefined);
|
||||
})}
|
||||
/>
|
||||
</Stack>}
|
||||
/>;
|
||||
})}
|
||||
<TableRow label={<NewEntryRow fontEntry={fontEntries} />} />
|
||||
<TableRow label={<NewEntryRow fontName={props.name} fontEntry={fontEntries} />} />
|
||||
</TableRowGroup>
|
||||
{errors && <Text style={styles.errorText}>Some font entries cannot be imported. Please modify the entries and try again.</Text>}
|
||||
<View style={{ flexDirection: "row", justifyContent: "flex-end", bottom: 0, left: 0 }}>
|
||||
<Button
|
||||
size="lg"
|
||||
|
|
@ -332,6 +385,7 @@ export default function FontEditor(props: {
|
|||
onPress={async () => {
|
||||
if (!name) return;
|
||||
|
||||
setErrors(undefined);
|
||||
setIsImporting(true);
|
||||
|
||||
if (!props.name) {
|
||||
|
|
@ -339,18 +393,21 @@ export default function FontEditor(props: {
|
|||
spec: 1,
|
||||
name: name,
|
||||
main: fontEntries,
|
||||
__source: source
|
||||
source
|
||||
})
|
||||
.then(() => navigation.goBack())
|
||||
.catch(e => setErrors(e))
|
||||
.finally(() => setIsImporting(false));
|
||||
} else {
|
||||
Object.assign(fonts[props.name], {
|
||||
name: name,
|
||||
main: fontEntries,
|
||||
__edited: true
|
||||
});
|
||||
setIsImporting(false);
|
||||
navigation.goBack();
|
||||
|
||||
updateFont(fonts[props.name])
|
||||
.then(() => navigation.goBack())
|
||||
.catch(e => setErrors(e))
|
||||
.finally(() => setIsImporting(false));
|
||||
}
|
||||
}}
|
||||
icon={findAssetId(props.name ? "toast_image_saved" : "DownloadIcon")}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import { safeFetch } from "@lib/utils";
|
|||
|
||||
type FontMap = Record<string, string>;
|
||||
|
||||
export interface FontDefinition {
|
||||
export type FontDefinition = {
|
||||
spec: 1;
|
||||
name: string;
|
||||
description?: string;
|
||||
main: FontMap;
|
||||
__source?: string;
|
||||
__edited?: boolean;
|
||||
source?: string
|
||||
}
|
||||
|
||||
type FontStorage = Record<string, FontDefinition> & { __selected?: string; };
|
||||
|
|
@ -40,12 +39,9 @@ export function validateFont(font: FontDefinition) {
|
|||
export async function saveFont(data: string | FontDefinition, selected = false) {
|
||||
let fontDefJson: FontDefinition;
|
||||
|
||||
if (typeof data === "object" && data.__source) data = data.__source;
|
||||
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
fontDefJson = await (await safeFetch(data)).json();
|
||||
fontDefJson.__source = data;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to fetch fonts at ${data}`, { cause: e });
|
||||
}
|
||||
|
|
@ -55,16 +51,14 @@ export async function saveFont(data: string | FontDefinition, selected = false)
|
|||
|
||||
validateFont(fontDefJson);
|
||||
|
||||
try {
|
||||
await Promise.all(Object.entries(fontDefJson.main).map(async ([font, url]) => {
|
||||
let ext = url.split(".").pop();
|
||||
if (ext !== "ttf" && ext !== "otf") ext = "ttf";
|
||||
const path = `downloads/fonts/${fontDefJson.name}/${font}.${ext}`;
|
||||
if (!await fileExists(path)) await downloadFile(url, path);
|
||||
}));
|
||||
} catch (e) {
|
||||
throw new Error("Failed to download font assets", { cause: e });
|
||||
}
|
||||
const errors = await Promise.allSettled(Object.entries(fontDefJson.main).map(async ([font, url]) => {
|
||||
let ext = url.split(".").pop();
|
||||
if (ext !== "ttf" && ext !== "otf") ext = "ttf";
|
||||
const path = `downloads/fonts/${fontDefJson.name}/${font}.${ext}`;
|
||||
if (!await fileExists(path)) await downloadFile(url, path);
|
||||
})).then(it => it.map(it => it.status === 'fulfilled' ? undefined : it.reason));
|
||||
|
||||
if (errors.some(it => it)) throw errors
|
||||
|
||||
fonts[fontDefJson.name] = fontDefJson;
|
||||
|
||||
|
|
@ -72,14 +66,21 @@ export async function saveFont(data: string | FontDefinition, selected = false)
|
|||
return fontDefJson;
|
||||
}
|
||||
|
||||
export async function installFont(url: string, selected = false) {
|
||||
if (
|
||||
typeof url !== "string"
|
||||
|| Object.values(fonts).some(f => typeof f === "object" && f.__source === url)
|
||||
) {
|
||||
throw new Error("Invalid source or font was already installed");
|
||||
export async function updateFont(fontDef: FontDefinition) {
|
||||
let fontDefCopy = { ...fontDef }
|
||||
if (fontDefCopy.source) fontDefCopy = {
|
||||
...await fetch(fontDefCopy.source).then(it => it.json()),
|
||||
// Can't change these properties
|
||||
name: fontDef.name,
|
||||
source: fontDef.source
|
||||
}
|
||||
|
||||
const selected = fonts.__selected === fontDef.name
|
||||
await removeFont(fontDef.name)
|
||||
await saveFont(fontDefCopy, selected)
|
||||
}
|
||||
|
||||
export async function installFont(url: string, selected = false) {
|
||||
const font = await saveFont(url);
|
||||
if (selected) await selectFont(font.name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export async function readFile(path: string, { prefix = "pyoncord/" } = {}): Pro
|
|||
export async function downloadFile(url: string, path: string, { prefix = "pyoncord/" } = {}) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download file from ${url}: ${response.statusText}`);
|
||||
throw new Error(`Failed to download file from ${url}: ${response.status}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue