Use transmission for snackbar, fix issue with ownership of currently viewed item

The list object of the root library view had ownership of the
currently selected item, so object modifications that removed
the item from the current library list (like move or archive)
could cause the object to be released and the current screen
to continue operating on an invalid object.
This commit is contained in:
Jackson Harper
2024-02-05 14:02:31 +08:00
parent 03766734e5
commit 85caaa7ea3
8 changed files with 106 additions and 66 deletions

View File

@ -24,9 +24,9 @@
@State var showLabelsModal = false
@State var showNotebookView = false
@State var showOperationToast = false
@State var showSnackbar = false
@State var operationStatus: OperationStatus = .none
@State var operationMessage: String?
@State var snackbarMessage: String?
var playPauseButtonImage: String {
switch audioController.state {
@ -384,8 +384,8 @@
func playerContent(_: LinkedItemAudioProperties) -> some View {
ZStack {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showOperationToast) {
OperationToast(operationMessage: $operationMessage, showOperationToast: $showOperationToast, operationStatus: $operationStatus)
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showSnackbar) {
OperationToast(operationMessage: $snackbarMessage, showOperationToast: $showSnackbar, operationStatus: $operationStatus)
.offset(y: -90)
} label: {
EmptyView()

View File

@ -29,29 +29,15 @@ struct LibraryItemListNavigationLink: View {
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@ObservedObject var item: Models.LibraryItem
@ObservedObject var viewModel: HomeFeedViewModel
let item: Models.LibraryItem
let viewModel: HomeFeedViewModel
var body: some View {
ZStack {
Button(action: {
viewModel.presentItem(item: item)
}, label: {
LibraryItemCard(item: LibraryItemData.make(from: item), viewer: dataService.currentViewer)
PresentationLink(
transition: PresentationLinkTransition.slide(
options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing,
options:
PresentationLinkTransition.Options(
modalPresentationCapturesStatusBarAppearance: true
))),
destination: {
LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
)
}, label: {
EmptyView()
}
)
}
})
}
}

View File

@ -461,6 +461,15 @@ struct AnimatingCellHeight: AnimatableModifier {
@ObservedObject var viewModel: HomeFeedViewModel
let showFeatureCards: Bool
var slideTransition: PresentationLinkTransition {
PresentationLinkTransition.slide(
options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing,
options:
PresentationLinkTransition.Options(
modalPresentationCapturesStatusBarAppearance: true
)
))
}
var body: some View {
VStack(spacing: 0) {
@ -481,6 +490,16 @@ struct AnimatingCellHeight: AnimatableModifier {
}
)
}
PresentationLink(transition: slideTransition, isPresented: $viewModel.linkIsActive) {
if let presentingItem = viewModel.selectedItem {
WebReaderContainerView(item: presentingItem)
} else {
EmptyView()
}
} label: {
EmptyView()
}.buttonStyle(.plain)
if prefersListLayout || !enableGrid {
HomeFeedListView(
listTitle: $listTitle,

View File

@ -13,7 +13,7 @@ enum LoadingBarStyle {
@MainActor final class HomeFeedViewModel: NSObject, ObservableObject {
let filterKey: String
@ObservedObject var fetcher: LibraryItemFetcher
@Published var fetcher: LibraryItemFetcher
let folderConfigs: [String: LibraryListConfig]
@Published var isLoading = false
@ -67,6 +67,13 @@ enum LoadingBarStyle {
super.init()
}
func presentItem(item: Models.LibraryItem) {
withAnimation {
self.selectedItem = item
self.linkIsActive = true
}
}
private var filterState: FetcherFilterState? {
if let appliedFilter = appliedFilter {
return FetcherFilterState(
@ -307,7 +314,7 @@ enum LoadingBarStyle {
Task {
do {
try await dataService.moveItem(itemID: item.unwrappedID, folder: folder)
snackbar("Item moved")
snackbar("Moved to library")
} catch {
snackbar("Error moving item to \(folder)")
}

View File

@ -137,18 +137,6 @@ struct LibraryTabView: View {
)
}
.navigationBarHidden(true)
.onReceive(NSNotification.snackBarPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
showOperationToast = true
operationMessage = message
operationStatus = .isPerforming
if let dismissAfter = notification.userInfo?["dismissAfter"] as? Int {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(dismissAfter)) {
showOperationToast = false
}
}
}
}
.onReceive(NSNotification.performSyncPublisher) { _ in
Task {
await syncManager.syncUpdates(dataService: dataService)

View File

@ -71,7 +71,7 @@ struct LinkItemDetailView: View {
}
.navigationViewStyle(.stack)
} else if let item = viewModel.item {
WebReaderContainerView(item: item, pop: { dismiss() })
WebReaderContainerView(item: item)
.background(ThemeManager.currentBgColor)
}
}

View File

@ -2,12 +2,39 @@ import Models
import Services
import SwiftUI
import Views
import Transmission
@MainActor public struct PrimaryContentView: View {
@State var searchTerm: String = ""
@State var showSnackbar = false
@State var snackbarMessage: String?
@State var snackbarUndoAction: (() -> Void)?
@State private var snackbarTimer: Timer?
public var body: some View {
innerBody
ZStack {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showSnackbar) {
InformationalSnackbar(message: snackbarMessage, undoAction: snackbarUndoAction)
} label: {
EmptyView()
}.buttonStyle(.plain)
innerBody
}
.onReceive(NSNotification.snackBarPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
snackbarUndoAction = notification.userInfo?["undoAction"] as? (() -> Void)
snackbarMessage = message
showSnackbar = true
let dismissAfter = notification.userInfo?["dismissAfter"] as? Int ?? 2000
if snackbarTimer == nil {
startTimer(amount: dismissAfter)
} else {
increaseTimeout(amount: dismissAfter)
}
}
}
}
public var innerBody: some View {
@ -25,4 +52,21 @@ import Views
return AnyView(splitView)
#endif
}
func startTimer(amount: Int) {
self.snackbarTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(amount / 1000), repeats: false) { _ in
DispatchQueue.main.async {
self.showSnackbar = false
}
}
}
func stopTimer() {
snackbarTimer?.invalidate()
}
func increaseTimeout(amount: Int) {
stopTimer()
startTimer(amount: amount)
}
}

View File

@ -9,8 +9,8 @@ import WebKit
// swiftlint:disable file_length type_body_length
struct WebReaderContainerView: View {
let item: Models.LibraryItem
let pop: () -> Void
@State var item: Models.LibraryItem
@Environment(\.dismiss) private var dismiss
@State private var showPreferencesPopover = false
@State private var showPreferencesFormsheet = false
@ -44,7 +44,6 @@ struct WebReaderContainerView: View {
@EnvironmentObject var audioController: AudioController
@Environment(\.openURL) var openURL
@StateObject var viewModel = WebReaderViewModel()
@Environment(\.dismiss) var dismiss
@AppStorage(UserDefaultKey.prefersHideStatusBarInReader.rawValue) var prefersHideStatusBarInReader = false
@ -252,7 +251,7 @@ struct WebReaderContainerView: View {
#if os(iOS)
Button(
action: {
pop()
dismiss()
},
label: {
Image.chevronRight
@ -492,13 +491,6 @@ struct WebReaderContainerView: View {
}
}
}
.sheet(isPresented: $showLabelsModal) {
ApplyLabelsView(mode: .item(item), onSave: { labels in
showLabelsModal = false
item.labels = NSSet(array: labels)
readerSettingsChangedTransactionID = UUID()
})
}
.sheet(isPresented: $showTitleEdit) {
LinkedItemMetadataEditView(item: item, onSave: { title, _ in
item.title = title
@ -506,6 +498,13 @@ struct WebReaderContainerView: View {
readerSettingsChangedTransactionID = UUID()
})
}
.sheet(isPresented: $showLabelsModal) {
ApplyLabelsView(mode: .item(item), onSave: { labels in
showLabelsModal = false
item.labels = NSSet(array: labels)
readerSettingsChangedTransactionID = UUID()
})
}
#if os(iOS)
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
NotebookView(
@ -616,26 +615,18 @@ struct WebReaderContainerView: View {
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
pop()
dismiss()
}
.ignoresSafeArea(.all, edges: .bottom)
}
func moveToInbox() {
Task {
viewModel.showOperationToast = true
viewModel.operationMessage = "Moving to library..."
viewModel.operationStatus = .isPerforming
do {
try await dataService.moveItem(itemID: item.unwrappedID, folder: "inbox")
viewModel.operationMessage = "Moved to library"
viewModel.operationStatus = .success
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
viewModel.showOperationToast = false
}
Snackbar.show(message: "Moved to library", dismissAfter: 2000)
} catch {
viewModel.operationMessage = "Error moving"
viewModel.operationStatus = .failure
Snackbar.show(message: "Error moving item to inbox", dismissAfter: 2000)
}
}
}
@ -644,8 +635,12 @@ struct WebReaderContainerView: View {
let isArchived = item.isArchived
dataService.archiveLink(objectID: item.objectID, archived: !isArchived)
#if os(iOS)
pop()
Snackbar.show(message: isArchived ? "Unarchived" : "Archived", dismissAfter: 2000)
dismiss()
Snackbar.show(message: isArchived ? "Unarchived" : "Archived", undoAction: {
dataService.archiveLink(objectID: item.objectID, archived: isArchived)
Snackbar.show(message: isArchived ? "Archived" : "Unarchived", dismissAfter: 2000)
}, dismissAfter: 2000)
#endif
}
@ -673,7 +668,8 @@ struct WebReaderContainerView: View {
}
func delete() {
pop()
dismiss()
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
removeLibraryItemAction(dataService: dataService, objectID: item.objectID)