From 43a4a5b6655d4500c1b65dc7772d824712ba50a3 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Sun, 8 Mar 2026 18:45:16 +0200 Subject: [PATCH 1/3] docs: update readme --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 633cb8b..e219a60 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,7 @@ If you do not know what root is, go with the manager ### iOS -- **Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) -- **Jailed** - [BTLoader](https://github.com/CloudySn0w/BTLoader) - -if you dont know what jailbreak is, go with BTLoader +[KettuTweak](https://github.com/C0C0B01/KettuTweak) ## Building 1. Install a Kettu loader with loader config support (any mentioned in the [Installing](#installing) section). From cb6a242fe8292cf0d8939d3878a6add5af74d21e Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Wed, 18 Mar 2026 17:53:26 +0200 Subject: [PATCH 2/3] feat: make i18n work (thanks serstars) --- src/core/i18n/index.ts | 165 ++++++++++++++++++++++++++++++++--------- 1 file changed, 129 insertions(+), 36 deletions(-) diff --git a/src/core/i18n/index.ts b/src/core/i18n/index.ts index 3ddf999..c3ec1ef 100644 --- a/src/core/i18n/index.ts +++ b/src/core/i18n/index.ts @@ -1,10 +1,13 @@ +import { logger } from "@lib/utils/logger"; import { FluxDispatcher } from "@metro/common"; -import { findByNameLazy } from "@metro/wrappers"; +import { findByNameLazy, findByStoreName } from "@metro/wrappers"; import { PrimitiveType } from "intl-messageformat"; import langDefault from "./default.json"; -const IntlMessageFormat = findByNameLazy("MessageFormat") as typeof import("intl-messageformat").default; +const IntlMessageFormat = findByNameLazy( + "MessageFormat", +) as typeof import("intl-messageformat").default; type I18nKey = keyof typeof langDefault; @@ -14,54 +17,144 @@ let _lastSetLocale: string | null = null; const _loadedLocale = new Set(); const _loadedStrings = {} as Record; -export const Strings = new Proxy({}, { - get: (_t, prop: keyof typeof langDefault) => { - if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) { - return _loadedStrings[_currentLocale]?.[prop]; +export const Strings = new Proxy( + {}, + { + get: (_t, prop: keyof typeof langDefault) => { + if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) { + return _loadedStrings[_currentLocale]?.[prop]; + } + return langDefault[prop]; + }, + }, +) as Record; + +const languageMap: Record = { + "ar-SA": "ar", + "bn-BD": "bn", + "ca-ES": "ca", + "de-DE": "de", + "es-ES": "es", + "es-419": "es", + "fa-IR": "fa", + "fi-FI": "fi", + "fr-FR": "fr", + "hi-IN": "hi", + "hr-HR": "hr", + "hu-HU": "hu", + "id-ID": "id", + "it-IT": "it", + "ja-JP": "ja", + "pl-PL": "pl", + "pt-BR": "pt_BR", + "ru-RU": "ru", + "sk-SK": "sk", + "sv-SE": "sv", + "tr-TR": "tr", + "vi-VN": "vi", +}; + +function fetchLocale(locale: string) { + const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale); + + logger.log("[i18n] fetchLocale called:", locale, "->", resolvedLocale); + + if (!_loadedLocale.has(resolvedLocale)) { + _loadedLocale.add(resolvedLocale); + + if (resolvedLocale.toLowerCase().startsWith("en")) { + // use local default.json for english + logger.log("[i18n] Using local default.json for English locale"); + _loadedStrings[resolvedLocale] = langDefault; + _currentLocale = resolvedLocale; + } else { + fetch(`https://codeberg.org/cocobo1/kettu-i18n/raw/branch/main/base/${resolvedLocale}.json`) + .then(r => r.json()) + .then(strings => { + logger.log("[i18n] Loaded strings for:", resolvedLocale); + _loadedStrings[resolvedLocale] = strings; + _currentLocale = resolvedLocale; + }) + .catch(e => + logger.error(`[i18n] Error fetching strings for ${resolvedLocale}: ${e}`), + ); } - return langDefault[prop]; + } else { + _currentLocale = resolvedLocale; } -}) as Record; +} export function initFetchI18nStrings() { - const cb = ({ locale }: { locale: string; }) => { - const languageMap = { - "es-ES": "es", - "es-419": "es_419", - "zh-TW": "zh-Hant", - "zh-CN": "zh-Hans", - "pt-PT": "pt", - "pt-BR": "pt_BR", - "sv-SE": "sv" - } as Record; + let attempts = 0; + const checkAndFetch = () => { + attempts++; - const resolvedLocale = _lastSetLocale = languageMap[locale] ?? locale; + try { + const LocaleStore = findByStoreName("LocaleStore"); + logger.log("[i18n] Attempt", attempts, "- LocaleStore:", !!LocaleStore); - if (resolvedLocale.startsWith("en-")) { - _currentLocale = null; - return; + if (!LocaleStore) { + logger.log("[i18n] LocaleStore not found yet"); + return false; + } + + if (LocaleStore?._isInitialized !== true) { + logger.log("[i18n] LocaleStore not initialized yet"); + return false; + } + + const locale = LocaleStore.locale; + + if (locale) { + logger.log("[i18n] Using LocaleStore:", locale); + fetchLocale(locale); + return true; + } + + } catch (e) { + logger.log("[i18n] Error:", e); } + return false; + }; - if (!_loadedLocale.has(resolvedLocale)) { - _loadedLocale.add(resolvedLocale); - - fetch(`https://codeberg.org/cocobo1/kettu-i18n/src/branch/main/base/${resolvedLocale}.json`) - .then(r => r.json()) - .then(strings => _loadedStrings[resolvedLocale] = strings) - .then(() => resolvedLocale === _lastSetLocale && (_currentLocale = resolvedLocale)) - .catch(e => console.error(`An error occured while fetching strings for ${resolvedLocale}: ${e}`)); - } else { - _currentLocale = resolvedLocale; + const tryTimes = () => { + if (checkAndFetch()) return; + if (attempts < 15) { + setTimeout(tryTimes, 500); } }; - FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb); - return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb); + tryTimes(); + + const cb = (e: any) => { + if (e?.settings?.changes?.loading) { + logger.log("[i18n] Settings loading, skipping..."); + return; + } + + const locale = e?.settings?.changes?.protoToSave?.localization?.locale?.value; + logger.log("[i18n] Locale changed:", locale); + if (locale) { + logger.log("[i18n] Found locale in event:", locale); + fetchLocale(locale); + } + }; + + FluxDispatcher.subscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb); + + return () => { + FluxDispatcher.unsubscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb); + }; } -type FormatStringRet = T extends PrimitiveType ? string : string | T | (string | T)[]; +type FormatStringRet = T extends PrimitiveType + ? string + : string | T | (string | T)[]; -export function formatString(key: I18nKey, val: Record): FormatStringRet { +export function formatString( + key: I18nKey, + val: Record, +): FormatStringRet { const str = Strings[key]; // @ts-ignore return new IntlMessageFormat(str).format(val); From 1cf2787f3ae4a1f62bfb46779b697518d0147234 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Wed, 18 Mar 2026 22:12:52 +0200 Subject: [PATCH 3/3] fix: make badges work again --- src/core/plugins/badges/index.tsx | 148 +++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 43 deletions(-) diff --git a/src/core/plugins/badges/index.tsx b/src/core/plugins/badges/index.tsx index 79baa15..7cc5ea6 100644 --- a/src/core/plugins/badges/index.tsx +++ b/src/core/plugins/badges/index.tsx @@ -1,15 +1,43 @@ import { after } from "@lib/api/patcher"; import { onJsxCreate } from "@lib/api/react/jsx"; -import { findByName } from "@metro"; +import { findByName, findByNameLazy } from "@metro"; import { useEffect, useState } from "react"; import { defineCorePlugin } from ".."; +import { FluxDispatcher } from "@metro/common"; -interface BunnyBadge { +interface Badge { label: string; url: string; } -const useBadgesModule = findByName("useBadges", false); +interface CustomBadge { + label: string; + url: string; +} + +interface UserBadgeData { + roles?: string[]; + custom?: CustomBadge[]; +} + +interface BadgeData { + [userId: string]: UserBadgeData; +} + +interface RoleData { + label: string; + url: string; +} + +interface RolesData { + [roleName: string]: RoleData; +} + +const useBadgesModule = findByNameLazy("useBadges", false); + +const badgesCache = new Map(); +const badgeProps = new Map>(); +const pendingRequests = new Set(); export default defineCorePlugin({ manifest: { @@ -26,12 +54,9 @@ export default defineCorePlugin({ }, start() { - let allBadges: { [x: string]: any; } | null = null; - const badgeProps = {} as Record; - onJsxCreate("ProfileBadge", (component, ret) => { - if (ret.props.id?.startsWith("bunny-")) { - const cachedProps = badgeProps[ret.props.id]; + if (ret.props.id?.startsWith("rain-")) { + const cachedProps = badgeProps.get(ret.props.id); if (cachedProps) { ret.props.source = cachedProps.source; ret.props.label = cachedProps.label; @@ -39,54 +64,91 @@ export default defineCorePlugin({ } } }); - + onJsxCreate("RenderedBadge", (component, ret) => { - if (ret.props.id?.startsWith("bunny-")) { - const cachedProps = badgeProps[ret.props.id]; + if (ret.props.id?.startsWith("rain-")) { + const cachedProps = badgeProps.get(ret.props.id); if (cachedProps) { Object.assign(ret.props, cachedProps); } } }); - - after("default", useBadgesModule, ([user], result) => { - const [badges, setBadges] = useState([]); - - useEffect(() => { - if (!user) return; - - if (!allBadges) { - fetch("https://codeberg.org/raincord/badges/raw/branch/main/badges.json") - .then(r => r.json()) - .then(data => { - allBadges = data; - //@ts-expect-error - setBadges(allBadges[user.userId] || []); - }) - } else { - //user has no badges, but maybe they should get some by contributing - setBadges(allBadges[user.userId] || []); + + const fetchAndProcessBadges = async (userId: string) => { + if (pendingRequests.has(userId)) return; + pendingRequests.add(userId); + + try { + const [badgesRes, rolesRes] = await Promise.all([ + fetch("https://codeberg.org/raincord/badges/raw/branch/main/badges.json"), + fetch("https://codeberg.org/raincord/badges/raw/branch/main/assets/roles/roles.json"), + ]); + + const badgesData: BadgeData = await badgesRes.json(); + const rolesData: RolesData = await rolesRes.json(); + + const userBadgeData = badgesData[userId] || { roles: [], custom: [] }; + + const allBadges: Badge[] = []; + + // process role badges + if (userBadgeData.roles) { + userBadgeData.roles.forEach(roleName => { + const roleData = rolesData[roleName]; + if (roleData) { + allBadges.push({ + label: roleData.label, + url: roleData.url, + }); + } + }); } - }, [user?.userId]); - - if (user && badges.length > 0) { - badges.forEach((badge, i) => { - const badgeId = `bunny-${user.userId}-${i}`; - - badgeProps[badgeId] = { + + // process custom badges + if (userBadgeData.custom) { + allBadges.push(...userBadgeData.custom); + } + + badgesCache.set(userId, allBadges); + + allBadges.forEach((badge, i) => { + const badgeId = `rain-${userId}-${i}`; + badgeProps.set(badgeId, { id: badgeId, source: { uri: badge.url }, label: badge.label, - userId: user.userId, - }; - - result.push({ - id: badgeId, - description: badge.label, - icon: " _", + userId, }); }); + + FluxDispatcher.dispatch({ type: "USER_UPDATE", user: { id: userId } }); + } finally { + pendingRequests.delete(userId); } + }; + + after("default", useBadgesModule, ([user], result) => { + if (!user) return; + + const userId = user.userId; + const cached = badgesCache.get(userId); + + if (!cached) { + if (!pendingRequests.has(userId)) { + fetchAndProcessBadges(userId); + } + return; + } + + cached.forEach((badge, i) => { + const badgeId = `rain-${userId}-${i}`; + + result.unshift({ + id: badgeId, + description: badge.label, + icon: " _", + }); + }); }); } }); \ No newline at end of file