Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 5c4401f

Browse files
authored
update
1 parent de87bea commit 5c4401f

2 files changed

Lines changed: 137 additions & 70 deletions

File tree

Sources/prostore/prostore.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ struct MainSidebarView: View {
1414

1515
var body: some View {
1616
NavigationSplitView {
17-
List(selection: $selected) {
18-
NavigationLink(value: SidebarItem.apps) {
19-
Label("Apps", systemImage: "square.grid.2x2.fill")
17+
List(selection: $selected) {
18+
NavigationLink(value: SidebarItem.apps) {
19+
Label("Apps", systemImage: "square.grid.2x2.fill")
2020
}
2121
NavigationLink(value: SidebarItem.signer) {
2222
Label("Signer", systemImage: "hammer")
Lines changed: 134 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import SwiftUI
2-
import Combine
32

4-
// MARK: - Models (decodable for AltStore source format)
3+
// MARK: - Models (AltStore-ish)
54
struct AltSource: Decodable {
65
let name: String?
76
let subtitle: String?
@@ -10,16 +9,15 @@ struct AltSource: Decodable {
109
}
1110

1211
struct AltApp: Decodable, Identifiable {
13-
// Use bundleIdentifier as stable id
1412
var id: String { bundleIdentifier }
1513
let name: String
1614
let bundleIdentifier: String
1715
let developerName: String?
1816
let subtitle: String?
1917
let iconURL: URL?
18+
let localizedDescription: String?
2019
let versions: [AppVersion]?
2120

22-
// A convenience computed property for a primary download URL (if present on the latest version)
2321
var latestDownloadURL: URL? {
2422
versions?.first?.downloadURL
2523
}
@@ -49,110 +47,96 @@ final class RepoViewModel: ObservableObject {
4947
Task { await load() }
5048
}
5149

52-
/// Load JSON and decode apps. Tries both `{ "apps": [...] }` and direct `[App]` shapes.
5350
func load() async {
5451
isLoading = true
5552
errorMessage = nil
5653
defer { isLoading = false }
5754

5855
do {
5956
var request = URLRequest(url: sourceURL)
60-
// Some repos expect a GET; set a reasonable user-agent
61-
request.setValue("AppTestersListView/1.0 (iOS)", forHTTPHeaderField: "User-Agent")
62-
57+
request.setValue("ProStore/1.0 (iOS)", forHTTPHeaderField: "User-Agent")
6358
let (data, response) = try await URLSession.shared.data(for: request)
6459
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
6560
throw NSError(domain: "RepoFetcher", code: http.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP \(http.statusCode)"])
6661
}
6762

6863
let decoder = JSONDecoder()
69-
// First try decoding top-level AltSource (common altstore format)
64+
// try common shapes
7065
if let source = try? decoder.decode(AltSource.self, from: data), let apps = source.apps {
7166
self.apps = apps
7267
return
7368
}
74-
75-
// Fallback: some repos publish a raw array of apps
7669
if let appsArray = try? decoder.decode([AltApp].self, from: data) {
7770
self.apps = appsArray
7871
return
7972
}
80-
81-
// Another fallback: some repos wrap apps in a top-level dictionary under other keys
82-
if let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
83-
// try to extract "apps" key and re-decode that fragment
84-
if let appsFragment = jsonObject["apps"] {
85-
let fragmentData = try JSONSerialization.data(withJSONObject: appsFragment)
86-
let appsArray = try decoder.decode([AltApp].self, from: fragmentData)
87-
self.apps = appsArray
88-
return
89-
}
73+
// fallbacks: try extracting "apps" key manually
74+
if let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any],
75+
let appsFragment = jsonObject["apps"] {
76+
let fragmentData = try JSONSerialization.data(withJSONObject: appsFragment)
77+
let appsArray = try decoder.decode([AltApp].self, from: fragmentData)
78+
self.apps = appsArray
79+
return
9080
}
91-
92-
throw NSError(domain: "RepoFetcher", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unexpected JSON format."])
93-
81+
throw NSError(domain: "RepoFetcher", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unexpected JSON format"])
9482
} catch {
95-
self.errorMessage = "Failed to load repository: \(error.localizedDescription)"
83+
self.errorMessage = "Failed to load: \(error.localizedDescription)"
9684
self.apps = []
9785
}
9886
}
9987

100-
/// Public method to refresh on demand
10188
func refresh() {
10289
Task { await load() }
10390
}
10491
}
10592

106-
// MARK: - The SwiftUI View
93+
// MARK: - AppsView (no NavigationView)
10794
public struct AppsView: View {
10895
@StateObject private var vm: RepoViewModel
10996

110-
// Public initializer so you can pass a custom repository URL (defaults to user's provided URL)
97+
/// Provide custom repo URL if you want (defaults to https://repository.apptesters.org/)
11198
public init(repoURL: URL = URL(string: "https://repository.apptesters.org/")!) {
11299
_vm = StateObject(wrappedValue: RepoViewModel(sourceURL: repoURL))
113100
}
114101

115102
public var body: some View {
116-
NavigationView {
117-
Group {
118-
if vm.isLoading && vm.apps.isEmpty {
119-
VStack(spacing: 12) {
120-
ProgressView()
121-
Text("Loading apps...")
122-
.font(.subheadline)
123-
.foregroundColor(.secondary)
124-
}
125-
.padding()
126-
} else if let error = vm.errorMessage, vm.apps.isEmpty {
127-
VStack(spacing: 12) {
128-
Text("Error")
129-
.font(.headline)
130-
Text(error)
131-
.font(.subheadline)
132-
.foregroundColor(.secondary)
133-
.multilineTextAlignment(.center)
134-
Button("Retry") { vm.refresh() }
135-
.padding(.top, 8)
136-
}
137-
.padding()
138-
} else {
139-
List(vm.apps) { app in
103+
Group {
104+
if vm.isLoading && vm.apps.isEmpty {
105+
VStack(spacing: 12) {
106+
ProgressView()
107+
Text("Loading apps...")
108+
.font(.subheadline)
109+
.foregroundColor(.secondary)
110+
}
111+
.padding()
112+
} else if let error = vm.errorMessage, vm.apps.isEmpty {
113+
VStack(spacing: 12) {
114+
Text("Error")
115+
.font(.headline)
116+
Text(error)
117+
.font(.subheadline)
118+
.foregroundColor(.secondary)
119+
.multilineTextAlignment(.center)
120+
Button("Retry") { vm.refresh() }
121+
.padding(.top, 8)
122+
}
123+
.padding()
124+
} else {
125+
List(vm.apps) { app in
126+
// parent NavigationStack handles navigation; provide a NavigationLink to a detail view
127+
NavigationLink(value: app) {
140128
AppRowView(app: app)
141129
}
142-
.listStyle(PlainListStyle())
143-
.refreshable { vm.refresh() } // iOS 15+ pull-to-refresh
144130
}
145-
}
146-
.navigationTitle("AppTesters")
147-
.toolbar {
148-
ToolbarItem(placement: .navigationBarTrailing) {
149-
Button(action: { vm.refresh() }) {
150-
Image(systemName: "arrow.clockwise")
151-
}
152-
.help("Refresh repository")
131+
.listStyle(.plain)
132+
.refreshable { vm.refresh() } // iOS 15+
133+
// If you don't want navigation links, swap NavigationLink -> Button/openURL as you prefer.
134+
.navigationDestination(for: AltApp.self) { app in
135+
AppDetailView(app: app)
153136
}
154137
}
155138
}
139+
// Do not set navigationTitle here — your parent already does that
156140
}
157141
}
158142

@@ -162,19 +146,17 @@ private struct AppRowView: View {
162146

163147
var body: some View {
164148
HStack(spacing: 12) {
165-
// AsyncImage is available iOS 15+
166149
if let iconURL = app.iconURL {
167150
AsyncImage(url: iconURL) { phase in
168151
switch phase {
169152
case .empty:
170153
ProgressView()
171154
.frame(width: 48, height: 48)
172155
case .success(let image):
173-
image
174-
.resizable()
156+
image.resizable()
175157
.scaledToFill()
176158
.frame(width: 48, height: 48)
177-
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
159+
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
178160
.shadow(radius: 1, y: 1)
179161
case .failure:
180162
Image(systemName: "app")
@@ -213,7 +195,6 @@ private struct AppRowView: View {
213195

214196
Spacer()
215197

216-
// optionally show a chevron or small metadata
217198
if let size = app.versions?.first?.size {
218199
Text(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file))
219200
.font(.caption2)
@@ -225,4 +206,90 @@ private struct AppRowView: View {
225206
}
226207
.padding(.vertical, 6)
227208
}
209+
}
210+
211+
// MARK: - Simple Detail View
212+
private struct AppsView: View {
213+
let app: AltApp
214+
@Environment(\.openURL) private var openURL
215+
216+
var body: some View {
217+
ScrollView {
218+
VStack(spacing: 16) {
219+
if let iconURL = app.iconURL {
220+
AsyncImage(url: iconURL) { phase in
221+
switch phase {
222+
case .empty:
223+
ProgressView()
224+
.frame(width: 120, height: 120)
225+
case .success(let image):
226+
image.resizable()
227+
.scaledToFit()
228+
.frame(width: 120, height: 120)
229+
.clipShape(RoundedRectangle(cornerRadius: 16))
230+
default:
231+
Image(systemName: "app")
232+
.resizable()
233+
.scaledToFit()
234+
.frame(width: 100, height: 100)
235+
}
236+
}
237+
}
238+
239+
VStack(alignment: .leading, spacing: 6) {
240+
Text(app.name)
241+
.font(.title2)
242+
.bold()
243+
if let dev = app.developerName {
244+
Text(dev)
245+
.font(.subheadline)
246+
.foregroundColor(.secondary)
247+
}
248+
if let desc = app.localizedDescription {
249+
Text(desc)
250+
.font(.body)
251+
.padding(.top, 6)
252+
}
253+
}
254+
.frame(maxWidth: .infinity, alignment: .leading)
255+
.padding(.horizontal)
256+
257+
if let version = app.versions?.first {
258+
VStack(alignment: .leading, spacing: 6) {
259+
Text("Latest")
260+
.font(.headline)
261+
HStack {
262+
VStack(alignment: .leading) {
263+
if let v = version.version { Text("Version: \(v)") }
264+
if let b = version.buildVersion { Text("Build: \(b)") }
265+
if let min = version.minOSVersion { Text("Min iOS: \(min)") }
266+
if let size = version.size {
267+
Text("Size: \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file))")
268+
}
269+
}
270+
Spacer()
271+
}
272+
}
273+
.padding()
274+
.background(.thinMaterial)
275+
.clipShape(RoundedRectangle(cornerRadius: 12))
276+
.padding(.horizontal)
277+
}
278+
279+
if let url = app.latestDownloadURL {
280+
Button(action: { openURL(url) }) {
281+
Label("Open download URL", systemImage: "arrow.down.doc")
282+
.frame(maxWidth: .infinity)
283+
}
284+
.buttonStyle(.borderedProminent)
285+
.padding(.horizontal)
286+
}
287+
288+
Spacer(minLength: 20)
289+
}
290+
.padding(.top)
291+
}
292+
.navigationTitle(app.name)
293+
.navigationBarTitleDisplayMode(.inline)
294+
}
228295
}

0 commit comments

Comments
 (0)