Skip to content

Commit a84607d

Browse files
committed
New download management UI
1 parent fa073dc commit a84607d

File tree

4 files changed

+322
-115
lines changed

4 files changed

+322
-115
lines changed

WWDC.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@
211211
F4F189762C0773C9006EA9A2 /* MacPreviewUtils in Frameworks */ = {isa = PBXBuildFile; productRef = F4F189752C0773C9006EA9A2 /* MacPreviewUtils */; };
212212
F4F189782C0774BE006EA9A2 /* PUIPlaybackSpeedToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F189772C0774BE006EA9A2 /* PUIPlaybackSpeedToggle.swift */; };
213213
F4F1897A2C0775C5006EA9A2 /* NumericContentTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F189792C0775C5006EA9A2 /* NumericContentTransition.swift */; };
214+
F4F2792A2C0F777200A029A3 /* DownloadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F279292C0F777200A029A3 /* DownloadManagerView.swift */; };
214215
F4FB069F2A2148EA00799F84 /* ExploreTabRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FB069E2A2148EA00799F84 /* ExploreTabRootView.swift */; };
215216
F4FB06A12A21493B00799F84 /* PreviewSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FB06A02A21493B00799F84 /* PreviewSupport.swift */; };
216217
F4FB06BF2A216C1F00799F84 /* RemoteGlyph.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FB06BE2A216C1F00799F84 /* RemoteGlyph.swift */; };
@@ -500,6 +501,7 @@
500501
F4F189772C0774BE006EA9A2 /* PUIPlaybackSpeedToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PUIPlaybackSpeedToggle.swift; sourceTree = "<group>"; };
501502
F4F189792C0775C5006EA9A2 /* NumericContentTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericContentTransition.swift; sourceTree = "<group>"; };
502503
F4F1C9A22A24FF50002C3709 /* TeamID.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TeamID.xcconfig; sourceTree = "<group>"; };
504+
F4F279292C0F777200A029A3 /* DownloadManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManagerView.swift; sourceTree = "<group>"; };
503505
F4FB069E2A2148EA00799F84 /* ExploreTabRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTabRootView.swift; sourceTree = "<group>"; };
504506
F4FB06A02A21493B00799F84 /* PreviewSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewSupport.swift; sourceTree = "<group>"; };
505507
F4FB06BE2A216C1F00799F84 /* RemoteGlyph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteGlyph.swift; sourceTree = "<group>"; };
@@ -558,6 +560,7 @@
558560
4D66CA51217E2C9B0006A8C9 /* DownloadsManagementTableView.swift */,
559561
4DDF6A772177A00C008E5539 /* DownloadsManagementTableCellView.swift */,
560562
4DBA2F7520FE71BF00ED0253 /* DownloadsStatusButton.swift */,
563+
F4F279292C0F777200A029A3 /* DownloadManagerView.swift */,
561564
);
562565
name = Downloads;
563566
sourceTree = "<group>";
@@ -1586,6 +1589,7 @@
15861589
F4578D9F2A26A218005B311A /* WWDCAppCommand.swift in Sources */,
15871590
F4FB06BF2A216C1F00799F84 /* RemoteGlyph.swift in Sources */,
15881591
DDDF807E20BA4FFA007284F8 /* WWDCHorizontalScrollView.swift in Sources */,
1592+
F4F2792A2C0F777200A029A3 /* DownloadManagerView.swift in Sources */,
15891593
DD7F387D1EAC113A002D8C00 /* WWDCTextField.swift in Sources */,
15901594
DDB352801EC7C4CA00254815 /* Arguments.swift in Sources */,
15911595
4D9EE96424BCE097001B1720 /* FilterState.swift in Sources */,

WWDC/DownloadManagerView.swift

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import SwiftUI
2+
import ConfCore
3+
import PlayerUI
4+
5+
private typealias Metrics = DownloadsManagementViewController.Metrics
6+
7+
struct DownloadManagerView: View {
8+
@EnvironmentObject private var manager: MediaDownloadManager
9+
@ObservedObject var controller: DownloadsManagementViewController
10+
11+
var body: some View {
12+
List {
13+
ForEach(manager.downloads) { download in
14+
DownloadItemView(download: download)
15+
.tag(download)
16+
}
17+
}
18+
.frame(minWidth: Metrics.defaultWidth, maxWidth: .infinity, minHeight: Metrics.defaultHeight, maxHeight: .infinity)
19+
.animation(.smooth, value: manager.downloads.count)
20+
}
21+
}
22+
23+
struct DownloadItemView: View {
24+
@EnvironmentObject private var manager: MediaDownloadManager
25+
@ObservedObject var download: MediaDownload
26+
27+
var body: some View {
28+
VStack(alignment: .leading, spacing: 4) {
29+
HStack {
30+
Text(download.title)
31+
.font(.headline)
32+
33+
Spacer()
34+
35+
DownloadActionsView(download: download)
36+
}
37+
38+
DownloadProgressView(download: download)
39+
}
40+
.wwdc_listRowSeparatorHidden()
41+
.contentShape(Rectangle())
42+
.padding(8)
43+
.swipeActions(edge: .trailing, allowsFullSwipe: true) { contextActions }
44+
.contextMenu { contextActions }
45+
}
46+
47+
@ViewBuilder
48+
private var contextActions: some View {
49+
if download.isCompleted {
50+
Button("Clear") {
51+
manager.clear(download)
52+
}
53+
} else if download.isPaused {
54+
Button("Resume") {
55+
catchingErrors {
56+
try manager.resume(download)
57+
}
58+
}
59+
} else if download.isFailed {
60+
Button("Try Again") {
61+
retry(download)
62+
}
63+
} else {
64+
Button("Pause") {
65+
catchingErrors {
66+
try manager.pause(download)
67+
}
68+
}
69+
70+
Button("Cancel", role: .destructive) {
71+
catchingErrors {
72+
try manager.cancel(download)
73+
}
74+
}
75+
}
76+
}
77+
78+
private func catchingErrors(perform action: () throws -> Void) {
79+
do {
80+
try action()
81+
} catch {
82+
NSAlert(error: error).runModal()
83+
}
84+
}
85+
86+
private func retry(_ download: MediaDownload) {
87+
Task {
88+
do {
89+
try await manager.retry(download)
90+
} catch {
91+
NSAlert(error: error).runModal()
92+
}
93+
}
94+
}
95+
}
96+
97+
struct DownloadProgressView: View {
98+
@EnvironmentObject private var manager: MediaDownloadManager
99+
@ObservedObject var download: MediaDownload
100+
101+
var body: some View {
102+
Group {
103+
switch download.state {
104+
case .waiting:
105+
progressState(message: "Starting…")
106+
case .downloading:
107+
progressState()
108+
case .paused:
109+
progressState(message: "Paused")
110+
case .failed(let message):
111+
progressState(message: message)
112+
.foregroundStyle(.red)
113+
case .completed:
114+
progressState(message: "Finished!")
115+
case .cancelled:
116+
progressState(message: "Canceled")
117+
}
118+
}
119+
}
120+
121+
@ViewBuilder
122+
private func progressState(message: String? = nil) -> some View {
123+
VStack(alignment: .leading, spacing: 1) {
124+
if let progress = download.progress {
125+
ProgressView(value: min(1, max(0, progress)))
126+
.opacity(download.isPaused ? 0.5 : 1)
127+
.opacity(progress >= 1 ? 0.2 : 1)
128+
} else {
129+
ProgressView(value: download.isCompleted ? 1 : nil, total: 1)
130+
.opacity(0.5)
131+
}
132+
133+
progressDetail(message: message)
134+
}
135+
}
136+
137+
@ViewBuilder
138+
private func progressDetail(message: String?) -> some View {
139+
HStack {
140+
progressIndicator(message: message)
141+
142+
Spacer()
143+
144+
if !download.isPaused, let stats = download.stats, let formattedETA = stats.formattedETA, let eta = stats.eta, eta > 0 {
145+
Text("\(formattedETA)")
146+
.numericContentTransition(value: eta, countsDown: true)
147+
} else if download.isCompleted {
148+
clearButton
149+
}
150+
}
151+
.progressViewStyle(.linear)
152+
.monospacedDigit()
153+
.font(.subheadline)
154+
.foregroundStyle(.secondary)
155+
.animation(.smooth, value: download.stats?.eta)
156+
}
157+
158+
@ViewBuilder
159+
private func progressIndicator(message: String?) -> some View {
160+
if let progress = download.progress, progress < 1 {
161+
Text(progress, format: .percent.precision(.fractionLength(0)))
162+
.font(.subheadline.weight(.medium))
163+
.numericContentTransition(value: progress)
164+
} else if let message {
165+
Text(message)
166+
}
167+
}
168+
169+
@ViewBuilder
170+
private var clearButton: some View {
171+
Button {
172+
manager.clear(download)
173+
} label: {
174+
Image(systemName: "xmark.circle.fill")
175+
}
176+
.buttonStyle(.borderless)
177+
}
178+
}
179+
180+
struct DownloadActionsView: View {
181+
@EnvironmentObject private var manager: MediaDownloadManager
182+
@ObservedObject var download: MediaDownload
183+
184+
var body: some View {
185+
Group {
186+
switch download.state {
187+
case .waiting:
188+
pauseButton
189+
case .downloading:
190+
pauseButton
191+
case .paused:
192+
resumeButton
193+
case .failed(let message):
194+
errorButton(with: message)
195+
case .completed:
196+
Image(systemName: "checkmark.circle.fill")
197+
.foregroundStyle(.green)
198+
case .cancelled:
199+
Image(systemName: "xmark.circle.fill")
200+
.foregroundStyle(.secondary)
201+
}
202+
}
203+
.progressViewStyle(.circular)
204+
.buttonStyle(.borderless)
205+
}
206+
207+
@ViewBuilder
208+
private var pauseButton: some View {
209+
Button {
210+
handlingErrors {
211+
try manager.pause(download)
212+
}
213+
} label: {
214+
Image(systemName: "pause.circle.fill")
215+
}
216+
}
217+
218+
@ViewBuilder
219+
private var resumeButton: some View {
220+
Button {
221+
handlingErrors {
222+
try manager.resume(download)
223+
}
224+
} label: {
225+
Image(systemName: "play.circle.fill")
226+
}
227+
}
228+
229+
@ViewBuilder
230+
private func errorButton(with message: String) -> some View {
231+
Button {
232+
NSAlert(error: message).runModal()
233+
} label: {
234+
Image(systemName: "exclamationmark.circle.fill")
235+
}
236+
.foregroundStyle(.red)
237+
}
238+
239+
private func handlingErrors(perform action: () throws -> Void) {
240+
do {
241+
try action()
242+
} catch {
243+
NSAlert(error: error).runModal()
244+
}
245+
}
246+
}
247+
248+
extension View {
249+
@ViewBuilder
250+
func wwdc_listRowSeparatorHidden(_ hidden: Bool = true) -> some View {
251+
if #available(macOS 13.0, *) {
252+
listRowSeparator(hidden ? .hidden : .automatic)
253+
} else {
254+
self
255+
}
256+
}
257+
}

0 commit comments

Comments
 (0)