feat: fonts overhaul
This commit is contained in:
parent
6243316c35
commit
2e3dc3c487
7 changed files with 295 additions and 104 deletions
|
|
@ -25,6 +25,7 @@ interface AddonPageProps<T> {
|
|||
card: React.ComponentType<CardWrapper<T>>;
|
||||
isRemoveMode?: boolean;
|
||||
headerComponent?: JSX.Element;
|
||||
onFABPress?: () => void;
|
||||
}
|
||||
|
||||
function getItemsByQuery<T extends AddonPageProps<unknown>["items"][string]>(items: T[], query: string): T[] {
|
||||
|
|
@ -45,7 +46,7 @@ function getItemsByQuery<T extends AddonPageProps<unknown>["items"][string]>(ite
|
|||
const reanimated = findByProps("useSharedValue");
|
||||
const { FloatingActionButton } = findByProps("FloatingActionButton");
|
||||
|
||||
export default function AddonPage<T>({ floatingButtonText, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent, isRemoveMode, headerComponent }: AddonPageProps<T>) {
|
||||
export default function AddonPage<T>({ floatingButtonText, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent, isRemoveMode, headerComponent, onFABPress }: AddonPageProps<T>) {
|
||||
useProxy(items);
|
||||
useProxy(settings);
|
||||
|
||||
|
|
@ -83,9 +84,9 @@ export default function AddonPage<T>({ floatingButtonText, fetchFunction, items,
|
|||
/>
|
||||
<FloatingActionButton
|
||||
text={floatingButtonText}
|
||||
icon={getAssetIDByName("DownloadIcon")}
|
||||
icon={getAssetIDByName("PlusLargeIcon")}
|
||||
state={{ collapseText }}
|
||||
onPress={() => {
|
||||
onPress={onFABPress ?? (() => {
|
||||
// from ./InstallButton.tsx
|
||||
clipboard.getString().then(content =>
|
||||
showInputAlert({
|
||||
|
|
@ -97,7 +98,7 @@ export default function AddonPage<T>({ floatingButtonText, fetchFunction, items,
|
|||
cancelText: Strings.CANCEL,
|
||||
})
|
||||
);
|
||||
}}
|
||||
})}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export default function Card(props: CardProps) {
|
|||
})}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={getAssetIDByName("CircleInformationIcon")}
|
||||
icon={getAssetIDByName("CircleInformationIcon-primary")}
|
||||
/>}
|
||||
{props.actions?.map(({ icon, onPress, disabled }) => (
|
||||
<IconButton
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export default function FontCard({ item: font, index }: CardWrapper<FontDefiniti
|
|||
<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(() => {
|
||||
|
|
|
|||
274
src/core/ui/settings/pages/FontImporter.tsx
Normal file
274
src/core/ui/settings/pages/FontImporter.tsx
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { getAssetIDByName } from "@lib/api/assets";
|
||||
import { createProxy, useProxy } from "@lib/api/storage";
|
||||
import { FontDefinition, saveFont, validateFont } from "@lib/managers/fonts";
|
||||
import { getCurrentTheme } from "@lib/managers/themes";
|
||||
import { findByProps } from "@lib/metro";
|
||||
import { ErrorBoundary } from "@lib/ui/components";
|
||||
import { ActionSheet, BottomSheetTitleHeader, Button, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput, useNavigation } from "@lib/ui/components/discord/Redesign";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { ScrollView, 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 RevengeFontsExtractor({ fonts, setName }: {
|
||||
fonts: Record<string, string>;
|
||||
setName: (name: string) => void;
|
||||
}) {
|
||||
const currentTheme = getCurrentTheme()!.data;
|
||||
// @ts-ignore
|
||||
const themeFonts = currentTheme!.fonts as Record<string, string>;
|
||||
|
||||
const [fontName, setFontName] = useState(guessFontName(Object.values(themeFonts)));
|
||||
const [error, setError] = useState<string | void>(undefined);
|
||||
|
||||
return <View style={{ padding: 8, paddingBottom: 16, gap: 12 }}>
|
||||
<TextInput
|
||||
autoFocus
|
||||
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(themeFonts).join(Strings.SEPARATOR)
|
||||
})}
|
||||
</Text>
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
text={Strings.EXTRACT}
|
||||
disabled={!fontName}
|
||||
onPress={() => {
|
||||
if (!fontName) return;
|
||||
try {
|
||||
validateFont({
|
||||
spec: 1,
|
||||
name: fontName,
|
||||
main: themeFonts
|
||||
});
|
||||
|
||||
setName(fontName);
|
||||
Object.assign(fonts, themeFonts);
|
||||
actionSheet.hideActionSheet();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>;
|
||||
}
|
||||
|
||||
function JsonFontImporter({ fonts, setName }: {
|
||||
fonts: Record<string, string>;
|
||||
setName: (name: string) => void;
|
||||
}) {
|
||||
const [fontLink, setFontLink] = useState<string>("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | void>(undefined);
|
||||
|
||||
return <View style={{ padding: 8, paddingBottom: 16, gap: 12 }}>
|
||||
<TextInput
|
||||
autoFocus
|
||||
size="md"
|
||||
label={"Font Link"}
|
||||
value={fontLink}
|
||||
placeholder={"https://link.to/font/pack.json"}
|
||||
onChange={setFontLink}
|
||||
errorMessage={error}
|
||||
status={error ? "error" : void 0}
|
||||
/>
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
text={"Import"}
|
||||
disabled={!fontLink || saving}
|
||||
loading={saving}
|
||||
onPress={() => {
|
||||
setSaving(true);
|
||||
|
||||
(async () => {
|
||||
const res = await safeFetch(fontLink, { cache: "no-store" });
|
||||
const json = await res.json() as FontDefinition;
|
||||
validateFont(json);
|
||||
|
||||
setName(json.name);
|
||||
Object.assign(fonts, json.main);
|
||||
})()
|
||||
.then(() => actionSheet.hideActionSheet())
|
||||
.catch(e => setError(String(e)))
|
||||
.finally(() => setSaving(false));
|
||||
|
||||
}}
|
||||
/>
|
||||
</View>;
|
||||
}
|
||||
|
||||
function promptActionSheet(Component: any, fontEntries: Record<string, string>, setName: (name: string) => void) {
|
||||
actionSheet.openLazy(
|
||||
Promise.resolve({
|
||||
default: () => (
|
||||
<ErrorBoundary>
|
||||
<ActionSheet>
|
||||
<BottomSheetTitleHeader title={"Import Font"} />
|
||||
<Component fonts={fontEntries} setName={setName} />
|
||||
</ActionSheet>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}),
|
||||
"FontImporterActionSheet"
|
||||
);
|
||||
}
|
||||
|
||||
function EntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
|
||||
const nameRef = useRef<string>();
|
||||
const urlRef = useRef<string>();
|
||||
|
||||
const [nameSet, setNameSet] = useState(false);
|
||||
const [error, setError] = useState<string | void>();
|
||||
|
||||
return <View style={{ flexDirection: "row", gap: 8, justifyContent: "flex-start" }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<TextInput
|
||||
isRound
|
||||
size="md"
|
||||
label={nameSet ? nameRef.current : void 0}
|
||||
placeholder={nameSet ? "https://path.to/the/file.ttf" : "PostScript name (e.g. ggsans-Bold)"}
|
||||
leadingIcon={() => nameSet ? null : <TableRow.Icon source={getAssetIDByName("PlusSmallIcon")} />}
|
||||
leadingText={nameSet ? nameRef.current : ""}
|
||||
onChange={(text: string) => (nameSet ? urlRef : nameRef).current = text}
|
||||
errorMessage={error}
|
||||
state={error ? "error" : void 0}
|
||||
/>
|
||||
</View>
|
||||
{nameSet && <IconButton
|
||||
size="md"
|
||||
variant="secondary"
|
||||
onPress={() => {
|
||||
nameRef.current = "";
|
||||
setNameSet(false);
|
||||
}}
|
||||
icon={getAssetIDByName("TrashIcon")}
|
||||
/>}
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="primary"
|
||||
onPress={() => {
|
||||
if (!nameSet && nameRef.current) {
|
||||
setNameSet(true);
|
||||
} else if (nameSet && nameRef.current && urlRef.current) {
|
||||
try {
|
||||
const parsedUrl = new URL(urlRef.current);
|
||||
if (!parsedUrl.protocol || !parsedUrl.host) {
|
||||
throw "Invalid URL";
|
||||
}
|
||||
|
||||
fontEntry[nameRef.current] = urlRef.current;
|
||||
nameRef.current = undefined;
|
||||
urlRef.current = undefined;
|
||||
setNameSet(false);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}
|
||||
}}
|
||||
icon={getAssetIDByName(nameSet ? "PlusSmallIcon" : "ArrowLargeRightIcon")}
|
||||
/>
|
||||
</View>;
|
||||
}
|
||||
|
||||
export default function FontImporter() {
|
||||
const [name, setName] = useState<string>();
|
||||
const [importing, setIsImporting] = useState<boolean>(false);
|
||||
|
||||
const memoEntry = useMemo(() => createProxy({}).proxy, []);
|
||||
const fontEntries: Record<string, string> = useProxy(memoEntry);
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
return <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
|
||||
<Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={12}>
|
||||
<TableRowGroup title="Import">
|
||||
{/** @ts-ignore */}
|
||||
{getCurrentTheme()?.data?.fonts && <TableRow
|
||||
label={Strings.LABEL_EXTRACT_FONTS_FROM_THEME}
|
||||
subLabel={Strings.DESC_EXTRACT_FONTS_FROM_THEME}
|
||||
icon={<TableRow.Icon source={getAssetIDByName("HammerIcon")} />}
|
||||
onPress={() => promptActionSheet(RevengeFontsExtractor, fontEntries, setName)}
|
||||
/>}
|
||||
<TableRow
|
||||
label={"Import font entries from a link"}
|
||||
subLabel={"Directly import from a link with a pre-configured JSON file"}
|
||||
icon={<TableRow.Icon source={getAssetIDByName("LinkIcon")} />}
|
||||
onPress={() => promptActionSheet(JsonFontImporter, fontEntries, setName)}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={name}
|
||||
label={Strings.FONT_NAME}
|
||||
placeholder={"ggsans"}
|
||||
onChange={setName}
|
||||
/>
|
||||
<TableRowGroup title={"Font Entries"}>
|
||||
{Object.entries(fontEntries).map(([name, url]) => {
|
||||
return <TableRow
|
||||
label={name}
|
||||
subLabel={url}
|
||||
trailing={<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={getAssetIDByName("TrashIcon")}
|
||||
onPress={() => delete fontEntries[name]}
|
||||
/>}
|
||||
/>;
|
||||
})}
|
||||
<TableRow label={<EntryRow fontEntry={fontEntries} />} />
|
||||
</TableRowGroup>
|
||||
<View style={{ flexDirection: "row", justifyContent: "flex-end", bottom: 0, left: 0 }}>
|
||||
<Button
|
||||
size="lg"
|
||||
loading={importing}
|
||||
disabled={importing || !name || Object.keys(fontEntries).length === 0}
|
||||
variant="primary"
|
||||
text={"Import"}
|
||||
onPress={async () => {
|
||||
if (!name) return;
|
||||
|
||||
setIsImporting(true);
|
||||
|
||||
saveFont({
|
||||
spec: 1,
|
||||
name: name,
|
||||
main: fontEntries
|
||||
})
|
||||
.then(() => navigation.goBack())
|
||||
.finally(() => setIsImporting(false));
|
||||
}}
|
||||
icon={getAssetIDByName("DownloadIcon")}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
</Stack>
|
||||
</ScrollView>;
|
||||
}
|
||||
|
|
@ -1,95 +1,15 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { 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 { FontDefinition, fonts, installFont } from "@lib/managers/fonts";
|
||||
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 { useNavigation } from "@lib/ui/components/discord/Redesign";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { TouchableOpacity } 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"
|
||||
);
|
||||
}
|
||||
import FontImporter from "./FontImporter";
|
||||
|
||||
export default function Plugins() {
|
||||
useProxy(settings);
|
||||
|
|
@ -122,17 +42,12 @@ export default function Plugins() {
|
|||
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>}
|
||||
</>}
|
||||
onFABPress={() => {
|
||||
navigation.push("VendettaCustomPage", {
|
||||
title: "Font Importer",
|
||||
render: () => <FontImporter />
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default function General() {
|
|||
<TableRow
|
||||
arrow
|
||||
label={Strings.ABOUT}
|
||||
icon={<TableRow.Icon source={getAssetIDByName("CircleInformationIcon")} />}
|
||||
icon={<TableRow.Icon source={getAssetIDByName("CircleInformationIcon-primary")} />}
|
||||
trailing={TableRow.Arrow}
|
||||
onPress={() => navigation.push("VendettaCustomPage", {
|
||||
title: Strings.ABOUT,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ export async function saveFont(data: string | FontDefinition, selected = false)
|
|||
}
|
||||
|
||||
export async function installFont(url: string, selected = false) {
|
||||
if (typeof url !== "string" || Object.values(fonts).some(f => typeof f === "object" && f.__source === url)
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue