Start using toast operation handler
This commit is contained in:
@ -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.",
|
||||
|
||||
@ -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() }
|
||||
)
|
||||
}
|
||||
|
||||
@ -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")")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user