diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml
new file mode 100644
index 0000000..af6c50c
--- /dev/null
+++ b/.forgejo/workflows/build.yaml
@@ -0,0 +1,56 @@
+name: Node.js CI / CD (Bun + Forgejo)
+
+on:
+ push:
+ branches: [ "main", "dev" ]
+jobs:
+ buildAndRelease:
+ runs-on: codeberg-tiny
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install Bun
+ run: |
+ curl -fsSL https://bun.sh/install | bash
+ export BUN_INSTALL="$HOME/.bun"
+ export PATH="$BUN_INSTALL/bin:$PATH"
+ echo "$BUN_INSTALL/bin" >> $GITHUB_PATH
+ - name: Install dependencies
+ run: |
+ export BUN_INSTALL="$HOME/.bun"
+ export PATH="$BUN_INSTALL/bin:$PATH"
+ bun install
+ - name: Build project
+ run: |
+ export BUN_INSTALL="$HOME/.bun"
+ export PATH="$BUN_INSTALL/bin:$PATH"
+ bun run build --release-branch=${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME}} --build-minify
+ - name: Push JS files to dist branch
+ run: |
+ git config --local user.email "actions@forgejo.local"
+ git config --local user.name "Forgejo Actions"
+ mkdir -p temp_dist
+ find dist -name "*.js" -exec cp {} temp_dist/ \;
+ git checkout --orphan dist-temp
+ git rm -rf --cached .
+ cp -r temp_dist/* .
+ rm -rf temp_dist
+ git add *.js
+ git commit -m "Update build $GITHUB_SHA" || exit 0
+
+ if [ "${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME}}" = "dev" ]; then
+ TARGET_BRANCH="testing-dist"
+ else
+ TARGET_BRANCH="dist"
+ fi
+
+ git push -f origin HEAD:$TARGET_BRANCH
+ git checkout -f ${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME}}
+ - name: Merge main into github
+ if: github.ref == 'refs/heads/main'
+ run: |
+ git config --local user.email "actions@forgejo.local"
+ git config --local user.name "Forgejo Actions"
+ git fetch origin
+ git checkout github || git checkout -b github
+ git merge origin/main --no-edit --allow-unrelated-histories
+ git push --force origin github
\ No newline at end of file
diff --git a/src/core/plugins/kettuenhancements/index.tsx b/src/core/plugins/kettuenhancements/index.tsx
new file mode 100644
index 0000000..b3cd75a
--- /dev/null
+++ b/src/core/plugins/kettuenhancements/index.tsx
@@ -0,0 +1,18 @@
+import { defineCorePlugin } from "..";
+
+export default defineCorePlugin({
+ manifest: {
+ id: "bunny.enhancements",
+ version: "0.1.0",
+ type: "plugin",
+ spec: 3,
+ main: "",
+ display: {
+ name: "Kettu Enhancements",
+ description: "Fixes common discord bugs because discord wont",
+ authors: [{ name: "cocobo1"}]
+ }
+ },
+ start() {
+ }
+});
\ No newline at end of file
diff --git a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx
index b1bdcbb..785cb78 100644
--- a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx
+++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx
@@ -1,30 +1,70 @@
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
-import { iterateAssets } from "@lib/api/assets";
+import { iterateAssets, findAssetId } from "@lib/api/assets";
import { Text } from "@metro/common/components";
import { ErrorBoundary, Search } from "@ui/components";
import { useMemo } from "react";
-import { FlatList, View } from "react-native";
+import { FlatList, View, ScrollView, TouchableOpacity, Image } from "react-native";
+
+const displayable = new Set(['png', 'jpg', 'svg']);
export default function AssetBrowser() {
const [search, setSearch] = React.useState("");
+ const [showNonImages, setShowNonImages] = React.useState(false);
+
const all = useMemo(() => Array.from(iterateAssets()), []);
+ const filteredData = useMemo(() => {
+ let result = all.filter(a => a.name.includes(search) || a.id.toString() === search);
+
+ if (!showNonImages) {
+ result = result.filter(a => displayable.has(a.type));
+ }
+
+ return result;
+ }, [all, search, showNonImages]);
+
+
return (
- setSearch(v)}
- />
-
- Some assets types cannot be displayed and will be marked in red.
- a.name.includes(search) || a.id.toString() === search)}
- renderItem={({ item }: any) => }
- contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
- keyExtractor={a => a.name}
+
+
+ setSearch(v)}
/>
+ setShowNonImages(!showNonImages)}
+ >
+
+
+
+
+
+ {showNonImages && (
+ Some assets types cannot be displayed and will be marked in red.
+ )}
+ }
+ contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
+ scrollEnabled={false}
+ keyExtractor={a => a.id.toString()}
+ />
+
+
);
diff --git a/src/lib/addons/fonts/index.ts b/src/lib/addons/fonts/index.ts
index 730ceb1..47fc4d4 100644
--- a/src/lib/addons/fonts/index.ts
+++ b/src/lib/addons/fonts/index.ts
@@ -109,7 +109,7 @@ export async function removeFont(name: string) {
export async function updateFonts() {
await awaitStorage(fonts);
- await Promise.allSettled(
+ Promise.allSettled(
Object.keys(fonts).map(
name => saveFont(fonts[name], fonts.__selected === name)
)
diff --git a/src/lib/addons/themes/colors/patches/background.tsx b/src/lib/addons/themes/colors/patches/background.tsx
index 63d4e0f..4c337ca 100644
--- a/src/lib/addons/themes/colors/patches/background.tsx
+++ b/src/lib/addons/themes/colors/patches/background.tsx
@@ -55,6 +55,5 @@ export default function patchChatBackground() {
return {ret};
})
];
-
return () => patches.forEach(x => x());
}
diff --git a/src/lib/addons/themes/colors/types.ts b/src/lib/addons/themes/colors/types.ts
index 99b6e39..203f3c0 100644
--- a/src/lib/addons/themes/colors/types.ts
+++ b/src/lib/addons/themes/colors/types.ts
@@ -39,7 +39,7 @@ export interface VendettaThemeManifest {
/** @internal */
export interface InternalColorDefinition {
- spec: 2 | 3;
+ spec: 1 | 2 | 3;
reference: "darker" | "light";
semantic: Record;
+
+export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload;
+
+export function isModuleRegistered(module: string) {
+ return loaderPayload.loader.modules[module] !== undefined;
+}
+
+export function isFunctionRegistered(module: string, function_: string) {
+ return loaderPayload.loader.modules[module]?.functions[function_] !== undefined;
+}
+
+export async function callFunction(module: string, functionName: string, args: unknown[]) {
+ if (!isFunctionRegistered(module, functionName)) {
+ throw new Error(`Function ${module}.${functionName} is not registered`);
+ }
+
+ const promise: InteropReturn = ImageLoader.queryCache([
+ "__pyon_bridge",
+ JSON.stringify({ m: module, f: functionName, a: args }),
+ ]);
+
+ const result = await promise;
+
+ if ("ret" in result) {
+ return JSON.parse(result.ret);
+ }
+
+ if ("cancelled" in result) {
+ throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`);
+ }
+
+ if ("err" in result) {
+ throw new Error(result.err);
+ }
+}
+
+export interface LoaderModule {
+ hasFunction(functionName: string): boolean;
+ callFunction(functionName: string, args: unknown[]): Promise;
+}
+
+type ModuleProps = {
+ name: string;
+ argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
+};
+
+export function getModule({ name, argumentProcessors: processArgs }: ModuleProps) {
+ const module = loaderPayload.loader.modules[name];
+ if (!module) {
+ throw new Error(`Module ${name} is not registered`);
+ }
+
+ const baseObject: Record = {
+ hasFunction: (functionName: string) => isFunctionRegistered(name, functionName),
+ callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args),
+ };
+
+ for (const [functionName] of Object.entries(module.functions)) {
+ baseObject[functionName] = (...args: unknown[]) =>
+ callFunction(
+ name,
+ functionName,
+ // @ts-expect-error
+ processArgs && functionName in processArgs ? processArgs[functionName](args) : args,
+ );
+ }
+
+ return baseObject as T;
+}
+
+/**
+ * Determines if safe mode is currently enabled. The result remains consistent over time.
+ * This will not change until the app is restarted. For draft changes, use the InitConfigStore.
+ * @returns True if safe mode is enabled for the current instance, otherwise false.
+ */
+export function isSafeModeEnabled() {
+ return Boolean(loaderPayload.loader.initConfig.safeMode);
+}
\ No newline at end of file
diff --git a/src/metro/internals/modules.ts b/src/metro/internals/modules.ts
index afabcf3..4e5c291 100644
--- a/src/metro/internals/modules.ts
+++ b/src/metro/internals/modules.ts
@@ -361,3 +361,100 @@ export function waitForModule(
waitFor(filter, (exports) => resolve(exports), options);
});
}
+
+export interface WaitForOptions {
+ count?: number;
+}
+
+export type ModuleFilter = {
+ (exports: any): T | undefined;
+ key?: string;
+}
+
+export function waitFor(
+ filter: ModuleFilter,
+ callback: (exports: T, id: Metro.ModuleID) => void,
+ options: WaitForOptions = {}
+): () => void {
+ const { count = 1 } = options;
+ let currentCount = 0;
+ const unsubscribers: Array<() => void> = [];
+ let isActive = true;
+
+ const cleanup = () => {
+ if (!isActive) return;
+ isActive = false;
+ unsubscribers.forEach(unsub => unsub());
+ unsubscribers.length = 0;
+ };
+
+ function checkModule(id: Metro.ModuleID): boolean {
+ if (!isActive) return true;
+
+ const exports = requireModule(id);
+ if (isBadExports(exports)) return false;
+
+ const result = filter(exports);
+ if (!result) return false;
+
+ callback(result, id);
+
+ if (++currentCount >= count) {
+ cleanup();
+ return true;
+ }
+
+ return false;
+ }
+
+ if (filter.key) {
+ const cache = getMetroCache().findIndex[filter.key];
+ if (cache) {
+ for (const id in cache) {
+ if (id[0] === "_") continue;
+ const numId = Number(id);
+
+ if (metroModules[numId]?.isInitialized) {
+ if (checkModule(numId)) return cleanup;
+ } else {
+ const unsub = subscribeModule(numId, () => {
+ checkModule(numId);
+ });
+ unsubscribers.push(unsub);
+ }
+ }
+ }
+ }
+
+ for (const id in metroModules) {
+ if (!isActive) break;
+ const numId = Number(id);
+
+ if (metroModules[numId]?.isInitialized && !metroModules[numId]?.hasError) {
+ if (checkModule(numId)) return cleanup;
+ }
+ }
+
+ if (isActive) {
+ for (const id in metroModules) {
+ const numId = Number(id);
+ if (!metroModules[numId]?.isInitialized) {
+ const unsub = subscribeModule(numId, () => {
+ checkModule(numId);
+ });
+ unsubscribers.push(unsub);
+ }
+ }
+ }
+
+ return cleanup;
+}
+
+export function waitForModule(
+ filter: ModuleFilter,
+ options: WaitForOptions = {}
+): Promise {
+ return new Promise((resolve) => {
+ waitFor(filter, (exports) => resolve(exports), options);
+ });
+}