diff --git a/.zed/settings.json b/.zed/settings.json index eb35176..d8a1fe6 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,7 +1,3 @@ { - "languages": { - "TypeScript": { - "format_on_save": "off" - } - } + "format_on_save": "off", } diff --git a/bun.lock b/bun.lock index 08fd846..d43d55e 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "react-devtools": "^7.0.1", "react-native": "0.83.1", "react-native-reanimated": "^4.2.1", + "react-native-webview": "^13.16.0", "spitroast": "^2.1.6", "type-fest": "^5.3.1", }, @@ -1366,6 +1367,8 @@ "react-native-reanimated": ["react-native-reanimated@4.2.1", "", { "dependencies": { "react-native-is-edge-to-edge": "1.2.1", "semver": "7.7.3" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" } }, "sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg=="], + "react-native-webview": ["react-native-webview@13.16.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA=="], + "react-native-worklets": ["react-native-worklets@0.6.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, ""], "react-refresh": ["react-refresh@0.14.2", "", {}, ""], @@ -1708,7 +1711,7 @@ "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "@electron/get/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@electron/get/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, ""], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, ""], @@ -1832,6 +1835,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""], + "fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, ""], @@ -2256,7 +2261,7 @@ "send/statuses": ["statuses@2.0.1", "", {}, ""], - "signale/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, ""], + "signale/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "signale/figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, ""], @@ -2504,8 +2509,6 @@ "react-devtools/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": "bin/which" }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - "react-native/react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, ""], - "react-native/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, ""], "react-native/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], @@ -2534,13 +2537,13 @@ "send/debug/ms": ["ms@2.0.0", "", {}, ""], - "signale/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, ""], + "signale/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - "signale/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, ""], + "signale/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "signale/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, ""], + "signale/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "signale/figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, ""], + "signale/figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "term-size/execa/cross-spawn": ["cross-spawn@5.1.0", "", { "dependencies": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A=="], @@ -2720,9 +2723,9 @@ "semantic-release/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, ""], - "signale/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, ""], + "signale/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "signale/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, ""], + "signale/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "term-size/execa/cross-spawn/lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], @@ -2806,9 +2809,7 @@ "read-pkg-up/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, ""], - "signale/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, ""], - - "term-size/execa/cross-spawn/lru-cache/yallist": ["yallist@2.1.2", "", {}, "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="], + "signale/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "term-size/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], @@ -2826,10 +2827,6 @@ "@saithodev/semantic-release-backmerge/semantic-release/@semantic-release/github/@octokit/plugin-throttling/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, ""], - "@saithodev/semantic-release-backmerge/semantic-release/@semantic-release/npm/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, ""], - "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, ""], - - "@saithodev/semantic-release-backmerge/semantic-release/@semantic-release/npm/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, ""], } } diff --git a/package.json b/package.json index e800ed0..cebc7d9 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-devtools": "^7.0.1", "react-native": "0.83.1", "react-native-reanimated": "^4.2.1", + "react-native-webview": "^13.16.0", "spitroast": "^2.1.6", "type-fest": "^5.3.1" }, diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx index bd29af8..06dd58f 100644 --- a/src/core/ui/components/AddonPage.tsx +++ b/src/core/ui/components/AddonPage.tsx @@ -22,14 +22,13 @@ import { } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; import { isNotNil } from "es-toolkit"; +import fuzzysort from "fuzzysort"; import { ComponentType, ReactNode, useCallback, useEffect, useMemo, - useRef, - useState, } from "react"; import { Image, ScrollView, View } from "react-native"; @@ -199,27 +198,6 @@ export default function AddonPage({ } }, [navigation]); - const fuzzysortRef = useRef(null); - const [fuzzLoaded, setFuzzLoaded] = useState(false); - - useEffect(() => { - let cancelled = false; - if (!fuzzysortRef.current) { - import("fuzzysort") - .then((m) => { - if (cancelled) return; - fuzzysortRef.current = m.default || m; - setFuzzLoaded(true); - }) - .catch(() => { - // ignore dynamic import failures; we'll fallback to simple filtering - }); - } - return () => { - cancelled = true; - }; - }, []); - const results = useMemo(() => { let values = props.items; if (props.resolveItem) @@ -227,84 +205,11 @@ export default function AddonPage({ const items = values.filter((i) => isNotNil(i) && typeof i === "object"); if (!search && sortFn) items.sort(sortFn); - // Prefer fuzzysort when loaded and a search query exists - if (fuzzysortRef.current && search) { - try { - return fuzzysortRef.current.go(search, items, { - keys: props.searchKeywords, - all: true, - }); - } catch { - // If fuzzysort throws for some reason, fall back to the simple filter below - } - } - - // If there is no search query, return items shaped similarly to fuzzysort results - const makeFallbackResult = (obj: any, keys: any[]) => { - const r: any = { obj }; - // current search query in the outer scope; only highlight when there's an active query - const q = String(search ?? "") - .toLowerCase() - .trim(); - for (let k = 0; k < keys.length; k++) { - const key = keys[k]; - let text = ""; - try { - text = - typeof key === "string" ? String(obj[key] ?? "") : String(key(obj)); - } catch { - text = ""; - } - r[k] = { - target: text, - highlight: (cb: any) => { - // Do not highlight anything when there is no active search. - if (!q) return []; - const lower = text.toLowerCase(); - const idx = lower.indexOf(q); - if (idx === -1) return []; - // Split into before/match/after and invoke the callback for each segment so - // consumers can render matched substring(s) with highlight styling while - // preserving surrounding context. - const parts: any[] = []; - let i = 0; - const before = text.slice(0, idx); - const match = text.slice(idx, idx + q.length); - const after = text.slice(idx + q.length); - if (before) parts.push(cb(before, i++)); - parts.push(cb(match, i++)); - if (after) parts.push(cb(after, i++)); - return parts; - }, - }; - } - return r; - }; - if (!search) { - return items.map((obj: any) => - makeFallbackResult(obj, props.searchKeywords), - ); - } - - // Basic fallback: case-insensitive substring match across configured keys - const q = String(search).toLowerCase(); - const filtered = items.filter((it: any) => { - for (const key of props.searchKeywords) { - try { - const text = - typeof key === "string" ? String(it[key] ?? "") : String(key(it)); - if (text.toLowerCase().includes(q)) return true; - } catch { - // ignore any accessor errors and continue - } - } - return false; + return fuzzysort.go(search, items, { + keys: props.searchKeywords, + all: true, }); - - return filtered.map((obj: any) => - makeFallbackResult(obj, props.searchKeywords), - ); - }, [props.items, sortFn, search, fuzzLoaded]); + }, [props.items, sortFn, search]); const onInstallPress = useCallback(() => { if (!props.installAction) return () => {}; diff --git a/src/core/ui/settings/pages/Fonts/FontCard.tsx b/src/core/ui/settings/pages/Fonts/FontCard.tsx index fee8412..d2442a5 100644 --- a/src/core/ui/settings/pages/Fonts/FontCard.tsx +++ b/src/core/ui/settings/pages/Fonts/FontCard.tsx @@ -16,8 +16,9 @@ import { Text, } from "@metro/common/components"; import { findByProps } from "@metro"; -import { useMemo, useState, useEffect } from "react"; +import { useMemo, useState } from "react"; import { PixelRatio, View } from "react-native"; +import { WebView } from "react-native-webview"; import previewHtml from "./preview.html"; import FontEditor from "./FontEditor"; @@ -35,7 +36,6 @@ const useStyles = createStyles({ function FontPreview({ font }: { font: FontDefinition }) { const [loaded, setLoaded] = useState(false); - const [WebViewComponent, setWebViewComponent] = useState(null); const styles = useStyles(); const TEXT_DEFAULT = useToken(tokens.colors.TEXT_DEFAULT); @@ -53,61 +53,25 @@ function FontPreview({ font }: { font: FontDefinition }) { [font.main, fontFamily, fontSize, TEXT_DEFAULT], ); - // Memoize generated HTML to avoid string replacement on every render - const html = useMemo( - () => previewHtml.replace("$$props", JSON.stringify(props)), - [props], - ); - - // Lazy-load the WebView implementation so it's not included in the main bundle - useEffect(() => { - let cancelled = false; - import("react-native-webview") - .then((m) => { - if (cancelled) return; - // support both default export and named WebView export - setWebViewComponent(m.WebView || m.default || m); - }) - .catch(() => { - // ignore dynamic import failures; component will show placeholder - }); - return () => { - cancelled = true; - }; - }, []); - return ( - {WebViewComponent ? ( - <> - setLoaded(true)} - source={{ html }} - javaScriptEnabled - scrollEnabled={false} - overScrollMode="never" - showsHorizontalScrollIndicator={false} - showsVerticalScrollIndicator={false} - pointerEvents="none" - style={[ - styles.full, - { backgroundColor: "transparent", opacity: Number(loaded) }, - ]} - /> - {!loaded && ( - - - Loading... - - - )} - - ) : ( + setLoaded(true)} + source={{ + html: previewHtml.replace("$$props", JSON.stringify(props)), + }} + javaScriptEnabled + scrollEnabled={false} + overScrollMode="never" + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + pointerEvents="none" + style={[ + styles.full, + { backgroundColor: "transparent", opacity: Number(loaded) }, + ]} + /> + {!loaded && (