Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
495b92b943
9 changed files with 309 additions and 22 deletions
56
.forgejo/workflows/build.yaml
Normal file
56
.forgejo/workflows/build.yaml
Normal file
|
|
@ -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
|
||||||
18
src/core/plugins/kettuenhancements/index.tsx
Normal file
18
src/core/plugins/kettuenhancements/index.tsx
Normal file
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,30 +1,70 @@
|
||||||
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
|
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 { Text } from "@metro/common/components";
|
||||||
import { ErrorBoundary, Search } from "@ui/components";
|
import { ErrorBoundary, Search } from "@ui/components";
|
||||||
import { useMemo } from "react";
|
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() {
|
export default function AssetBrowser() {
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
|
const [showNonImages, setShowNonImages] = React.useState(false);
|
||||||
|
|
||||||
const all = useMemo(() => Array.from(iterateAssets()), []);
|
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 (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Search
|
<View style={{ flexDirection: 'row', alignItems: 'center', margin: 10 }}>
|
||||||
style={{ margin: 10 }}
|
<View style={{ flex: 1, marginRight: 10 }}>
|
||||||
onChangeText={(v: string) => setSearch(v)}
|
<Search
|
||||||
/>
|
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={all.filter(a => a.name.includes(search) || a.id.toString() === search)}
|
|
||||||
renderItem={({ item }: any) => <AssetDisplay asset={item} />}
|
|
||||||
contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
|
|
||||||
keyExtractor={a => a.name}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: showNonImages ? '#0f1013' : '#303139',
|
||||||
|
borderRadius: 8,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minWidth: 44,
|
||||||
|
minHeight: 44,
|
||||||
|
}}
|
||||||
|
onPress={() => setShowNonImages(!showNonImages)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
source={findAssetId("ic_image")!}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<ScrollView>
|
||||||
|
<View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}>
|
||||||
|
{showNonImages && (
|
||||||
|
<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={filteredData}
|
||||||
|
renderItem={({ item }: any) => <AssetDisplay asset={item} showNonImages={showNonImages} />}
|
||||||
|
contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
|
||||||
|
scrollEnabled={false}
|
||||||
|
keyExtractor={a => a.id.toString()}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export async function removeFont(name: string) {
|
||||||
|
|
||||||
export async function updateFonts() {
|
export async function updateFonts() {
|
||||||
await awaitStorage(fonts);
|
await awaitStorage(fonts);
|
||||||
await Promise.allSettled(
|
Promise.allSettled(
|
||||||
Object.keys(fonts).map(
|
Object.keys(fonts).map(
|
||||||
name => saveFont(fonts[name], fonts.__selected === name)
|
name => saveFont(fonts[name], fonts.__selected === name)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,5 @@ export default function patchChatBackground() {
|
||||||
return <ThemeBackground>{ret}</ThemeBackground>;
|
return <ThemeBackground>{ret}</ThemeBackground>;
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => patches.forEach(x => x());
|
return () => patches.forEach(x => x());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export interface VendettaThemeManifest {
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export interface InternalColorDefinition {
|
export interface InternalColorDefinition {
|
||||||
spec: 2 | 3;
|
spec: 1 | 2 | 3;
|
||||||
reference: "darker" | "light";
|
reference: "darker" | "light";
|
||||||
semantic: Record<string, {
|
semantic: Record<string, {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
|
||||||
|
|
@ -216,12 +216,7 @@ export function getReactDevToolsVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSysColorsSupported() {
|
export function isSysColorsSupported() {
|
||||||
if (isPyonLoader()) return pyonLoaderIdentity.isSysColorsSupported;
|
return true;
|
||||||
else if (isVendettaLoader()) {
|
|
||||||
return vendettaLoaderIdentity!!.features.syscolors != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSysColors() {
|
export function getSysColors() {
|
||||||
|
|
|
||||||
82
src/lib/api/native/modules/loader.ts
Normal file
82
src/lib/api/native/modules/loader.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { ImageLoader } from "./";
|
||||||
|
import type { LoaderPayload } from "./types";
|
||||||
|
|
||||||
|
type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>;
|
||||||
|
|
||||||
|
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<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModuleProps<T extends LoaderModule> = {
|
||||||
|
name: string;
|
||||||
|
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
|
||||||
|
const module = loaderPayload.loader.modules[name];
|
||||||
|
if (!module) {
|
||||||
|
throw new Error(`Module ${name} is not registered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseObject: Record<string, any> = {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -361,3 +361,100 @@ export function waitForModule<T = any>(
|
||||||
waitFor(filter, (exports) => resolve(exports), options);
|
waitFor(filter, (exports) => resolve(exports), options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WaitForOptions {
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModuleFilter<T = any> = {
|
||||||
|
(exports: any): T | undefined;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitFor<T = any>(
|
||||||
|
filter: ModuleFilter<T>,
|
||||||
|
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<T = any>(
|
||||||
|
filter: ModuleFilter<T>,
|
||||||
|
options: WaitForOptions = {}
|
||||||
|
): Promise<T> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
waitFor(filter, (exports) => resolve(exports), options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue