mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 13:17:42 +02:00
feat(composer_hooks): ts modules
This commit is contained in:
4
composer/src/main/ts/composer.ts
Normal file
4
composer/src/main/ts/composer.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const jsx = require('composer_core/src/JSX').jsx;
|
||||
export const assetCatalog = require("composer_core/src/AssetCatalog")
|
||||
export const style = require("composer_core/src/Style");
|
||||
export const colors = require("coreui/src/styles/semanticColors");
|
21
composer/src/main/ts/imports.ts
Normal file
21
composer/src/main/ts/imports.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Config, FriendInfo } from "./types";
|
||||
|
||||
declare var _getImportsFunctionName: string;
|
||||
const remoteImports = require('composer_core/src/DeviceBridge')[_getImportsFunctionName]();
|
||||
|
||||
function callRemoteFunction(method: string, ...args: any[]): any | null {
|
||||
return remoteImports[method](...args);
|
||||
}
|
||||
|
||||
|
||||
export const log = (logLevel: string, message: string) => callRemoteFunction("log", logLevel, message);
|
||||
|
||||
export const getConfig = () => callRemoteFunction("getConfig") as Config;
|
||||
|
||||
export const downloadLastOperaMedia = (isLongPress: boolean) => callRemoteFunction("downloadLastOperaMedia", isLongPress);
|
||||
|
||||
export function getFriendInfoByUsername(username: string): FriendInfo | null {
|
||||
const friendInfo = callRemoteFunction("getFriendInfoByUsername", username);
|
||||
if (!friendInfo) return null;
|
||||
return JSON.parse(friendInfo);
|
||||
}
|
36
composer/src/main/ts/main.ts
Normal file
36
composer/src/main/ts/main.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { getConfig, log } from "./imports";
|
||||
import { Module } from "./types";
|
||||
|
||||
import operaDownloadButton from "./modules/operaDownloadButton";
|
||||
import firstCreatedUsername from "./modules/firstCreatedUsername";
|
||||
import bypassCameraRollSelectionLimit from "./modules/bypassCameraRollSelectionLimit";
|
||||
|
||||
|
||||
try {
|
||||
const config = getConfig();
|
||||
|
||||
if (config.composerLogs) {
|
||||
["log", "error", "warn", "info", "debug"].forEach(method => {
|
||||
console[method] = (...args: any) => log(method, Array.from(args).join(" "));
|
||||
})
|
||||
}
|
||||
|
||||
const modules: Module[] = [
|
||||
operaDownloadButton,
|
||||
firstCreatedUsername,
|
||||
bypassCameraRollSelectionLimit
|
||||
];
|
||||
|
||||
modules.forEach(module => {
|
||||
if (!module.enabled(config)) return
|
||||
try {
|
||||
module.init();
|
||||
} catch (e) {
|
||||
console.error(`failed to initialize module ${module.name}`, e, e.stack);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("composer modules loaded!");
|
||||
} catch (e) {
|
||||
log("error", "Failed to load composer modules\n" + e + "\n" + e.stack)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { defineModule } from "../types";
|
||||
import { interceptComponent } from "../utils";
|
||||
|
||||
export default defineModule({
|
||||
name: "Bypass Camera Roll Selection Limit",
|
||||
enabled: config => config.bypassCameraRollLimit,
|
||||
init() {
|
||||
interceptComponent(
|
||||
'memories_ui/src/clickhandlers/MultiSelectClickHandler',
|
||||
'MultiSelectClickHandler',
|
||||
{
|
||||
"<init>": (args: any[], superCall: () => void) => {
|
||||
args[1].selectionLimit = 9999999;
|
||||
superCall();
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
28
composer/src/main/ts/modules/firstCreatedUsername.ts
Normal file
28
composer/src/main/ts/modules/firstCreatedUsername.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { defineModule } from "../types";
|
||||
import { getFriendInfoByUsername } from "../imports";
|
||||
import { interceptComponent } from "../utils";
|
||||
|
||||
export default defineModule({
|
||||
name: "Show First Created Username",
|
||||
enabled: config => config.showFirstCreatedUsername,
|
||||
init() {
|
||||
interceptComponent(
|
||||
'common_profile/src/identity/ProfileIdentityView',
|
||||
'ProfileIdentityView',
|
||||
{
|
||||
onRender: (component: any, _args: any[], render: () => void) => {
|
||||
if (component.viewModel) {
|
||||
let userInfo = getFriendInfoByUsername(component.viewModel.username);
|
||||
if (userInfo) {
|
||||
let firstCreatedUsername = userInfo.username.split("|")[0];
|
||||
if (firstCreatedUsername != component.viewModel.username) {
|
||||
component.viewModel.username += " (" + firstCreatedUsername + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
render();
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
35
composer/src/main/ts/modules/operaDownloadButton.ts
Normal file
35
composer/src/main/ts/modules/operaDownloadButton.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { assetCatalog, jsx, style } from "../composer"
|
||||
import { defineModule } from "../types"
|
||||
import { downloadLastOperaMedia } from "../imports"
|
||||
import { interceptComponent } from "../utils"
|
||||
|
||||
|
||||
export default defineModule({
|
||||
name: "Opera Download Button",
|
||||
enabled: config => config.operaDownloadButton,
|
||||
init() {
|
||||
const downloadIcon = assetCatalog.loadCatalog("share_sheet/res").downloadIcon
|
||||
interceptComponent(
|
||||
'context_chrome_header/src/ChromeHeaderRenderer',
|
||||
'ChromeHeaderRenderer',
|
||||
{
|
||||
onRenderBaseHeader: (_component: any, _args: any[], render: () => void) => {
|
||||
render()
|
||||
jsx.beginRender(jsx.makeNodePrototype("image"))
|
||||
jsx.setAttributeStyle("style", new style.Style({
|
||||
height: 32,
|
||||
marginTop: 4,
|
||||
marginLeft: 8,
|
||||
marginRight: 12,
|
||||
objectFit: "contain",
|
||||
tint: "white"
|
||||
}))
|
||||
jsx.setAttribute("src", downloadIcon)
|
||||
jsx.setAttributeFunction("onTap", () => downloadLastOperaMedia(false))
|
||||
jsx.setAttributeFunction("onLongPress", () => downloadLastOperaMedia(true))
|
||||
jsx.endRender()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
44
composer/src/main/ts/types.ts
Normal file
44
composer/src/main/ts/types.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export interface Config {
|
||||
readonly operaDownloadButton: boolean
|
||||
readonly bypassCameraRollLimit: boolean
|
||||
readonly showFirstCreatedUsername: boolean
|
||||
readonly composerLogs: boolean
|
||||
}
|
||||
|
||||
export interface FriendInfo {
|
||||
readonly id: number
|
||||
readonly lastModifiedTimestamp: number
|
||||
readonly username: string
|
||||
readonly userId: string
|
||||
readonly displayName: string
|
||||
readonly bitmojiAvatarId: string
|
||||
readonly bitmojiSelfieId: string
|
||||
readonly bitmojiSceneId: string
|
||||
readonly bitmojiBackgroundId: string
|
||||
readonly friendmojis: string
|
||||
readonly friendmojiCategories: string
|
||||
readonly snapScore: number
|
||||
readonly birthday: number
|
||||
readonly addedTimestamp: number
|
||||
readonly reverseAddedTimestamp: number
|
||||
readonly serverDisplayName: string
|
||||
readonly streakLength: number
|
||||
readonly streakExpirationTimestamp: number
|
||||
readonly reverseBestFriendRanking: number
|
||||
readonly isPinnedBestFriend: number
|
||||
readonly plusBadgeVisibility: number
|
||||
readonly usernameForSorting: string
|
||||
readonly friendLinkType: number
|
||||
readonly postViewEmoji: string
|
||||
readonly businessCategory: number
|
||||
}
|
||||
|
||||
export interface Module {
|
||||
readonly name: string
|
||||
enabled: (config: Config) => boolean
|
||||
init: () => void
|
||||
}
|
||||
|
||||
export function defineModule<T extends Module>(module: T & Record<string, any>): T {
|
||||
return module
|
||||
}
|
57
composer/src/main/ts/utils.ts
Normal file
57
composer/src/main/ts/utils.ts
Normal file
@ -0,0 +1,57 @@
|
||||
export function dumpObject(obj: any, indent = 0) {
|
||||
if (typeof obj !== "object") return console.log(obj);
|
||||
let prefix = ""
|
||||
for (let i = 0; i < indent; i++) {
|
||||
prefix += " ";
|
||||
}
|
||||
for (let key of Object.keys(obj)) {
|
||||
try {
|
||||
console.log(prefix, key, typeof obj[key], obj[key]);
|
||||
if (key == "renderer") continue
|
||||
if (typeof obj[key] === "object" && indent < 10) dumpObject(obj[key], indent + 1);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
export function proxyProperty(module: any, functionName: string, handler: any) {
|
||||
if (!module || !module[functionName]) {
|
||||
console.warn("Function not found", functionName);
|
||||
return;
|
||||
}
|
||||
module[functionName] = new Proxy(module[functionName], {
|
||||
apply: (a, b, c) => handler(a, b, c),
|
||||
construct: (a, b, c) => handler(a, b, c)
|
||||
});
|
||||
}
|
||||
|
||||
export function interceptComponent(moduleName: string, className: string, functions: any) {
|
||||
proxyProperty(require(moduleName), className, (target: any, args: any[], newTarget: any) => {
|
||||
let initProxy = functions["<init>"]
|
||||
let component: any;
|
||||
|
||||
if (initProxy) {
|
||||
initProxy(args, (newArgs: any[]) => {
|
||||
component = Reflect.construct(target, newArgs || args, newTarget);
|
||||
});
|
||||
} else {
|
||||
component = Reflect.construct(target, args, newTarget);
|
||||
}
|
||||
|
||||
for (let funcName of Object.keys(functions)) {
|
||||
if (funcName == "<init>" || !component[funcName]) continue
|
||||
proxyProperty(component, funcName, (target: any, thisArg: any, argumentsList: any[]) => {
|
||||
let result: any;
|
||||
try {
|
||||
functions[funcName](component, argumentsList, (newArgs: any[]) => {
|
||||
result = Reflect.apply(target, thisArg, newArgs || argumentsList);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error in", funcName, e);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
return component;
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user