Quality of life enhancements

This commit is contained in:
Stephen 2025-05-30 02:39:47 -04:00
parent 3aa9b4603e
commit 47dfbbed6c
11 changed files with 443 additions and 91 deletions

View file

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,95 @@
//
// DebugWidget.swift
// DebugWidget
//
// Created by Stephen on 5/30/25.
//
import WidgetKit
import SwiftUI
import UIKit
// MARK: - Timeline Entry
struct AppsEntry: TimelineEntry {
let date: Date
let bundleIDs: [String]
}
// MARK: - Provider
struct AppsProvider: TimelineProvider {
private let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj")
func placeholder(in context: Context) -> AppsEntry {
AppsEntry(date: .now, bundleIDs: [])
}
func getSnapshot(in context: Context, completion: @escaping (AppsEntry) -> Void) {
completion(makeEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<AppsEntry>) -> Void) {
let entry = makeEntry()
completion(Timeline(entries: [entry], policy: .never))
}
private func makeEntry() -> AppsEntry {
let favs = sharedDefaults?.stringArray(forKey: "favoriteApps") ?? []
let bundleIDs = Array(favs.prefix(4))
return AppsEntry(date: .now, bundleIDs: bundleIDs)
}
}
// MARK: - Widget View
struct AppsWidgetEntryView: View {
let entry: AppsEntry
var body: some View {
HStack(spacing: 8) {
ForEach(0..<4, id: \.self) { idx in
if idx < entry.bundleIDs.count {
IconCell(bundleID: entry.bundleIDs[idx])
} else {
PlaceholderCell()
}
}
}
.padding(8)
.containerBackground(Color(UIColor.systemBackground), for: .widget)
}
@ViewBuilder
private func IconCell(bundleID: String) -> some View {
if let img = loadIcon(for: bundleID) {
Link(destination: URL(string: "stikjit://enable-jit?bundle-id=\(bundleID)")!) {
Image(uiImage: img)
.resizable()
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
}
} else {
PlaceholderCell()
}
}
@ViewBuilder
private func PlaceholderCell() -> some View {
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color(UIColor.systemGray5))
Image(systemName: "plus")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.gray)
}
.aspectRatio(1, contentMode: .fit)
}
private func loadIcon(for bundleID: String) -> UIImage? {
guard let container = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.stik.sj")
else { return nil }
let url = container
.appendingPathComponent("icons", isDirectory: true)
.appendingPathComponent("\(bundleID).png")
return UIImage(contentsOfFile: url.path)
}
}

View file

@ -0,0 +1,23 @@
//
// DebugWidgetBundle.swift
// DebugWidget
//
// Created by Stephen on 5/30/25.
//
import WidgetKit
import SwiftUI
@main
struct AppsWidget: Widget {
let kind: String = "AppsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: AppsProvider()) { entry in
AppsWidgetEntryView(entry: entry)
}
.configurationDisplayName("StikDebug Favorites")
.description("Quick-launch your top 4 favorite debug targets.")
.supportedFamilies([.systemMedium])
}
}

11
DebugWidget/Info.plist Normal file
View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!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.application-groups</key>
<array>
<string>group.com.stik.sj</string>
</array>
</dict>
</plist>

View file

