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:
@ -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()
|
||||
|
||||
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user