feat: improve fonts management ux
- 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
This commit is contained in:
parent
9511f17449
commit
b30e33d2c6
3 changed files with 81 additions and 37 deletions
|
|
@ -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,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";
|
||||
|
|
@ -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 => {
|
||||
|
|
@ -130,7 +150,6 @@ function JsonFontImporter({ fonts, setName, setSource }: {
|
|||
.then(() => actionSheet.hideActionSheet())
|
||||
.catch(e => setError(String(e)))
|
||||
.finally(() => setSaving(false));
|
||||
|
||||
}}
|
||||
/>
|
||||
</View>;
|
||||
|
|
@ -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]);
|
||||
|
|
@ -166,6 +186,8 @@ function EntryEditorActionSheet(props: {
|
|||
onPress={() => {
|
||||
delete props.fontEntries[props.name];
|
||||
props.fontEntries[familyName] = fontUrl;
|
||||
props.onChange();
|
||||
actionSheet.hideActionSheet();
|
||||
}}
|
||||
/>
|
||||
</View>;
|
||||
|
|
@ -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>();
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
@ -242,7 +264,7 @@ function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
|
|||
setError(String(e));
|
||||
}
|
||||
}
|
||||
}}
|
||||
})}
|
||||
icon={findAssetId(nameSet ? "PlusSmallIcon" : "ArrowLargeRightIcon")}
|
||||
/>
|
||||
</View>;
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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>}
|
||||
|
|
@ -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];
|
||||
|
|
@ -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 }}>
|
||||
|
|
@ -347,6 +385,7 @@ export default function FontEditor(props: {
|
|||
onPress={async () => {
|
||||
if (!name) return;
|
||||
|
||||
setErrors(undefined);
|
||||
setIsImporting(true);
|
||||
|
||||
if (!props.name) {
|
||||
|
|
@ -354,19 +393,21 @@ export default function FontEditor(props: {
|
|||
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")}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue