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)