From 85caaa7ea36aa9ccf96ecd43b40587c05824bb06 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 5 Feb 2024 14:02:31 +0800 Subject: [PATCH] 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. --- .../AudioPlayer/ExpandedAudioPlayer.swift | 8 ++-- .../LibraryItemListNavigationLink.swift | 26 +++------- .../App/Views/Home/HomeFeedViewIOS.swift | 19 ++++++++ .../App/Views/Home/HomeFeedViewModel.swift | 11 ++++- .../Sources/App/Views/LibraryTabView.swift | 12 ----- .../App/Views/LinkItemDetailView.swift | 2 +- .../App/Views/PrimaryContentView.swift | 48 ++++++++++++++++++- .../Views/WebReader/WebReaderContainer.swift | 46 ++++++++---------- 8 files changed, 106 insertions(+), 66 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift index 51b7de10e..89035da24 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift @@ -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() diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift index 02b4514a0..4f987d7d1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift @@ -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() - } - ) - } + }) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index ed39d891d..93865e28c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -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, diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 6f458a251..513274b58 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -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)") } diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index aa86d5ad3..40cdb5f48 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 4b6fb28c5..96891e0cb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -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) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift b/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift index 95ff33563..634553453 100644 --- a/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift @@ -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) + } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index bfc17dca4..baff59d8d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -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)