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 showLabelsModal = false
|
||||||
@State var showNotebookView = false
|
@State var showNotebookView = false
|
||||||
|
|
||||||
@State var showOperationToast = false
|
@State var showSnackbar = false
|
||||||
@State var operationStatus: OperationStatus = .none
|
@State var operationStatus: OperationStatus = .none
|
||||||
@State var operationMessage: String?
|
@State var snackbarMessage: String?
|
||||||
|
|
||||||
var playPauseButtonImage: String {
|
var playPauseButtonImage: String {
|
||||||
switch audioController.state {
|
switch audioController.state {
|
||||||
@ -384,8 +384,8 @@
|
|||||||
|
|
||||||
func playerContent(_: LinkedItemAudioProperties) -> some View {
|
func playerContent(_: LinkedItemAudioProperties) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showOperationToast) {
|
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showSnackbar) {
|
||||||
OperationToast(operationMessage: $operationMessage, showOperationToast: $showOperationToast, operationStatus: $operationStatus)
|
OperationToast(operationMessage: $snackbarMessage, showOperationToast: $showSnackbar, operationStatus: $operationStatus)
|
||||||
.offset(y: -90)
|
.offset(y: -90)
|
||||||
} label: {
|
} label: {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
|||||||
@ -29,29 +29,15 @@ struct LibraryItemListNavigationLink: View {
|
|||||||
@EnvironmentObject var dataService: DataService
|
@EnvironmentObject var dataService: DataService
|
||||||
@EnvironmentObject var audioController: AudioController
|
@EnvironmentObject var audioController: AudioController
|
||||||
|
|
||||||
@ObservedObject var item: Models.LibraryItem
|
let item: Models.LibraryItem
|
||||||
@ObservedObject var viewModel: HomeFeedViewModel
|
let viewModel: HomeFeedViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
Button(action: {
|
||||||
|
viewModel.presentItem(item: item)
|
||||||
|
}, label: {
|
||||||
LibraryItemCard(item: LibraryItemData.make(from: item), viewer: dataService.currentViewer)
|
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
|
@ObservedObject var viewModel: HomeFeedViewModel
|
||||||
|
|
||||||
let showFeatureCards: Bool
|
let showFeatureCards: Bool
|
||||||
|
var slideTransition: PresentationLinkTransition {
|
||||||
|
PresentationLinkTransition.slide(
|
||||||
|
options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing,
|
||||||
|
options:
|
||||||
|
PresentationLinkTransition.Options(
|
||||||
|
modalPresentationCapturesStatusBarAppearance: true
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
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 {
|
if prefersListLayout || !enableGrid {
|
||||||
HomeFeedListView(
|
HomeFeedListView(
|
||||||
listTitle: $listTitle,
|
listTitle: $listTitle,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ enum LoadingBarStyle {
|
|||||||
|
|
||||||
@MainActor final class HomeFeedViewModel: NSObject, ObservableObject {
|
@MainActor final class HomeFeedViewModel: NSObject, ObservableObject {
|
||||||
let filterKey: String
|
let filterKey: String
|
||||||
@ObservedObject var fetcher: LibraryItemFetcher
|
@Published var fetcher: LibraryItemFetcher
|
||||||
let folderConfigs: [String: LibraryListConfig]
|
let folderConfigs: [String: LibraryListConfig]
|
||||||
|
|
||||||
@Published var isLoading = false
|
@Published var isLoading = false
|
||||||
@ -67,6 +67,13 @@ enum LoadingBarStyle {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentItem(item: Models.LibraryItem) {
|
||||||
|
withAnimation {
|
||||||
|
self.selectedItem = item
|
||||||
|
self.linkIsActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var filterState: FetcherFilterState? {
|
private var filterState: FetcherFilterState? {
|
||||||
if let appliedFilter = appliedFilter {
|
if let appliedFilter = appliedFilter {
|
||||||
return FetcherFilterState(
|
return FetcherFilterState(
|
||||||
@ -307,7 +314,7 @@ enum LoadingBarStyle {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await dataService.moveItem(itemID: item.unwrappedID, folder: folder)
|
try await dataService.moveItem(itemID: item.unwrappedID, folder: folder)
|
||||||
snackbar("Item moved")
|
snackbar("Moved to library")
|
||||||
} catch {
|
} catch {
|
||||||
snackbar("Error moving item to \(folder)")
|
snackbar("Error moving item to \(folder)")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,18 +137,6 @@ struct LibraryTabView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.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
|
.onReceive(NSNotification.performSyncPublisher) { _ in
|
||||||
Task {
|
Task {
|
||||||
await syncManager.syncUpdates(dataService: dataService)
|
await syncManager.syncUpdates(dataService: dataService)
|
||||||
|
|||||||
@ -71,7 +71,7 @@ struct LinkItemDetailView: View {
|
|||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
} else if let item = viewModel.item {
|
} else if let item = viewModel.item {
|
||||||
WebReaderContainerView(item: item, pop: { dismiss() })
|
WebReaderContainerView(item: item)
|
||||||
.background(ThemeManager.currentBgColor)
|
.background(ThemeManager.currentBgColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,39 @@ import Models
|
|||||||
import Services
|
import Services
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Views
|
import Views
|
||||||
|
import Transmission
|
||||||
|
|
||||||
@MainActor public struct PrimaryContentView: View {
|
@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 {
|
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 {
|
public var innerBody: some View {
|
||||||
@ -25,4 +52,21 @@ import Views
|
|||||||
return AnyView(splitView)
|
return AnyView(splitView)
|
||||||
#endif
|
#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
|
// swiftlint:disable file_length type_body_length
|
||||||
struct WebReaderContainerView: View {
|
struct WebReaderContainerView: View {
|
||||||
let item: Models.LibraryItem
|
@State var item: Models.LibraryItem
|
||||||
let pop: () -> Void
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State private var showPreferencesPopover = false
|
@State private var showPreferencesPopover = false
|
||||||
@State private var showPreferencesFormsheet = false
|
@State private var showPreferencesFormsheet = false
|
||||||
@ -44,7 +44,6 @@ struct WebReaderContainerView: View {
|
|||||||
@EnvironmentObject var audioController: AudioController
|
@EnvironmentObject var audioController: AudioController
|
||||||
@Environment(\.openURL) var openURL
|
@Environment(\.openURL) var openURL
|
||||||
@StateObject var viewModel = WebReaderViewModel()
|
@StateObject var viewModel = WebReaderViewModel()
|
||||||
@Environment(\.dismiss) var dismiss
|
|
||||||
|
|
||||||
@AppStorage(UserDefaultKey.prefersHideStatusBarInReader.rawValue) var prefersHideStatusBarInReader = false
|
@AppStorage(UserDefaultKey.prefersHideStatusBarInReader.rawValue) var prefersHideStatusBarInReader = false
|
||||||
|
|
||||||
@ -252,7 +251,7 @@ struct WebReaderContainerView: View {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
Button(
|
Button(
|
||||||
action: {
|
action: {
|
||||||
pop()
|
dismiss()
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
Image.chevronRight
|
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) {
|
.sheet(isPresented: $showTitleEdit) {
|
||||||
LinkedItemMetadataEditView(item: item, onSave: { title, _ in
|
LinkedItemMetadataEditView(item: item, onSave: { title, _ in
|
||||||
item.title = title
|
item.title = title
|
||||||
@ -506,6 +498,13 @@ struct WebReaderContainerView: View {
|
|||||||
readerSettingsChangedTransactionID = UUID()
|
readerSettingsChangedTransactionID = UUID()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showLabelsModal) {
|
||||||
|
ApplyLabelsView(mode: .item(item), onSave: { labels in
|
||||||
|
showLabelsModal = false
|
||||||
|
item.labels = NSSet(array: labels)
|
||||||
|
readerSettingsChangedTransactionID = UUID()
|
||||||
|
})
|
||||||
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
|
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
|
||||||
NotebookView(
|
NotebookView(
|
||||||
@ -616,26 +615,18 @@ struct WebReaderContainerView: View {
|
|||||||
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
|
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
|
||||||
pop()
|
dismiss()
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(.all, edges: .bottom)
|
.ignoresSafeArea(.all, edges: .bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveToInbox() {
|
func moveToInbox() {
|
||||||
Task {
|
Task {
|
||||||
viewModel.showOperationToast = true
|
|
||||||
viewModel.operationMessage = "Moving to library..."
|
|
||||||
viewModel.operationStatus = .isPerforming
|
|
||||||
do {
|
do {
|
||||||
try await dataService.moveItem(itemID: item.unwrappedID, folder: "inbox")
|
try await dataService.moveItem(itemID: item.unwrappedID, folder: "inbox")
|
||||||
viewModel.operationMessage = "Moved to library"
|
Snackbar.show(message: "Moved to library", dismissAfter: 2000)
|
||||||
viewModel.operationStatus = .success
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
|
|
||||||
viewModel.showOperationToast = false
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
viewModel.operationMessage = "Error moving"
|
Snackbar.show(message: "Error moving item to inbox", dismissAfter: 2000)
|
||||||
viewModel.operationStatus = .failure
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -644,8 +635,12 @@ struct WebReaderContainerView: View {
|
|||||||
let isArchived = item.isArchived
|
let isArchived = item.isArchived
|
||||||
dataService.archiveLink(objectID: item.objectID, archived: !isArchived)
|
dataService.archiveLink(objectID: item.objectID, archived: !isArchived)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
pop()
|
dismiss()
|
||||||
Snackbar.show(message: isArchived ? "Unarchived" : "Archived", dismissAfter: 2000)
|
|
||||||
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -673,7 +668,8 @@ struct WebReaderContainerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func delete() {
|
func delete() {
|
||||||
pop()
|
dismiss()
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||||
removeLibraryItemAction(dataService: dataService, objectID: item.objectID)
|
removeLibraryItemAction(dataService: dataService, objectID: item.objectID)
|
||||||
|
|||||||
Reference in New Issue
Block a user