chore: merge dev to main (#54)
This commit is contained in:
commit
fd5eaa3cbd
22 changed files with 587 additions and 219 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,3 +1,16 @@
|
|||
# [1.4.0-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.3.1...v1.4.0-dev.1) (2024-10-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* actually fix improper FAB insets ([e987723](https://github.com/revenge-mod/revenge-bundle/commit/e9877234befc837126f4a24b23c837e7ed8f6b5b))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* port ErrorBoundary from dev ([f447910](https://github.com/revenge-mod/revenge-bundle/commit/f44791049a762fbce777b03e9d930fba0c0c6570))
|
||||
* **settings/pages/Developer:** show assets types, mark non-previewable assets in AssetBrowser ([b7aacf7](https://github.com/revenge-mod/revenge-bundle/commit/b7aacf734e665a7af613aa5eaae8507ea31be5cd))
|
||||
|
||||
## [1.3.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.3.0...v1.3.1) (2024-10-23)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ const metroDeps = await (async () => {
|
|||
const args = yargs(process.argv.slice(2));
|
||||
const {
|
||||
"release-branch": releaseBranch,
|
||||
"build-minify": buildMinify
|
||||
"build-minify": buildMinify,
|
||||
"dev": dev
|
||||
} = args;
|
||||
|
||||
let context = null;
|
||||
|
|
@ -44,7 +45,7 @@ const config = {
|
|||
".png": "dataurl"
|
||||
},
|
||||
define: {
|
||||
__DEV__: JSON.stringify(releaseBranch !== "main")
|
||||
__DEV__: dev ?? JSON.stringify(releaseBranch !== "main")
|
||||
},
|
||||
inject: ["./shims/asyncIteratorSymbol.js", "./shims/promiseAllSettled.js"],
|
||||
legalComments: "none",
|
||||
|
|
|
|||
21
src/core/debug/patches/patchErrorBoundary.tsx
Normal file
21
src/core/debug/patches/patchErrorBoundary.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import ErrorBoundaryScreen from "@core/ui/reporter/components/ErrorBoundaryScreen";
|
||||
import { after } from "@lib/api/patcher";
|
||||
import { _lazyContextSymbol } from "@metro/lazy";
|
||||
import { LazyModuleContext } from "@metro/types";
|
||||
import { findByNameLazy } from "@metro/wrappers";
|
||||
|
||||
function getErrorBoundaryContext() {
|
||||
const ctxt: LazyModuleContext = findByNameLazy("ErrorBoundary")[_lazyContextSymbol];
|
||||
return new Promise(resolve => ctxt.getExports(exp => resolve(exp.prototype)));
|
||||
}
|
||||
|
||||
export default function patchErrorBoundary() {
|
||||
return after.await("render", getErrorBoundaryContext(), function (this: any) {
|
||||
if (!this.state.error) return;
|
||||
|
||||
return <ErrorBoundaryScreen
|
||||
error={this.state.error}
|
||||
rerender={() => this.setState({ info: null, error: null })}
|
||||
/>;
|
||||
});
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@ import { findAssetId } from "@lib/api/assets";
|
|||
import { settings } from "@lib/api/settings";
|
||||
import AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal";
|
||||
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { lazyDestructure, proxyLazy } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { Button, FlashList, FloatingActionButton, HelpMessage, IconButton, SafeAreaView, Stack, Text, TextInput } from "@metro/common/components";
|
||||
import { Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput } from "@metro/common/components";
|
||||
import { ErrorBoundary, Search } from "@ui/components";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { ComponentType, ReactNode, useCallback, useMemo } from "react";
|
||||
|
|
@ -39,6 +39,8 @@ interface AddonPageProps<T extends object, I = any> {
|
|||
ListFooterComponent?: ComponentType<any>;
|
||||
}
|
||||
|
||||
const useSafeAreaInsets = proxyLazy(() => findByProps("useSafeAreaInsets").useSafeAreaInsets);
|
||||
|
||||
function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<void>; }) {
|
||||
const [value, setValue] = React.useState("");
|
||||
const [error, setError] = React.useState("");
|
||||
|
|
@ -111,6 +113,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [sortFn, setSortFn] = React.useState<((a: unknown, b: unknown) => number) | null>(() => null);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const results = useMemo(() => {
|
||||
let values = props.items;
|
||||
|
|
@ -198,12 +201,13 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
renderItem={({ item }: any) => <CardComponent item={item.obj} result={item} />}
|
||||
/>
|
||||
{props.installAction && (
|
||||
<SafeAreaView>
|
||||
<View style={{ paddingBottom: insets.bottom }}>
|
||||
<FloatingActionButton
|
||||
positionBottom={insets.bottom + 16}
|
||||
icon={findAssetId("PlusLargeIcon")}
|
||||
onPress={onInstallPress}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
52
src/core/ui/reporter/components/ErrorBoundaryScreen.tsx
Normal file
52
src/core/ui/reporter/components/ErrorBoundaryScreen.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { hasStack, isComponentStack } from "@core/ui/reporter/utils/isStack";
|
||||
import { getDebugInfo, toggleSafeMode } from "@lib/api/debug";
|
||||
import { BundleUpdaterManager } from "@lib/api/native/modules";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { Codeblock, ErrorBoundary } from "@lib/ui/components";
|
||||
import { createStyles } from "@lib/ui/styles";
|
||||
import { tokens } from "@metro/common";
|
||||
import { Button, Card, SafeAreaView, Text } from "@metro/common/components";
|
||||
import { ScrollView, View } from "react-native";
|
||||
|
||||
import ErrorComponentStackCard from "./ErrorComponentStackCard";
|
||||
import ErrorStackCard from "./ErrorStackCard";
|
||||
|
||||
const useStyles = createStyles({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: tokens.colors.BG_BASE_SECONDARY,
|
||||
paddingHorizontal: 16,
|
||||
height: "100%",
|
||||
padding: 8,
|
||||
gap: 12
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default function ErrorBoundaryScreen(props: {
|
||||
error: Error;
|
||||
rerender: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const debugInfo = getDebugInfo();
|
||||
|
||||
return <ErrorBoundary>
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text variant="display-lg">Uh oh.</Text>
|
||||
<Text variant="text-md/normal">A crash occured while rendering a component. This could be caused by a plugin, Bunny or Discord itself.</Text>
|
||||
<Text variant="text-sm/normal" color="text-muted">{debugInfo.os.name}; {debugInfo.discord.build} ({debugInfo.discord.version}); {debugInfo.bunny.version}</Text>
|
||||
</View>
|
||||
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
|
||||
<Codeblock selectable={true}>{props.error.message}</Codeblock>
|
||||
{hasStack(props.error) && <ErrorStackCard error={props.error} />}
|
||||
{isComponentStack(props.error) ? <ErrorComponentStackCard componentStack={props.error.componentStack} /> : null}
|
||||
</ScrollView>
|
||||
<Card style={{ gap: 6 }}>
|
||||
<Button text="Reload Discord" onPress={() => BundleUpdaterManager.reload()} />
|
||||
{!settings.safeMode?.enabled && <Button text="Reload in Safe Mode" onPress={() => toggleSafeMode()} />}
|
||||
<Button variant="destructive" text="Retry Render" onPress={() => props.rerender()} />
|
||||
</Card>
|
||||
</SafeAreaView>
|
||||
</ErrorBoundary>;
|
||||
}
|
||||
44
src/core/ui/reporter/components/ErrorCard.tsx
Normal file
44
src/core/ui/reporter/components/ErrorCard.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { Codeblock } from "@lib/ui/components";
|
||||
import { showSheet } from "@lib/ui/sheets";
|
||||
import { Button, Card, Stack, Text, TwinButtons } from "@metro/common/components";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import ErrorDetailsActionSheet from "./ErrorDetailsActionSheet";
|
||||
|
||||
export const INDEX_BUNDLE_FILE: string = window.HermesInternal.getFunctionLocation(window.__r).fileName;
|
||||
|
||||
interface ErrorCardProps {
|
||||
error: unknown;
|
||||
header?: string | ReactNode;
|
||||
onRetryRender?: () => void;
|
||||
}
|
||||
|
||||
export default function ErrorCard(props: ErrorCardProps) {
|
||||
return <Card>
|
||||
<Stack>
|
||||
{props.header && typeof props.header !== "string"
|
||||
? props.header
|
||||
: <Text variant="heading-lg/bold">{props.header ?? Strings.UH_OH}</Text>
|
||||
}
|
||||
<Codeblock selectable={true}>{String(props.error)}</Codeblock>
|
||||
<TwinButtons>
|
||||
{props.onRetryRender && <Button
|
||||
variant="destructive"
|
||||
// icon={findAssetId("RetryIcon")}
|
||||
text={Strings.RETRY_RENDER}
|
||||
onPress={props.onRetryRender}
|
||||
/>}
|
||||
{props.error instanceof Error ? <Button
|
||||
text={"Details"}
|
||||
// icon={findAssetId("CircleInformationIcon-primary")}
|
||||
onPress={() => showSheet(
|
||||
"BunnyErrorDetailsActionSheet",
|
||||
ErrorDetailsActionSheet,
|
||||
{ error: props.error as Error }
|
||||
)}
|
||||
/> : null}
|
||||
</TwinButtons>
|
||||
</Stack>
|
||||
</Card>;
|
||||
}
|
||||
53
src/core/ui/reporter/components/ErrorComponentStackCard.tsx
Normal file
53
src/core/ui/reporter/components/ErrorComponentStackCard.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { parseComponentStack } from "@core/ui/reporter/utils/parseComponentStack";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { Button, Card, Text } from "@metro/common/components";
|
||||
import { useState } from "react";
|
||||
import { Image, View } from "react-native";
|
||||
|
||||
export default function ErrorComponentStackCard(props: {
|
||||
componentStack: string;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
|
||||
let stack: string[];
|
||||
try {
|
||||
stack = parseComponentStack(props.componentStack);
|
||||
stack = collapsed ? stack.slice(0, 4) : stack;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
return <Card>
|
||||
<View style={{ gap: 8 }}>
|
||||
<Text variant="heading-lg/bold">
|
||||
Component Stack
|
||||
</Text>
|
||||
<View style={{ gap: 4 }}>
|
||||
{stack.map(component => (
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Text variant="text-md/bold" color="text-muted">{"<"}</Text>
|
||||
<Text variant="text-md/bold">{component}</Text>
|
||||
<Text variant="text-md/bold" color="text-muted">{"/>"}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{collapsed && <Text>...</Text>}
|
||||
<View style={{ gap: 8, flexDirection: "row", justifyContent: "center", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
text={`Show ${collapsed ? "more" : "less"}`}
|
||||
icon={collapsed ? findAssetId("down_arrow") : <Image
|
||||
style={{ transform: [{ rotate: `${collapsed ? 0 : 180}deg` }] }}
|
||||
source={findAssetId("down_arrow")!}
|
||||
/>}
|
||||
onPress={() => setCollapsed(v => !v)} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
text="Copy"
|
||||
icon={findAssetId("CopyIcon")}
|
||||
onPress={() => clipboard.setString(props.componentStack)} />
|
||||
</View>
|
||||
</View>
|
||||
</Card>;
|
||||
}
|
||||
20
src/core/ui/reporter/components/ErrorDetailsActionSheet.tsx
Normal file
20
src/core/ui/reporter/components/ErrorDetailsActionSheet.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { hasStack, isComponentStack } from "@core/ui/reporter/utils/isStack";
|
||||
import { Codeblock } from "@lib/ui/components";
|
||||
import { ActionSheet, Text } from "@metro/common/components";
|
||||
import { View } from "react-native";
|
||||
|
||||
import ErrorComponentStackCard from "./ErrorComponentStackCard";
|
||||
import ErrorStackCard from "./ErrorStackCard";
|
||||
|
||||
export default function ErrorDetailsActionSheet(props: {
|
||||
error: Error;
|
||||
}) {
|
||||
return <ActionSheet>
|
||||
<View style={{ gap: 12, paddingVertical: 12 }}>
|
||||
<Text variant="heading-lg/extrabold">Error</Text>
|
||||
<Codeblock selectable={true}>{props.error.message}</Codeblock>
|
||||
{hasStack(props.error) && <ErrorStackCard error={props.error} />}
|
||||
{isComponentStack(props.error) ? <ErrorComponentStackCard componentStack={props.error.componentStack} /> : null}
|
||||
</View>
|
||||
</ActionSheet>;
|
||||
}
|
||||
63
src/core/ui/reporter/components/ErrorStackCard.tsx
Normal file
63
src/core/ui/reporter/components/ErrorStackCard.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import parseErrorStack, { StackFrame } from "@core/ui/reporter/utils/parseErrorStack";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { clipboard, constants } from "@metro/common";
|
||||
import { Button, Card, Text } from "@metro/common/components";
|
||||
import { useState } from "react";
|
||||
import { Image, Pressable, View } from "react-native";
|
||||
|
||||
import { INDEX_BUNDLE_FILE } from "./ErrorCard";
|
||||
|
||||
export default function ErrorStackCard(props: {
|
||||
error: Error & { stack: string };
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
|
||||
let stack: StackFrame[];
|
||||
|
||||
try {
|
||||
const parsedErrorStack = parseErrorStack(props.error.stack);
|
||||
stack = collapsed ? parsedErrorStack.slice(0, 4) : parsedErrorStack;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Card>
|
||||
<View style={{ gap: 12 }}>
|
||||
<Text variant="heading-lg/bold">
|
||||
Call Stack
|
||||
</Text>
|
||||
<View style={{ gap: 4 }}>
|
||||
{stack.map((f, id) => <Line id={id} frame={f} />)}
|
||||
</View>
|
||||
{collapsed && <Text>...</Text>}
|
||||
<View style={{ gap: 8, flexDirection: "row", justifyContent: "center", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
text={`Show ${collapsed ? "more" : "less"}`}
|
||||
icon={collapsed ? findAssetId("down_arrow") : <Image
|
||||
style={{ transform: [{ rotate: `${collapsed ? 0 : 180}deg` }] }}
|
||||
source={findAssetId("down_arrow")!}
|
||||
/>}
|
||||
onPress={() => setCollapsed(v => !v)} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
text="Copy"
|
||||
icon={findAssetId("CopyIcon")}
|
||||
onPress={() => clipboard.setString(props.error.stack)} />
|
||||
</View>
|
||||
</View>
|
||||
</Card>;
|
||||
}
|
||||
function Line(props: { id: number, frame: StackFrame }) {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
|
||||
return <Pressable onPress={() => setCollapsed(v => !v)} key={props.id}>
|
||||
<Text style={{ fontFamily: constants.Fonts.CODE_BOLD }}>
|
||||
{props.frame.methodName}
|
||||
</Text>
|
||||
<Text style={{ fontFamily: constants.Fonts.CODE_NORMAL }} ellipsizeMode="middle" numberOfLines={collapsed ? 1 : undefined}>
|
||||
<Text color="text-muted">{props.frame.file === INDEX_BUNDLE_FILE ? "jsbundle" : props.frame.file}:{props.frame.lineNumber}:{props.frame.column}</Text>
|
||||
</Text>
|
||||
</Pressable>;
|
||||
}
|
||||
|
||||
6
src/core/ui/reporter/utils/isStack.tsx
Normal file
6
src/core/ui/reporter/utils/isStack.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export function isComponentStack(error: Error): error is Error & { componentStack: string; } {
|
||||
return "componentStack" in error && typeof error.componentStack === "string";
|
||||
}
|
||||
export function hasStack(error: Error): error is Error & { stack: string; } {
|
||||
return !!error.stack;
|
||||
}
|
||||
3
src/core/ui/reporter/utils/parseComponentStack.tsx
Normal file
3
src/core/ui/reporter/utils/parseComponentStack.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function parseComponentStack(componentStack: string) {
|
||||
return componentStack.split(/[\s|\n]+?in /).filter(Boolean);
|
||||
}
|
||||
202
src/core/ui/reporter/utils/parseErrorStack.ts
Normal file
202
src/core/ui/reporter/utils/parseErrorStack.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* * furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export type StackFrame = {
|
||||
column?: number,
|
||||
file?: string,
|
||||
lineNumber?: number,
|
||||
methodName: string,
|
||||
collapse?: boolean,
|
||||
};
|
||||
|
||||
type HermesStackLocationNative = {
|
||||
readonly type: "NATIVE",
|
||||
};
|
||||
|
||||
type HermesStackLocationSource = Readonly<{
|
||||
type: "SOURCE",
|
||||
sourceUrl: string,
|
||||
line1Based: number,
|
||||
column1Based: number,
|
||||
}>;
|
||||
|
||||
type HermesStackLocationInternalBytecode = Readonly<{
|
||||
type: "INTERNAL_BYTECODE",
|
||||
sourceUrl: string,
|
||||
line1Based: number,
|
||||
virtualOffset0Based: number,
|
||||
}>;
|
||||
|
||||
type HermesStackLocationBytecode = Readonly<{
|
||||
type: "BYTECODE",
|
||||
sourceUrl: string,
|
||||
line1Based: number,
|
||||
virtualOffset0Based: number,
|
||||
}>;
|
||||
|
||||
type HermesStackLocation =
|
||||
| HermesStackLocationNative
|
||||
| HermesStackLocationSource
|
||||
| HermesStackLocationInternalBytecode
|
||||
| HermesStackLocationBytecode;
|
||||
|
||||
type HermesStackEntryFrame = Readonly<{
|
||||
type: "FRAME",
|
||||
location: HermesStackLocation,
|
||||
functionName: string,
|
||||
}>;
|
||||
|
||||
type HermesStackEntrySkipped = Readonly<{
|
||||
type: "SKIPPED",
|
||||
count: number,
|
||||
}>;
|
||||
|
||||
type HermesStackEntry = HermesStackEntryFrame | HermesStackEntrySkipped;
|
||||
|
||||
export type HermesParsedStack = Readonly<{
|
||||
message: string,
|
||||
entries: ReadonlyArray<HermesStackEntry>,
|
||||
}>;
|
||||
|
||||
// Capturing groups:
|
||||
// 1. function name
|
||||
// 2. is this a native stack frame?
|
||||
// 3. is this a bytecode address or a source location?
|
||||
// 4. source URL (filename)
|
||||
// 5. line number (1 based)
|
||||
// 6. column number (1 based) or virtual offset (0 based)
|
||||
const RE_FRAME =
|
||||
/^ {4}at (.+?)(?: \((native)\)?| \((address at )?(.*?):(\d+):(\d+)\))$/;
|
||||
|
||||
// Capturing groups:
|
||||
// 1. count of skipped frames
|
||||
const RE_SKIPPED = /^ {4}... skipping (\d+) frames$/;
|
||||
const RE_COMPONENT_NO_STACK = /^ {4}at .*$/;
|
||||
|
||||
function isInternalBytecodeSourceUrl(sourceUrl: string): boolean {
|
||||
// See https://github.com/facebook/hermes/blob/3332fa020cae0bab751f648db7c94e1d687eeec7/lib/VM/Runtime.cpp#L1100
|
||||
return sourceUrl === "InternalBytecode.js";
|
||||
}
|
||||
|
||||
function parseLine(line: string): HermesStackEntry | void {
|
||||
const asFrame = line.match(RE_FRAME);
|
||||
if (asFrame) {
|
||||
return {
|
||||
type: "FRAME",
|
||||
functionName: asFrame[1],
|
||||
location:
|
||||
asFrame[2] === "native"
|
||||
? { type: "NATIVE" }
|
||||
: asFrame[3] === "address at "
|
||||
? isInternalBytecodeSourceUrl(asFrame[4])
|
||||
? {
|
||||
type: "INTERNAL_BYTECODE",
|
||||
sourceUrl: asFrame[4],
|
||||
line1Based: Number.parseInt(asFrame[5], 10),
|
||||
virtualOffset0Based: Number.parseInt(asFrame[6], 10),
|
||||
}
|
||||
: {
|
||||
type: "BYTECODE",
|
||||
sourceUrl: asFrame[4],
|
||||
line1Based: Number.parseInt(asFrame[5], 10),
|
||||
virtualOffset0Based: Number.parseInt(asFrame[6], 10),
|
||||
}
|
||||
: {
|
||||
type: "SOURCE",
|
||||
sourceUrl: asFrame[4],
|
||||
line1Based: Number.parseInt(asFrame[5], 10),
|
||||
column1Based: Number.parseInt(asFrame[6], 10),
|
||||
},
|
||||
};
|
||||
}
|
||||
const asSkipped = line.match(RE_SKIPPED);
|
||||
if (asSkipped) {
|
||||
return {
|
||||
type: "SKIPPED",
|
||||
count: Number.parseInt(asSkipped[1], 10),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseHermesStack(stack: string): HermesParsedStack {
|
||||
const lines = stack.split(/\n/);
|
||||
let entries: Array<HermesStackEntryFrame | HermesStackEntrySkipped> = [];
|
||||
let lastMessageLine = -1;
|
||||
for (let i = 0; i < lines.length; ++i) {
|
||||
const line = lines[i];
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
const entry = parseLine(line);
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
continue;
|
||||
}
|
||||
if (RE_COMPONENT_NO_STACK.test(line)) {
|
||||
// Skip component stacks without source location.
|
||||
// TODO: This will not be displayed, not sure how to handle it.
|
||||
continue;
|
||||
}
|
||||
// No match - we're still in the message
|
||||
lastMessageLine = i;
|
||||
entries = [];
|
||||
}
|
||||
const message = lines.slice(0, lastMessageLine + 1).join("\n");
|
||||
return { message, entries };
|
||||
}
|
||||
|
||||
function convertHermesStack(stack: HermesParsedStack): Array<StackFrame> {
|
||||
const frames: Array<StackFrame> = [];
|
||||
for (const entry of stack.entries) {
|
||||
if (entry.type !== "FRAME") {
|
||||
continue;
|
||||
}
|
||||
const { location, functionName } = entry;
|
||||
if (location.type === "NATIVE" || location.type === "INTERNAL_BYTECODE") {
|
||||
continue;
|
||||
}
|
||||
frames.push({
|
||||
methodName: functionName,
|
||||
file: location.sourceUrl,
|
||||
lineNumber: location.line1Based,
|
||||
column:
|
||||
location.type === "SOURCE"
|
||||
? location.column1Based - 1
|
||||
: location.virtualOffset0Based,
|
||||
});
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
export default function parseErrorStack(errorStack?: string): Array<StackFrame> {
|
||||
if (errorStack == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsedStack = Array.isArray(errorStack)
|
||||
? errorStack
|
||||
: convertHermesStack(parseHermesStack(errorStack));
|
||||
|
||||
return parsedStack;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
|
||||
import { assetsMap } from "@lib/api/assets";
|
||||
import { Text } from "@metro/common/components";
|
||||
import { ErrorBoundary, Search } from "@ui/components";
|
||||
import { FlatList, View } from "react-native";
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ export default function AssetBrowser() {
|
|||
onChangeText={(v: string) => setSearch(v)}
|
||||
/>
|
||||
<View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}>
|
||||
<Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text>
|
||||
<FlatList
|
||||
data={Object.values(assetsMap).filter(a => a.name.includes(search) || a.id.toString() === search)}
|
||||
renderItem={({ item }: any) => <AssetDisplay asset={item} />}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,60 @@
|
|||
import { Asset } from "@lib/api/assets";
|
||||
import { Asset, findAssetId } from "@lib/api/assets";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { Stack, TableRow } from "@metro/common/components";
|
||||
import { Stack, TableRow, Text } from "@metro/common/components";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { Image } from "react-native";
|
||||
|
||||
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
|
||||
|
||||
const displayable = new Set(['png', 'jpg', 'svg']);
|
||||
|
||||
const iconMap = {
|
||||
jsona: 'ic_file_text',
|
||||
lottie: 'ic_image',
|
||||
webm: 'CirclePlayIcon-primary',
|
||||
ttf: 'ic_add_text',
|
||||
default: 'UnknownGameIcon'
|
||||
};
|
||||
|
||||
interface AssetDisplayProps { asset: Asset; }
|
||||
|
||||
export default function AssetDisplay({ asset }: AssetDisplayProps) {
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
variant={displayable.has(asset.type) ? 'default' : 'danger'}
|
||||
label={asset.name}
|
||||
subLabel={`Index: ${asset.index}`}
|
||||
icon={<Image source={asset.index} style={{ width: 32, height: 32 }} />}
|
||||
onPress={() => {
|
||||
subLabel={`Index: ${asset.index} Type: ${asset.type}`}
|
||||
icon={
|
||||
displayable.has(asset.type)
|
||||
? <Image source={asset.index} style={{ width: 32, height: 32 }} />
|
||||
: <TableRow.Icon
|
||||
variant='danger'
|
||||
source={findAssetId(asset.type in iconMap ? iconMap[asset.type as keyof typeof iconMap] : iconMap.default)}
|
||||
/>
|
||||
}
|
||||
onPress={() =>
|
||||
openAlert("revenge-asset-display-details", <AlertModal
|
||||
title={asset.name}
|
||||
content={`Index: ${asset.index}\nModule ID: ${asset.moduleId}`}
|
||||
extraContent={<Image resizeMode="contain" source={asset.index} style={{ flex: 1, width: 'auto', height: 192 }} />}
|
||||
content={`Index: ${asset.index}\nModule ID: ${asset.moduleId}\nType: ${asset.type}`}
|
||||
extraContent={
|
||||
displayable.has(asset.type)
|
||||
? <Image resizeMode="contain" source={asset.index} style={{ flex: 1, width: 'auto', height: 192 }} />
|
||||
: (<Text variant='text-sm/medium' color="text-danger" style={{ width: '100%', textAlign: 'center' }}>
|
||||
Asset type {asset.type.toUpperCase()} is not supported for preview.
|
||||
</Text>)
|
||||
}
|
||||
actions={
|
||||
<Stack>
|
||||
<AlertActionButton text="Copy asset name" variant="primary" onPress={() => copyToClipboard(asset.name)} />
|
||||
<AlertActionButton text="Copy asset index" variant="secondary" onPress={() => copyToClipboard(asset.index.toString())} />
|
||||
</Stack>
|
||||
}
|
||||
/>);
|
||||
}}
|
||||
/>)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { createProxy, useProxy } from "@core/vendetta/storage";
|
|||
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";
|
||||
|
|
@ -14,13 +12,6 @@ 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"));
|
||||
|
|
@ -278,8 +269,6 @@ export default function FontEditor(props: {
|
|||
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;
|
||||
}, [props.name]);
|
||||
|
|
@ -343,7 +332,7 @@ export default function FontEditor(props: {
|
|||
|
||||
return <TableRow
|
||||
label={name}
|
||||
subLabel={error ? <Text style={styles.errorText}>{error.message}</Text> : url}
|
||||
subLabel={error ? <Text variant='text-xs/medium' color='text-danger'>{error.message}</Text> : url}
|
||||
trailing={<Stack spacing={8} direction="horizontal">
|
||||
<IconButton
|
||||
size="sm"
|
||||
|
|
@ -374,7 +363,7 @@ export default function FontEditor(props: {
|
|||
})}
|
||||
<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>}
|
||||
{errors && <Text variant='text-sm/medium' color='text-danger'>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"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import patchErrorBoundary from "@core/debug/patches/patchErrorBoundary";
|
||||
import initFixes from "@core/fixes";
|
||||
import { initFetchI18nStrings } from "@core/i18n";
|
||||
import initSettings from "@core/ui/settings";
|
||||
|
|
@ -13,7 +14,6 @@ import { writeFile } from "@lib/api/native/fs";
|
|||
import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
|
||||
import { patchJsx } from "@lib/api/react/jsx";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
import initSafeMode from "@ui/safeMode";
|
||||
import { patchSettings } from "@ui/settings";
|
||||
|
||||
import * as lib from "./lib";
|
||||
|
|
@ -46,7 +46,7 @@ export default async () => {
|
|||
initFetchI18nStrings(),
|
||||
initSettings(),
|
||||
initFixes(),
|
||||
initSafeMode(),
|
||||
patchErrorBoundary(),
|
||||
updatePlugins()
|
||||
]).then(
|
||||
// Push them all to unloader
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export const assetsMap: Record<string, Asset> = new Proxy<any>({}, {
|
|||
export interface Asset {
|
||||
/** @deprecated */
|
||||
id: number;
|
||||
type: 'jsona' | 'jpg' | 'png' | 'svg' | 'lottie' | 'ttf' | 'webm'
|
||||
index: number;
|
||||
name: string;
|
||||
moduleId: number;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import ErrorCard from "@core/ui/reporter/components/ErrorCard";
|
||||
import { React } from "@metro/common";
|
||||
import { Button, LegacyFormText } from "@metro/common/components";
|
||||
import { Codeblock } from "@ui/components";
|
||||
import { createThemedStyleSheet } from "@ui/styles";
|
||||
import { Falsy, ScrollView } from "react-native";
|
||||
import { ThemeContext } from "@ui/styles";
|
||||
import { Falsy } from "react-native";
|
||||
|
||||
type ErrorBoundaryState = {
|
||||
hasErr: false;
|
||||
|
|
@ -16,45 +14,23 @@ export interface ErrorBoundaryProps {
|
|||
children: JSX.Element | Falsy | (JSX.Element | Falsy)[];
|
||||
}
|
||||
|
||||
const styles = createThemedStyleSheet({
|
||||
view: {
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
margin: 10,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
textAlign: "center",
|
||||
marginBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasErr: false };
|
||||
}
|
||||
|
||||
static contextType = ThemeContext;
|
||||
static getDerivedStateFromError = (error: Error) => ({ hasErr: true, error });
|
||||
|
||||
render() {
|
||||
if (!this.state.hasErr) return this.props.children;
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.view}>
|
||||
<LegacyFormText style={styles.title}>{Strings.UH_OH}</LegacyFormText>
|
||||
<Codeblock selectable style={{ marginBottom: 5 }}>{this.state.error.name}</Codeblock>
|
||||
<Codeblock selectable style={{ marginBottom: 5 }}>{this.state.error.message}</Codeblock>
|
||||
{this.state.error.stack && <ScrollView style={{ maxHeight: 420, marginBottom: 5 }}>
|
||||
<Codeblock selectable>{this.state.error.stack}</Codeblock>
|
||||
</ScrollView>}
|
||||
<Button
|
||||
size="md"
|
||||
variant="destructive"
|
||||
onPress={() => this.setState({ hasErr: false })}
|
||||
text={Strings.RETRY}
|
||||
/>
|
||||
</ScrollView>
|
||||
<ErrorCard
|
||||
error={this.state.error}
|
||||
onRetryRender={() => this.setState({ hasErr: false })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,151 +0,0 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { toggleSafeMode } from "@lib/api/debug";
|
||||
import { DeviceManager } from "@lib/api/native/modules";
|
||||
import { after } from "@lib/api/patcher";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { Button, CompatButton, SafeAreaView } from "@metro/common/components";
|
||||
import { _lazyContextSymbol } from "@metro/lazy";
|
||||
import { LazyModuleContext } from "@metro/types";
|
||||
import { findByNameLazy, findByProps } from "@metro/wrappers";
|
||||
import { semanticColors } from "@ui/color";
|
||||
import { Codeblock, ErrorBoundary as _ErrorBoundary } from "@ui/components";
|
||||
import { createThemedStyleSheet, TextStyleSheet } from "@ui/styles";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
const ErrorBoundary = findByNameLazy("ErrorBoundary");
|
||||
|
||||
// Let's just pray they have this.
|
||||
const { BadgableTabBar } = lazyDestructure(() => findByProps("BadgableTabBar"));
|
||||
|
||||
const styles = createThemedStyleSheet({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: semanticColors.BACKGROUND_PRIMARY,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
header: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginVertical: 8,
|
||||
},
|
||||
headerTitle: {
|
||||
...TextStyleSheet["heading-md/semibold"],
|
||||
textAlign: "center",
|
||||
textTransform: "uppercase",
|
||||
color: semanticColors.HEADER_PRIMARY,
|
||||
},
|
||||
headerDescription: {
|
||||
...TextStyleSheet["text-sm/medium"],
|
||||
textAlign: "center",
|
||||
color: semanticColors.TEXT_MUTED,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: DeviceManager.isTablet ? "row" : "column",
|
||||
justifyContent: "flex-end",
|
||||
marginVertical: 8,
|
||||
},
|
||||
});
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
title: () => string;
|
||||
trimWhitespace?: boolean;
|
||||
}
|
||||
|
||||
interface Button {
|
||||
text: string;
|
||||
// TODO: Proper types for the below
|
||||
color?: string;
|
||||
size?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ id: "message", title: () => Strings.MESSAGE },
|
||||
{ id: "stack", title: () => Strings.STACK_TRACE },
|
||||
{ id: "componentStack", title: () => Strings.COMPONENT, trimWhitespace: true },
|
||||
];
|
||||
|
||||
function getErrorBoundaryContext() {
|
||||
const ctxt: LazyModuleContext = findByNameLazy("ErrorBoundary")[_lazyContextSymbol];
|
||||
return new Promise(resolve => {
|
||||
ctxt.getExports(exp => {
|
||||
resolve(exp.prototype);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default () => after.await("render", getErrorBoundaryContext(), function (this: any, _, ret) {
|
||||
if (!this.state.error) return;
|
||||
|
||||
// Not using setState here as we don't want to cause a re-render, we want this to be set in the initial render
|
||||
this.state.activeTab ??= "message";
|
||||
const tabData = tabs.find(t => t.id === this.state.activeTab);
|
||||
const errorText: string = this.state.error[this.state.activeTab];
|
||||
|
||||
// This is in the patch and not outside of it so that we can use `this`, e.g. for setting state
|
||||
const buttons: Button[] = [
|
||||
{ text: Strings.RELOAD_DISCORD, onPress: this.handleReload },
|
||||
...!settings.safeMode?.enabled ? [{ text: Strings.RELOAD_IN_SAFE_MODE, onPress: toggleSafeMode }] : [],
|
||||
{ text: Strings.RETRY_RENDER, color: "red", onPress: () => this.setState({ info: null, error: null }) },
|
||||
];
|
||||
|
||||
return (
|
||||
<_ErrorBoundary>
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<ret.props.Illustration style={{ transform: [{ scale: 0.6 }], marginLeft: -40, marginRight: -80 }} />
|
||||
<View style={{ flex: 2, paddingLeft: 24 }}>
|
||||
<Text style={styles.headerTitle}>{ret.props.title}</Text>
|
||||
<Text style={styles.headerDescription}>{ret.props.body}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flex: 6 }}>
|
||||
<View style={{ paddingBottom: 8 }}>
|
||||
{/* Are errors caught by ErrorBoundary guaranteed to have the component stack? */}
|
||||
<BadgableTabBar
|
||||
tabs={tabs.map(t => ({ ...t, title: t.title() }))}
|
||||
activeTab={this.state.activeTab}
|
||||
onTabSelected={(tab: string) => { this.setState({ activeTab: tab }); }}
|
||||
/>
|
||||
</View>
|
||||
<Codeblock
|
||||
selectable
|
||||
style={{ flex: 1, textAlignVertical: "top" }}
|
||||
>
|
||||
{/*
|
||||
TODO: I tried to get this working as intended using regex and failed.
|
||||
When trimWhitespace is true, each line should have it's whitespace removed but with it's spaces kept.
|
||||
*/}
|
||||
{tabData?.trimWhitespace ? errorText.split("\n").filter(i => i.length !== 0).map(i => i.trim()).join("\n") : errorText}
|
||||
</Codeblock>
|
||||
</View>
|
||||
<View style={styles.footer}>
|
||||
{buttons.map(button => {
|
||||
const buttonIndex = buttons.indexOf(button) !== 0 ? 8 : 0;
|
||||
|
||||
return <CompatButton
|
||||
text={button.text}
|
||||
color={button.color ?? "brand"}
|
||||
size={button.size ?? "small"}
|
||||
onPress={button.onPress}
|
||||
style={{
|
||||
...(DeviceManager.isTablet ? {
|
||||
flex: `0.${buttons.length}`,
|
||||
marginLeft: buttonIndex
|
||||
} : {
|
||||
marginTop: buttonIndex
|
||||
}),
|
||||
|
||||
borderRadius: 16
|
||||
}}
|
||||
/>;
|
||||
})}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</_ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
|
@ -4,9 +4,10 @@ const actionSheet = findByPropsLazy("openLazy", "hideActionSheet");
|
|||
|
||||
export function showSheet<T extends React.ComponentType<any>>(
|
||||
key: string,
|
||||
lazyImport: Promise<{ default: T; }>,
|
||||
lazyImport: Promise<{ default: T; }> | T,
|
||||
props?: React.ComponentProps<T>
|
||||
) {
|
||||
if (!("then" in lazyImport)) lazyImport = Promise.resolve({ default: lazyImport });
|
||||
actionSheet.openLazy(lazyImport, key, props ?? {});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,18 +6,60 @@ import { ImageStyle, StyleSheet, TextStyle, ViewStyle } from "react-native";
|
|||
|
||||
type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };
|
||||
|
||||
const CompatfulRedesign = findByPropsLazy("createStyles");
|
||||
const Styles = findByPropsLazy("createStyles");
|
||||
|
||||
export const { ThemeContext } = lazyDestructure(() => findByProps("ThemeContext"), { hint: "object" });
|
||||
export const { TextStyleSheet } = lazyDestructure(() => findByProps("TextStyleSheet")) as unknown as {
|
||||
TextStyleSheet: { [key in DiscordTextStyles]: TextStyle; };
|
||||
TextStyleSheet: Record<DiscordTextStyles, TextStyle>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get themed styles based on the current theme
|
||||
* @returns A hook that returns the themed stylesheet
|
||||
* @example
|
||||
* const useStyles = createStyles({
|
||||
* container: {
|
||||
* flex: 1,
|
||||
* backgroundColor: tokens.colors.BACKGROUND_PRIMARY,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* function MyComponent() {
|
||||
* const styles = useStyles();
|
||||
* return <View style={styles.container} />;
|
||||
* }
|
||||
*/
|
||||
export function createStyles<T extends NamedStyles<T>>(sheet: T | ((props: any) => T)): () => T {
|
||||
return proxyLazy(() => CompatfulRedesign.createStyles(sheet));
|
||||
return proxyLazy(() => Styles.createStyles(sheet));
|
||||
}
|
||||
|
||||
// Reimplementation of Discord's createThemedStyleSheet, which was removed since 204201
|
||||
// Not exactly a 1:1 reimplementation, but sufficient to keep compatibility with existing plugins
|
||||
/**
|
||||
* Get themed styles based on the current theme for class components
|
||||
* @example
|
||||
* const getStyles = createStyles({
|
||||
* container: {
|
||||
* flex: 1,
|
||||
* backgroundColor: tokens.colors.BACKGROUND_PRIMARY,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* class MyComponent extends React.Component {
|
||||
* static contextType = ThemeContext;
|
||||
* render() {
|
||||
* const styles = getStyles(this.context);
|
||||
* return <View style={styles.container} />;
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export function createLegacyClassComponentStyles<T extends NamedStyles<T>>(sheet: T | ((props: any) => T)): (ctxt: typeof ThemeContext) => T {
|
||||
return proxyLazy(() => Styles.createLegacyClassComponentStyles(sheet));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reimplementation of Discord's createThemedStyleSheet, which was removed since 204201
|
||||
* Not exactly a 1:1 reimplementation, but sufficient to keep compatibility with existing plugins
|
||||
* @deprecated Use createStyles or createLegacyClassComponentStyles instead
|
||||
*/
|
||||
export function createThemedStyleSheet<T extends StyleSheet.NamedStyles<T>>(sheet: T) {
|
||||
for (const key in sheet) {
|
||||
// @ts-ignore
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ interface FABProps {
|
|||
icon: ImageSourcePropType | ReactNode;
|
||||
style?: Style;
|
||||
onPress: () => void;
|
||||
positionBottom: number;
|
||||
}
|
||||
|
||||
export type FloatingActionButton = React.FC<FABProps>;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue