feat: status view basic ui

This commit is contained in:
wibus-wee
2024-07-31 18:19:27 +08:00
parent 87c933b0cb
commit 30111e1bbd
9 changed files with 314 additions and 32 deletions

View File

@@ -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 = "<group>"; };
38877A392C4A730F009F5910 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
38877A3C2C4A9EB7009F5910 /* SetupApplicationSupportDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupApplicationSupportDirectory.swift; sourceTree = "<group>"; };
38AD95E92C58E70E0032E79F /* Injector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Injector.swift; sourceTree = "<group>"; };
38AD95ED2C58F59C0032E79F /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
38BC1F522C4B587900C3B60E /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
38BC1F542C4B622500C3B60E /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
38BC1F592C4B98A300C3B60E /* AppDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailView.swift; sourceTree = "<group>"; };
@@ -100,6 +104,7 @@
38877A2D2C4A6FFA009F5910 /* InjectConfiguration.swift */,
38877A2F2C4A70DB009F5910 /* SoftwareManager.swift */,
38877A362C4A7294009F5910 /* Configuration.swift */,
38AD95E92C58E70E0032E79F /* Injector.swift */,
);
path = Backend;
sourceTree = "<group>";
@@ -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;
};

View File

@@ -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<AnyCancellable>()
private init() {
updateRemoteConf()

View File

@@ -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) {}
}

View File

@@ -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()
)
}

View File

@@ -46,15 +46,21 @@ 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) }
Button {
action(true)
dismiss()
} label: { Text(primaryButton) }
.buttonStyle(.borderedProminent)
}
}

View File

@@ -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))

View File

@@ -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
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}
}