Skip to content

Commit

Permalink
feat: improve fonts management ux
Browse files Browse the repository at this point in the history
- Fixed an issue where errors aren't reset when reimporting after fixing an invalid URL
- Fixed an edge case where if you edit a font entry and resync with a source URL, the edit gets overwritten, by adding a detaching mechanism
- Allow editing source URLs
- Rerender at correct times
  • Loading branch information
PalmDevs committed Oct 21, 2024
1 parent 9511f17 commit b30e33d
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 37 deletions.
2 changes: 1 addition & 1 deletion src/core/ui/settings/pages/Fonts/FontCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
89 changes: 65 additions & 24 deletions src/core/ui/settings/pages/Fonts/FontEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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";
Expand All @@ -18,9 +19,28 @@ const useStyles = createStyles({
...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 => {
Expand Down Expand Up @@ -130,7 +150,6 @@ function JsonFontImporter({ fonts, setName, setSource }: {
.then(() => actionSheet.hideActionSheet())
.catch(e => setError(String(e)))
.finally(() => setSaving(false));

}}
/>
</View>;
Expand All @@ -139,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]);
Expand Down Expand Up @@ -166,6 +186,8 @@ function EntryEditorActionSheet(props: {
onPress={() => {
delete props.fontEntries[props.name];
props.fontEntries[familyName] = fontUrl;
props.onChange();
actionSheet.hideActionSheet();
}}
/>
</View>;
Expand All @@ -191,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>();

Expand Down Expand Up @@ -224,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) {
Expand All @@ -242,7 +264,7 @@ function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
setError(String(e));
}
}
}}
})}
icon={findAssetId(nameSet ? "PlusSmallIcon" : "ArrowLargeRightIcon")}
/>
</View>;
Expand All @@ -252,9 +274,9 @@ 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>>();
const [errors, setErrors] = useState<Array<Error | undefined> | undefined>();

const styles = useStyles();

Expand All @@ -266,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
Expand All @@ -289,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>}
Expand All @@ -308,6 +331,12 @@ 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], index) => {
const error = errors?.[index];
Expand All @@ -320,21 +349,30 @@ export default function FontEditor(props: {
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 }}>
Expand All @@ -347,26 +385,29 @@ export default function FontEditor(props: {
onPress={async () => {
if (!name) return;

setErrors(undefined);
setIsImporting(true);

if (!props.name) {
saveFont({
spec: 1,
name: name,
main: fontEntries,
__source: source
source
})
.then(() => navigation.goBack())
.catch(es => setErrors(es))
.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")}
Expand Down
27 changes: 15 additions & 12 deletions src/lib/addons/fonts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; };
Expand Down Expand Up @@ -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 });
}
Expand All @@ -70,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);
}
Expand Down

0 comments on commit b30e33d

Please sign in to comment.