@@ -9,47 +9,45 @@ import WidgetKit
99import SwiftUI
1010import UIKit
1111
12- // MARK: - Timeline Entry
13- struct AppsEntry : TimelineEntry {
12+ // MARK: - Favorites Widget ----------------------------------------------------
13+
14+ struct FavoritesEntry : TimelineEntry {
1415 let date : Date
1516 let bundleIDs : [ String ]
1617}
1718
18- // MARK: - Provider
19- struct AppsProvider : TimelineProvider {
19+ struct FavoritesProvider : TimelineProvider {
2020 private let sharedDefaults = UserDefaults ( suiteName: " group.com.stik.sj " )
2121
22- func placeholder( in context: Context ) -> AppsEntry {
23- AppsEntry ( date: . now, bundleIDs: [ ] )
22+ func placeholder( in context: Context ) -> FavoritesEntry {
23+ FavoritesEntry ( date: . now, bundleIDs: [ ] )
2424 }
2525
26- func getSnapshot( in context: Context , completion: @escaping ( AppsEntry ) -> Void ) {
26+ func getSnapshot( in context: Context , completion: @escaping ( FavoritesEntry ) -> Void ) {
2727 completion ( makeEntry ( ) )
2828 }
2929
30- func getTimeline( in context: Context , completion: @escaping ( Timeline < AppsEntry > ) -> Void ) {
30+ func getTimeline( in context: Context , completion: @escaping ( Timeline < FavoritesEntry > ) -> Void ) {
3131 let entry = makeEntry ( )
3232 completion ( Timeline ( entries: [ entry] , policy: . never) )
3333 }
3434
35- private func makeEntry( ) -> AppsEntry {
36- let favs = sharedDefaults? . stringArray ( forKey: " favoriteApps " ) ?? [ ]
37- let bundleIDs = Array ( favs. prefix ( 4 ) )
38- return AppsEntry ( date: . now, bundleIDs: bundleIDs)
35+ private func makeEntry( ) -> FavoritesEntry {
36+ let favorites = sharedDefaults? . stringArray ( forKey: " favoriteApps " ) ?? [ ]
37+ return FavoritesEntry ( date: . now, bundleIDs: Array ( favorites. prefix ( 4 ) ) )
3938 }
4039}
4140
42- // MARK: - Widget View
43- struct AppsWidgetEntryView : View {
44- let entry : AppsEntry
41+ struct FavoritesWidgetEntryView : View {
42+ let entry : FavoritesEntry
4543
4644 var body : some View {
4745 HStack ( spacing: 8 ) {
4846 ForEach ( 0 ..< 4 , id: \. self) { idx in
4947 if idx < entry. bundleIDs. count {
50- IconCell ( bundleID: entry. bundleIDs [ idx] )
48+ iconCell ( bundleID: entry. bundleIDs [ idx] )
5149 } else {
52- PlaceholderCell ( )
50+ placeholderCell ( )
5351 }
5452 }
5553 }
@@ -58,7 +56,7 @@ struct AppsWidgetEntryView: View {
5856 }
5957
6058 @ViewBuilder
61- private func IconCell ( bundleID: String ) -> some View {
59+ private func iconCell ( bundleID: String ) -> some View {
6260 if let img = loadIcon ( for: bundleID) {
6361 Link ( destination: URL ( string: " stikjit://enable-jit?bundle-id= \( bundleID) " ) !) {
6462 Image ( uiImage: img)
@@ -67,12 +65,12 @@ struct AppsWidgetEntryView: View {
6765 . cornerRadius ( 12 )
6866 }
6967 } else {
70- PlaceholderCell ( )
68+ placeholderCell ( )
7169 }
7270 }
7371
7472 @ViewBuilder
75- private func PlaceholderCell ( ) -> some View {
73+ private func placeholderCell ( ) -> some View {
7674 ZStack {
7775 RoundedRectangle ( cornerRadius: 12 )
7876 . fill ( Color ( UIColor . systemGray5) )
@@ -82,14 +80,153 @@ struct AppsWidgetEntryView: View {
8280 }
8381 . aspectRatio ( 1 , contentMode: . fit)
8482 }
83+ }
84+
85+ struct FavoritesWidget : Widget {
86+ let kind : String = " FavoritesWidget "
87+
88+ var body : some WidgetConfiguration {
89+ StaticConfiguration ( kind: kind, provider: FavoritesProvider ( ) ) { entry in
90+ FavoritesWidgetEntryView ( entry: entry)
91+ }
92+ . configurationDisplayName ( " StikDebug Favorites " )
93+ . description ( " Quick-launch your top 4 favorite debug targets. " )
94+ . supportedFamilies ( [ . systemMedium] )
95+ }
96+ }
97+
98+ // MARK: - System Apps Widget -------------------------------------------------
99+
100+ struct SystemAppSnapshot : Identifiable {
101+ let bundleID : String
102+ let displayName : String
103+ var id : String { bundleID }
104+ }
105+
106+ struct SystemAppsEntry : TimelineEntry {
107+ let date : Date
108+ let items : [ SystemAppSnapshot ]
109+ }
110+
111+ struct SystemAppsProvider : TimelineProvider {
112+ private let sharedDefaults = UserDefaults ( suiteName: " group.com.stik.sj " )
113+
114+ func placeholder( in context: Context ) -> SystemAppsEntry {
115+ SystemAppsEntry ( date: . now, items: [ ] )
116+ }
117+
118+ func getSnapshot( in context: Context , completion: @escaping ( SystemAppsEntry ) -> Void ) {
119+ completion ( makeEntry ( ) )
120+ }
121+
122+ func getTimeline( in context: Context , completion: @escaping ( Timeline < SystemAppsEntry > ) -> Void ) {
123+ let entry = makeEntry ( )
124+ completion ( Timeline ( entries: [ entry] , policy: . never) )
125+ }
126+
127+ private func makeEntry( ) -> SystemAppsEntry {
128+ let pinned = sharedDefaults? . stringArray ( forKey: " pinnedSystemApps " ) ?? [ ]
129+ let names = sharedDefaults? . dictionary ( forKey: " pinnedSystemAppNames " ) as? [ String : String ] ?? [ : ]
130+ let snapshots = pinned. prefix ( 4 ) . map { bundleID -> SystemAppSnapshot in
131+ let displayName = names [ bundleID] ?? friendlyName ( bundleID: bundleID)
132+ return SystemAppSnapshot ( bundleID: bundleID, displayName: displayName)
133+ }
134+ return SystemAppsEntry ( date: . now, items: snapshots)
135+ }
85136
86- private func loadIcon( for bundleID: String ) -> UIImage ? {
87- guard let container = FileManager . default. containerURL (
88- forSecurityApplicationGroupIdentifier: " group.com.stik.sj " )
89- else { return nil }
90- let url = container
91- . appendingPathComponent ( " icons " , isDirectory: true )
92- . appendingPathComponent ( " \( bundleID) .png " )
93- return UIImage ( contentsOfFile: url. path)
137+ private func friendlyName( bundleID: String ) -> String {
138+ let components = bundleID. split ( separator: " . " )
139+ if let last = components. last {
140+ let cleaned = last. replacingOccurrences ( of: " _ " , with: " " )
141+ let trimmed = cleaned. trimmingCharacters ( in: . whitespacesAndNewlines)
142+ if !trimmed. isEmpty { return trimmed. capitalized }
143+ }
144+ return bundleID
145+ }
146+ }
147+
148+ struct SystemAppsWidgetEntryView : View {
149+ let entry : SystemAppsEntry
150+
151+ var body : some View {
152+ VStack ( alignment: . leading, spacing: 8 ) {
153+ ForEach ( entry. items) { item in
154+ Link ( destination: URL ( string: " stikjit://enable-jit?bundle-id= \( item. bundleID) " ) !) {
155+ HStack ( spacing: 10 ) {
156+ icon ( for: item. bundleID)
157+ . frame ( width: 44 , height: 44 )
158+
159+ VStack ( alignment: . leading, spacing: 2 ) {
160+ Text ( item. displayName)
161+ . font ( . headline)
162+ . foregroundStyle ( . primary)
163+ . lineLimit ( 1 )
164+ Text ( item. bundleID)
165+ . font ( . caption)
166+ . foregroundStyle ( . secondary)
167+ . lineLimit ( 1 )
168+ }
169+ Spacer ( )
170+ }
171+ . padding ( 8 )
172+ . background (
173+ RoundedRectangle ( cornerRadius: 12 , style: . continuous)
174+ . fill ( Color ( UIColor . secondarySystemBackground) . opacity ( 0.35 ) )
175+ )
176+ }
177+ }
178+
179+ if entry. items. isEmpty {
180+ Text ( " Pin system apps from StikDebug to see them here. " )
181+ . font ( . caption)
182+ . foregroundStyle ( . secondary)
183+ . padding ( . top, 4 )
184+ }
185+ }
186+ . padding ( 12 )
187+ . containerBackground ( Color ( UIColor . systemBackground) , for: . widget)
94188 }
189+
190+ @ViewBuilder
191+ private func icon( for bundleID: String ) -> some View {
192+ if let image = loadIcon ( for: bundleID) {
193+ Image ( uiImage: image)
194+ . resizable ( )
195+ . aspectRatio ( 1 , contentMode: . fill)
196+ . cornerRadius ( 10 )
197+ } else {
198+ RoundedRectangle ( cornerRadius: 10 )
199+ . fill ( Color ( UIColor . systemGray5) )
200+ . overlay (
201+ Image ( systemName: " app " )
202+ . font ( . system( size: 20 , weight: . regular) )
203+ . foregroundColor ( . gray)
204+ )
205+ }
206+ }
207+ }
208+
209+ struct SystemAppsWidget : Widget {
210+ let kind : String = " SystemAppsWidget "
211+
212+ var body : some WidgetConfiguration {
213+ StaticConfiguration ( kind: kind, provider: SystemAppsProvider ( ) ) { entry in
214+ SystemAppsWidgetEntryView ( entry: entry)
215+ }
216+ . configurationDisplayName ( " Hidden System Apps " )
217+ . description ( " Launch your pinned hidden system apps directly from the widget. " )
218+ . supportedFamilies ( [ . systemMedium] )
219+ }
220+ }
221+
222+ // MARK: - Shared Helpers -----------------------------------------------------
223+
224+ private func loadIcon( for bundleID: String ) -> UIImage ? {
225+ guard let container = FileManager . default. containerURL (
226+ forSecurityApplicationGroupIdentifier: " group.com.stik.sj " )
227+ else { return nil }
228+ let url = container
229+ . appendingPathComponent ( " icons " , isDirectory: true )
230+ . appendingPathComponent ( " \( bundleID) .png " )
231+ return UIImage ( contentsOfFile: url. path)
95232}
0 commit comments