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

Commit 6397003

Browse files
authored
Add AppDetailView
1 parent 60875e8 commit 6397003

1 file changed

Lines changed: 142 additions & 15 deletions

File tree

Sources/prostore/views/appsView.swift

Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ struct AltSource: Decodable {
1111

1212
struct AppVersion: Decodable {
1313
let version: String?
14-
let buildVersion: String?
1514
let date: String?
1615
let downloadURL: URL?
1716
let size: Int?
1817
let minOSVersion: String?
19-
let maxOSVersion: String?
18+
let localizedDescription: String?
2019
}
2120

2221
struct AltApp: Decodable, Identifiable {
@@ -26,11 +25,8 @@ struct AltApp: Decodable, Identifiable {
2625
let developerName: String?
2726
let subtitle: String?
2827
let iconURL: URL?
28+
let localizedDescription: String?
2929
let versions: [AppVersion]?
30-
31-
var latestDownloadURL: URL? {
32-
versions?.first?.downloadURL
33-
}
3430
}
3531

3632
// MARK: - ViewModel
@@ -97,9 +93,9 @@ final class RepoViewModel: ObservableObject {
9793
// MARK: - AppsView
9894
public struct AppsView: View {
9995
@StateObject private var vm: RepoViewModel
100-
10196
@State private var searchText: String = ""
10297
@FocusState private var searchFieldFocused: Bool
98+
@State private var selectedApp: AltApp? = nil
10399

104100
public init(repoURL: URL = URL(string: "https://repository.apptesters.org/")!) {
105101
_vm = StateObject(wrappedValue: RepoViewModel(sourceURL: repoURL))
@@ -168,14 +164,22 @@ public struct AppsView: View {
168164
.padding()
169165
} else {
170166
List(filteredApps) { app in
171-
AppRowView(app: app)
167+
Button {
168+
selectedApp = app
169+
} label: {
170+
AppRowView(app: app)
171+
}
172+
.buttonStyle(.plain)
172173
}
173174
.listStyle(PlainListStyle())
174175
.refreshable { vm.refresh() }
175176
}
176177
}
177178
.padding(.top, 8)
178179
}
180+
.sheet(item: $selectedApp) { app in
181+
AppDetailView(app: app)
182+
}
179183
.toolbar {
180184
ToolbarItem(placement: .navigationBarTrailing) {
181185
Button(action: { vm.refresh() }) {
@@ -205,13 +209,6 @@ private struct AppRowView: View {
205209
.scaledToFill()
206210
.frame(width: 48, height: 48)
207211
.clipShape(RoundedRectangle(cornerRadius: 10))
208-
.shadow(radius: 1, y: 1)
209-
.onAppear {
210-
let renderer = ImageRenderer(content: image)
211-
if let uiImage = renderer.uiImage {
212-
ImageCache.shared.set(uiImage, for: iconURL)
213-
}
214-
}
215212
case .failure:
216213
Image(systemName: "app")
217214
.resizable()
@@ -260,4 +257,134 @@ private struct AppRowView: View {
260257
}
261258
.padding(.vertical, 6)
262259
}
260+
}
261+
262+
// MARK: - AppDetailView
263+
private struct AppDetailView: View {
264+
let app: AltApp
265+
266+
private var latestVersion: AppVersion? {
267+
app.versions?.first
268+
}
269+
270+
private func formatSize(_ size: Int?) -> String {
271+
guard let size = size else { return "Unknown" }
272+
return ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file)
273+
}
274+
275+
private func formatDate(_ dateString: String?) -> String {
276+
guard let dateString = dateString, let date = ISO8601DateFormatter().date(from: dateString) else {
277+
return "Unknown"
278+
}
279+
let formatter = DateFormatter()
280+
formatter.dateStyle = .medium
281+
return formatter.string(from: date)
282+
}
283+
284+
var body: some View {
285+
ScrollView {
286+
VStack(alignment: .leading, spacing: 16) {
287+
HStack(alignment: .top, spacing: 16) {
288+
if let iconURL = app.iconURL {
289+
AsyncImage(url: iconURL) { phase in
290+
switch phase {
291+
case .empty:
292+
ProgressView()
293+
.frame(width: 80, height: 80)
294+
case .success(let image):
295+
image
296+
.resizable()
297+
.scaledToFit()
298+
.frame(width: 80, height: 80)
299+
.clipShape(RoundedRectangle(cornerRadius: 12))
300+
case .failure:
301+
Image(systemName: "app")
302+
.resizable()
303+
.scaledToFit()
304+
.frame(width: 60, height: 60)
305+
.foregroundColor(.secondary)
306+
@unknown default:
307+
EmptyView()
308+
}
309+
}
310+
}
311+
VStack(alignment: .leading, spacing: 4) {
312+
Text(app.name)
313+
.font(.title2)
314+
.bold()
315+
if let dev = app.developerName {
316+
Text(dev)
317+
.font(.subheadline)
318+
.foregroundColor(.secondary)
319+
}
320+
Text(app.bundleIdentifier)
321+
.font(.caption)
322+
.foregroundColor(.secondary)
323+
}
324+
}
325+
326+
if let generalDesc = app.localizedDescription, generalDesc != latestVersion?.localizedDescription {
327+
Text(generalDesc)
328+
}
329+
330+
if let latest = latestVersion, latest.localizedDescription != nil, latest.localizedDescription != app.localizedDescription {
331+
VStack(alignment: .leading, spacing: 8) {
332+
Text("What's New?")
333+
.font(.headline)
334+
Text(latest.localizedDescription!)
335+
}
336+
}
337+
338+
if let latest = latestVersion {
339+
VStack(alignment: .leading, spacing: 4) {
340+
HStack {
341+
Text("Version:")
342+
.bold()
343+
Text(latest.version ?? "Unknown")
344+
}
345+
HStack {
346+
Text("Released:")
347+
.bold()
348+
Text(formatDate(latest.date))
349+
}
350+
HStack {
351+
Text("Size:")
352+
.bold()
353+
Text(formatSize(latest.size))
354+
}
355+
HStack {
356+
Text("Min OS:")
357+
.bold()
358+
Text(latest.minOSVersion ?? "Unknown")
359+
}
360+
}
361+
}
362+
363+
if let screenshots = latestVersion?.downloadURL != nil ? [] : nil, !screenshots!.isEmpty {
364+
ScrollView(.horizontal, showsIndicators: false) {
365+
HStack {
366+
ForEach(screenshots!, id: \.self) { url in
367+
AsyncImage(url: url) { phase in
368+
switch phase {
369+
case .empty: ProgressView()
370+
case .success(let image):
371+
image
372+
.resizable()
373+
.scaledToFit()
374+
.frame(height: 200)
375+
.cornerRadius(10)
376+
case .failure: Image(systemName: "photo").resizable().scaledToFit().frame(height: 200)
377+
@unknown default: EmptyView()
378+
}
379+
}
380+
}
381+
}
382+
}
383+
}
384+
385+
Spacer()
386+
}
387+
.padding()
388+
}
389+
}
263390
}

0 commit comments

Comments
 (0)