diff --git a/App/Mochi.xcodeproj/project.pbxproj b/App/Mochi.xcodeproj/project.pbxproj index 8c209fb..5a670ef 100644 --- a/App/Mochi.xcodeproj/project.pbxproj +++ b/App/Mochi.xcodeproj/project.pbxproj @@ -376,7 +376,7 @@ CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ..; - DEVELOPMENT_TEAM = GYXF583PFT; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -418,7 +418,7 @@ CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ..; - DEVELOPMENT_TEAM = GYXF583PFT; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; diff --git a/Sources/Clients/OfflineManagerClient/Client.swift b/Sources/Clients/OfflineManagerClient/Client.swift index 0c0f581..748f934 100644 --- a/Sources/Clients/OfflineManagerClient/Client.swift +++ b/Sources/Clients/OfflineManagerClient/Client.swift @@ -20,6 +20,7 @@ public struct OfflineManagerClient { public var cache: @Sendable (CacheAsset) async throws -> Void public var remove: @Sendable (RemoveType, String, String?) async throws -> Void public var togglePause: @Sendable (Int) async throws -> Void + public var cancel: @Sendable (Int) async throws -> Void public var observeDownloading: @Sendable () -> AsyncStream<[DownloadingItem]> } @@ -31,6 +32,7 @@ extension OfflineManagerClient: TestDependencyKey { cache: unimplemented("\(Self.self).cache"), remove: unimplemented("\(Self.self).remove"), togglePause: unimplemented("\(Self.self).togglePause"), + cancel: unimplemented("\(Self.self).cancel"), observeDownloading: unimplemented("\(Self.self).observeDownloading") ) } diff --git a/Sources/Clients/OfflineManagerClient/Live.swift b/Sources/Clients/OfflineManagerClient/Live.swift index afef99d..b2f4d16 100644 --- a/Sources/Clients/OfflineManagerClient/Live.swift +++ b/Sources/Clients/OfflineManagerClient/Live.swift @@ -62,6 +62,9 @@ extension OfflineManagerClient: DependencyKey { togglePause: { taskId in downloadManager.togglePauseDownload(taskId) }, + cancel: { taskId in + downloadManager.cancelDownload(taskId) + }, observeDownloading: { .init { continuation in let cancellable = Task.detached { @@ -178,6 +181,16 @@ private class OfflineDownloadManager: NSObject { } } + func cancelDownload(_ taskId: Int) { + downloadSession.getAllTasks { taskArray in + taskArray.first(where: { $0.taskIdentifier == taskId })?.cancel() + if let idx = self.downloadingItems.firstIndex(where: { $0.taskId == taskId }) { + self.downloadingItems.remove(at: idx) + } + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: ["type": Notification.Name.AssetDownloadStateChanged, "taskId": taskId, "status": OfflineManagerClient.StatusType.cancelled]) + } + } + func restorePendingDownloads() { downloadSession.getAllTasks { tasksArray in for task in tasksArray { diff --git a/Sources/Features/DownloadQueue/DownloadQueueFeature+Reducer.swift b/Sources/Features/DownloadQueue/DownloadQueueFeature+Reducer.swift index eb3f68f..c9a1810 100644 --- a/Sources/Features/DownloadQueue/DownloadQueueFeature+Reducer.swift +++ b/Sources/Features/DownloadQueue/DownloadQueueFeature+Reducer.swift @@ -19,6 +19,11 @@ extension DownloadQueueFeature: Reducer { await send(.internal(.updateDownloadingItems(items))) } } + + case let .view(.didTapCancelDownload(item)): + return .run { send in + try await offlineManagerClient.cancel(item.taskId) + } case let .view(.pause(item)): return .run { send in diff --git a/Sources/Features/DownloadQueue/DownloadQueueFeature+View.swift b/Sources/Features/DownloadQueue/DownloadQueueFeature+View.swift index 0d74ce4..a46850e 100644 --- a/Sources/Features/DownloadQueue/DownloadQueueFeature+View.swift +++ b/Sources/Features/DownloadQueue/DownloadQueueFeature+View.swift @@ -37,51 +37,65 @@ extension DownloadQueueFeature.View: View { } Spacer() - switch item.status { - case .suspended: - CircularProgressView(progress: item.percentComplete, barStyle: .init(fill: Theme.pastelRed.opacity(0.4), width: 4, blurRadius: 0)) { - Image(systemName: "play.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .padding(6) - .foregroundStyle(Theme.pastelRed) - } - .onTapGesture { - viewStore.send(.pause(item)) - } - .frame(width: 30, height: 30) - .animation(.easeInOut, value: item.status) - case .finished: - Image(systemName: "checkmark.circle.fill") + switch item.status { + case .suspended: + CircularProgressView(progress: item.percentComplete, barStyle: .init(fill: Theme.pastelRed.opacity(0.4), width: 4, blurRadius: 0)) { + Image(systemName: "play.fill") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 29, height: 29) + .padding(6) .foregroundStyle(Theme.pastelRed) - .animation(.easeInOut, value: item.status) - case .cancelled: - EmptyView() - case .downloading: - CircularProgressView(progress: item.percentComplete, barStyle: .init(fill: Theme.pastelRed, width: 4, blurRadius: 0)) { - Image(systemName: "pause.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .padding(6) - .foregroundStyle(Theme.pastelRed) - } - .frame(width: 30, height: 30) - .contentShape(Rectangle()) - .onTapGesture { - viewStore.send(.pause(item)) - } + } + .onTapGesture { + viewStore.send(.pause(item)) + } + .frame(width: 30, height: 30) + .animation(.easeInOut, value: item.status) + case .finished: + Image(systemName: "checkmark.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 29, height: 29) + .foregroundStyle(Theme.pastelRed) + .animation(.easeInOut, value: item.status) + case .cancelled: + Image(systemName: "xmark.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 29, height: 29) + .foregroundStyle(Color.secondary.opacity(0.4)) .animation(.easeInOut, value: item.status) - case .error: - Image(systemName: "exclamationmark.circle.fill") + case .downloading: + CircularProgressView(progress: item.percentComplete, barStyle: .init(fill: Theme.pastelRed, width: 4, blurRadius: 0)) { + Image(systemName: "pause.fill") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 29, height: 29) + .padding(6) .foregroundStyle(Theme.pastelRed) - .animation(.easeInOut, value: item.status) - } + } + .frame(width: 30, height: 30) + .contentShape(Rectangle()) + .onTapGesture { + viewStore.send(.pause(item)) + } + .animation(.easeInOut, value: item.status) + case .error: + Image(systemName: "exclamationmark.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 29, height: 29) + .foregroundStyle(Theme.pastelRed) + .animation(.easeInOut, value: item.status) + } + } + .contentShape(Rectangle()) + .contextMenu { + Button(role: .destructive) { + viewStore.send(.didTapCancelDownload(item)) + } label: { + Label("Cancel Download", systemImage: "xmark") + } + .buttonStyle(.plain) } } } diff --git a/Sources/Features/DownloadQueue/DownloadQueueFeature.swift b/Sources/Features/DownloadQueue/DownloadQueueFeature.swift index 4f08077..40844b3 100644 --- a/Sources/Features/DownloadQueue/DownloadQueueFeature.swift +++ b/Sources/Features/DownloadQueue/DownloadQueueFeature.swift @@ -30,6 +30,7 @@ public struct DownloadQueueFeature: Feature { @dynamicMemberLookup public enum ViewAction: SendableAction { case didAppear + case didTapCancelDownload(OfflineManagerClient.DownloadingItem) case pause(OfflineManagerClient.DownloadingItem) }