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); + }); +}