Better unsubscribe view
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
|
||||
@ -25,7 +26,7 @@ public struct LibrarySplitView: View {
|
||||
hasFeatureCards: false,
|
||||
hasReadNowSection: false,
|
||||
leadingSwipeActions: [.moveToInbox],
|
||||
trailingSwipeActions: [.archive, .delete],
|
||||
trailingSwipeActions: [.delete],
|
||||
cardStyle: .library
|
||||
)
|
||||
)
|
||||
@ -49,6 +50,27 @@ public struct LibrarySplitView: View {
|
||||
$0.preferredPrimaryColumnWidth = 230
|
||||
$0.displayModeButtonVisibility = .always
|
||||
}
|
||||
.onOpenURL { url in
|
||||
inboxViewModel.linkRequest = nil
|
||||
if let deepLink = DeepLink.make(from: url) {
|
||||
switch deepLink {
|
||||
case let .search(query):
|
||||
inboxViewModel.searchTerm = query
|
||||
case let .savedSearch(named):
|
||||
if let filter = inboxViewModel.findFilter(dataService, named: named) {
|
||||
inboxViewModel.appliedFilter = filter
|
||||
}
|
||||
case let .webAppLinkRequest(requestID):
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
withoutAnimation {
|
||||
inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
|
||||
inboxViewModel.presentWebContainer = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// selectedTab = "inbox"
|
||||
}
|
||||
.onReceive(NSNotification.performSyncPublisher) { _ in
|
||||
Task {
|
||||
await syncManager.syncItems(dataService: dataService)
|
||||
|
||||
@ -29,7 +29,7 @@ struct LibraryTabView: View {
|
||||
UITabBar.appearance().isHidden = true
|
||||
}
|
||||
|
||||
@StateObject private var libraryViewModel = HomeFeedViewModel(
|
||||
@StateObject private var inboxViewModel = HomeFeedViewModel(
|
||||
folder: "inbox",
|
||||
fetcher: LibraryItemFetcher(),
|
||||
listConfig: LibraryListConfig(
|
||||
@ -48,7 +48,7 @@ struct LibraryTabView: View {
|
||||
hasFeatureCards: false,
|
||||
hasReadNowSection: false,
|
||||
leadingSwipeActions: [.moveToInbox],
|
||||
trailingSwipeActions: [.archive, .delete],
|
||||
trailingSwipeActions: [.delete],
|
||||
cardStyle: .library
|
||||
)
|
||||
)
|
||||
@ -67,7 +67,7 @@ struct LibraryTabView: View {
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: libraryViewModel)
|
||||
HomeFeedContainerView(viewModel: inboxViewModel)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(.stack)
|
||||
}.tag("inbox")
|
||||
@ -100,20 +100,20 @@ struct LibraryTabView: View {
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
libraryViewModel.linkRequest = nil
|
||||
inboxViewModel.linkRequest = nil
|
||||
if let deepLink = DeepLink.make(from: url) {
|
||||
switch deepLink {
|
||||
case let .search(query):
|
||||
libraryViewModel.searchTerm = query
|
||||
inboxViewModel.searchTerm = query
|
||||
case let .savedSearch(named):
|
||||
if let filter = libraryViewModel.findFilter(dataService, named: named) {
|
||||
libraryViewModel.appliedFilter = filter
|
||||
if let filter = inboxViewModel.findFilter(dataService, named: named) {
|
||||
inboxViewModel.appliedFilter = filter
|
||||
}
|
||||
case let .webAppLinkRequest(requestID):
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
withoutAnimation {
|
||||
libraryViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
|
||||
libraryViewModel.presentWebContainer = true
|
||||
inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
|
||||
inboxViewModel.presentWebContainer = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Transmission
|
||||
import Views
|
||||
|
||||
enum UnsubscribeState {
|
||||
case none
|
||||
case isUnsubscribing
|
||||
case unsubscribeSuccess
|
||||
case unsubscribeFailure
|
||||
}
|
||||
|
||||
@MainActor final class SubscriptionsViewModel: ObservableObject {
|
||||
@Published var isLoading = true
|
||||
@Published var feeds = [Subscription]()
|
||||
@Published var newsletters = [Subscription]()
|
||||
@Published var hasNetworkError = false
|
||||
@Published var subscriptionNameToCancel: String?
|
||||
@Published var presentingSubscription: Subscription?
|
||||
@Published var unsubscribeState: UnsubscribeState = .none
|
||||
|
||||
func loadSubscriptions(dataService: DataService) async {
|
||||
isLoading = true
|
||||
@ -24,20 +34,56 @@ import Views
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func cancelSubscription(dataService: DataService) async -> Bool {
|
||||
guard let subscriptionName = subscriptionNameToCancel else { return false }
|
||||
func cancelSubscription(dataService _: DataService, subscription: Subscription) async {
|
||||
unsubscribeState = .isUnsubscribing
|
||||
let subscriptionName = subscription.name
|
||||
|
||||
do {
|
||||
try await dataService.deleteSubscription(subscriptionName: subscriptionName)
|
||||
// do {
|
||||
// try await dataService.deleteSubscription(subscriptionName: subscriptionName)
|
||||
// let index = subscriptions.firstIndex { $0.name == subscriptionName }
|
||||
// if let index = index {
|
||||
// subscriptions.remove(at: index)
|
||||
// }
|
||||
return true
|
||||
} catch {
|
||||
appLogger.debug("failed to remove subscription")
|
||||
return false
|
||||
// unsubscribeState = .unsubscribeSuccess
|
||||
// } catch {
|
||||
// appLogger.debug("failed to remove subscription")
|
||||
// unsubscribeState = .unsubscribeFailure
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
struct UnsubscribeToast: 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom, 30)
|
||||
.padding(.horizontal, 15)
|
||||
.background(Color.systemBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,10 +91,18 @@ struct SubscriptionsView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@StateObject var viewModel = SubscriptionsViewModel()
|
||||
@State private var deleteConfirmationShown = false
|
||||
@State private var progressViewOpacity = 0.0
|
||||
@State private var showDeleteCompleted = 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)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else if viewModel.hasNetworkError {
|
||||
@ -79,6 +133,25 @@ struct SubscriptionsView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.formSheet(isPresented: $showSubscriptionsSheet) {
|
||||
if let presentingSubscription = viewModel.presentingSubscription {
|
||||
SubscriptionSettingsView(
|
||||
subscription: presentingSubscription,
|
||||
viewModel: viewModel,
|
||||
dataService: dataService,
|
||||
dismiss: { showSubscriptionsSheet = false },
|
||||
unsubscribe: { subscription in
|
||||
showSubscriptionsSheet = false
|
||||
|
||||
viewModel.unsubscribeState = .isUnsubscribing
|
||||
showUnsubscribeToast = true
|
||||
Task {
|
||||
await viewModel.cancelSubscription(dataService: dataService, subscription: subscription)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadSubscriptions(dataService: dataService)
|
||||
}
|
||||
@ -92,59 +165,72 @@ struct SubscriptionsView: View {
|
||||
Group {
|
||||
Section("Feeds") {
|
||||
ForEach(viewModel.feeds, id: \.subscriptionID) { subscription in
|
||||
SubscriptionCell(subscription: subscription)
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(
|
||||
role: .destructive,
|
||||
action: {
|
||||
deleteConfirmationShown = true
|
||||
viewModel.subscriptionNameToCancel = subscription.name
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
)
|
||||
}
|
||||
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")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) {
|
||||
Button("Yes", role: .destructive) {
|
||||
Task {
|
||||
let unsubscribed = await viewModel.cancelSubscription(dataService: dataService)
|
||||
// Snackbar.show(message: unsubscribed ? "Subscription cancelled." : "Could not unsubscribe.")
|
||||
}
|
||||
}
|
||||
Button("No", role: .cancel) {
|
||||
viewModel.subscriptionNameToCancel = nil
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
struct SubscriptionCell: View {
|
||||
struct SubscriptionRow<Content: View>: View {
|
||||
let subscription: Subscription
|
||||
let useImageSpacer: Bool
|
||||
|
||||
@ViewBuilder let trailingButton: Content
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Group {
|
||||
if let icon = subscription.icon, let imageURL = URL(string: icon) {
|
||||
AsyncImage(url: imageURL) { phase in
|
||||
if let image = phase.image {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 40, height: 40)
|
||||
.cornerRadius(6)
|
||||
} else if phase.error != nil {
|
||||
Color.clear.frame(width: 40, height: 40, alignment: .top)
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(width: 40, height: 40)
|
||||
.cornerRadius(2)
|
||||
}
|
||||
}
|
||||
} else if useImageSpacer {
|
||||
Color.clear
|
||||
.frame(width: 40, height: 40)
|
||||
.cornerRadius(2)
|
||||
}
|
||||
}.padding(.trailing, 10)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(subscription.name)
|
||||
.font(.appCallout)
|
||||
@ -164,25 +250,81 @@ struct SubscriptionCell: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Group {
|
||||
if let icon = subscription.icon, let imageURL = URL(string: icon) {
|
||||
AsyncImage(url: imageURL) { phase in
|
||||
if let image = phase.image {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 40, height: 40)
|
||||
.cornerRadius(6)
|
||||
} else if phase.error != nil {
|
||||
EmptyView().frame(width: 40, height: 40, alignment: .top)
|
||||
} else {
|
||||
Color.appButtonBackground
|
||||
.frame(width: 40, height: 40)
|
||||
.cornerRadius(2)
|
||||
}
|
||||
trailingButton
|
||||
}.frame(minHeight: 50)
|
||||
}
|
||||
}
|
||||
|
||||
struct SubscriptionCell: View {
|
||||
let subscription: Subscription
|
||||
|
||||
var body: some View {
|
||||
SubscriptionRow(subscription: subscription, useImageSpacer: true, trailingButton: {
|
||||
Image(systemName: "ellipsis")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct SubscriptionSettingsView: View {
|
||||
let subscription: Subscription
|
||||
let viewModel: SubscriptionsViewModel
|
||||
let dataService: DataService
|
||||
|
||||
@State var prefetchContent = false
|
||||
@State var deleteConfirmationShown = false
|
||||
@State var showDeleteCompleted = false
|
||||
|
||||
let dismiss: () -> Void
|
||||
let unsubscribe: (_: Subscription) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
SubscriptionRow(subscription: subscription, useImageSpacer: false, trailingButton: {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(Color.circleButtonBackground)
|
||||
.frame(width: 30, height: 30)
|
||||
|
||||
Image(systemName: "xmark")
|
||||
.resizable(resizingMode: Image.ResizingMode.stretch)
|
||||
.foregroundColor(Color.circleButtonForeground)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.font(Font.title.weight(.bold))
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
})
|
||||
})
|
||||
.padding(.top, 15)
|
||||
.padding(.horizontal, 15)
|
||||
|
||||
List {
|
||||
Toggle(isOn: $prefetchContent, label: { Text("Prefetch Content:").foregroundColor(.appGrayText) })
|
||||
HStack {
|
||||
Text("Destination Folder:")
|
||||
.foregroundColor(.appGrayText)
|
||||
Spacer()
|
||||
Menu(content: {}, label: {
|
||||
Text("Following <>")
|
||||
})
|
||||
}
|
||||
}.frame(minHeight: 50)
|
||||
}.listStyle(.insetGrouped)
|
||||
|
||||
Spacer()
|
||||
Button("Unsubscribe", role: .destructive) { deleteConfirmationShown = true }
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(RoundedRectButtonStyle(color: Color.red, textColor: Color.white))
|
||||
}
|
||||
.frame(minWidth: 200, minHeight: 200)
|
||||
.alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) {
|
||||
Button("Yes", role: .destructive) {
|
||||
unsubscribe(subscription)
|
||||
}
|
||||
Button("No", role: .cancel) {
|
||||
viewModel.subscriptionNameToCancel = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user