feat: Configuration & InjectConfiguraion

This commit is contained in:
wibus-wee
2024-07-19 18:54:04 +08:00
parent a193f66d28
commit 0bb36d2275
10 changed files with 677 additions and 32 deletions

View File

@@ -11,6 +11,12 @@
38877A1F2C4A6F83009F5910 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A1E2C4A6F83009F5910 /* ContentView.swift */; };
38877A212C4A6F87009F5910 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38877A202C4A6F87009F5910 /* Assets.xcassets */; };
38877A252C4A6F87009F5910 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38877A242C4A6F87009F5910 /* Preview Assets.xcassets */; };
38877A2E2C4A6FFA009F5910 /* InjectConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A2D2C4A6FFA009F5910 /* InjectConfiguration.swift */; };
38877A302C4A70DB009F5910 /* SoftwareManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A2F2C4A70DB009F5910 /* SoftwareManager.swift */; };
38877A332C4A7222009F5910 /* PublishedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A322C4A7222009F5910 /* PublishedStorage.swift */; };
38877A352C4A7254009F5910 /* ViewKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A342C4A7254009F5910 /* ViewKit.swift */; };
38877A372C4A7294009F5910 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A362C4A7294009F5910 /* Configuration.swift */; };
38877A3A2C4A730F009F5910 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A392C4A730F009F5910 /* Constants.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -20,6 +26,12 @@
38877A202C4A6F87009F5910 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
38877A222C4A6F87009F5910 /* InjectGUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InjectGUI.entitlements; sourceTree = "<group>"; };
38877A242C4A6F87009F5910 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
38877A2D2C4A6FFA009F5910 /* InjectConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InjectConfiguration.swift; sourceTree = "<group>"; };
38877A2F2C4A70DB009F5910 /* SoftwareManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareManager.swift; sourceTree = "<group>"; };
38877A322C4A7222009F5910 /* PublishedStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedStorage.swift; sourceTree = "<group>"; };
38877A342C4A7254009F5910 /* ViewKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewKit.swift; sourceTree = "<group>"; };
38877A362C4A7294009F5910 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
38877A392C4A730F009F5910 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -52,8 +64,10 @@
38877A1B2C4A6F83009F5910 /* InjectGUI */ = {
isa = PBXGroup;
children = (
38877A1C2C4A6F83009F5910 /* InjectGUIApp.swift */,
38877A1E2C4A6F83009F5910 /* ContentView.swift */,
38877A382C4A7306009F5910 /* App */,
38877A312C4A7217009F5910 /* Extension */,
38877A2C2C4A6FD7009F5910 /* View */,
38877A2B2C4A6FCB009F5910 /* Backend */,
38877A202C4A6F87009F5910 /* Assets.xcassets */,
38877A222C4A6F87009F5910 /* InjectGUI.entitlements */,
38877A232C4A6F87009F5910 /* Preview Content */,
@@ -69,6 +83,42 @@
path = "Preview Content";
sourceTree = "<group>";
};
38877A2B2C4A6FCB009F5910 /* Backend */ = {
isa = PBXGroup;
children = (
38877A2D2C4A6FFA009F5910 /* InjectConfiguration.swift */,
38877A2F2C4A70DB009F5910 /* SoftwareManager.swift */,
38877A362C4A7294009F5910 /* Configuration.swift */,
);
path = Backend;
sourceTree = "<group>";
};
38877A2C2C4A6FD7009F5910 /* View */ = {
isa = PBXGroup;
children = (
38877A1E2C4A6F83009F5910 /* ContentView.swift */,
);
path = View;
sourceTree = "<group>";
};
38877A312C4A7217009F5910 /* Extension */ = {
isa = PBXGroup;
children = (
38877A322C4A7222009F5910 /* PublishedStorage.swift */,
38877A342C4A7254009F5910 /* ViewKit.swift */,
);
path = Extension;
sourceTree = "<group>";
};
38877A382C4A7306009F5910 /* App */ = {
isa = PBXGroup;
children = (
38877A1C2C4A6F83009F5910 /* InjectGUIApp.swift */,
38877A392C4A730F009F5910 /* Constants.swift */,
);
path = App;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -141,6 +191,12 @@
files = (
38877A1F2C4A6F83009F5910 /* ContentView.swift in Sources */,
38877A1D2C4A6F83009F5910 /* InjectGUIApp.swift in Sources */,
38877A372C4A7294009F5910 /* Configuration.swift in Sources */,
38877A302C4A70DB009F5910 /* SoftwareManager.swift in Sources */,
38877A332C4A7222009F5910 /* PublishedStorage.swift in Sources */,
38877A2E2C4A6FFA009F5910 /* InjectConfiguration.swift in Sources */,
38877A352C4A7254009F5910 /* ViewKit.swift in Sources */,
38877A3A2C4A730F009F5910 /* Constants.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -7,8 +7,12 @@
import SwiftUI
let configuration = Configuration.shared
let injectConfiguration = InjectConfiguration.shared
@main
struct InjectGUIApp: App {
var body: some Scene {
WindowGroup {
ContentView()

View File

@@ -0,0 +1,47 @@
//
// Configuration.swift
// InjectGUI
//
// Created by wibus on 2024/7/19.
//
import Foundation
import Combine
class Configuration: NSObject, ObservableObject {
static let shared = Configuration()
var cancellable: Set<AnyCancellable> = .init()
override private init() {
super.init()
print("[I] Configuration inited.")
objectWillChange
.sink { _ in
self.save()
}
.store(in: &cancellable)
}
@PublishedStorage(key: "\(Constants.appKey).remoteGit", defaultValue: "https://github.com/QiuChenly/InjectLib")
var remoteGit: String
@PublishedStorage(key: "\(Constants.appKey).remoteGitCommit", defaultValue: nil)
var remoteGitCommit: String?
@PublishedStorage(key: "\(Constants.appKey).remoteGitBranch", defaultValue: nil)
var remoteGitBranch: String?
public func save() {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(saveNow), object: nil)
perform(#selector(saveNow), with: nil, afterDelay: 1)
}
@objc func saveNow() {
DispatchQueue.global().async {
}
}
}

View File

@@ -0,0 +1,231 @@
//
// InjectConfiguration.swift
// InjectGUI
//
// Created by wibus on 2024/7/19.
//
import Foundation
// MARK: - InjectConfigurationModel
struct InjectConfigurationModel: Codable {
let project, author: String
let version: Double
let description: Description
let basePublicConfig: BasePublicConfig
let appList: [AppList]
enum CodingKeys: String, CodingKey {
case project
case author = "Author"
case version = "Version"
case description = "Description"
case basePublicConfig
case appList = "AppList"
}
}
// MARK: - AppList
struct AppList: Codable {
let packageName: PackageName
let appBaseLocate, bridgeFile, injectFile: String?
let needCopyToAppDir, noSignTarget, autoHandleHelper: Bool?
let helperFile: HelperFile?
let tccutil: Tccutil?
let forQiuChenly, onlysh: Bool?
let extraShell, smExtra: String?
let componentApp: [String]?
let deepSignApp, noDeep: Bool?
let entitlements: String?
let useOptool, autoHandleSetapp: Bool?
enum CodingKeys: String, CodingKey {
case packageName, appBaseLocate, bridgeFile, injectFile, needCopyToAppDir, noSignTarget, autoHandleHelper, helperFile, tccutil, forQiuChenly, onlysh, extraShell
case smExtra = "SMExtra"
case componentApp, deepSignApp, noDeep, entitlements, useOptool, autoHandleSetapp
}
}
enum HelperFile: Codable {
case string(String)
case stringArray([String])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([String].self) {
self = .stringArray(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(HelperFile.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for HelperFile"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .stringArray(let x):
try container.encode(x)
}
}
}
enum PackageName: Codable {
case string(String)
case stringArray([String])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([String].self) {
self = .stringArray(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(PackageName.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for PackageName"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .stringArray(let x):
try container.encode(x)
}
}
var allStrings: [String] {
switch self {
case .string(let x):
return [x]
case .stringArray(let x):
return x
}
}
}
enum Tccutil: Codable {
case bool(Bool)
case stringArray([String])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode([String].self) {
self = .stringArray(x)
return
}
throw DecodingError.typeMismatch(Tccutil.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Tccutil"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .stringArray(let x):
try container.encode(x)
}
}
}
// MARK: - BasePublicConfig
struct BasePublicConfig: Codable {
let bridgeFile: String
}
// MARK: - Description
struct Description: Codable {
let desc, bridgeFile, packageName, injectFile: String
let supportVersion, supportSubVersion, extraShell, needCopyToAppDir: String
let deepSignApp, disableLibraryValidate, entitlements, noSignTarget: String
let noDeep, tccutil, autoHandleSetapp, autoHandleHelper: String
let helperFile, componentApp, forQiuChenly: String
}
class InjectConfiguration {
static let shared = InjectConfiguration()
var remoteConf = nil as InjectConfigurationModel?
private init() {
updateRemoteConf()
}
private func downloadConfig(data: Data?) {
let decoder = JSONDecoder()
let conf = try! decoder.decode(InjectConfigurationModel.self, from: data!)
remoteConf = conf
#if DEBUG
print("[I] Downloaded config.json")
print(conf)
#endif
}
///
func updateRemoteConf() {
let url = configuration.remoteGit
let commit = configuration.remoteGitCommit
let branch = configuration.remoteGitBranch
// <url>/raw/<branch or commit>/config.json
let _url = "\(url)/raw/\(branch ?? commit ?? "main")/config.json"
let dataUrl = URL(string: _url)!
let task = URLSession.shared.dataTask(with: dataUrl) { data, response, error in
if let error = error {
print("[W] Failed to download config.json: \(error.localizedDescription)")
return
}
self.downloadConfig(data: data)
}
task.resume()
}
///
func customRemoteConf(url: String, commit: String?, branch: String?) {
configuration.remoteGit = url
configuration.remoteGitBranch = branch
configuration.remoteGitCommit = commit
updateRemoteConf()
}
/// Package
func getSupportedPackages() -> [String] {
guard let conf = remoteConf else {
return []
}
return conf.appList.flatMap { $0.packageName.allStrings }
}
func downloadInjectLib() {
}
func updateInjectLib() {
}
func update() {
updateInjectLib()
updateRemoteConf()
}
}
// injectConfiguration.getSupportedPackages()
// injectConfiguration.injectDetail(package: "com.xxx.xxx")
// injectConfiguration.customRemoteConf(url: "", commit: nil)
// injectConfiguration.updateRemoteConf()
// injectConfiguration.downloadInjectLib()
// injectConfiguration.updateInjectLib()
// injectConfiguration.update()

View File

@@ -0,0 +1,9 @@
//
// SoftwareManager.swift
// InjectGUI
//
// Created by wibus on 2024/7/19.
//
import Foundation

View File

@@ -1,26 +0,0 @@
//
// ContentView.swift
// InjectGUI
//
// Created by wibus on 2024/7/19.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

View File

@@ -0,0 +1,95 @@
//
// PublishedStorage.swift
// InjectGUI
//
// Created by Lakr Aream on 2023/6/5.
// Copyright © 2023 Lakr Aream. All rights reserved.
//
// Remove SwiftUI stuff, use Combine & Foundation only.
import Combine
import Foundation
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
@propertyWrapper
public struct PublishedStorage<ValueA: Codable> {
@CodableDefault private var storedValue: ValueA
public let subject: CurrentValueSubject<ValueA, Never>
public var defaultValue: ValueA {
_storedValue.defaultValue
}
@available(*, unavailable, message: "accessing wrappedValue will result undefined behavior")
// PublishedStorage only accept to work inside class, which class confirms to ObservableObject
public var wrappedValue: ValueA {
get { subject.value }
set { storedValue = newValue }
}
public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped _: ReferenceWritableKeyPath<EnclosingSelf, ValueA>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedStorage<ValueA>>
) -> ValueA {
get {
object[keyPath: storageKeyPath].subject.value
}
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
object[keyPath: storageKeyPath].subject.send(newValue)
object[keyPath: storageKeyPath].storedValue = newValue
}
}
init(key: String, defaultValue: ValueA, storage: UserDefaults = .standard) {
let storageCore = CodableDefault(key: key, defaultValue: defaultValue, storage: storage)
_storedValue = storageCore
subject = .init(storageCore.wrappedValue)
}
public func saveFromSubjectValueImmediately() {
_storedValue.save(value: subject.value)
}
}
private extension PublishedStorage {
@propertyWrapper
struct CodableDefault<ValueB: Codable> {
let key: String
let defaultValue: ValueB
var storage: UserDefaults = .standard
public init(key: String, defaultValue: ValueB, storage: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.storage = storage
}
public var wrappedValue: ValueB {
get {
if let read = storage.value(forKey: key) as? Data,
let object = try? decoder.decode(ValueB.self, from: read)
{
return object
}
return defaultValue
}
set {
save(value: newValue)
}
}
public func save(value: ValueB) {
do {
let data = try encoder.encode(value)
storage.setValue(data, forKey: key)
return
} catch {}
storage.setValue(nil, forKey: key)
}
}
}

View File

@@ -0,0 +1,178 @@
//
// ViewKit.swift
// InjectGUI
//
// Created by wibus on 2024/7/19.
//
import SwiftUI
enum InternalKit {
typealias IsClickPrimaryButton = Bool
struct AnySheetView: View {
@Environment(\.dismiss) var dismiss // dismiss the sheet
let title: String
let secondaryButton: String
let primaryButton: String
let toolbar: (() -> (AnyView))?
let content: (() -> (AnyView))
let action: (IsClickPrimaryButton) -> Void
init(
title: String,
secondaryButton: String,
primaryButton: String,
toolbar: (() -> (AnyView))? = nil,
content: @escaping () -> AnyView,
action: @escaping (IsClickPrimaryButton) -> Void
) {
self.title = title
self.secondaryButton = secondaryButton
self.primaryButton = primaryButton
self.toolbar = toolbar
self.content = content
self.action = action
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
if title.isEmpty, toolbar == nil {
} else {
HStack {
Text(title).font(.headline)
Spacer()
if let toolbar { toolbar() }
}
Divider()
}
content().frame(maxWidth: .infinity, maxHeight: .infinity)
Divider()
HStack {
if !secondaryButton.isEmpty {
Button { action(false) } label: { Text(secondaryButton) }
}
Spacer()
if !primaryButton.isEmpty {
Button { action(true) } label: { Text(primaryButton) }
.buttonStyle(.borderedProminent)
}
}
.background(
Button("") { dismiss() }
.keyboardShortcut(.cancelAction)
.opacity(0)
)
}
.padding()
}
}
static func useSheet(
title: String,
secondaryButton: String = "Cancel",
primaryButton: String = "OK",
toolbar: (() -> (AnyView))? = nil,
content: @escaping () -> AnyView,
action: @escaping (IsClickPrimaryButton) -> Void?
) -> some View {
AnySheetView(
title: title,
secondaryButton: secondaryButton,
primaryButton: primaryButton,
toolbar: toolbar,
content: content,
action: { isClickPrimaryButton in
action(isClickPrimaryButton)
}
)
}
static func useAlert(
title: String,
message: String,
primaryButton: String = "OK",
secondaryButton: String = "Cancel",
action: @escaping (IsClickPrimaryButton) -> Void?
) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.addButton(withTitle: primaryButton)
alert.addButton(withTitle: secondaryButton)
alert.alertStyle = .informational
alert.beginSheetModal(for: NSApp.mainWindow!) { response in
action(response == .alertFirstButtonReturn)
}
}
static func eazyAlert(
title: String,
message: String
) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.alertStyle = .informational
alert.beginSheetModal(for: NSApp.mainWindow!) { response in
}
}
static func useFilePanel(
title: String,
message: String,
primaryButton: String = "OK",
secondaryButton: String = "Cancel",
action: @escaping (URL?) -> Void
) {
let panel = NSOpenPanel()
panel.title = title
panel.message = message
panel.prompt = primaryButton
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.canCreateDirectories = false
panel.allowsMultipleSelection = false
panel.beginSheetModal(for: NSApp.mainWindow!) { response in
action(panel.url)
}
}
static func useDirectoryPanel(
title: String,
message: String,
primaryButton: String = "OK",
secondaryButton: String = "Cancel",
action: @escaping (URL?) -> Void
) {
let panel = NSOpenPanel()
panel.title = title
panel.message = message
panel.prompt = primaryButton
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.canCreateDirectories = true
panel.allowsMultipleSelection = false
panel.beginSheetModal(for: NSApp.mainWindow!) { response in
action(panel.url)
}
}
static func saveFilePanel(
title: String,
message: String,
primaryButton: String = "OK",
secondaryButton: String = "Cancel",
action: @escaping (URL?) -> Void
) {
let panel = NSSavePanel()
panel.title = title
panel.message = message
panel.prompt = primaryButton
panel.canCreateDirectories = true
panel.beginSheetModal(for: NSApp.mainWindow!) { response in
action(panel.url)
}
}
}

View File

@@ -2,9 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,49 @@
//
// ContentView.swift
// InjectGUI
//
// Created by wibus on 2024/7/19.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
VStack {
Text("InjectGUI")
.font(.largeTitle)
Text("remoteGit: \(configuration.remoteGit)")
Text("remoteGitBranch: \(configuration.remoteGitBranch ?? "nil")")
Text("remoteGitCommit: \(configuration.remoteGitCommit ?? "nil")")
}
Spacer()
Button("updateRemoteConf") {
injectConfiguration.updateRemoteConf()
}
Button("getSupportedPackages") {
print(injectConfiguration.getSupportedPackages())
}
Button("downloadInjectLib") {
injectConfiguration.downloadInjectLib()
}
Button("updateInjectLib") {
injectConfiguration.updateInjectLib()
}
Button("update") {
injectConfiguration.update()
}
}
.padding()
}
}