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

Commit 47aa56d

Browse files
authored
Fix sidebar and navigation & other bug fixes
1 parent 5b26624 commit 47aa56d

2 files changed

Lines changed: 45 additions & 117 deletions

File tree

Lines changed: 43 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import SwiftUI
2+
import Combine
23

3-
// MARK: - Models (AltStore-ish)
4+
// MARK: - Models (decodable for AltStore source format)
45
struct AltSource: Decodable {
56
let name: String?
67
let subtitle: String?
@@ -9,15 +10,16 @@ struct AltSource: Decodable {
910
}
1011

1112
struct AltApp: Decodable, Identifiable {
13+
// Use bundleIdentifier as stable id
1214
var id: String { bundleIdentifier }
1315
let name: String
1416
let bundleIdentifier: String
1517
let developerName: String?
1618
let subtitle: String?
1719
let iconURL: URL?
18-
let localizedDescription: String?
1920
let versions: [AppVersion]?
2021

22+
// A convenience computed property for a primary download URL (if present on the latest version)
2123
var latestDownloadURL: URL? {
2224
versions?.first?.downloadURL
2325
}
@@ -47,54 +49,65 @@ final class RepoViewModel: ObservableObject {
4749
Task { await load() }
4850
}
4951

52+
/// Load JSON and decode apps. Tries both `{ "apps": [...] }` and direct `[App]` shapes.
5053
func load() async {
5154
isLoading = true
5255
errorMessage = nil
5356
defer { isLoading = false }
5457

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

6368
let decoder = JSONDecoder()
64-
// try common shapes
69+
// First try decoding top-level AltSource (common altstore format)
6570
if let source = try? decoder.decode(AltSource.self, from: data), let apps = source.apps {
6671
self.apps = apps
6772
return
6873
}
74+
75+
// Fallback: some repos publish a raw array of apps
6976
if let appsArray = try? decoder.decode([AltApp].self, from: data) {
7077
self.apps = appsArray
7178
return
7279
}
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
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+
}
8090
}
81-
throw NSError(domain: "RepoFetcher", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unexpected JSON format"])
91+
92+
throw NSError(domain: "RepoFetcher", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unexpected JSON format."])
93+
8294
} catch {
83-
self.errorMessage = "Failed to load: \(error.localizedDescription)"
95+
self.errorMessage = "Failed to load repository: \(error.localizedDescription)"
8496
self.apps = []
8597
}
8698
}
8799

100+
/// Public method to refresh on demand
88101
func refresh() {
89102
Task { await load() }
90103
}
91104
}
92105

93-
// MARK: - AppsView (no NavigationView)
106+
// MARK: - The SwiftUI View (no NavigationView wrapper)
94107
public struct AppsView: View {
95108
@StateObject private var vm: RepoViewModel
96109

97-
/// Provide custom repo URL if you want (defaults to https://repository.apptesters.org/)
110+
// Public initializer so you can pass a custom repository URL (defaults to user's provided URL)
98111
public init(repoURL: URL = URL(string: "https://repository.apptesters.org/")!) {
99112
_vm = StateObject(wrappedValue: RepoViewModel(sourceURL: repoURL))
100113
}
@@ -123,20 +136,21 @@ public struct AppsView: View {
123136
.padding()
124137
} else {
125138
List(vm.apps) { app in
126-
// parent NavigationStack handles navigation; provide a NavigationLink to a detail view
127-
NavigationLink(value: app) {
128-
AppRowView(app: app)
129-
}
139+
AppRowView(app: app)
130140
}
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)
141+
.listStyle(PlainListStyle())
142+
.refreshable { vm.refresh() }
143+
}
144+
}
145+
.toolbar {
146+
ToolbarItem(placement: .navigationBarTrailing) {
147+
Button(action: { vm.refresh() }) {
148+
Image(systemName: "arrow.clockwise")
136149
}
150+
.help("Refresh repository")
137151
}
138152
}
139-
// Do not set navigationTitle here — your parent already does that
153+
.padding(.horizontal, 0)
140154
}
141155
}
142156

@@ -153,10 +167,11 @@ private struct AppRowView: View {
153167
ProgressView()
154168
.frame(width: 48, height: 48)
155169
case .success(let image):
156-
image.resizable()
170+
image
171+
.resizable()
157172
.scaledToFill()
158173
.frame(width: 48, height: 48)
159-
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
174+
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
160175
.shadow(radius: 1, y: 1)
161176
case .failure:
162177
Image(systemName: "app")
@@ -206,91 +221,4 @@ private struct AppRowView: View {
206221
}
207222
.padding(.vertical, 6)
208223
}
209-
}
210-
211-
// MARK: - Simple Detail View
212-
private struct AppDetailView: 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-
}
295-
296-
}
224+
}

project.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ targets:
3030
properties:
3131
CFBundleDisplayName: "ProStore"
3232
CFBundleName: "prostore"
33-
CFBundleVersion: "4"
34-
CFBundleShortVersionString: "0.3.0"
33+
CFBundleVersion: "5"
34+
CFBundleShortVersionString: "0.3.1"
3535
UILaunchStoryboardName: "LaunchScreen"
3636
NSPrincipalClass: "UIApplication"
3737
NSAppTransportSecurity:

0 commit comments

Comments
 (0)