@ -7,11 +7,21 @@
objects = {
/* Begin PBXBuildFile section */
DC139F6E2DE97EA400F63846 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC139F6D2DE97EA400F63846 /* WidgetKit.framework */; };
DC139F702DE97EA400F63846 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC139F6F2DE97EA400F63846 /* SwiftUI.framework */; };
DC139F812DE97EA600F63846 /* DebugWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DC139F6C2DE97EA400F63846 /* DebugWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
DCA690122DAF660E007C91A8 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA690112DAF660E007C91A8 /* NetworkExtension.framework */; };
DCA6901A2DAF660E007C91A8 /* TunnelProv.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DCA690102DAF660E007C91A8 /* TunnelProv.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
DC139F7F2DE97EA600F63846 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DC6F1D2F2D94EADD0071B2B6 /* Project object */;
proxyType = 1;
remoteGlobalIDString = DC139F6B2DE97EA400F63846;
remoteInfo = DebugWidgetExtension;
};
DC6F1D492D94EADF0071B2B6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DC6F1D2F2D94EADD0071B2B6 /* Project object */;
@ -42,6 +52,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
DC139F812DE97EA600F63846 /* DebugWidgetExtension.appex in Embed Foundation Extensions */,
DCA6901A2DAF660E007C91A8 /* TunnelProv.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
@ -50,6 +61,10 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
DC139F6C2DE97EA400F63846 /* DebugWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DebugWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
DC139F6D2DE97EA400F63846 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
DC139F6F2DE97EA400F63846 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
DC139F862DE97F2000F63846 /* DebugWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugWidgetExtension.entitlements; sourceTree = "<group>"; };
DC6F1D372D94EADD0071B2B6 /* StikDebug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StikDebug.app; sourceTree = BUILT_PRODUCTS_DIR; };
DC6F1D482D94EADF0071B2B6 /* StikDebugTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StikDebugTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DC6F1D522D94EADF0071B2B6 /* StikDebugUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StikDebugUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -66,6 +81,13 @@
);
target = DC6F1D362D94EADD0071B2B6 /* StikDebug */;
};
DC139F852DE97EA600F63846 /* Exceptions for "DebugWidget" folder in "DebugWidgetExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = DC139F6B2DE97EA400F63846 /* DebugWidgetExtension */;
};
DCA6901E2DAF660E007C91A8 /* Exceptions for "TunnelProv" folder in "TunnelProv" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
@ -76,6 +98,14 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
DC139F712DE97EA400F63846 /* DebugWidget */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
DC139F852DE97EA600F63846 /* Exceptions for "DebugWidget" folder in "DebugWidgetExtension" target */,
);
path = DebugWidget;
sourceTree = "<group>";
};
DC6F1D392D94EADD0071B2B6 /* StikJIT */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
@ -105,6 +135,15 @@
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
DC139F692DE97EA400F63846 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DC139F702DE97EA400F63846 /* SwiftUI.framework in Frameworks */,
DC139F6E2DE97EA400F63846 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
DC6F1D342D94EADD0071B2B6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -140,10 +179,12 @@
DC6F1D2E2D94EADD0071B2B6 = {
isa = PBXGroup;
children = (
DC139F862DE97F2000F63846 /* DebugWidgetExtension.entitlements */,
DC6F1D392D94EADD0071B2B6 /* StikJIT */,
DC6F1D4B2D94EADF0071B2B6 /* StikJITTests */,
DC6F1D552D94EADF0071B2B6 /* StikJITUITests */,
DCA690132DAF660E007C91A8 /* TunnelProv */,
DC139F712DE97EA400F63846 /* DebugWidget */,
DC6F1D752D94EB620071B2B6 /* Frameworks */,
DC6F1D382D94EADD0071B2B6 /* Products */,
);
@ -156,6 +197,7 @@
DC6F1D482D94EADF0071B2B6 /* StikDebugTests.xctest */,
DC6F1D522D94EADF0071B2B6 /* StikDebugUITests.xctest */,
DCA690102DAF660E007C91A8 /* TunnelProv.appex */,
DC139F6C2DE97EA400F63846 /* DebugWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -164,6 +206,8 @@
isa = PBXGroup;
children = (
DCA690112DAF660E007C91A8 /* NetworkExtension.framework */,
DC139F6D2DE97EA400F63846 /* WidgetKit.framework */,
DC139F6F2DE97EA400F63846 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -171,6 +215,28 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
DC139F6B2DE97EA400F63846 /* DebugWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = DC139F842DE97EA600F63846 /* Build configuration list for PBXNativeTarget "DebugWidgetExtension" */;
buildPhases = (
DC139F682DE97EA400F63846 /* Sources */,
DC139F692DE97EA400F63846 /* Frameworks */,
DC139F6A2DE97EA400F63846 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
DC139F712DE97EA400F63846 /* DebugWidget */,
);
name = DebugWidgetExtension;
packageProductDependencies = (
);
productName = DebugWidgetExtension;
productReference = DC139F6C2DE97EA400F63846 /* DebugWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
DC6F1D362D94EADD0071B2B6 /* StikDebug */ = {
isa = PBXNativeTarget;
buildConfigurationList = DC6F1D5C2D94EADF0071B2B6 /* Build configuration list for PBXNativeTarget "StikDebug" */;
@ -184,6 +250,7 @@
);
dependencies = (
DCA690192DAF660E007C91A8 /* PBXTargetDependency */,
DC139F802DE97EA600F63846 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
DC6F1D392D94EADD0071B2B6 /* StikJIT */,
@ -273,6 +340,9 @@
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620;
TargetAttributes = {
DC139F6B2DE97EA400F63846 = {
CreatedOnToolsVersion = 16.2;
};
DC6F1D362D94EADD0071B2B6 = {
CreatedOnToolsVersion = 16.2;
LastSwiftMigration = 1620;
@ -308,11 +378,19 @@
DC6F1D472D94EADF0071B2B6 /* StikDebugTests */,
DC6F1D512D94EADF0071B2B6 /* StikDebugUITests */,
DCA6900F2DAF660E007C91A8 /* TunnelProv */,
DC139F6B2DE97EA400F63846 /* DebugWidgetExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
DC139F6A2DE97EA400F63846 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
DC6F1D352D94EADD0071B2B6 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -344,6 +422,13 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
DC139F682DE97EA400F63846 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
DC6F1D332D94EADD0071B2B6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -375,6 +460,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
DC139F802DE97EA600F63846 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DC139F6B2DE97EA400F63846 /* DebugWidgetExtension */;
targetProxy = DC139F7F2DE97EA600F63846 /* PBXContainerItemProxy */;
};
DC6F1D4A2D94EADF0071B2B6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DC6F1D362D94EADD0071B2B6 /* StikDebug */;
@ -393,6 +483,67 @@
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
DC139F822DE97EA600F63846 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = DebugWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SZ977XLF24;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = DebugWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = DebugWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj.DebugWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
DC139F832DE97EA600F63846 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = DebugWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SZ977XLF24;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = DebugWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = DebugWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj.DebugWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
DC6F1D5A2D94EADF0071B2B6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -754,6 +905,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
DC139F842DE97EA600F63846 /* Build configuration list for PBXNativeTarget "DebugWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DC139F822DE97EA600F63846 /* Debug */,
DC139F832DE97EA600F63846 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
DC6F1D322D94EADD0071B2B6 /* Build configuration list for PBXProject "StikDebug" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View file

@ -10,6 +10,10 @@
<string>allow-vpn</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.stik.sj</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>

View file

@ -7,13 +7,25 @@
import SwiftUI
import UIKit
import WidgetKit
struct InstalledAppsListView: View {
@StateObject private var viewModel = InstalledAppsViewModel()
@State private var appIcons: [String: UIImage] = [:]
@Environment(\.dismiss) private var dismiss
private let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj")!
@AppStorage("recentApps") private var recentApps: [String] = []
@AppStorage("favoriteApps") private var favoriteApps: [String] = []
@AppStorage("favoriteApps") private var favoriteApps: [String] = [] {
didSet {
if favoriteApps.count > 4 {
favoriteApps = Array(favoriteApps.prefix(4))
}
sharedDefaults.set(favoriteApps, forKey: "favoriteApps")
WidgetCenter.shared.reloadAllTimelines()
}
}
@Environment(\.dismiss) private var dismiss
var onSelectApp: (String) -> Void
var body: some View {
@ -26,51 +38,37 @@ struct InstalledAppsListView: View {
}
}
.navigationTitle("Installed Apps")
.navigationBarItems(
trailing: Button("Done") {
dismiss()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { dismiss() }
}
)
}
}
.background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
}
// MARK: - Empty State
private var emptyState: some View {
VStack(spacing: 16) {
Image(systemName: "magnifyingglass")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
.foregroundColor(.gray)
.resizable().scaledToFit().frame(width: 60, height: 60).foregroundColor(.gray)
Text("No Debuggable App Found")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.primary)
.font(.title2).fontWeight(.semibold).foregroundColor(.primary)
Text("""
StikDebug can only connect to apps with the "**get-task-allow**" entitlement. \
Please check if the app you want to connect to is signed with a **development** certificate.
""")
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
.font(.body).foregroundColor(.secondary).multilineTextAlignment(.center).padding(.horizontal)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.padding().frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(UIColor.systemBackground))
}
// MARK: - List of Apps
private var appsList: some View {
List {
// Favorites
if !favoriteApps.isEmpty {
Section(header: Text("Favorites")) {
Section(header: Text("Favorites - (\(favoriteApps.count)/4)")) {
ForEach(favoriteApps, id: \.self) { bundleID in
AppButton(
bundleID: bundleID,
@ -78,13 +76,13 @@ Please check if the app you want to connect to is signed with a **development**
recentApps: $recentApps,
favoriteApps: $favoriteApps,
appIcons: $appIcons,
onSelectApp: onSelectApp
onSelectApp: onSelectApp,
sharedDefaults: sharedDefaults
)
}
}
}
// Recents
if !recentApps.isEmpty {
Section(header: Text("Recents")) {
ForEach(recentApps, id: \.self) { bundleID in
@ -94,12 +92,16 @@ Please check if the app you want to connect to is signed with a **development**
recentApps: $recentApps,
favoriteApps: $favoriteApps,
appIcons: $appIcons,
onSelectApp: onSelectApp
onSelectApp: onSelectApp,
sharedDefaults: sharedDefaults
)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
withAnimation {
recentApps.removeAll(where: { $0 == bundleID })
recentApps.removeAll { $0 == bundleID }
sharedDefaults.set(recentApps, forKey: "recentApps")
sharedDefaults.set(favoriteApps, forKey: "favoriteApps")
WidgetCenter.shared.reloadAllTimelines()
}
} label: {
Label("Delete", systemImage: "trash")
@ -109,32 +111,17 @@ Please check if the app you want to connect to is signed with a **development**
}
}
// All Applications
if favoriteApps.isEmpty && recentApps.isEmpty {
Section {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
AppButton(
bundleID: bundleID,
appName: appName,
recentApps: $recentApps,
favoriteApps: $favoriteApps,
appIcons: $appIcons,
onSelectApp: onSelectApp
)
}
}
} else {
Section(header: Text("All Applications")) {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
AppButton(
bundleID: bundleID,
appName: appName,
recentApps: $recentApps,
favoriteApps: $favoriteApps,
appIcons: $appIcons,
onSelectApp: onSelectApp
)
}
Section(header: Text((favoriteApps.isEmpty && recentApps.isEmpty) ? "" : "All Applications")) {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
AppButton(
bundleID: bundleID,
appName: appName,
recentApps: $recentApps,
favoriteApps: $favoriteApps,
appIcons: $appIcons,
onSelectApp: onSelectApp,
sharedDefaults: sharedDefaults
)
}
}
}
@ -143,13 +130,14 @@ Please check if the app you want to connect to is signed with a **development**
}
struct AppButton: View {
@State var bundleID: String
@State var appName: String
let bundleID: String
let appName: String
@Binding var recentApps: [String]
@Binding var favoriteApps: [String]
@Binding var appIcons: [String: UIImage]
@AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true
var onSelectApp: (String) -> Void
let sharedDefaults: UserDefaults
@Environment(\.colorScheme) private var colorScheme
@ -163,18 +151,14 @@ struct AppButton: View {
.padding(.vertical, loadAppIconsOnJIT ? 0 : 8)
}
.contextMenu {
Button {
toggleFavorite()
} label: {
Button(action: toggleFavorite) {
Label(
favoriteApps.contains(bundleID) ? "Remove Favorite" : "Add to Favorites",
systemImage: favoriteApps.contains(bundleID) ? "star.slash" : "star"
)
.disabled(!favoriteApps.contains(bundleID) && favoriteApps.count >= 4)
}
Button {
UIPasteboard.general.string = bundleID
} label: {
Button { UIPasteboard.general.string = bundleID } label: {
Label("Copy Bundle ID", systemImage: "doc.on.doc")
}
}
@ -184,23 +168,14 @@ struct AppButton: View {
Group {
if loadAppIconsOnJIT, let image = appIcons[bundleID] {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.cornerRadius(12)
.shadow(
color: colorScheme == .dark ? .black.opacity(0.2) : .gray.opacity(0.2),
radius: 3, x: 0, y: 1
)
.resizable().aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60).cornerRadius(12)
.shadow(color: colorScheme == .dark ? .black.opacity(0.2) : .gray.opacity(0.2), radius: 3, x: 0, y: 1)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color(UIColor.systemGray5))
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "app")
.font(.system(size: 26))
.foregroundColor(.gray)
)
.overlay(Image(systemName: "app").font(.system(size: 26)).foregroundColor(.gray))
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
.onAppear { loadAppIcon(for: bundleID) }
}
@ -209,31 +184,30 @@ struct AppButton: View {
private var appText: some View {
VStack(alignment: .leading, spacing: 4) {
Text(appName)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.primary)
Text(bundleID)
.font(.system(size: 15))
.foregroundColor(.gray)
.lineLimit(1)
Text(appName).font(.system(size: 18, weight: .semibold)).foregroundColor(.primary)
Text(bundleID).font(.system(size: 15)).foregroundColor(.gray).lineLimit(1)
}
}
private func selectApp() {
recentApps.removeAll(where: { $0 == bundleID })
recentApps.removeAll { $0 == bundleID }
recentApps.insert(bundleID, at: 0)
if recentApps.count > 3 {
recentApps = Array(recentApps.prefix(3))
}
if recentApps.count > 3 { recentApps = Array(recentApps.prefix(3)) }
sharedDefaults.set(recentApps, forKey: "recentApps")
sharedDefaults.set(favoriteApps, forKey: "favoriteApps")
WidgetCenter.shared.reloadAllTimelines()
onSelectApp(bundleID)
}
private func toggleFavorite() {
if favoriteApps.contains(bundleID) {
favoriteApps.removeAll(where: { $0 == bundleID })
} else {
favoriteApps.removeAll { $0 == bundleID }
} else if favoriteApps.count < 4 {
favoriteApps.insert(bundleID, at: 0)
}
sharedDefaults.set(recentApps, forKey: "recentApps")
sharedDefaults.set(favoriteApps, forKey: "favoriteApps")
WidgetCenter.shared.reloadAllTimelines()
}
private func loadAppIcon(for bundleID: String) {
@ -243,6 +217,7 @@ struct AppButton: View {
DispatchQueue.main.async {
withAnimation(.easeIn(duration: 0.2)) {
appIcons[bundleID] = image
saveIconToGroup(image, bundleID: bundleID)
}
}
}
@ -250,6 +225,17 @@ struct AppButton: View {
}
}
fileprivate func saveIconToGroup(_ image: UIImage, bundleID: String) {
guard
let data = image.pngData(),
let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.stik.sj")
else { return }
let iconsDir = container.appendingPathComponent("icons", isDirectory: true)
try? FileManager.default.createDirectory(at: iconsDir, withIntermediateDirectories: true)
let fileURL = iconsDir.appendingPathComponent("\(bundleID).png")
try? data.write(to: fileURL)
}
extension Array: @retroactive RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),