Skip to content

Commit 8d5c59f

Browse files
authored
Add AppDetailView for displaying app details
1 parent 4ed96dc commit 8d5c59f

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import SwiftUI
2+
import Foundation
3+
4+
public struct AppDetailView: View {
5+
let app: AltApp
6+
7+
private var latestVersion: AppVersion? {
8+
app.versions?.first
9+
}
10+
11+
private func formatSize(_ size: Int?) -> String {
12+
guard let size = size else { return "Unknown" }
13+
return ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file)
14+
}
15+
16+
private func formatDate(_ dateString: String?) -> String {
17+
guard let dateString = dateString, let date = ISO8601DateFormatter().date(from: dateString) else {
18+
return "Unknown"
19+
}
20+
let formatter = DateFormatter()
21+
formatter.dateStyle = .medium
22+
return formatter.string(from: date)
23+
}
24+
25+
public var body: some View {
26+
ScrollView {
27+
VStack(alignment: .leading, spacing: 16) {
28+
29+
// App Header
30+
HStack(alignment: .top, spacing: 16) {
31+
// Reserve a fixed column for the icon to avoid shifting
32+
ZStack {
33+
RoundedRectangle(cornerRadius: 12)
34+
.fill(Color.gray.opacity(0.12))
35+
.frame(width: 92, height: 92)
36+
37+
if let iconURL = app.iconURL {
38+
RetryAsyncImage(
39+
url: iconURL,
40+
size: CGSize(width: 80, height: 80),
41+
maxAttempts: 3,
42+
content: { image in
43+
image
44+
.resizable()
45+
.scaledToFit()
46+
.frame(width: 80, height: 80)
47+
.clipShape(RoundedRectangle(cornerRadius: 12))
48+
},
49+
placeholder: {
50+
ProgressView()
51+
.frame(width: 80, height: 80)
52+
},
53+
failure: {
54+
Image(systemName: "app")
55+
.resizable()
56+
.scaledToFit()
57+
.frame(width: 60, height: 60)
58+
.foregroundColor(.secondary)
59+
}
60+
)
61+
} else {
62+
Image(systemName: "app")
63+
.resizable()
64+
.scaledToFit()
65+
.frame(width: 60, height: 60)
66+
.foregroundColor(.secondary)
67+
}
68+
}
69+
.frame(width: 92, height: 92, alignment: .top)
70+
71+
VStack(alignment: .leading, spacing: 4) {
72+
Text(app.name)
73+
.font(.title2)
74+
.bold()
75+
if let dev = app.developerName {
76+
Text(dev)
77+
.font(.subheadline)
78+
.foregroundColor(.secondary)
79+
}
80+
Text(app.bundleIdentifier)
81+
.font(.caption)
82+
.foregroundColor(.secondary)
83+
.lineLimit(2)
84+
}
85+
}
86+
87+
// General description
88+
if let generalDesc = app.localizedDescription, generalDesc != latestVersion?.localizedDescription {
89+
Text(generalDesc)
90+
}
91+
92+
// What's New
93+
if let latest = latestVersion, let latestDesc = latest.localizedDescription,
94+
latestDesc != app.localizedDescription {
95+
VStack(alignment: .leading, spacing: 8) {
96+
Text("What's New?")
97+
.font(.headline)
98+
Text(latestDesc)
99+
}
100+
}
101+
102+
// Version info
103+
if let latest = latestVersion {
104+
VStack(alignment: .leading, spacing: 4) {
105+
if let version = latest.version, !version.isEmpty {
106+
HStack {
107+
Text("Version:").bold()
108+
Text(version)
109+
}
110+
}
111+
112+
if let dateString = latest.date, !dateString.isEmpty {
113+
HStack {
114+
Text("Released:").bold()
115+
Text(formatDate(dateString))
116+
}
117+
}
118+
119+
if let size = latest.size {
120+
HStack {
121+
Text("Size:").bold()
122+
Text(formatSize(size))
123+
}
124+
}
125+
126+
if let minOS = latest.minOSVersion, !minOS.isEmpty {
127+
HStack {
128+
Text("Min OS:").bold()
129+
Text(minOS)
130+
}
131+
}
132+
}
133+
}
134+
135+
// Screenshots (from general app)
136+
if let screenshots = app.screenshotURLs, !screenshots.isEmpty {
137+
ScrollView(.horizontal, showsIndicators: false) {
138+
HStack(spacing: 12) {
139+
ForEach(screenshots, id: \.self) { url in
140+
AsyncImage(url: url) { phase in
141+
switch phase {
142+
case .empty:
143+
ProgressView()
144+
.frame(height: 200)
145+
.frame(minWidth: 120)
146+
.background(Color.gray.opacity(0.08))
147+
.cornerRadius(10)
148+
case .success(let image):
149+
image
150+
.resizable()
151+
.scaledToFit()
152+
.frame(height: 200)
153+
.cornerRadius(10)
154+
case .failure:
155+
Image(systemName: "photo")
156+
.resizable()
157+
.scaledToFit()
158+
.frame(height: 200)
159+
.frame(minWidth: 120)
160+
.background(Color.gray.opacity(0.08))
161+
.cornerRadius(10)
162+
@unknown default:
163+
EmptyView()
164+
}
165+
}
166+
}
167+
}
168+
.padding(.vertical, 6)
169+
}
170+
}
171+
172+
Spacer()
173+
}
174+
.frame(maxWidth: .infinity, alignment: .leading)
175+
.padding()
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)