Start using toast operation handler

This commit is contained in:
Jackson Harper
2023-12-19 09:59:40 +08:00
parent 4291c1149b
commit e17f243db0
8 changed files with 245 additions and 102 deletions

View File

@ -732,7 +732,7 @@ struct AnimatingCellHeight: AnimatableModifier {
NavigationView {
LibraryAddFeedView(dismiss: {
showAddFeedView = false
})
}, toastOperationHandler: nil)
}
}
.alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.",

View File

@ -10,6 +10,8 @@ struct LibraryAddFeedView: View {
@State var newLinkURL: String = ""
@EnvironmentObject var dataService: DataService
let toastOperationHandler: ToastOperationHandler?
enum FocusField: Hashable {
case addLinkEditor
}
@ -42,7 +44,10 @@ struct LibraryAddFeedView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: LibraryScanFeedView(dismiss: self.dismiss, viewModel: LibraryAddFeedViewModel(dataService: dataService, feedURL: newLinkURL)),
destination: LibraryScanFeedView(
dismiss: self.dismiss,
viewModel: LibraryAddFeedViewModel(dataService: dataService, feedURL: newLinkURL, toastOperationHandler: toastOperationHandler)
),
label: { Text("Add").bold() }
)
}

View File

@ -7,6 +7,7 @@ import Utils
public class LibraryAddFeedViewModel: NSObject, ObservableObject {
let dataService: DataService
let feedURL: String
let toastOperationHandler: ToastOperationHandler?
@Published var isLoading = true
@Published var errorMessage: String = ""
@ -15,9 +16,10 @@ public class LibraryAddFeedViewModel: NSObject, ObservableObject {
@Published var feeds: [Feed] = []
@Published var selected: [String] = []
init(dataService: DataService, feedURL: String) {
init(dataService: DataService, feedURL: String, toastOperationHandler: ToastOperationHandler?) {
self.dataService = dataService
self.feedURL = feedURL
self.toastOperationHandler = toastOperationHandler
}
func scanFeed() async {
@ -38,28 +40,64 @@ public class LibraryAddFeedViewModel: NSObject, ObservableObject {
}
func addFeeds() async {
_ = await withTaskGroup(of: Bool.self) { group in
for feedURL in selected {
group.addTask {
(try? await self.dataService.subscribeToFeed(feedURL: feedURL)) ?? false
if let toastOperationHandler = toastOperationHandler {
toastOperationHandler.update(OperationStatus.isPerforming, "Subscribing...")
let selected = self.selected
let addTask = Task.detached(priority: .background) {
_ = await withTaskGroup(of: Bool.self) { group in
for feedURL in selected {
group.addTask {
(try? await self.dataService.subscribeToFeed(feedURL: feedURL)) ?? false
}
}
var successCount = 0
var failureCount = 0
for await value in group {
if value {
successCount += 1
} else {
failureCount += 1
}
}
let hasFailures = failureCount
DispatchQueue.main.async {
if hasFailures > 0 {
toastOperationHandler.update(OperationStatus.failure, "Failed to subscribe to \(hasFailures) feeds")
} else {
toastOperationHandler.update(OperationStatus.success, "Subscribed")
}
}
}
}
var successCount = 0
var failureCount = 0
for await value in group {
if value {
successCount += 1
} else {
failureCount += 1
toastOperationHandler.performOperation(addTask)
} else {
_ = await withTaskGroup(of: Bool.self) { group in
for feedURL in selected {
group.addTask {
(try? await self.dataService.subscribeToFeed(feedURL: feedURL)) ?? false
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) {
if failureCount > 0 {
showInLibrarySnackbar("Failed to add \(failureCount) feeds")
} else {
showInLibrarySnackbar("Added \(successCount) feed\(successCount == 0 ? "" : "s")")
var successCount = 0
var failureCount = 0
for await value in group {
if value {
successCount += 1
} else {
failureCount += 1
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) {
if failureCount > 0 {
showInLibrarySnackbar("Failed to add \(failureCount) feeds")
} else {
showInLibrarySnackbar("Added \(successCount) feed\(successCount == 0 ? "" : "s")")
}
}
}
}

View File

@ -4,13 +4,21 @@ import SwiftUI
import Transmission
import Views
enum UnsubscribeState {
enum OperationStatus {
case none
case isUnsubscribing
case unsubscribeSuccess
case unsubscribeFailure
case isPerforming
case success
case failure
}
@MainActor
struct ToastOperationHandler {
let performOperation: (_: Sendable?) -> Void
let update: (_: OperationStatus, _: String) -> Void
}
typealias OperationStatusHandler = (_: OperationStatus) -> Void
@MainActor final class SubscriptionsViewModel: ObservableObject {
@Published var isLoading = true
@Published var feeds = [Subscription]()
@ -18,7 +26,10 @@ enum UnsubscribeState {
@Published var hasNetworkError = false
@Published var subscriptionNameToCancel: String?
@Published var presentingSubscription: Subscription?
@Published var unsubscribeState: UnsubscribeState = .none
@Published var showOperationToast = false
@Published var operationStatus: OperationStatus = .none
@Published var operationMessage: String?
func loadSubscriptions(dataService: DataService) async {
isLoading = true
@ -34,56 +45,67 @@ enum UnsubscribeState {
isLoading = false
}
func cancelSubscription(dataService _: DataService, subscription: Subscription) async {
unsubscribeState = .isUnsubscribing
let subscriptionName = subscription.name
func cancelSubscription(dataService: DataService, subscription: Subscription) async {
operationMessage = "Unsubscribing..."
operationStatus = .isPerforming
// do {
// try await dataService.deleteSubscription(subscriptionName: subscriptionName)
// let index = subscriptions.firstIndex { $0.name == subscriptionName }
// if let index = index {
// subscriptions.remove(at: index)
// }
// unsubscribeState = .unsubscribeSuccess
// } catch {
// appLogger.debug("failed to remove subscription")
// unsubscribeState = .unsubscribeFailure
// }
do {
try await dataService.deleteSubscription(subscriptionName: subscription.name, subscriptionId: subscription.subscriptionID)
var list = subscription.type == .feed ? feeds : newsletters
let index = list.firstIndex { $0.subscriptionID == subscription.subscriptionID }
if let index = index {
list.remove(at: index)
switch subscription.type {
case .feed:
feeds = list
case .newsletter:
newsletters = list
}
}
operationMessage = "Unsubscribed"
operationStatus = .success
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) {
self.showOperationToast = false
}
} catch {
appLogger.debug("failed to remove subscription")
operationMessage = "Failed to unsubscribe"
operationStatus = .failure
}
}
}
struct UnsubscribeToast: View {
struct OperationToast: View {
@ObservedObject var viewModel: SubscriptionsViewModel
var body: some View {
HStack {
if viewModel.unsubscribeState == .isUnsubscribing {
Text("Unsubscribing...")
Spacer()
ProgressView()
} else if viewModel.unsubscribeState == .unsubscribeSuccess {
Text("You have been unsubscribed.")
Spacer()
Button(action: {
}, label: {
Text("Done").bold()
})
} else if viewModel.unsubscribeState == .unsubscribeFailure {
Text("There was an error unsubscribing")
Spacer()
Button(action: {
}, label: {
Text("Done").bold()
})
VStack {
HStack {
if viewModel.operationStatus == .isPerforming {
Text(viewModel.operationMessage ?? "Performing...")
Spacer()
ProgressView()
} else if viewModel.operationStatus == .success {
Text(viewModel.operationMessage ?? "Success")
Spacer()
} else if viewModel.operationStatus == .failure {
Text(viewModel.operationMessage ?? "Failure")
Spacer()
Button(action: { viewModel.showOperationToast = false }, label: {
Text("Done").bold()
})
}
}
.padding(10)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(Color(hex: "2A2A2A"))
.cornerRadius(4.0)
.tint(Color.green)
}
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.padding(.bottom, 30)
.padding(.horizontal, 15)
.background(Color.systemBackground)
.padding(.bottom, 70)
.padding(.horizontal, 10)
.ignoresSafeArea(.all, edges: .bottom)
}
}
@ -93,13 +115,13 @@ struct SubscriptionsView: View {
@State private var deleteConfirmationShown = false
@State private var showDeleteCompleted = false
@State private var showAddFeedView = false
@State private var showSubscriptionsSheet = false
@State private var showUnsubscribeToast = false
var body: some View {
Group {
WindowLink(level: .alert, transition: .move(edge: .bottom).combined(with: .opacity), isPresented: $showUnsubscribeToast) {
UnsubscribeToast(viewModel: viewModel)
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) {
OperationToast(viewModel: viewModel)
} label: {
EmptyView()
}
@ -133,6 +155,31 @@ struct SubscriptionsView: View {
#endif
}
}
.sheet(isPresented: $showAddFeedView) {
let handler = ToastOperationHandler(performOperation: { sendable in
self.viewModel.showOperationToast = true
Task {
_ = await sendable
viewModel.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) {
Task {
await self.viewModel.loadSubscriptions(dataService: dataService)
self.viewModel.showOperationToast = false
}
}
}
}, update: { state, text in
viewModel.operationStatus = state
viewModel.operationMessage = text
})
NavigationView {
LibraryAddFeedView(dismiss: {
showAddFeedView = false
}, toastOperationHandler: handler)
}
}
.formSheet(isPresented: $showSubscriptionsSheet) {
if let presentingSubscription = viewModel.presentingSubscription {
SubscriptionSettingsView(
@ -143,8 +190,8 @@ struct SubscriptionsView: View {
unsubscribe: { subscription in
showSubscriptionsSheet = false
viewModel.unsubscribeState = .isUnsubscribing
showUnsubscribeToast = true
viewModel.operationStatus = .isPerforming
viewModel.showOperationToast = true
Task {
await viewModel.cancelSubscription(dataService: dataService, subscription: subscription)
}
@ -164,36 +211,43 @@ struct SubscriptionsView: View {
private var innerBody: some View {
Group {
Section("Feeds") {
ForEach(viewModel.feeds, id: \.subscriptionID) { subscription in
Button(action: {
viewModel.presentingSubscription = subscription
showSubscriptionsSheet = true
}, label: {
SubscriptionCell(subscription: subscription)
})
if viewModel.feeds.count <= 0, !viewModel.isLoading {
VStack(alignment: .center, spacing: 20) {
Text("You don't have any Feed items.")
.font(Font.system(size: 18, weight: .bold))
Text("Add an RSS/Atom feed")
.foregroundColor(Color.blue)
.onTapGesture {
showAddFeedView = true
}
}
.frame(minHeight: 80)
.frame(maxWidth: .infinity)
.padding()
} else {
ForEach(viewModel.feeds, id: \.subscriptionID) { subscription in
Button(action: {
viewModel.presentingSubscription = subscription
showSubscriptionsSheet = true
}, label: {
SubscriptionCell(subscription: subscription)
})
}
}
}
if viewModel.newsletters.count > 0, !viewModel.isLoading {
Section("Newsletters") {
ForEach(viewModel.newsletters, id: \.subscriptionID) { subscription in
Button(action: {
viewModel.presentingSubscription = subscription
showSubscriptionsSheet = true
}, label: {
SubscriptionCell(subscription: subscription)
})
}
}
}
// Section("Newsletters") {
// ForEach(viewModel.newsletters, id: \.subscriptionID) { subscription in
// SubscriptionCell(subscription: subscription)
// .swipeActions(edge: .trailing) {
// Button(
// role: .destructive,
// action: {
// deleteConfirmationShown = true
// viewModel.subscriptionNameToCancel = subscription.name
// },
// label: {
// Image(systemName: "trash")
// }
// )
// }
// .onTapGesture {
// expandedSubscription = subscription
// showSubscriptionsSheet = true
// }
// }
// }
}
.navigationTitle(LocalText.subscriptionsGeneric)
}

View File

@ -7,6 +7,8 @@ public struct Subscription {
public let subscriptionID: String
public let name: String
public let type: SubscriptionType
public let folder: String
public let fetchContent: Bool
public let newsletterEmailAddress: String?
public let status: SubscriptionStatus
public let unsubscribeHttpUrl: String?
@ -21,6 +23,8 @@ public struct Subscription {
subscriptionID: String,
name: String,
type: SubscriptionType,
folder: String,
fetchContent: Bool,
newsletterEmailAddress: String?,
status: SubscriptionStatus,
unsubscribeHttpUrl: String?,
@ -34,6 +38,8 @@ public struct Subscription {
self.subscriptionID = subscriptionID
self.name = name
self.type = type
self.folder = folder
self.fetchContent = fetchContent
self.newsletterEmailAddress = newsletterEmailAddress
self.status = status
self.unsubscribeHttpUrl = unsubscribeHttpUrl

View File

@ -4,7 +4,7 @@ import Models
import SwiftGraphQL
public extension DataService {
func deleteSubscription(subscriptionName: String) async throws {
func deleteSubscription(subscriptionName: String, subscriptionId: String) async throws {
enum MutationResult {
case success(id: String)
case error(errorMessage: String)
@ -20,7 +20,7 @@ public extension DataService {
}
let mutation = Selection.Mutation {
try $0.unsubscribe(name: subscriptionName, selection: selection)
try $0.unsubscribe(name: subscriptionName, subscriptionId: OptionalArgument(subscriptionId), selection: selection)
}
let path = appEnvironment.graphqlPath

View File

@ -10,6 +10,8 @@ let subscriptionSelection = Selection.Subscription {
subscriptionID: try $0.id(),
name: try $0.name(),
type: try SubscriptionType.from($0.type()),
folder: try $0.folder() ?? "inbox",
fetchContent: try $0.fetchContent() ?? true,
newsletterEmailAddress: try $0.newsletterEmail(),
status: try SubscriptionStatus.make(from: $0.status()),
unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(),

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFE"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2A",
"green" : "0x2A",
"red" : "0x2A"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}