Merge pull request #20 from castdrian/objc-rewrite
refactor: rewrite to objc and logos
This commit is contained in:
commit
0ddeccfcfe
28 changed files with 654 additions and 596 deletions
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
|
@ -3,5 +3,12 @@
|
||||||
"MD029": false,
|
"MD029": false,
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
"MD045": false,
|
"MD045": false,
|
||||||
}
|
},
|
||||||
|
"VsCodeTaskButtons.tasks": [
|
||||||
|
{
|
||||||
|
"label": "$(tools) Make and Copy",
|
||||||
|
"task": "Make and Copy Tweak",
|
||||||
|
"tooltip": "Make and copy Tweak"
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
.vscode/tasks.json
vendored
Normal file
35
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"label": "Make and Copy Tweak",
|
||||||
|
"command": "zsh",
|
||||||
|
"args": [
|
||||||
|
"-c",
|
||||||
|
"rm -rf packages && make clean && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless && cp ./packages/app.pyoncord_*arm64.deb ~/Library/Mobile\\ Documents/com~apple~CloudDocs/"
|
||||||
|
],
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "cpp",
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"message": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true,
|
||||||
|
"close": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
Headers/Fonts.h
Normal file
5
Headers/Fonts.h
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
extern NSMutableDictionary<NSString *, NSString *> *fontMap;
|
||||||
|
void patchFonts(NSDictionary<NSString *, NSString *> *mainFonts, NSString *fontDefName);
|
||||||
8
Headers/LoaderConfig.h
Normal file
8
Headers/LoaderConfig.h
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@interface LoaderConfig : NSObject
|
||||||
|
@property (nonatomic, assign) BOOL customLoadUrlEnabled;
|
||||||
|
@property (nonatomic, strong) NSURL *customLoadUrl;
|
||||||
|
+ (instancetype)defaultConfig;
|
||||||
|
+ (instancetype)getLoaderConfig;
|
||||||
|
@end
|
||||||
4
Headers/Logger.h
Normal file
4
Headers/Logger.h
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#define LOG_PREFIX @"[Bunny]"
|
||||||
|
#define BunnyLog(fmt, ...) NSLog((LOG_PREFIX @" " fmt), ##__VA_ARGS__)
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
@import Foundation;
|
|
||||||
|
|
||||||
@interface RCTCxxBridge : NSObject
|
@interface RCTCxxBridge : NSObject
|
||||||
|
|
||||||
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async;
|
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async;
|
||||||
|
@end
|
||||||
@end
|
|
||||||
5
Headers/Theme.h
Normal file
5
Headers/Theme.h
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
void swizzleDCDThemeColor(NSDictionary<NSString *, NSArray<NSString *> *> *semanticColors);
|
||||||
|
void swizzleUIColor(NSDictionary<NSString *, NSString *> *rawColors);
|
||||||
6
Headers/Utils.h
Normal file
6
Headers/Utils.h
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
NSURL *getPyoncordDirectory(void);
|
||||||
|
UIColor *hexToUIColor(NSString *hex);
|
||||||
|
void showErrorAlert(NSString *title, NSString *message);
|
||||||
35
Makefile
35
Makefile
|
|
@ -1,19 +1,30 @@
|
||||||
ifeq ($(THEOS_PACKAGE_SCHEME),rootless)
|
TARGET := iphone:clang:latest:14.0
|
||||||
TARGET := iphone:clang:latest:15.0
|
ARCHS = arm64
|
||||||
else
|
INSTALL_TARGET_PROCESSES = Discord
|
||||||
TARGET := iphone:clang:latest:12.2
|
|
||||||
endif
|
|
||||||
|
|
||||||
|
|
||||||
include $(THEOS)/makefiles/common.mk
|
include $(THEOS)/makefiles/common.mk
|
||||||
|
|
||||||
TWEAK_NAME = BunnyTweak
|
TWEAK_NAME = Bunny
|
||||||
|
BUNDLE_NAME = BunnyResources
|
||||||
|
|
||||||
BunnyTweak_FILES = $(shell find Sources/BunnyTweak -name '*.swift') $(shell find Sources/BunnyTweakC -name '*.m' -o -name '*.c' -o -name '*.mm' -o -name '*.cpp')
|
Bunny_FILES = $(wildcard Sources/*.x Sources/*.m Sources/**/*.x Sources/**/*.m)
|
||||||
BunnyTweak_SWIFTFLAGS = -ISources/BunnyTweakC/include
|
Bunny_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' -I$(THEOS_PROJECT_DIR)/Headers
|
||||||
BunnyTweak_CFLAGS = -fobjc-arc -ISources/BunnyTweakC/include
|
Bunny_FRAMEWORKS = Foundation UIKit CoreGraphics CoreText CoreFoundation
|
||||||
|
|
||||||
BunnyTweak_BUNDLE_NAME = BunnyPatches
|
BunnyResources_INSTALL_PATH = "/Library/Application\ Support/"
|
||||||
BunnyTweak_BUNDLE_RESOURCE_DIRS = Resources
|
BunnyResources_RESOURCE_DIRS = Resources
|
||||||
|
|
||||||
include $(THEOS_MAKE_PATH)/tweak.mk
|
include $(THEOS_MAKE_PATH)/tweak.mk
|
||||||
|
include $(THEOS_MAKE_PATH)/bundle.mk
|
||||||
|
|
||||||
|
before-all::
|
||||||
|
$(ECHO_NOTHING)mkdir -p Resources$(ECHO_END)
|
||||||
|
$(ECHO_NOTHING)sed -e 's/@PACKAGE_VERSION@/$(THEOS_PACKAGE_BASE_VERSION)/g' \
|
||||||
|
-e 's/@TWEAK_NAME@/$(TWEAK_NAME)/g' \
|
||||||
|
Sources/payload-base.template.js > Resources/payload-base.js$(ECHO_END)
|
||||||
|
|
||||||
|
after-stage::
|
||||||
|
$(ECHO_NOTHING)find $(THEOS_STAGING_DIR) -name ".DS_Store" -delete$(ECHO_END)
|
||||||
|
|
||||||
|
after-package::
|
||||||
|
$(ECHO_NOTHING)rm -rf Resources$(ECHO_END)
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
// swift-tools-version:5.2
|
|
||||||
|
|
||||||
import PackageDescription
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
let projectDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
|
|
||||||
|
|
||||||
@dynamicMemberLookup struct TheosConfiguration {
|
|
||||||
private let dict: [String: String]
|
|
||||||
init(at path: String) {
|
|
||||||
let configURL = URL(fileURLWithPath: path, relativeTo: projectDir)
|
|
||||||
guard let infoString = try? String(contentsOf: configURL) else {
|
|
||||||
fatalError("""
|
|
||||||
Could not find Theos SPM config. Have you run `make spm` yet?
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
let pairs = infoString.split(separator: "\n").map {
|
|
||||||
$0.split(
|
|
||||||
separator: "=", maxSplits: 1,
|
|
||||||
omittingEmptySubsequences: false
|
|
||||||
).map(String.init)
|
|
||||||
}.map { ($0[0], $0[1]) }
|
|
||||||
dict = Dictionary(uniqueKeysWithValues: pairs)
|
|
||||||
}
|
|
||||||
subscript(
|
|
||||||
key: String,
|
|
||||||
or defaultValue: @autoclosure () -> String? = nil
|
|
||||||
) -> String {
|
|
||||||
if let value = dict[key] {
|
|
||||||
return value
|
|
||||||
} else if let def = defaultValue() {
|
|
||||||
return def
|
|
||||||
} else {
|
|
||||||
fatalError("""
|
|
||||||
Could not get value of key '\(key)' from Theos SPM config. \
|
|
||||||
Try running `make spm` again.
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subscript(dynamicMember key: String) -> String { self[key] }
|
|
||||||
}
|
|
||||||
let conf = TheosConfiguration(at: ".theos/spm_config")
|
|
||||||
|
|
||||||
let theosPath = conf.theos
|
|
||||||
let sdk = conf.sdk
|
|
||||||
let resourceDir = conf.swiftResourceDir
|
|
||||||
let deploymentTarget = conf.deploymentTarget
|
|
||||||
let triple = "arm64-apple-ios\(deploymentTarget)"
|
|
||||||
|
|
||||||
let libFlags: [String] = [
|
|
||||||
"-F\(theosPath)/vendor/lib", "-F\(theosPath)/lib",
|
|
||||||
"-I\(theosPath)/vendor/include", "-I\(theosPath)/include"
|
|
||||||
]
|
|
||||||
|
|
||||||
let cFlags: [String] = libFlags + [
|
|
||||||
"-target", triple, "-isysroot", sdk,
|
|
||||||
"-Wno-unused-command-line-argument", "-Qunused-arguments",
|
|
||||||
]
|
|
||||||
|
|
||||||
let cxxFlags: [String] = [
|
|
||||||
]
|
|
||||||
|
|
||||||
let swiftFlags: [String] = libFlags + [
|
|
||||||
"-target", triple, "-sdk", sdk, "-resource-dir", resourceDir,
|
|
||||||
]
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "BunnyTweak",
|
|
||||||
platforms: [.iOS(deploymentTarget)],
|
|
||||||
products: [
|
|
||||||
.library(
|
|
||||||
name: "BunnyTweak",
|
|
||||||
targets: ["BunnyTweak"]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
.target(
|
|
||||||
name: "BunnyTweakC",
|
|
||||||
cSettings: [.unsafeFlags(cFlags)],
|
|
||||||
cxxSettings: [.unsafeFlags(cxxFlags)]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "BunnyTweak",
|
|
||||||
dependencies: ["BunnyTweakC"],
|
|
||||||
swiftSettings: [.unsafeFlags(swiftFlags)]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
14
README.md
14
README.md
|
|
@ -3,22 +3,22 @@
|
||||||
Tweak to inject [Bunny](https://github.com/pyoncord/Bunny) into Discord. Forked [VendettaTweak](https://github.com/vendetta-mod/VendettaTweak), modified to match with [BunnyXposed](https://github.com/pyoncord/BunnyXposed) behavior. There are still slight differences between these two, and this tweak may be missing some loader features.
|
Tweak to inject [Bunny](https://github.com/pyoncord/Bunny) into Discord. Forked [VendettaTweak](https://github.com/vendetta-mod/VendettaTweak), modified to match with [BunnyXposed](https://github.com/pyoncord/BunnyXposed) behavior. There are still slight differences between these two, and this tweak may be missing some loader features.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> As of right now this tweak does not encompass some functionalities when running in a jailed environment with a wildcard certificate \
|
> As of right now this tweak does not encompass some functionalities when running in a jailed environment with a distribution certificate that has a different App ID \
|
||||||
> If you value these features sign the application with a local dev certificate:
|
> If you value these features sign the application with a local dev certificate:
|
||||||
> - setAlternateAppIcon does not work, thus breaking dynamic app icons
|
>
|
||||||
> - sharing files to the application/selecting items via the Files app does not work
|
> - setAlternateAppIcon does not work, thus breaking dynamic app icons (unlikely to be able to be fixed unless iOS behavior changes)
|
||||||
|
> - sharing files to the application/selecting items via the Files app does not work (this might be more of a keychain/app group issue)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Builds can be found in the [Releases](https://github.com/pyoncord/BunnyTweak/releases/latest) tab.
|
Builds can be found in the [Releases](https://github.com/pyoncord/BunnyTweak/releases/latest) tab.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Raw decrypted IPAs which are used to provide prepatched IPAs are sourced from the [Enmity](https://github.com/enmity-mod/) community. These raw decrypted IPAs are also used throughout Enmity related projects such as [enmity-mod/tweak](https://github.com/enmity-mod/tweak/) and [acquitelol/rosiecord](https://github.com/acquitelol/rosiecord).\
|
> Decrypted IPAs are sourced from the [Enmity](https://github.com/enmity-mod/) community. These are also used throughout Enmity related projects such as [enmity-mod/tweak](https://github.com/enmity-mod/tweak/) and [acquitelol/rosiecord](https://github.com/acquitelol/rosiecord).\
|
||||||
> All credits are attributed to the owner(s) of the raw IPAs.
|
> All credits are attributed to the owner(s).
|
||||||
|
|
||||||
### Jailbroken
|
### Jailbroken
|
||||||
|
|
||||||
1. Install the Orion runtime via your preferred package manager, by adding `https://repo.chariz.com/` to your sources, then finding `Orion Runtime`.
|
|
||||||
1. Install Bunny by downloading the appropriate Debian package (or by building your own, see [Building BunnyTweak locally](#building-bunnytweak-locally)) and adding it to your package manager. Use the file ending in `arm.deb` for rootful jailbreaks, and the file ending in `arm64.deb` for rootless jailbreaks.
|
1. Install Bunny by downloading the appropriate Debian package (or by building your own, see [Building BunnyTweak locally](#building-bunnytweak-locally)) and adding it to your package manager. Use the file ending in `arm.deb` for rootful jailbreaks, and the file ending in `arm64.deb` for rootless jailbreaks.
|
||||||
|
|
||||||
### Jailed
|
### Jailed
|
||||||
|
|
@ -34,7 +34,7 @@ Builds can be found in the [Releases](https://github.com/pyoncord/BunnyTweak/rel
|
||||||
## Building BunnyTweak locally
|
## Building BunnyTweak locally
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> These steps assume you use MacOS.
|
> These steps assume you use macOS.
|
||||||
|
|
||||||
1. Install Xcode from the App Store. If you've previously installed the `Command Line Utilities` package, you will need to run `sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer` to make sure you're using the Xcode tools instead.
|
1. Install Xcode from the App Store. If you've previously installed the `Command Line Utilities` package, you will need to run `sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer` to make sure you're using the Xcode tools instead.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import BunnyTweakC
|
|
||||||
import Orion
|
|
||||||
import UIKit
|
|
||||||
import os
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
// This is not the full implementation, we only take what we need in here
|
|
||||||
// For full definition, find it from the core codebase (JS side)
|
|
||||||
struct FontDefinition: Codable {
|
|
||||||
let name: String
|
|
||||||
let main: [String: String]?
|
|
||||||
}
|
|
||||||
|
|
||||||
var fontMap: [String: String] = [:]
|
|
||||||
|
|
||||||
class FontsHook: ClassHook<UIFont> {
|
|
||||||
class func fontWithName(_ name: String, size: CGFloat) -> UIFont {
|
|
||||||
if let replacementName = fontMap[name] {
|
|
||||||
let replacementDescriptor = UIFontDescriptor(name: replacementName, size: size)
|
|
||||||
let fallbackDescriptor = replacementDescriptor.addingAttributes([.name: [name]])
|
|
||||||
let finalDescriptor = replacementDescriptor.addingAttributes([.cascadeList: [fallbackDescriptor]])
|
|
||||||
|
|
||||||
return orig.fontWithDescriptor(finalDescriptor, size: size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return orig.fontWithName(name, size: size)
|
|
||||||
}
|
|
||||||
class func fontWithDescriptor(_ descriptor: UIFontDescriptor, size: CGFloat) -> UIFont {
|
|
||||||
if let replacementName = fontMap[descriptor.postscriptName] {
|
|
||||||
let replacementDescriptor = UIFontDescriptor(name: replacementName, size: size)
|
|
||||||
let finalDescriptor = replacementDescriptor.addingAttributes([.cascadeList: [descriptor]])
|
|
||||||
|
|
||||||
return orig.fontWithDescriptor(finalDescriptor, size: size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return orig.fontWithDescriptor(descriptor, size: size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func patchFonts(_ main: [String: String], fontDefName: String) {
|
|
||||||
for (fontName, url) in main {
|
|
||||||
os_log("Replacing font %{public}@ with URL: %{public}@", log: bunnyLog, type: .info, fontName, url)
|
|
||||||
|
|
||||||
let fontExtension = URL(string: url)!.pathExtension
|
|
||||||
let fontCachePath = pyoncordDirectory
|
|
||||||
.appendingPathComponent("downloads", isDirectory: true)
|
|
||||||
.appendingPathComponent("fonts", isDirectory: true)
|
|
||||||
.appendingPathComponent(fontDefName, isDirectory: true)
|
|
||||||
.appendingPathComponent("\(fontName).\(fontExtension)")
|
|
||||||
|
|
||||||
do {
|
|
||||||
os_log("Attempting to register font %{public}@ from %{public}@", log: bunnyLog, type: .info, fontName, url)
|
|
||||||
|
|
||||||
let parent = fontCachePath.deletingLastPathComponent()
|
|
||||||
if !FileManager.default.fileExists(atPath: parent.path) {
|
|
||||||
os_log("Creating parent directory: %{public}@", log: bunnyLog, type: .debug, parent.path)
|
|
||||||
try FileManager.default.createDirectory(
|
|
||||||
at: parent, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS side should download these already, but just in case...
|
|
||||||
if !FileManager.default.fileExists(atPath: fontCachePath.path) {
|
|
||||||
os_log("Downloading font %{public}@ from %{public}@", log: bunnyLog, type: .debug, fontName, url)
|
|
||||||
if let data = try? Data(contentsOf: URL(string: url)!) {
|
|
||||||
os_log("Writing font data to: %{public}@", log: bunnyLog, type: .debug, fontCachePath.path)
|
|
||||||
try? data.write(to: fontCachePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let data = try? Data(contentsOf: fontCachePath) {
|
|
||||||
os_log("Registering font %{public}@ with provider", log: bunnyLog, type: .debug, fontName)
|
|
||||||
let provider = CGDataProvider(data: data as CFData)
|
|
||||||
let font = CGFont(provider!)
|
|
||||||
var error: Unmanaged<CFError>?
|
|
||||||
|
|
||||||
// This does not work with system/app fonts unfortunately. Throws a CTFontManagerError.systemRequired
|
|
||||||
if let existingFont = CGFont(font!.postScriptName!) {
|
|
||||||
var unregisterError: Unmanaged<CFError>?
|
|
||||||
if !CTFontManagerUnregisterGraphicsFont(existingFont, &unregisterError) {
|
|
||||||
os_log("Failed to deregister font %{public}@: %{public}@", log: bunnyLog, type: .error,
|
|
||||||
font!.postScriptName! as String,
|
|
||||||
String(describing: unregisterError!.takeUnretainedValue()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if CTFontManagerRegisterGraphicsFont(font!, &error) {
|
|
||||||
fontMap[fontName] = font!.postScriptName! as String
|
|
||||||
os_log("Successfully registered font %{public}@ to %{public}@", log: bunnyLog, type: .info, fontName, font!.postScriptName! as String)
|
|
||||||
} else {
|
|
||||||
os_log("Failed to register font %{public}@: %{public}@", log: bunnyLog, type: .error, fontName, String(describing: error!.takeUnretainedValue()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
os_log("Failed to read font data from: %{public}@", log: bunnyLog, type: .error, fontCachePath.path)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
os_log("Failed to register font %{public}@: %{public}@", log: bunnyLog, type: .error, fontName, error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import Foundation
|
|
||||||
import os
|
|
||||||
|
|
||||||
enum LoaderConfigError: Error {
|
|
||||||
case doesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CustomLoadUrl: Codable {
|
|
||||||
let enabled: Bool
|
|
||||||
let url: URL
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LoaderConfig: Codable {
|
|
||||||
let customLoadUrl: CustomLoadUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
let defaultLoaderConfig = LoaderConfig(
|
|
||||||
customLoadUrl: CustomLoadUrl(
|
|
||||||
enabled: false,
|
|
||||||
url: URL(string: "http://localhost:4040/bunny.js")!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
let pyoncordDirectory = getPyoncordDirectory()
|
|
||||||
let loaderConfigUrl = pyoncordDirectory.appendingPathComponent("loader.json")
|
|
||||||
|
|
||||||
func getLoaderConfig() -> LoaderConfig {
|
|
||||||
os_log("Getting loader config", log: bunnyLog, type: .debug)
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
|
||||||
do {
|
|
||||||
if fileManager.fileExists(atPath: loaderConfigUrl.path) {
|
|
||||||
let data = try Data(contentsOf: loaderConfigUrl)
|
|
||||||
let loaderConfig = try JSONDecoder().decode(LoaderConfig.self, from: data)
|
|
||||||
|
|
||||||
os_log("Got loader config", log: bunnyLog, type: .debug)
|
|
||||||
|
|
||||||
return loaderConfig
|
|
||||||
} else {
|
|
||||||
throw LoaderConfigError.doesNotExist
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
os_log("Couldn't get loader config", log: bunnyLog, type: .error)
|
|
||||||
|
|
||||||
return defaultLoaderConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
import Orion
|
|
||||||
import UIKit
|
|
||||||
import BunnyTweakC
|
|
||||||
import os
|
|
||||||
|
|
||||||
struct Author: Codable {
|
|
||||||
let name: String
|
|
||||||
let id: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThemeData: Codable {
|
|
||||||
let name: String
|
|
||||||
let description: String?
|
|
||||||
let authors: [Author]?
|
|
||||||
let spec: Int
|
|
||||||
let semanticColors: [String: [String]]?
|
|
||||||
let rawColors: [String: String]?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Theme: Codable {
|
|
||||||
let id: String
|
|
||||||
let selected: Bool
|
|
||||||
let data: ThemeData
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexToUIColor(_ hex: String) -> UIColor? {
|
|
||||||
let r: CGFloat
|
|
||||||
let g: CGFloat
|
|
||||||
let b: CGFloat
|
|
||||||
let a: CGFloat
|
|
||||||
|
|
||||||
if hex.hasPrefix("#") {
|
|
||||||
let start = hex.index(hex.startIndex, offsetBy: 1)
|
|
||||||
var hexColor = String(hex[start...])
|
|
||||||
|
|
||||||
if hexColor.count == 6 {
|
|
||||||
hexColor.append("ff")
|
|
||||||
}
|
|
||||||
|
|
||||||
if hexColor.count == 8 {
|
|
||||||
let scanner = Scanner(string: hexColor)
|
|
||||||
var hexNumber: UInt64 = 0
|
|
||||||
|
|
||||||
if scanner.scanHexInt64(&hexNumber) {
|
|
||||||
r = CGFloat((hexNumber & 0xff00_0000) >> 24) / 255
|
|
||||||
g = CGFloat((hexNumber & 0x00ff_0000) >> 16) / 255
|
|
||||||
b = CGFloat((hexNumber & 0x0000_ff00) >> 8) / 255
|
|
||||||
a = CGFloat(hexNumber & 0x0000_00ff) / 255
|
|
||||||
|
|
||||||
return UIColor(red: r, green: g, blue: b, alpha: a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func swizzleDCDThemeColor(_ semanticColors: [String: [String]]) {
|
|
||||||
os_log("Swizzling DCDThemeColor", log: bunnyLog, type: .info)
|
|
||||||
|
|
||||||
let DCDTheme: AnyClass = NSClassFromString("DCDTheme")!
|
|
||||||
let dcdThemeTarget: AnyClass = object_getClass(DCDTheme)!
|
|
||||||
|
|
||||||
let themeIndexSelector = NSSelectorFromString("themeIndex")
|
|
||||||
let themeIndexMethod = class_getClassMethod(dcdThemeTarget, themeIndexSelector)!
|
|
||||||
let themeIndexImpl = method_getImplementation(themeIndexMethod)
|
|
||||||
typealias ThemeIndexType = @convention(c) (AnyObject, Selector) -> Int
|
|
||||||
let themeIndex: ThemeIndexType = unsafeBitCast(themeIndexImpl, to: ThemeIndexType.self)
|
|
||||||
|
|
||||||
let DCDThemeColor: AnyClass = NSClassFromString("DCDThemeColor")!
|
|
||||||
os_log("Found instance of DCDThemeColor", log: bunnyLog, type: .debug)
|
|
||||||
let target: AnyClass = object_getClass(DCDThemeColor)!
|
|
||||||
|
|
||||||
os_log("Found DCDThemeColor", log: bunnyLog, type: .debug)
|
|
||||||
|
|
||||||
var methodCount: UInt32 = 0
|
|
||||||
let methods = class_copyMethodList(target, &methodCount)!
|
|
||||||
|
|
||||||
os_log("DCDThemeColor has %{public}d methods", log: bunnyLog, type: .debug, methodCount)
|
|
||||||
|
|
||||||
for i in 0..<Int(methodCount) {
|
|
||||||
let method = methods[i]
|
|
||||||
let selector = method_getName(method)
|
|
||||||
let methodName = NSStringFromSelector(selector)
|
|
||||||
os_log(
|
|
||||||
"Found method %{public}@", log: bunnyLog, type: .debug, methodName)
|
|
||||||
if let semanticColor = semanticColors[methodName] {
|
|
||||||
os_log(
|
|
||||||
"Swizzling %{public}@", log: bunnyLog, type: .debug, methodName)
|
|
||||||
|
|
||||||
let originalImpl = method_getImplementation(method)
|
|
||||||
typealias OriginalType = @convention(c) (AnyObject, Selector) -> UIColor
|
|
||||||
let original = unsafeBitCast(originalImpl, to: OriginalType.self)
|
|
||||||
let semanticColorBlock: @convention(block) (AnyObject) -> UIColor = {
|
|
||||||
(self: AnyObject) -> UIColor in
|
|
||||||
let themeIndexVal = themeIndex(dcdThemeTarget, themeIndexSelector)
|
|
||||||
if semanticColor.count - 1 >= themeIndexVal {
|
|
||||||
if let semanticUIColor = hexToUIColor(
|
|
||||||
semanticColor[themeIndexVal])
|
|
||||||
{
|
|
||||||
return semanticUIColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return original(target, selector)
|
|
||||||
}
|
|
||||||
let semanticColorImplementation = imp_implementationWithBlock(
|
|
||||||
unsafeBitCast(semanticColorBlock, to: AnyObject.self))
|
|
||||||
method_setImplementation(
|
|
||||||
method, semanticColorImplementation)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
free(methods)
|
|
||||||
}
|
|
||||||
|
|
||||||
func swizzleUIColor(_ rawColors: [String: String]) {
|
|
||||||
os_log("Swizzling UIColor", log: bunnyLog, type: .info)
|
|
||||||
|
|
||||||
let UIColor: AnyClass = NSClassFromString("UIColor")!
|
|
||||||
os_log("Found instance of UIColor", log: bunnyLog, type: .debug)
|
|
||||||
let target: AnyClass = object_getClass(UIColor)!
|
|
||||||
|
|
||||||
os_log("Found UIColor", log: bunnyLog, type: .debug)
|
|
||||||
|
|
||||||
var methodCount: UInt32 = 0
|
|
||||||
let methods = class_copyMethodList(target, &methodCount)!
|
|
||||||
|
|
||||||
os_log("UIColor has %{public}d methods", log: bunnyLog, type: .debug, methodCount)
|
|
||||||
|
|
||||||
for i in 0..<Int(methodCount) {
|
|
||||||
let method = methods[i]
|
|
||||||
let selector = method_getName(method)
|
|
||||||
let methodName = NSStringFromSelector(selector)
|
|
||||||
os_log(
|
|
||||||
"Found method %{public}@", log: bunnyLog, type: .debug, methodName)
|
|
||||||
if let rawColor = rawColors[methodName] {
|
|
||||||
os_log(
|
|
||||||
"Swizzling %{public}@", log: bunnyLog, type: .debug, methodName)
|
|
||||||
let rawColorBlock: @convention(block) (AnyObject) -> UIColor = {
|
|
||||||
(self: AnyObject) -> UIColor in
|
|
||||||
return hexToUIColor(rawColor)!
|
|
||||||
}
|
|
||||||
let rawColorImplementation = imp_implementationWithBlock(
|
|
||||||
unsafeBitCast(rawColorBlock, to: AnyObject.self))
|
|
||||||
method_setImplementation(
|
|
||||||
method, rawColorImplementation)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
free(methods)
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
import Orion
|
|
||||||
import BunnyTweakC
|
|
||||||
import os
|
|
||||||
|
|
||||||
let bunnyLog = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "bunny")
|
|
||||||
let source = URL(string: "bunny")!
|
|
||||||
|
|
||||||
let install_prefix = String(cString: get_install_prefix())
|
|
||||||
let isJailbroken = FileManager.default.fileExists(atPath: "\(install_prefix)/Library/Application Support/BunnyTweak/BunnyPatches.bundle")
|
|
||||||
|
|
||||||
let bunnyPatchesBundlePath = isJailbroken ? "\(install_prefix)/Library/Application Support/BunnyTweak/BunnyPatches.bundle" : "\(Bundle.main.bundleURL.path)/BunnyPatches.bundle"
|
|
||||||
|
|
||||||
class FileManagerLoadHook: ClassHook<FileManager> {
|
|
||||||
func containerURLForSecurityApplicationGroupIdentifier(_ groupIdentifier: NSString?) -> URL? {
|
|
||||||
os_log("containerURLForSecurityApplicationGroupIdentifier called! %{public}@ groupIdentifier", log: bunnyLog, type: .debug, groupIdentifier ?? "nil")
|
|
||||||
|
|
||||||
if (isJailbroken) {
|
|
||||||
return orig.containerURLForSecurityApplicationGroupIdentifier(groupIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
|
||||||
let lastPath = paths.last!
|
|
||||||
return lastPath.appendingPathComponent("AppGroup")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LoadHook: ClassHook<RCTCxxBridge> {
|
|
||||||
func executeApplicationScript(_ script: Data, url: URL, async: Bool) {
|
|
||||||
os_log("executeApplicationScript called!", log: bunnyLog, type: .debug)
|
|
||||||
|
|
||||||
let loaderConfig = getLoaderConfig()
|
|
||||||
|
|
||||||
let bunnyPatchesBundle = Bundle(path: bunnyPatchesBundlePath)!
|
|
||||||
|
|
||||||
if let patchPath = bunnyPatchesBundle.url(forResource: "payload-base", withExtension: "js") {
|
|
||||||
let patchData = try! Data(contentsOf: patchPath)
|
|
||||||
os_log("Executing payload base", log: bunnyLog, type: .debug)
|
|
||||||
orig.executeApplicationScript(patchData, url: source, async: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
let pyoncordDirectory = getPyoncordDirectory()
|
|
||||||
|
|
||||||
var bundle = try? Data(contentsOf: pyoncordDirectory.appendingPathComponent("bundle.js"))
|
|
||||||
|
|
||||||
let group = DispatchGroup()
|
|
||||||
|
|
||||||
group.enter()
|
|
||||||
var bundleUrl: URL
|
|
||||||
if loaderConfig.customLoadUrl.enabled {
|
|
||||||
os_log(
|
|
||||||
"Custom load URL enabled, with URL %{public}@ ", log: bunnyLog, type: .info,
|
|
||||||
loaderConfig.customLoadUrl.url.absoluteString)
|
|
||||||
bundleUrl = loaderConfig.customLoadUrl.url
|
|
||||||
} else {
|
|
||||||
bundleUrl = URL(
|
|
||||||
string: "https://raw.githubusercontent.com/pyoncord/bunny-builds/main/bunny.js")!
|
|
||||||
}
|
|
||||||
|
|
||||||
os_log("Fetching JS bundle", log: bunnyLog, type: .info)
|
|
||||||
var bundleRequest = URLRequest(
|
|
||||||
url: bundleUrl, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 3.0)
|
|
||||||
|
|
||||||
if let bundleEtag = try? String(
|
|
||||||
contentsOf: pyoncordDirectory.appendingPathComponent("etag.txt")), bundle != nil
|
|
||||||
{
|
|
||||||
bundleRequest.addValue(bundleEtag, forHTTPHeaderField: "If-None-Match")
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetchTask = URLSession.shared.dataTask(with: bundleRequest) { data, response, error in
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
|
||||||
os_log("Successfully fetched JS Bundle", log: bunnyLog, type: .debug)
|
|
||||||
bundle = data
|
|
||||||
try? bundle?.write(to: pyoncordDirectory.appendingPathComponent("bundle.js"))
|
|
||||||
|
|
||||||
let etag = httpResponse.allHeaderFields["Etag"] as? String
|
|
||||||
try? etag?.write(
|
|
||||||
to: pyoncordDirectory.appendingPathComponent("etag.txt"), atomically: true,
|
|
||||||
encoding: .utf8)
|
|
||||||
}
|
|
||||||
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTask.resume()
|
|
||||||
group.wait()
|
|
||||||
|
|
||||||
if let themeString = try? String(
|
|
||||||
contentsOf: pyoncordDirectory.appendingPathComponent("current-theme.json"))
|
|
||||||
{
|
|
||||||
orig.executeApplicationScript(
|
|
||||||
"globalThis.__PYON_LOADER__.storedTheme=\(themeString)".data(using: .utf8)!, url: source, async: async)
|
|
||||||
}
|
|
||||||
|
|
||||||
let preloadsDirectory = pyoncordDirectory.appendingPathComponent("preloads")
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: preloadsDirectory.path) {
|
|
||||||
do {
|
|
||||||
let contents = try FileManager.default.contentsOfDirectory(
|
|
||||||
at: preloadsDirectory, includingPropertiesForKeys: nil, options: [])
|
|
||||||
|
|
||||||
for fileURL in contents {
|
|
||||||
if fileURL.pathExtension == "js" {
|
|
||||||
os_log(
|
|
||||||
"Executing preload JS file %{public}@ ", log: bunnyLog, type: .info, fileURL.absoluteString)
|
|
||||||
|
|
||||||
if let data = try? Data(contentsOf: fileURL) {
|
|
||||||
orig.executeApplicationScript(data, url: source, async: async)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
os_log("Error reading contents of preloads directory", log: bunnyLog, type: .error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundle != nil {
|
|
||||||
os_log("Executing JS bundle", log: bunnyLog, type: .info)
|
|
||||||
orig.executeApplicationScript(bundle!, url: source, async: async)
|
|
||||||
} else {
|
|
||||||
os_log("Unable to fetch JS bundle", log: bunnyLog, type: .error)
|
|
||||||
}
|
|
||||||
|
|
||||||
os_log("Executing original script", log: bunnyLog, type: .info)
|
|
||||||
orig.executeApplicationScript(script, url: url, async: async)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BunnyTweak: Tweak {
|
|
||||||
func tweakDidActivate() {
|
|
||||||
if let themeData = try? Data(
|
|
||||||
contentsOf: pyoncordDirectory.appendingPathComponent("current-theme.json")) {
|
|
||||||
let theme = try? JSONDecoder().decode(Theme.self, from: themeData)
|
|
||||||
if let semanticColors = theme?.data.semanticColors { swizzleDCDThemeColor(semanticColors) }
|
|
||||||
if let rawColors = theme?.data.rawColors { swizzleUIColor(rawColors) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if let fontData = try? Data(
|
|
||||||
contentsOf: pyoncordDirectory.appendingPathComponent("fonts.json")) {
|
|
||||||
let fonts = try? JSONDecoder().decode(FontDefinition.self, from: fontData)
|
|
||||||
if let main = fonts?.main { patchFonts(main, fontDefName: fonts!.name) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func getPyoncordDirectory() -> URL {
|
|
||||||
let documentDirectoryURL = try! FileManager.default.url(
|
|
||||||
for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
|
||||||
|
|
||||||
let pyoncordFolderURL = documentDirectoryURL.appendingPathComponent("pyoncord")
|
|
||||||
|
|
||||||
// Create the "pyoncord" folder if it doesn't exist
|
|
||||||
if !FileManager.default.fileExists(atPath: pyoncordFolderURL.path) {
|
|
||||||
try! FileManager.default.createDirectory(
|
|
||||||
at: pyoncordFolderURL, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pyoncordFolderURL
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#import <Orion/Orion.h>
|
|
||||||
|
|
||||||
const char * const _Nonnull get_install_prefix(void) {
|
|
||||||
return THEOS_PACKAGE_INSTALL_PREFIX;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((constructor)) static void init() {
|
|
||||||
// Initialize Orion - do not remove this line.
|
|
||||||
orion_init();
|
|
||||||
// Custom initialization code goes here.
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#import "RCTCxxBridge.h"
|
|
||||||
|
|
||||||
const char * const _Nonnull get_install_prefix(void);
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
module BunnyTweakC {
|
|
||||||
umbrella "."
|
|
||||||
export *
|
|
||||||
}
|
|
||||||
150
Sources/Fonts.x
Normal file
150
Sources/Fonts.x
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#import "Fonts.h"
|
||||||
|
#import "Utils.h"
|
||||||
|
#import "Logger.h"
|
||||||
|
#import <CoreText/CoreText.h>
|
||||||
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
|
#import <CoreFoundation/CoreFoundation.h>
|
||||||
|
|
||||||
|
NSMutableDictionary<NSString *, NSString *> *fontMap;
|
||||||
|
|
||||||
|
%hook UIFont
|
||||||
|
|
||||||
|
+ (UIFont *)fontWithName:(NSString *)name size:(CGFloat)size {
|
||||||
|
NSString *replacementName = fontMap[name];
|
||||||
|
if (replacementName) {
|
||||||
|
UIFontDescriptor *replacementDescriptor = [UIFontDescriptor fontDescriptorWithName:replacementName size:size];
|
||||||
|
UIFontDescriptor *fallbackDescriptor = [replacementDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorNameAttribute: @[name]}];
|
||||||
|
UIFontDescriptor *finalDescriptor = [replacementDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorCascadeListAttribute: @[fallbackDescriptor]}];
|
||||||
|
|
||||||
|
return [UIFont fontWithDescriptor:finalDescriptor size:size];
|
||||||
|
}
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (UIFont *)fontWithDescriptor:(UIFontDescriptor *)descriptor size:(CGFloat)size {
|
||||||
|
NSString *replacementName = fontMap[descriptor.postscriptName];
|
||||||
|
if (replacementName) {
|
||||||
|
UIFontDescriptor *replacementDescriptor = [UIFontDescriptor fontDescriptorWithName:replacementName size:size];
|
||||||
|
UIFontDescriptor *finalDescriptor = [replacementDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorCascadeListAttribute: @[descriptor]}];
|
||||||
|
|
||||||
|
return [UIFont fontWithDescriptor:finalDescriptor size:size];
|
||||||
|
}
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (UIFont *)systemFontOfSize:(CGFloat)size {
|
||||||
|
NSString *replacementName = fontMap[@"systemFont"];
|
||||||
|
if (replacementName) {
|
||||||
|
return [UIFont fontWithName:replacementName size:size];
|
||||||
|
}
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (UIFont *)preferredFontForTextStyle:(UIFontTextStyle)style {
|
||||||
|
NSString *replacementName = fontMap[@"systemFont"];
|
||||||
|
if (replacementName) {
|
||||||
|
return [UIFont fontWithName:replacementName size:[UIFont systemFontSize]];
|
||||||
|
}
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
%end
|
||||||
|
|
||||||
|
void patchFonts(NSDictionary<NSString *, NSString *> *mainFonts, NSString *fontDefName) {
|
||||||
|
BunnyLog(@"patchFonts called with fonts: %@ and def name: %@", mainFonts, fontDefName);
|
||||||
|
|
||||||
|
if (!fontMap) {
|
||||||
|
BunnyLog(@"Creating new fontMap");
|
||||||
|
fontMap = [NSMutableDictionary dictionary];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *fontJson = [NSString stringWithContentsOfURL:[getPyoncordDirectory() URLByAppendingPathComponent:@"fonts.json"]
|
||||||
|
encoding:NSUTF8StringEncoding
|
||||||
|
error:nil];
|
||||||
|
if (fontJson) {
|
||||||
|
BunnyLog(@"Found existing fonts.json: %@", fontJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSString *fontName in mainFonts) {
|
||||||
|
NSString *url = mainFonts[fontName];
|
||||||
|
BunnyLog(@"Replacing font %@ with URL: %@", fontName, url);
|
||||||
|
|
||||||
|
NSURL *fontURL = [NSURL URLWithString:url];
|
||||||
|
NSString *fontExtension = fontURL.pathExtension;
|
||||||
|
|
||||||
|
NSURL *fontCachePath = [[[getPyoncordDirectory()
|
||||||
|
URLByAppendingPathComponent:@"downloads" isDirectory:YES]
|
||||||
|
URLByAppendingPathComponent:@"fonts" isDirectory:YES]
|
||||||
|
URLByAppendingPathComponent:fontDefName isDirectory:YES];
|
||||||
|
|
||||||
|
fontCachePath = [fontCachePath URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", fontName, fontExtension]];
|
||||||
|
|
||||||
|
NSURL *parentDir = [fontCachePath URLByDeletingLastPathComponent];
|
||||||
|
if (![[NSFileManager defaultManager] fileExistsAtPath:parentDir.path]) {
|
||||||
|
BunnyLog(@"Creating parent directory: %@", parentDir.path);
|
||||||
|
[[NSFileManager defaultManager] createDirectoryAtURL:parentDir
|
||||||
|
withIntermediateDirectories:YES
|
||||||
|
attributes:nil
|
||||||
|
error:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (![[NSFileManager defaultManager] fileExistsAtPath:fontCachePath.path]) {
|
||||||
|
BunnyLog(@"Downloading font %@ from %@", fontName, url);
|
||||||
|
NSData *data = [NSData dataWithContentsOfURL:fontURL];
|
||||||
|
if (data) {
|
||||||
|
BunnyLog(@"Writing font data to: %@", fontCachePath.path);
|
||||||
|
[data writeToURL:fontCachePath atomically:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSData *fontData = [NSData dataWithContentsOfURL:fontCachePath];
|
||||||
|
if (fontData) {
|
||||||
|
BunnyLog(@"Registering font %@ with provider", fontName);
|
||||||
|
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData);
|
||||||
|
CGFontRef font = CGFontCreateWithDataProvider(provider);
|
||||||
|
|
||||||
|
if (font) {
|
||||||
|
CFStringRef postScriptName = CGFontCopyPostScriptName(font);
|
||||||
|
|
||||||
|
CTFontRef existingFont = CTFontCreateWithName(postScriptName, 0, NULL);
|
||||||
|
if (existingFont) {
|
||||||
|
CFErrorRef unregisterError = NULL;
|
||||||
|
if (!CTFontManagerUnregisterGraphicsFont(font, &unregisterError)) {
|
||||||
|
BunnyLog(@"Failed to deregister font %@: %@", (__bridge NSString *)postScriptName,
|
||||||
|
unregisterError ? (__bridge NSString *)CFErrorCopyDescription(unregisterError) : @"Unknown error");
|
||||||
|
if (unregisterError) CFRelease(unregisterError);
|
||||||
|
}
|
||||||
|
CFRelease(existingFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFErrorRef error = NULL;
|
||||||
|
if (CTFontManagerRegisterGraphicsFont(font, &error)) {
|
||||||
|
fontMap[fontName] = (__bridge NSString *)postScriptName;
|
||||||
|
BunnyLog(@"Successfully registered font %@ to %@", fontName, (__bridge NSString *)postScriptName);
|
||||||
|
|
||||||
|
NSError *jsonError;
|
||||||
|
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:fontMap options:0 error:&jsonError];
|
||||||
|
if (!jsonError) {
|
||||||
|
[jsonData writeToURL:[getPyoncordDirectory() URLByAppendingPathComponent:@"fontMap.json"] atomically:YES];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NSString *errorDesc = error ? (__bridge NSString *)CFErrorCopyDescription(error) : @"Unknown error";
|
||||||
|
BunnyLog(@"Failed to register font %@: %@", fontName, errorDesc);
|
||||||
|
if (error) CFRelease(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFRelease(postScriptName);
|
||||||
|
CFRelease(font);
|
||||||
|
}
|
||||||
|
CGDataProviderRelease(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
%ctor {
|
||||||
|
@autoreleasepool {
|
||||||
|
fontMap = [NSMutableDictionary dictionary];
|
||||||
|
BunnyLog(@"Font hooks initialized");
|
||||||
|
%init;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
Sources/LoaderConfig.m
Normal file
55
Sources/LoaderConfig.m
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#import "LoaderConfig.h"
|
||||||
|
#import "Utils.h"
|
||||||
|
#import "Logger.h"
|
||||||
|
|
||||||
|
@implementation LoaderConfig
|
||||||
|
|
||||||
|
+ (instancetype)defaultConfig {
|
||||||
|
LoaderConfig *config = [[LoaderConfig alloc] init];
|
||||||
|
config.customLoadUrlEnabled = NO;
|
||||||
|
config.customLoadUrl = [NSURL URLWithString:@"http://localhost:4040/bunny.js"];
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (instancetype)getLoaderConfig {
|
||||||
|
BunnyLog(@"Getting loader config");
|
||||||
|
|
||||||
|
NSURL *loaderConfigUrl = [getPyoncordDirectory() URLByAppendingPathComponent:@"loader.json"];
|
||||||
|
|
||||||
|
if ([[NSFileManager defaultManager] fileExistsAtPath:loaderConfigUrl.path]) {
|
||||||
|
NSError *error = nil;
|
||||||
|
NSData *data = [NSData dataWithContentsOfURL:loaderConfigUrl];
|
||||||
|
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||||
|
|
||||||
|
if (json && !error) {
|
||||||
|
LoaderConfig *config = [[LoaderConfig alloc] init];
|
||||||
|
NSDictionary *customLoadUrl = json[@"customLoadUrl"];
|
||||||
|
if (customLoadUrl) {
|
||||||
|
config.customLoadUrlEnabled = [customLoadUrl[@"enabled"] boolValue];
|
||||||
|
NSString *urlString = customLoadUrl[@"url"];
|
||||||
|
if (urlString) {
|
||||||
|
config.customLoadUrl = [NSURL URLWithString:urlString];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BunnyLog(@"Couldn't get loader config");
|
||||||
|
return [LoaderConfig defaultConfig];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)saveConfig {
|
||||||
|
NSURL *loaderConfigUrl = [getPyoncordDirectory() URLByAppendingPathComponent:@"loader.json"];
|
||||||
|
NSDictionary *json = @{
|
||||||
|
@"customLoadUrl": @{
|
||||||
|
@"enabled": @(self.customLoadUrlEnabled),
|
||||||
|
@"url": self.customLoadUrl.absoluteString
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NSData *data = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil];
|
||||||
|
return [data writeToURL:loaderConfigUrl atomically:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
1
Sources/Logger.m
Normal file
1
Sources/Logger.m
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#import "Logger.h"
|
||||||
82
Sources/Theme.m
Normal file
82
Sources/Theme.m
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#import "Theme.h"
|
||||||
|
#import "Utils.h"
|
||||||
|
#import "Logger.h"
|
||||||
|
#import <objc/runtime.h>
|
||||||
|
|
||||||
|
void swizzleDCDThemeColor(NSDictionary<NSString *, NSArray<NSString *> *> *semanticColors) {
|
||||||
|
BunnyLog(@"Swizzling DCDThemeColor");
|
||||||
|
|
||||||
|
Class DCDTheme = NSClassFromString(@"DCDTheme");
|
||||||
|
Class dcdThemeTarget = object_getClass(DCDTheme);
|
||||||
|
|
||||||
|
SEL themeIndexSelector = NSSelectorFromString(@"themeIndex");
|
||||||
|
Method themeIndexMethod = class_getClassMethod(dcdThemeTarget, themeIndexSelector);
|
||||||
|
IMP themeIndexImpl = method_getImplementation(themeIndexMethod);
|
||||||
|
int (*themeIndex)(id, SEL) = (int (*)(id, SEL))themeIndexImpl;
|
||||||
|
|
||||||
|
Class DCDThemeColor = NSClassFromString(@"DCDThemeColor");
|
||||||
|
Class target = object_getClass(DCDThemeColor);
|
||||||
|
|
||||||
|
unsigned int methodCount;
|
||||||
|
Method *methods = class_copyMethodList(target, &methodCount);
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < methodCount; i++) {
|
||||||
|
Method method = methods[i];
|
||||||
|
SEL selector = method_getName(method);
|
||||||
|
NSString *methodName = NSStringFromSelector(selector);
|
||||||
|
|
||||||
|
NSArray<NSString *> *semanticColor = semanticColors[methodName];
|
||||||
|
if (semanticColor) {
|
||||||
|
BunnyLog(@"Swizzling %@", methodName);
|
||||||
|
|
||||||
|
IMP originalImpl = method_getImplementation(method);
|
||||||
|
UIColor *(*original)(id, SEL) = (UIColor *(*)(id, SEL))originalImpl;
|
||||||
|
|
||||||
|
id block = ^UIColor *(id self) {
|
||||||
|
int themeIndexVal = themeIndex(dcdThemeTarget, themeIndexSelector);
|
||||||
|
if (semanticColor.count - 1 >= themeIndexVal) {
|
||||||
|
UIColor *semanticUIColor = hexToUIColor(semanticColor[themeIndexVal]);
|
||||||
|
if (semanticUIColor) {
|
||||||
|
return semanticUIColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return original(target, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
IMP newImpl = imp_implementationWithBlock(block);
|
||||||
|
method_setImplementation(method, newImpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
void swizzleUIColor(NSDictionary<NSString *, NSString *> *rawColors) {
|
||||||
|
BunnyLog(@"Swizzling UIColor");
|
||||||
|
|
||||||
|
Class UIColorClass = NSClassFromString(@"UIColor");
|
||||||
|
Class target = object_getClass(UIColorClass);
|
||||||
|
|
||||||
|
unsigned int methodCount;
|
||||||
|
Method *methods = class_copyMethodList(target, &methodCount);
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < methodCount; i++) {
|
||||||
|
Method method = methods[i];
|
||||||
|
SEL selector = method_getName(method);
|
||||||
|
NSString *methodName = NSStringFromSelector(selector);
|
||||||
|
|
||||||
|
NSString *rawColor = rawColors[methodName];
|
||||||
|
if (rawColor) {
|
||||||
|
BunnyLog(@"Swizzling %@", methodName);
|
||||||
|
|
||||||
|
id block = ^UIColor *(id self) {
|
||||||
|
return hexToUIColor(rawColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
IMP newImpl = imp_implementationWithBlock(block);
|
||||||
|
method_setImplementation(method, newImpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(methods);
|
||||||
|
}
|
||||||
187
Sources/Tweak.x
Normal file
187
Sources/Tweak.x
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import "Utils.h"
|
||||||
|
#import "Logger.h"
|
||||||
|
#import "Theme.h"
|
||||||
|
#import "Fonts.h"
|
||||||
|
#import "LoaderConfig.h"
|
||||||
|
|
||||||
|
static NSURL *source;
|
||||||
|
static BOOL isJailbroken;
|
||||||
|
static NSString *bunnyPatchesBundlePath;
|
||||||
|
static NSURL *pyoncordDirectory;
|
||||||
|
static LoaderConfig *loaderConfig;
|
||||||
|
|
||||||
|
%hook NSFileManager
|
||||||
|
|
||||||
|
- (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier {
|
||||||
|
BunnyLog(@"containerURLForSecurityApplicationGroupIdentifier called! %@",
|
||||||
|
groupIdentifier ?: @"nil");
|
||||||
|
|
||||||
|
if (isJailbroken) {
|
||||||
|
return %orig(groupIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray *paths = [self URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
|
||||||
|
NSURL *lastPath = [paths lastObject];
|
||||||
|
return [lastPath URLByAppendingPathComponent:@"AppGroup"];
|
||||||
|
}
|
||||||
|
|
||||||
|
%end
|
||||||
|
|
||||||
|
%hook RCTCxxBridge
|
||||||
|
|
||||||
|
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async {
|
||||||
|
if (![url.absoluteString containsString:@"main.jsbundle"]) {
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSBundle *bunnyPatchesBundle = [NSBundle bundleWithPath:bunnyPatchesBundlePath];
|
||||||
|
if (!bunnyPatchesBundle) {
|
||||||
|
BunnyLog(@"Failed to load BunnyPatches bundle from path: %@", bunnyPatchesBundlePath);
|
||||||
|
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSURL *patchPath = [bunnyPatchesBundle URLForResource:@"payload-base" withExtension:@"js"];
|
||||||
|
if (!patchPath) {
|
||||||
|
BunnyLog(@"Failed to find payload-base.js in bundle");
|
||||||
|
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSData *patchData = [NSData dataWithContentsOfURL:patchPath];
|
||||||
|
BunnyLog(@"Injecting loader");
|
||||||
|
%orig(patchData, source, YES);
|
||||||
|
|
||||||
|
__block NSData *bundle = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"]];
|
||||||
|
|
||||||
|
dispatch_group_t group = dispatch_group_create();
|
||||||
|
dispatch_group_enter(group);
|
||||||
|
|
||||||
|
NSURL *bundleUrl;
|
||||||
|
if (loaderConfig.customLoadUrlEnabled && loaderConfig.customLoadUrl) {
|
||||||
|
bundleUrl = loaderConfig.customLoadUrl;
|
||||||
|
BunnyLog(@"Using custom load URL: %@", bundleUrl.absoluteString);
|
||||||
|
} else {
|
||||||
|
bundleUrl = [NSURL URLWithString:@"https://raw.githubusercontent.com/bunny-mod/builds/main/bunny.min.js"];
|
||||||
|
BunnyLog(@"Using default bundle URL: %@", bundleUrl.absoluteString);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableURLRequest *bundleRequest = [NSMutableURLRequest requestWithURL:bundleUrl
|
||||||
|
cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
|
||||||
|
timeoutInterval:3.0];
|
||||||
|
|
||||||
|
NSString *bundleEtag = [NSString stringWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"etag.txt"]
|
||||||
|
encoding:NSUTF8StringEncoding
|
||||||
|
error:nil];
|
||||||
|
if (bundleEtag && bundle) {
|
||||||
|
[bundleRequest setValue:bundleEtag forHTTPHeaderField:@"If-None-Match"];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
|
||||||
|
[[session dataTaskWithRequest:bundleRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||||
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||||
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||||
|
if (httpResponse.statusCode == 200) {
|
||||||
|
bundle = data;
|
||||||
|
[bundle writeToURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"] atomically:YES];
|
||||||
|
|
||||||
|
NSString *etag = [httpResponse.allHeaderFields objectForKey:@"Etag"];
|
||||||
|
if (etag) {
|
||||||
|
[etag writeToURL:[pyoncordDirectory URLByAppendingPathComponent:@"etag.txt"]
|
||||||
|
atomically:YES
|
||||||
|
encoding:NSUTF8StringEncoding
|
||||||
|
error:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
}] resume];
|
||||||
|
|
||||||
|
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
||||||
|
|
||||||
|
NSString *themeString = [NSString stringWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"current-theme.json"]
|
||||||
|
encoding:NSUTF8StringEncoding
|
||||||
|
error:nil];
|
||||||
|
if (themeString) {
|
||||||
|
NSString *jsCode = [NSString stringWithFormat:@"globalThis.__PYON_LOADER__.storedTheme=%@", themeString];
|
||||||
|
%orig([jsCode dataUsingEncoding:NSUTF8StringEncoding], source, async);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSData *fontData = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"fonts.json"]];
|
||||||
|
if (fontData) {
|
||||||
|
NSError *jsonError;
|
||||||
|
NSDictionary *fontDict = [NSJSONSerialization JSONObjectWithData:fontData options:0 error:&jsonError];
|
||||||
|
if (!jsonError && fontDict[@"main"]) {
|
||||||
|
BunnyLog(@"Found font configuration, applying...");
|
||||||
|
patchFonts(fontDict[@"main"], fontDict[@"name"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bundle) {
|
||||||
|
BunnyLog(@"Executing JS bundle");
|
||||||
|
%orig(bundle, source, async);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSURL *preloadsDirectory = [pyoncordDirectory URLByAppendingPathComponent:@"preloads"];
|
||||||
|
if ([[NSFileManager defaultManager] fileExistsAtPath:preloadsDirectory.path]) {
|
||||||
|
NSError *error = nil;
|
||||||
|
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:preloadsDirectory
|
||||||
|
includingPropertiesForKeys:nil
|
||||||
|
options:0
|
||||||
|
error:&error];
|
||||||
|
if (!error) {
|
||||||
|
for (NSURL *fileURL in contents) {
|
||||||
|
if ([[fileURL pathExtension] isEqualToString:@"js"]) {
|
||||||
|
BunnyLog(@"Executing preload JS file %@", fileURL.absoluteString);
|
||||||
|
NSData *data = [NSData dataWithContentsOfURL:fileURL];
|
||||||
|
if (data) {
|
||||||
|
%orig(data, source, async);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BunnyLog(@"Error reading contents of preloads directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
%orig(script, url, async);
|
||||||
|
}
|
||||||
|
|
||||||
|
%end
|
||||||
|
|
||||||
|
%ctor {
|
||||||
|
@autoreleasepool {
|
||||||
|
source = [NSURL URLWithString:@"bunny"];
|
||||||
|
|
||||||
|
NSString *install_prefix = @"/var/jb";
|
||||||
|
isJailbroken = [[NSFileManager defaultManager] fileExistsAtPath:install_prefix];
|
||||||
|
|
||||||
|
NSString *bundlePath = [NSString stringWithFormat:@"%@/Library/Application Support/BunnyResources.bundle", install_prefix];
|
||||||
|
BunnyLog(@"Is jailbroken: %d", isJailbroken);
|
||||||
|
BunnyLog(@"Bundle path for jailbroken: %@", bundlePath);
|
||||||
|
|
||||||
|
NSString *jailedPath = [[NSBundle mainBundle].bundleURL.path stringByAppendingPathComponent:@"BunnyResources.bundle"];
|
||||||
|
BunnyLog(@"Bundle path for jailed: %@", jailedPath);
|
||||||
|
|
||||||
|
bunnyPatchesBundlePath = isJailbroken ? bundlePath : jailedPath;
|
||||||
|
BunnyLog(@"Selected bundle path: %@", bunnyPatchesBundlePath);
|
||||||
|
|
||||||
|
BOOL bundleExists = [[NSFileManager defaultManager] fileExistsAtPath:bunnyPatchesBundlePath];
|
||||||
|
BunnyLog(@"Bundle exists at path: %d", bundleExists);
|
||||||
|
|
||||||
|
NSError *error = nil;
|
||||||
|
NSArray *bundleContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:bunnyPatchesBundlePath error:&error];
|
||||||
|
if (error) {
|
||||||
|
BunnyLog(@"Error listing bundle contents: %@", error);
|
||||||
|
} else {
|
||||||
|
BunnyLog(@"Bundle contents: %@", bundleContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
pyoncordDirectory = getPyoncordDirectory();
|
||||||
|
loaderConfig = [[LoaderConfig alloc] init];
|
||||||
|
|
||||||
|
%init;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
Sources/Utils.m
Normal file
69
Sources/Utils.m
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#import "Utils.h"
|
||||||
|
|
||||||
|
NSURL *getPyoncordDirectory(void) {
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
NSURL *documentDirectoryURL = [[fileManager URLsForDirectory:NSDocumentDirectory
|
||||||
|
inDomains:NSUserDomainMask] lastObject];
|
||||||
|
|
||||||
|
NSURL *pyoncordFolderURL = [documentDirectoryURL URLByAppendingPathComponent:@"pyoncord"];
|
||||||
|
|
||||||
|
if (![fileManager fileExistsAtPath:pyoncordFolderURL.path]) {
|
||||||
|
[fileManager createDirectoryAtURL:pyoncordFolderURL
|
||||||
|
withIntermediateDirectories:YES
|
||||||
|
attributes:nil
|
||||||
|
error:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
return pyoncordFolderURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIColor *hexToUIColor(NSString *hex) {
|
||||||
|
if (![hex hasPrefix:@"#"]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *hexColor = [hex substringFromIndex:1];
|
||||||
|
if (hexColor.length == 6) {
|
||||||
|
hexColor = [hexColor stringByAppendingString:@"ff"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hexColor.length == 8) {
|
||||||
|
unsigned int hexNumber;
|
||||||
|
NSScanner *scanner = [NSScanner scannerWithString:hexColor];
|
||||||
|
if ([scanner scanHexInt:&hexNumber]) {
|
||||||
|
CGFloat r = ((hexNumber & 0xFF000000) >> 24) / 255.0;
|
||||||
|
CGFloat g = ((hexNumber & 0x00FF0000) >> 16) / 255.0;
|
||||||
|
CGFloat b = ((hexNumber & 0x0000FF00) >> 8) / 255.0;
|
||||||
|
CGFloat a = (hexNumber & 0x000000FF) / 255.0;
|
||||||
|
|
||||||
|
return [UIColor colorWithRed:r green:g blue:b alpha:a];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showErrorAlert(NSString *title, NSString *message) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
|
||||||
|
message:message
|
||||||
|
preferredStyle:UIAlertControllerStyleAlert];
|
||||||
|
|
||||||
|
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK"
|
||||||
|
style:UIAlertActionStyleDefault
|
||||||
|
handler:nil];
|
||||||
|
|
||||||
|
[alert addAction:okAction];
|
||||||
|
|
||||||
|
UIWindow *window = nil;
|
||||||
|
NSArray *windows = [[UIApplication sharedApplication] windows];
|
||||||
|
for (UIWindow *w in windows) {
|
||||||
|
if (w.isKeyWindow) {
|
||||||
|
window = w;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[window.rootViewController presentViewController:alert animated:YES completion:nil];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
globalThis.__PYON_LOADER__ = {
|
globalThis.__PYON_LOADER__ = {
|
||||||
loaderName: "BunnyTweak",
|
loaderName: "@TWEAK_NAME@",
|
||||||
loaderVersion: "0.3.2",
|
loaderVersion: "@PACKAGE_VERSION@",
|
||||||
hasThemeSupport: true,
|
hasThemeSupport: true,
|
||||||
storedTheme: null,
|
storedTheme: null,
|
||||||
fontPatch: 2
|
fontPatch: 2
|
||||||
}
|
}
|
||||||
10
control
10
control
|
|
@ -1,11 +1,11 @@
|
||||||
Package: io.github.pyoncord.app
|
Package: app.pyoncord
|
||||||
Name: Bunny
|
Name: Bunny
|
||||||
Version: 0.3.2
|
Version: 0.4.0
|
||||||
Architecture: iphoneos-arm
|
Architecture: iphoneos-arm
|
||||||
Description: A mod for Discord's mobile apps.
|
Description: A client modification for Discord's mobile app.
|
||||||
Maintainer: Pylix
|
Maintainer: Pylix
|
||||||
Author: Pylix, FieryFlames
|
Author: Pylix, Adrian Castro, FieryFlames
|
||||||
Icon: https://avatars.githubusercontent.com/u/132727175
|
Icon: https://avatars.githubusercontent.com/u/132727175
|
||||||
Section: Tweaks
|
Section: Tweaks
|
||||||
Depends: ${ORION}, firmware (>= 13.0)
|
Depends: mobilesubstrate (>= 0.9.5000)
|
||||||
Conflicts: app.enmity, mod.vendetta
|
Conflicts: app.enmity, mod.vendetta
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue