diff --git a/InjectGUI.xcodeproj/project.pbxproj b/InjectGUI.xcodeproj/project.pbxproj index a5f7d1f..4576e76 100644 --- a/InjectGUI.xcodeproj/project.pbxproj +++ b/InjectGUI.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 38877A372C4A7294009F5910 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A362C4A7294009F5910 /* Configuration.swift */; }; 38877A3A2C4A730F009F5910 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A392C4A730F009F5910 /* Constants.swift */; }; 38877A3D2C4A9EB7009F5910 /* SetupApplicationSupportDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38877A3C2C4A9EB7009F5910 /* SetupApplicationSupportDirectory.swift */; }; + 38AD95EA2C58E70E0032E79F /* Injector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AD95E92C58E70E0032E79F /* Injector.swift */; }; + 38AD95EE2C58F59C0032E79F /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AD95ED2C58F59C0032E79F /* StatusView.swift */; }; 38BC1F532C4B587A00C3B60E /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BC1F522C4B587900C3B60E /* SidebarView.swift */; }; 38BC1F552C4B622500C3B60E /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BC1F542C4B622500C3B60E /* WelcomeView.swift */; }; 38BC1F5A2C4B98A300C3B60E /* AppDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BC1F592C4B98A300C3B60E /* AppDetailView.swift */; }; @@ -38,6 +40,8 @@ 38877A362C4A7294009F5910 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 38877A392C4A730F009F5910 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 38877A3C2C4A9EB7009F5910 /* SetupApplicationSupportDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupApplicationSupportDirectory.swift; sourceTree = ""; }; + 38AD95E92C58E70E0032E79F /* Injector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Injector.swift; sourceTree = ""; }; + 38AD95ED2C58F59C0032E79F /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 38BC1F522C4B587900C3B60E /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; 38BC1F542C4B622500C3B60E /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 38BC1F592C4B98A300C3B60E /* AppDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailView.swift; sourceTree = ""; }; @@ -100,6 +104,7 @@ 38877A2D2C4A6FFA009F5910 /* InjectConfiguration.swift */, 38877A2F2C4A70DB009F5910 /* SoftwareManager.swift */, 38877A362C4A7294009F5910 /* Configuration.swift */, + 38AD95E92C58E70E0032E79F /* Injector.swift */, ); path = Backend; sourceTree = ""; @@ -111,6 +116,7 @@ 38BC1F522C4B587900C3B60E /* SidebarView.swift */, 38BC1F542C4B622500C3B60E /* WelcomeView.swift */, 38BC1F592C4B98A300C3B60E /* AppDetailView.swift */, + 38AD95ED2C58F59C0032E79F /* StatusView.swift */, 38BC1F5B2C4BB02200C3B60E /* SettingsView.swift */, ); path = View; @@ -221,6 +227,7 @@ 38877A1D2C4A6F83009F5910 /* InjectGUIApp.swift in Sources */, 38877A3D2C4A9EB7009F5910 /* SetupApplicationSupportDirectory.swift in Sources */, 38877A372C4A7294009F5910 /* Configuration.swift in Sources */, + 38AD95EA2C58E70E0032E79F /* Injector.swift in Sources */, 38877A302C4A70DB009F5910 /* SoftwareManager.swift in Sources */, 38877A332C4A7222009F5910 /* PublishedStorage.swift in Sources */, 38BC1F552C4B622500C3B60E /* WelcomeView.swift in Sources */, @@ -229,6 +236,7 @@ 38877A352C4A7254009F5910 /* ViewKit.swift in Sources */, 38877A3A2C4A730F009F5910 /* Constants.swift in Sources */, 38BC1F5C2C4BB02200C3B60E /* SettingsView.swift in Sources */, + 38AD95EE2C58F59C0032E79F /* StatusView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/InjectGUI/Backend/InjectConfiguration.swift b/InjectGUI/Backend/InjectConfiguration.swift index 2079a8c..29e29d9 100644 --- a/InjectGUI/Backend/InjectConfiguration.swift +++ b/InjectGUI/Backend/InjectConfiguration.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine struct Package: Identifiable { let id: String @@ -16,7 +17,6 @@ struct Package: Identifiable { struct InjectConfigurationModel: Codable { let project, author: String let version: Double - let description: Description let basePublicConfig: BasePublicConfig let appList: [AppList] @@ -24,7 +24,6 @@ struct InjectConfigurationModel: Codable { case project case author = "Author" case version = "Version" - case description = "Description" case basePublicConfig case appList = "AppList" } @@ -43,11 +42,12 @@ struct AppList: Codable { let deepSignApp, noDeep: Bool? let entitlements: String? let useOptool, autoHandleSetapp: Bool? + let keygen: 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 + case componentApp, deepSignApp, noDeep, entitlements, useOptool, autoHandleSetapp, keygen } } @@ -159,19 +159,11 @@ 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: ObservableObject { static let shared = InjectConfiguration() - var remoteConf = nil as InjectConfigurationModel? + @Published var remoteConf = nil as InjectConfigurationModel? + private var cancellables = Set() private init() { updateRemoteConf() diff --git a/InjectGUI/Backend/Injector.swift b/InjectGUI/Backend/Injector.swift new file mode 100644 index 0000000..f316ec2 --- /dev/null +++ b/InjectGUI/Backend/Injector.swift @@ -0,0 +1,104 @@ +// +// Injector.swift +// InjectGUI +// +// Created by wibus on 2024/7/30. +// + +import Foundation + +enum InjectStatus { + case none + case running + case finished + case error +} + +enum InjectStage { + case start + case checkVersionIsSupported + case handleKeygen + case handleDeepCodeSign + case handleAutoHandleHelper + case handleSubApps + case handleTccutil + case handleExtraShell + case handleInjectLibInject + case end +} + +extension InjectStage { + static var allCases: [InjectStage] { + return [.start, .checkVersionIsSupported, .handleKeygen, .handleDeepCodeSign, .handleAutoHandleHelper, .handleSubApps, .handleTccutil, .handleExtraShell, .handleInjectLibInject, .end] + } + + var description: String { + switch self { + case .start: + return "Start Injecting" + case .checkVersionIsSupported: + return "Checking Version is supported" + case .handleKeygen: + return "Handling Keygen" + case .handleDeepCodeSign: + return "Handling Deep Code Sign" + case .handleAutoHandleHelper: + return "Handling Auto Handle Helper" + case .handleSubApps: + return "Handling Sub Apps" + case .handleTccutil: + return "Handling Tccutil" + case .handleExtraShell: + return "Handling Extra Shell" + case .handleInjectLibInject: + return "Handling Inject Lib Inject" + case .end: + return "Injecting Finished" + } + } +} + +struct InjectRunningError { + var error: String + var stage: InjectStage +} + +struct InjectRunningStage { + var stage: InjectStage + var message: String + var progress: Double + var error: InjectRunningError? + var status: InjectStatus +} + +struct InjectRunningStatus { + var appId: String + var appName: String + var stages: [InjectRunningStage] = [] + var message: String + var progress: Double + var error: InjectRunningError? +} + +class Injector: ObservableObject { + static let shared = Injector() + + @Published var stage: InjectRunningStatus = .init(appId: "", appName: "", stages: [], message: "", progress: 0) + + init() { + self.stage = InjectRunningStatus( + appId: "pl.maketheweb.cleanshotx", + appName: "CleanShot X", + stages: [ + .init(stage: .start, message: InjectStage.start.description, progress: 1, status: .finished), + .init(stage: .checkVersionIsSupported, message: InjectStage.checkVersionIsSupported.description, progress: 1, error: .init(error: "Version is not supported", stage: .checkVersionIsSupported), status: .error), + .init(stage: .handleKeygen, message: InjectStage.handleKeygen.description, progress: 1, status: .finished), + .init(stage: .handleDeepCodeSign, message: InjectStage.handleDeepCodeSign.description, progress: 0.6, status: .running), + ], + message: "Injecting", + progress: 0.6 + ) + } + + func startInjectApp(package: String) {} +} diff --git a/InjectGUI/Backend/SoftwareManager.swift b/InjectGUI/Backend/SoftwareManager.swift index b19f79d..11bd88b 100644 --- a/InjectGUI/Backend/SoftwareManager.swift +++ b/InjectGUI/Backend/SoftwareManager.swift @@ -14,6 +14,7 @@ struct AppDetail { let identifier: String // -> CFBundleIdentifier let version: String // -> CFBundleVersion let path: String // -> path + let executable: String // -> CFBundleExecutable let icon: NSImage } @@ -45,6 +46,7 @@ class SoftwareManager: ObservableObject { let bundleName = plist["CFBundleName"] as? String, let bundleIdentifier = plist["CFBundleIdentifier"] as? String, let bundleVersion = plist["CFBundleVersion"] as? String, + let bundleExecutable = plist["CFBundleExecutable"] as? String, let bundleShortVersion = plist["CFBundleShortVersionString"] as? String else { return nil } @@ -91,6 +93,7 @@ class SoftwareManager: ObservableObject { identifier: bundleIdentifier, version: bundleVersion, path: path, + executable: bundleExecutable, icon: icon ?? NSImage() ) } diff --git a/InjectGUI/Extension/ViewKit.swift b/InjectGUI/Extension/ViewKit.swift index 0a41304..3849bcd 100644 --- a/InjectGUI/Extension/ViewKit.swift +++ b/InjectGUI/Extension/ViewKit.swift @@ -46,16 +46,22 @@ enum ViewKit { } Divider() } - content().frame(maxWidth: .infinity, maxHeight: .infinity) + content() Divider() HStack { if !secondaryButton.isEmpty { - Button { action(false) } label: { Text(secondaryButton) } + Button { + action(false) + dismiss() + } label: { Text(secondaryButton) } } Spacer() if !primaryButton.isEmpty { - Button { action(true) } label: { Text(primaryButton) } - .buttonStyle(.borderedProminent) + Button { + action(true) + dismiss() + } label: { Text(primaryButton) } + .buttonStyle(.borderedProminent) } } .background( diff --git a/InjectGUI/View/AppDetailView.swift b/InjectGUI/View/AppDetailView.swift index 4b9348d..982ea99 100644 --- a/InjectGUI/View/AppDetailView.swift +++ b/InjectGUI/View/AppDetailView.swift @@ -38,7 +38,7 @@ struct AppDetailView: View { if getAppDetailFromSoftwareManager != nil { self.appDetail = SoftwareManager.shared.appListCache[appId]! } else { - self.appDetail = AppDetail(name: appInjectConfDetail?.packageName.allStrings.first ?? "", identifier: appInjectConfDetail?.packageName.allStrings.first ?? "", version: "", path: "", icon: NSImage()) + self.appDetail = AppDetail(name: appInjectConfDetail?.packageName.allStrings.first ?? "", identifier: appInjectConfDetail?.packageName.allStrings.first ?? "", version: "", path: "", executable: "", icon: NSImage()) } // self._appDetail = State(wrappedValue: SoftwareManager.shared.appListCache[appId] ?? AppDetail(name: "", identifier: "", version: "", path: "", icon: NSImage())) self._compatibility = State(wrappedValue: Compatibility(id: appId, inInjectLibList: false)) diff --git a/InjectGUI/View/ContentView.swift b/InjectGUI/View/ContentView.swift index 86edf1b..75b5e0c 100644 --- a/InjectGUI/View/ContentView.swift +++ b/InjectGUI/View/ContentView.swift @@ -8,6 +8,7 @@ import SwiftUI struct ContentView: View { @StateObject var softwareManager = SoftwareManager.shared + @State var showStatusSheet = false var body: some View { NavigationView { @@ -29,6 +30,19 @@ struct ContentView: View { Label("Toggle Sidebar", systemImage: "sidebar.leading") } } + + ToolbarItem { + Button { + showStatusSheet.toggle() + } label: { + Label("Status", systemImage: "list.bullet.rectangle") + } + } + } + .sheet(isPresented: $showStatusSheet) { + StatusView() + .background(.ultraThinMaterial) + .interactiveDismissDisabled(true) // disable esc to dismiss } } } @@ -37,4 +51,4 @@ struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } -} \ No newline at end of file +} diff --git a/InjectGUI/View/SettingsView.swift b/InjectGUI/View/SettingsView.swift index 1a4ecfd..edaaf26 100644 --- a/InjectGUI/View/SettingsView.swift +++ b/InjectGUI/View/SettingsView.swift @@ -100,7 +100,6 @@ struct SettingsView: View { SettingItemView("Download GenShineImpactStarter") { Button(action: { injectConfiguration.downloadGenShineImpactStarter() - isGenShineImpactStarterExist = true }) { Text(isGenShineImpactStarterExist ? "Downloaded" : "Download") } @@ -217,18 +216,6 @@ struct SettingsView: View { } .padding(20) } - .onAppear() { - print("SettingsView onAppear") - } - .onDisappear() { - print("SettingsView onDisappear") - } - .onReceive(NotificationCenter.default.publisher(for: NSApplication.willBecomeActiveNotification)) { _ in - print("SettingsView NSApplication.willBecomeActiveNotification") - } - .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in - print("SettingsView NSApplication.didBecomeActiveNotification") - } .tabItem { Label("General", systemImage: "gear") } diff --git a/InjectGUI/View/StatusView.swift b/InjectGUI/View/StatusView.swift new file mode 100644 index 0000000..43042ac --- /dev/null +++ b/InjectGUI/View/StatusView.swift @@ -0,0 +1,168 @@ +// +// StatusView.swift +// InjectGUI +// +// Created by wibus on 2024/7/30. +// + +import Foundation +import SwiftUI + +struct StatusView: View { + @Environment(\.dismiss) var dismiss // dismiss the sheet + + @StateObject var injector = Injector.shared + @State var appDetail: AppDetail = .init(name: "", identifier: "", version: "", path: "", executable: "", icon: NSImage()) + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + if appDetail.name.isEmpty { + Text("Why you are seeing this?") + .font(.title) + .bold() + .foregroundColor(.secondary) + .padding() + VStack(alignment: .leading, spacing: 4) { + Text("This is a status view for the injector.") + .font(.headline) + Text("It will only show when the injector is running.") + .font(.subheadline) + .foregroundColor(.secondary) + Text("Please contact the developer if you see this view without running the injector.") + .font(.subheadline) + .foregroundColor(.secondary) + } + .padding() + } else { + // MARK: - App Info Display Box + + HStack(spacing: 20) { + Image(nsImage: appDetail.icon) + .resizable() + .frame(width: 64, height: 64) + .cornerRadius(4) + Spacer() + VStack(alignment: .leading, spacing: 4) { + Text(appDetail.name) + .font(.headline) + Text(appDetail.identifier) + .font(.subheadline) + .foregroundColor(.secondary) + Text("Version: \(appDetail.version)") + .font(.caption) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: 260) + Divider() + + + + // MARK: - Inject Stage Display box + + HStack { + VStack(alignment: .leading, spacing: 4) { + ForEach(InjectStage.allCases, id: \.self) { stage in + HStack(spacing: 10) { + if let index = injector.stage.stages.firstIndex(where: { $0.stage == stage }) { + switch injector.stage.stages[index].status { + case .none: + Image(systemName: "questionmark.circle") + .foregroundColor(.secondary) + case .running: + Image(systemName: "circle.dotted") + case .finished: + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + case .error: + Image(systemName: "exclamationmark.circle.fill") + .foregroundColor(.red) + } + Text("\(stage.description)") + .fontDesign(.rounded) + .fontWeight(injector.stage.stages[index].status == .running ? .bold : .regular) + // Text(injector.stage.stages[index].message) + // .font(.subheadline) + // .foregroundColor(.secondary) + Spacer() + Text("\(Int(injector.stage.stages[index].progress * 100))%") + .font(.subheadline) + .foregroundColor(.secondary) + } else { + Image(systemName: "questionmark.circle.fill") + .foregroundColor(.secondary) + Text("\(stage.description)") + .fontDesign(.rounded) + } + } + } + } + } + Spacer() + + // MARK: - Progress Bar + ProgressBar(value: $injector.stage.progress) + .frame(height: 10) + HStack { + Text("Progress: \(Int(injector.stage.progress * 100))%") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + if let error = injector.stage.error { + Text("Error: \(error.error)") + .font(.subheadline) + .foregroundColor(.red) + } + } + + // MARK: - Buttons + Button(injector.stage.progress == 1 ? "Finished. Close" : "Stop Injecting") { + + dismiss() + } + .buttonStyle(.bordered) + .controlSize(.large) + .keyboardShortcut(.defaultAction) + .frame(maxWidth: .infinity) + + + + } + } + .onChange(of: injector.stage.appId) { appId in + guard let appDetail = SoftwareManager.shared.appListCache[appId] else { + return + } + self.appDetail = appDetail + } + .frame(minWidth: 350, minHeight: appDetail.name.isEmpty ? 200 : 380) + .padding() + .onAppear { + guard let appDetail = SoftwareManager.shared.appListCache[injector.stage.appId] else { + return + } + self.appDetail = appDetail + } + .background(Color(.windowBackgroundColor)) + } +} + + +struct ProgressBar: View { + @Binding var value: Double + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .leading) { + Rectangle() + .foregroundColor(Color(.systemGray)) + .frame(width: geometry.size.width, height: geometry.size.height) + .cornerRadius(geometry.size.height / 2) + Rectangle() + .foregroundColor(.accentColor) + .frame(width: min(CGFloat(value) * geometry.size.width, geometry.size.width), height: geometry.size.height) + .cornerRadius(geometry.size.height / 2) + } + } + } +}