Merge pull request #3496 from omnivore-app/feat/ios-views-cleanup

iOS snackbar crash fixes
This commit is contained in:
Jackson Harper 2024-02-06 13:23:33 +08:00 committed by GitHub
commit 87928a517a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 27739 additions and 27251 deletions

File diff suppressed because one or more lines are too long

View file

@ -1389,7 +1389,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.43.0;
MARKETING_VERSION = 1.44.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
@ -1424,7 +1424,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.43.0;
MARKETING_VERSION = 1.44.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1479,7 +1479,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.43.0;
MARKETING_VERSION = 1.44.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1820,7 +1820,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.43.0;
MARKETING_VERSION = 1.44.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -144,15 +144,6 @@
"version" : "2.30908.0"
}
},
{
"identity" : "popupview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/exyte/PopupView.git",
"state" : {
"revision" : "68349a0ae704b9a7041f756f3f4f460ddbf7ba8d",
"version" : "2.6.0"
}
},
{
"identity" : "posthog-ios",
"kind" : "remoteSourceControl",

View file

@ -27,7 +27,6 @@ let package = Package(
"Models",
.product(name: "Introspect", package: "SwiftUI-Introspect"),
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
.productItem(name: "PopupView", package: "PopupView"),
.product(name: "Transmission", package: "Transmission")
],
resources: [.process("Resources")]
@ -40,8 +39,7 @@ let package = Package(
"Valet",
.product(name: "SwiftGraphQL", package: "swift-graphql"),
"Models",
"Utils",
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms")
"Utils"
]
),
.testTarget(name: "ServicesTests", dependencies: ["Services"]),
@ -72,7 +70,6 @@ var dependencies: [Package.Dependency] {
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"),
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"),
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"),
.package(url: "https://github.com/exyte/PopupView.git", from: "2.6.0"),
.package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0"),
.package(url: "https://github.com/nathantannar4/Transmission", from: "1.0.1"),
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0")

View file

@ -17,16 +17,5 @@ struct MiniShareExtensionView: View {
var body: some View {
ProgressView()
.popup(isPresented: $showToast) {
Text("Saving to Omnivore")
.padding(20)
} customize: {
$0
.type(.toast)
.position(.bottom)
.animation(.spring())
.closeOnTapOutside(true)
.backgroundColor(.black.opacity(0.5))
}
}
}

View file

@ -332,7 +332,7 @@ import Utils
if let customHighlight = annotation.customData?["omnivoreHighlight"] as? [String: String] {
if customHighlight["id"]?.lowercased() == highlightId {
if !document.remove(annotations: [annotation]) {
viewModel.snackbar(message: "Error removing highlight")
Snackbar.show(message: "Error removing highlight", dismissAfter: 2000)
}
}
}

View file

@ -8,9 +8,6 @@ final class PDFViewerViewModel: ObservableObject {
@Published var errorMessage: String?
@Published var readerView: Bool = false
@Published var showSnackbar: Bool = false
var snackbarMessage: String?
let pdfItem: PDFItem
var highlights: [Highlight]
@ -19,11 +16,6 @@ final class PDFViewerViewModel: ObservableObject {
self.highlights = pdfItem.highlights
}
func snackbar(message: String) {
snackbarMessage = message
showSnackbar = true
}
func findHighlight(dataService: DataService, highlightID: String) -> Highlight? {
let libraryItem = LibraryItem.lookup(byID: pdfItem.itemID, inContext: dataService.viewContext)
return libraryItem?.highlights.asArray(of: Highlight.self).first { $0.id == highlightID }

View file

@ -3,7 +3,7 @@ import Services
import Views
extension Snackbar {
static func showInLibrary(message: String, undoAction: (() -> Void)? = nil) {
NSNotification.librarySnackBar(message: message, undoAction: undoAction)
static func show(message: String, undoAction: (() -> Void)? = nil, dismissAfter: Int?) {
NSNotification.snackBar(message: message, undoAction: undoAction, dismissAfter: dismissAfter)
}
}

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

@ -35,7 +35,7 @@
pasteBoard.writeObjects([highlightParams.quote as NSString])
#endif
// Snackbar.show(message: "Highlight copied")
Snackbar.show(message: "Highlight copied", dismissAfter: 2000)
},
label: { Label("Copy", systemImage: "doc.on.doc") }
)

View file

@ -21,23 +21,11 @@ struct LibraryFeatureCardNavigationLink: View {
@State var showFeatureActions = false
var body: some View {
PresentationLink(
transition: PresentationLinkTransition.slide(
options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing,
options:
PresentationLinkTransition.Options(
modalPresentationCapturesStatusBarAppearance: true
))),
destination: {
LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
)
.background(ThemeManager.currentBgColor)
}, label: {
LibraryFeatureCard(item: item, viewer: dataService.currentViewer)
}
)
Button(action: {
viewModel.presentItem(item: item)
}, label: {
LibraryFeatureCard(item: item, viewer: dataService.currentViewer)
})
.buttonStyle(.plain)
.confirmationDialog("", isPresented: $showFeatureActions) {
if FeaturedItemFilter(rawValue: viewModel.fetcher.featureFilter) == .pinned {

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()
}
)
}
})
}
}
@ -65,22 +51,11 @@ struct LibraryItemGridCardNavigationLink: View {
@ObservedObject var viewModel: HomeFeedViewModel
var body: some View {
PresentationLink(
transition: PresentationLinkTransition.slide(
options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing,
options:
PresentationLinkTransition.Options(
modalPresentationCapturesStatusBarAppearance: true
))),
destination: {
LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
)
}, label: {
GridCard(item: LibraryItemData.make(from: item))
}
)
Button(action: {
viewModel.presentItem(item: item)
}, label: {
GridCard(item: LibraryItemData.make(from: item))
})
.buttonStyle(.plain)
.aspectRatio(1.0, contentMode: .fill)
.background(Color.systemBackground)

View file

@ -222,7 +222,7 @@ struct AnimatingCellHeight: AnimatableModifier {
}
var body: some View {
ZStack {
ZStack {
HomeFeedView(
listTitle: $listTitle,
isListScrolled: $isListScrolled,
@ -369,58 +369,74 @@ struct AnimatingCellHeight: AnimatableModifier {
}
ToolbarItemGroup(placement: .barTrailing) {
if isEditMode == .active {
Button(action: { isEditMode = .inactive }, label: { Text("Cancel") })
} else {
if prefersListLayout {
if viewModel.appliedFilter?.name == "Deleted" {
if viewModel.isEmptyingTrash {
ProgressView()
} else {
Button(
action: {
viewModel.emptyTrash(dataService: dataService)
},
label: {
Text("Empty trash").tint(Color.blue)
})
.buttonStyle(.plain)
.foregroundColor(Color.blue)
}
} else {
if isEditMode == .active {
Button(action: { isEditMode = .inactive }, label: { Text("Cancel") })
} else {
if prefersListLayout {
Button(
action: { isEditMode = isEditMode == .active ? .inactive : .active },
label: {
Image
.selectMultiple
.foregroundColor(Color.toolbarItemForeground)
}
).buttonStyle(.plain)
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
}
if enableGrid {
Button(
action: { prefersListLayout.toggle() },
label: {
Image(systemName: prefersListLayout ? "square.grid.2x2" : "list.bullet")
.foregroundColor(Color.toolbarItemForeground)
}
).buttonStyle(.plain)
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
}
Button(
action: { isEditMode = isEditMode == .active ? .inactive : .active },
action: {
if viewModel.currentFolder == "inbox" {
showAddLinkView = true
} else if viewModel.currentFolder == "following" {
viewModel.showAddFeedView = true
}
},
label: {
Image
.selectMultiple
Image.addLink
.foregroundColor(Color.toolbarItemForeground)
}
).buttonStyle(.plain)
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
Button(
action: {
searchPresented = true
isEditMode = .inactive
},
label: {
Image
.magnifyingGlass
.foregroundColor(Color.toolbarItemForeground)
}
).buttonStyle(.plain)
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
}
if enableGrid {
Button(
action: { prefersListLayout.toggle() },
label: {
Image(systemName: prefersListLayout ? "square.grid.2x2" : "list.bullet")
.foregroundColor(Color.toolbarItemForeground)
}
).buttonStyle(.plain)
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
}
Button(
action: {
if viewModel.currentFolder == "inbox" {
showAddLinkView = true
} else if viewModel.currentFolder == "following" {
viewModel.showAddFeedView = true
}
},
label: {
Image.addLink
.foregroundColor(Color.toolbarItemForeground)
}
).buttonStyle(.plain)
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
Button(
action: {
searchPresented = true
isEditMode = .inactive
},
label: {
Image
.magnifyingGlass
.foregroundColor(Color.toolbarItemForeground)
}
).buttonStyle(.plain)
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
}
}
@ -430,19 +446,17 @@ struct AnimatingCellHeight: AnimatableModifier {
viewModel.bulkAction(dataService: dataService, action: .delete, items: Array(selection))
isEditMode = .inactive
}, label: { Image.toolbarTrash })
.disabled(selection.count < 1)
.padding(.horizontal, UIDevice.isIPad ? 10 : 5)
.disabled(selection.count < 1)
.padding(.horizontal, UIDevice.isIPad ? 10 : 5)
Spacer()
Text("\(selection.count) selected").font(.footnote)
Spacer()
Button(action: {
viewModel.bulkAction(dataService: dataService, action: .archive, items: Array(selection))
isEditMode = .inactive
}, label: { Image.toolbarArchive })
.disabled(selection.count < 1)
.padding(.horizontal, UIDevice.isIPad ? 10 : 5)
.disabled(selection.count < 1)
.padding(.horizontal, UIDevice.isIPad ? 10 : 5)
}
}
}
@ -461,6 +475,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 +504,20 @@ struct AnimatingCellHeight: AnimatableModifier {
}
)
}
PresentationLink(transition: slideTransition, isPresented: $viewModel.linkIsActive) {
if let presentingItem = viewModel.selectedItem {
if presentingItem.isPDF {
PDFContainerView(item: presentingItem)
} else {
WebReaderContainerView(item: presentingItem)
}
} else {
EmptyView()
}
} label: {
EmptyView()
}.buttonStyle(.plain)
if prefersListLayout || !enableGrid {
HomeFeedListView(
listTitle: $listTitle,
@ -506,29 +543,6 @@ struct AnimatingCellHeight: AnimatableModifier {
viewModel.negatedLabels = $1
}
}
.popup(isPresented: $viewModel.showSnackbar) {
if let operation = viewModel.snackbarOperation {
Snackbar(isShowing: $viewModel.showSnackbar, operation: operation)
} else {
EmptyView()
}
} customize: {
$0
.type(.toast)
.autohideIn(2)
.position(.bottom)
.animation(.spring())
.isOpaque(false)
}
.onReceive(NSNotification.librarySnackBarPublisher) { notification in
if !viewModel.showSnackbar {
if let message = notification.userInfo?["message"] as? String {
viewModel.snackbarOperation = SnackbarOperation(message: message,
undoAction: notification.userInfo?["undoAction"] as? SnackbarUndoAction)
viewModel.showSnackbar = true
}
}
}
}
}
@ -809,6 +823,15 @@ struct AnimatingCellHeight: AnimatableModifier {
.frame(maxWidth: .infinity)
.padding()
.listRowSeparator(.hidden, edges: .all)
} else if viewModel.isEmptyingTrash {
VStack {
Text("Emptying trash")
ProgressView()
}
.frame(minHeight: 400)
.frame(maxWidth: .infinity)
.padding()
.listRowSeparator(.hidden, edges: .all)
} else if viewModel.fetcher.items.isEmpty {
EmptyState(viewModel: viewModel)
.listRowSeparator(.hidden, edges: .all)
@ -1142,6 +1165,8 @@ struct BottomView: View {
var innerBody: some View {
if viewModel.fetcher.items.count < 3 {
AnyView(Color.clear)
} else if viewModel.appliedFilter?.name == "Deleted" {
AnyView(Color.clear)
} else {
AnyView(HStack {
if let totalCount = viewModel.fetcher.totalCount {

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
@ -28,10 +28,9 @@ enum LoadingBarStyle {
@Published var linkIsActive = false
@Published var showLabelsSheet = false
@Published var showSnackbar = false
@Published var showAddFeedView = false
@Published var showHideFollowingAlert = false
@Published var snackbarOperation: SnackbarOperation?
@Published var filters = [InternalFilter]()
@ -68,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(
@ -235,8 +241,7 @@ enum LoadingBarStyle {
}
func snackbar(_ message: String, undoAction: SnackbarUndoAction? = nil) {
snackbarOperation = SnackbarOperation(message: message, undoAction: undoAction)
showSnackbar = true
Snackbar.show(message: message, undoAction: undoAction, dismissAfter: 2000)
}
func setLinkArchived(dataService: DataService, objectID: NSManagedObjectID, archived: Bool) {
@ -309,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)")
}
@ -372,4 +377,18 @@ enum LoadingBarStyle {
fetcher.updateFeatureFilter(context: context, filter: filter)
}
}
@Published var isEmptyingTrash = false
func emptyTrash(dataService: DataService) {
self.isEmptyingTrash = true
Task {
if !(await dataService.emptyTrash()) {
snackbar("Error emptying trash")
} else {
snackbar("Trash emptied")
}
isEmptyingTrash = false
}
}
}

View file

@ -3,6 +3,7 @@ import Models
import Services
import SwiftUI
import Utils
import Views
@MainActor
public class LibraryAddFeedViewModel: NSObject, ObservableObject {
@ -101,9 +102,9 @@ public class LibraryAddFeedViewModel: NSObject, ObservableObject {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) {
if failureCount > 0 {
showInLibrarySnackbar("Failed to add \(failureCount) feeds")
Snackbar.show(message: "Failed to add \(failureCount) feeds", dismissAfter: 3000)
} else {
showInLibrarySnackbar("Added \(successCount) feed\(successCount == 0 ? "" : "s")")
Snackbar.show(message: "Added \(successCount) feed\(successCount == 0 ? "" : "s")", dismissAfter: 3000)
}
}
}
@ -211,7 +212,7 @@ public struct LibraryScanFeedView: View {
if viewModel.selected.count > 0 {
Button(action: {
dismiss()
showInLibrarySnackbar("Adding feeds...")
Snackbar.show(message: "Adding feeds...", dismissAfter: 2000)
Task {
await viewModel.addFeeds()
}

View file

@ -0,0 +1,39 @@
import SwiftUI
import Views
struct InformationalSnackbar: View {
let message: String?
let undoAction: (() -> Void)?
var body: some View {
VStack {
HStack {
if let message = message {
Text(message)
}
Spacer()
if let undoAction = self.undoAction {
Button(action: {
undoAction()
}, label: {
Text("Undo")
.bold()
.foregroundColor(.blue)
})
.padding(.trailing, 2)
}
}
.padding(10)
.frame(height: 50)
.frame(maxWidth: 380)
.background(Color(hex: "2A2A2A"))
.foregroundColor(Color(hex: "EBEBEB"))
.cornerRadius(4.0)
}
.padding(.bottom, 60)
.padding(.horizontal, 10)
.ignoresSafeArea(.all, edges: .bottom)
}
}

View file

@ -7,7 +7,6 @@
import Foundation
import Models
import PopupView
import Services
import SwiftUI
import Transmission
@ -71,8 +70,20 @@ struct LibraryTabView: View {
}
}
@State var showOperationToast = false
@State var operationStatus: OperationStatus = .none
@State var operationMessage: String?
var body: some View {
VStack(spacing: 0) {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showOperationToast) {
OperationToast(operationMessage: $operationMessage,
showOperationToast: $showOperationToast,
operationStatus: $operationStatus)
} label: {
EmptyView()
}.buttonStyle(.plain)
TabView(selection: $selectedTab) {
if !hideFollowingTab {
NavigationView {

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

@ -0,0 +1,66 @@
import Combine
import Models
import SwiftUI
import Utils
import Services
@MainActor final class PDFContainerViewModel: ObservableObject {
func trackReadEvent(item: Models.LibraryItem, reader: String) {
let itemID = item.unwrappedID
let slug = item.unwrappedSlug
let originalArticleURL = item.unwrappedPageURLString
EventTracker.track(
.linkRead(
linkID: itemID,
slug: slug,
reader: reader,
originalArticleURL: originalArticleURL
)
)
}
}
struct PDFContainerView: View {
let item: Models.LibraryItem
let pdfItem: PDFItem?
@EnvironmentObject var dataService: DataService
@StateObject private var viewModel = PDFContainerViewModel()
init(item: Models.LibraryItem) {
self.item = item
self.pdfItem = PDFItem.make(item: item)
}
var body: some View {
NavigationView {
pdfContainerView
.navigationBarBackButtonHidden(false)
}
.navigationViewStyle(.stack)
.ignoresSafeArea(.all, edges: .bottom)
.onAppear {
viewModel.trackReadEvent(item: item, reader: "PDF")
}
}
@ViewBuilder private var pdfContainerView: some View {
if let pdfItem = pdfItem, let pdfURL = pdfItem.pdfURL {
#if os(iOS)
PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem))
.navigationBarTitleDisplayMode(.inline)
#elseif os(macOS)
PDFWrapperView(pdfURL: pdfURL)
#endif
} else {
HStack(alignment: .center) {
Spacer()
Text("Loading")
Spacer()
}
}
}
}

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

@ -1,5 +1,4 @@
import Models
import PopupView
import Services
import SwiftUI
import Transmission
@ -7,7 +6,6 @@ import Views
@MainActor final class NewsletterEmailsViewModel: ObservableObject {
@Published var isLoading = false
@Published var showAddressCopied = false
@Published var emails = [NewsletterEmail]()
@Published var showOperationToast = false
@ -78,12 +76,6 @@ struct NewsletterEmailsView: View {
EmptyView()
}.buttonStyle(.plain)
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showAddressCopied) {
MessageToast()
} label: {
EmptyView()
}.buttonStyle(.plain)
#if os(iOS)
Form {
innerBody
@ -162,10 +154,7 @@ struct NewsletterEmailRow: View {
pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString])
#endif
viewModel.showAddressCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) {
viewModel.showAddressCopied = false
}
Snackbar.show(message: "Address copied", undoAction: nil, dismissAfter: 2000)
},
label: {
Text("Copy")
@ -191,22 +180,3 @@ struct NewsletterEmailRow: View {
}
}
struct MessageToast: View {
var body: some View {
VStack {
HStack {
Text("Address copied")
Spacer()
}
.padding(10)
.frame(minHeight: 50)
.frame(maxWidth: 380)
.background(Color(hex: "2A2A2A"))
.cornerRadius(4.0)
.tint(Color.green)
}
.padding(.bottom, 70)
.padding(.horizontal, 10)
.ignoresSafeArea(.all, edges: .bottom)
}
}

View file

@ -29,7 +29,7 @@
do {
try await dataService.leaveGroup(groupID: recommendationGroup.id)
// Snackbar.show(message: "You have left the club.")
Snackbar.show(message: "You have left the club.", dismissAfter: 2000)
} catch {
return false
}
@ -182,7 +182,7 @@
pasteBoard.writeObjects([highlightParams.quote as NSString])
#endif
// Snackbar.show(message: "Invite link copied")
Snackbar.show(message: "Invite link copied", dismissAfter: 2000)
}, label: {
Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
.font(.appCaption)

View file

@ -33,7 +33,7 @@ func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObject
print("checking if task is canceled: ", Task.isCancelled)
}
Snackbar.showInLibrary(message: "Item removed", undoAction: {
Snackbar.show(message: "Item removed", undoAction: {
print("canceling task", syncTask)
syncTask.cancel()
dataService.viewContext.performAndWait {
@ -42,5 +42,5 @@ func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObject
try? dataService.viewContext.save()
}
}
})
}, dismissAfter: 2000)
}

View file

@ -95,7 +95,7 @@
return AnyView(Button(action: {
Task {
if await viewModel.recommend(dataService: dataService) {
// Snackbar.show(message: "Recommendation sent")
Snackbar.show(message: "Recommendation sent", dismissAfter: 2000)
dismiss()
}
}

View file

@ -20,6 +20,7 @@ struct WebReader: PlatformViewRepresentable {
@Binding var showNavBarActionID: UUID?
@Binding var shareActionID: UUID?
@Binding var annotation: String
@Binding var showBottomBar: Bool
@Binding var showHighlightAnnotationModal: Bool
func makeCoordinator() -> WebReaderCoordinator {
@ -90,6 +91,9 @@ struct WebReader: PlatformViewRepresentable {
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibility = navBarVisibilityUpdater
context.coordinator.scrollPercentHandler = scrollPercentHandler
context.coordinator.updateShowBottomBar = { newValue in
self.showBottomBar = newValue
}
context.coordinator.articleContentID = articleContent.id
loadContent(webView: webView)
@ -103,7 +107,7 @@ struct WebReader: PlatformViewRepresentable {
do {
try (webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
} catch {
showInLibrarySnackbar("Error saving note.")
Snackbar.show(message: "Error saving note.", dismissAfter: 2000)
}
}

View file

@ -1,6 +1,5 @@
import AVFoundation
import Models
import PopupView
import Services
import SwiftUI
import Transmission
@ -10,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
@ -22,6 +21,7 @@ struct WebReaderContainerView: View {
@State private var hasPerformedHighlightMutations = false
@State var showHighlightAnnotationModal = false
@State private var navBarVisible = true
@State var showBottomBar = true
@State private var progressViewOpacity = 0.0
@State var readerSettingsChangedTransactionID: UUID?
@State var annotationSaveTransactionID: UUID?
@ -45,7 +45,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
@ -88,6 +87,7 @@ struct WebReaderContainerView: View {
private func tapHandler() {
withAnimation(.easeIn(duration: 0.08)) {
navBarVisible = !navBarVisible
showBottomBar = navBarVisible
showNavBarActionID = UUID()
}
}
@ -116,6 +116,7 @@ struct WebReaderContainerView: View {
case "dismissNavBars":
withAnimation {
navBarVisible = false
showBottomBar = false
showNavBarActionID = UUID()
}
default:
@ -233,6 +234,10 @@ struct WebReaderContainerView: View {
action: copyDeeplink,
label: { Label("Copy Deeplink", systemImage: "link") }
)
// Button(
// action: print,
// label: { Label("Print", systemImage: "printer") }
// )
Button(
action: delete,
label: { Label("Remove", systemImage: "trash") }
@ -253,7 +258,7 @@ struct WebReaderContainerView: View {
#if os(iOS)
Button(
action: {
pop()
dismiss()
},
label: {
Image.chevronRight
@ -355,12 +360,6 @@ struct WebReaderContainerView: View {
var body: some View {
ZStack {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) {
OperationToast(operationMessage: $viewModel.operationMessage, showOperationToast: $viewModel.showOperationToast, operationStatus: $viewModel.operationStatus)
} label: {
EmptyView()
}.buttonStyle(.plain)
if let articleContent = viewModel.articleContent {
WebReader(
item: item,
@ -384,6 +383,7 @@ struct WebReaderContainerView: View {
navBarVisibilityUpdater: { visible in
withAnimation {
navBarVisible = visible
showBottomBar = visible
}
},
readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID,
@ -391,6 +391,7 @@ struct WebReaderContainerView: View {
showNavBarActionID: $showNavBarActionID,
shareActionID: $shareActionID,
annotation: $annotation,
showBottomBar: $showBottomBar,
showHighlightAnnotationModal: $showHighlightAnnotationModal
)
.background(ThemeManager.currentBgColor)
@ -407,6 +408,7 @@ struct WebReaderContainerView: View {
Task {
await audioController.preload(itemIDs: [item.unwrappedID])
}
viewModel.trackReadEvent(item: item)
}
.confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet,
titleVisibility: .visible) {
@ -421,7 +423,7 @@ struct WebReaderContainerView: View {
#else
// Pasteboard.general.string = item.unwrappedPageURLString TODO: fix for mac
#endif
showInLibrarySnackbar("Link Copied")
Snackbar.show(message: "Link copied", dismissAfter: 2000)
}, label: { Text(LocalText.readerCopyLink) })
Button(action: {
if let linkToOpen = linkToOpen {
@ -499,13 +501,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
@ -513,6 +508,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(
@ -533,7 +535,7 @@ struct WebReaderContainerView: View {
self.isRecovering = true
Task {
if !(await dataService.recoverItem(itemID: item.unwrappedID)) {
viewModel.snackbar(message: "Error recovering item")
Snackbar.show(message: "Error recoviering item", dismissAfter: 2000)
} else {
await viewModel.loadContent(
dataService: dataService,
@ -589,13 +591,13 @@ struct WebReaderContainerView: View {
if let audioProperties = audioController.itemAudioProperties {
MiniPlayerViewer(itemAudioProperties: audioProperties)
.padding(.top, 10)
.padding(.bottom, navBarVisible ? 10 : 40)
.padding(.bottom, showBottomBar ? 10 : 40)
.background(Color.themeTabBarColor)
.onTapGesture {
showExpandedAudioPlayer = true
}
}
if navBarVisible {
if showBottomBar {
CustomToolBar(
isFollowing: item.folder == "following",
isArchived: item.isArchived,
@ -623,47 +625,18 @@ struct WebReaderContainerView: View {
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
pop()
}
.popup(isPresented: $viewModel.showSnackbar) {
if let operation = viewModel.snackbarOperation {
Snackbar(isShowing: $viewModel.showSnackbar, operation: operation)
} else {
EmptyView()
}
} customize: {
$0
.type(.toast)
.autohideIn(2)
.position(.bottom)
.animation(.spring())
.isOpaque(false)
dismiss()
}
.ignoresSafeArea(.all, edges: .bottom)
.onReceive(NSNotification.readerSnackBarPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
viewModel.snackbarOperation = SnackbarOperation(message: message,
undoAction: notification.userInfo?["undoAction"] as? SnackbarUndoAction)
viewModel.showSnackbar = true
}
}
}
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)
}
}
}
@ -672,7 +645,12 @@ struct WebReaderContainerView: View {
let isArchived = item.isArchived
dataService.archiveLink(objectID: item.objectID, archived: !isArchived)
#if os(iOS)
pop()
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
}
@ -683,6 +661,10 @@ struct WebReaderContainerView: View {
func share() {
shareActionID = UUID()
}
func print() {
shareActionID = UUID()
}
func copyDeeplink() {
if let deepLink = item.deepLink {
@ -693,14 +675,15 @@ struct WebReaderContainerView: View {
pasteBoard.clearContents()
pasteBoard.writeObjects([deepLink.absoluteString as NSString])
#endif
showInLibrarySnackbar("Deeplink Copied")
Snackbar.show(message: "Deeplink Copied", dismissAfter: 2000)
} else {
showInLibrarySnackbar("Error copying deeplink")
Snackbar.show(message: "Error copying deeplink", dismissAfter: 2000)
}
}
func delete() {
pop()
dismiss()
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
removeLibraryItemAction(dataService: dataService, objectID: item.objectID)

View file

@ -20,6 +20,7 @@ final class WebReaderCoordinator: NSObject {
var previousShowNavBarActionID: UUID?
var previousShareActionID: UUID?
var updateNavBarVisibility: (Bool) -> Void = { _ in }
var updateShowBottomBar: (Bool) -> Void = { _ in }
var articleContentID = UUID()
private var yOffsetAtStartOfDrag: Double?
private var lastYOffset: Double = 0
@ -123,8 +124,9 @@ extension WebReaderCoordinator: WKNavigationDelegate {
// if at bottom show the controls
if yOffset + scrollView.visibleSize.height > scrollView.contentSize.height - 140 {
navBarVisible = true
scrollView.contentInset.top = navBarVisible ? readerViewNavBarHeight : 0
updateShowBottomBar(true)
} else {
updateShowBottomBar(false)
}
let percent = Int(((yOffset + scrollView.visibleSize.height) / scrollView.contentSize.height) * 100)

View file

@ -45,11 +45,13 @@ public struct WebReaderLoadingContainer: View {
if let item = viewModel.item {
if let pdfItem = PDFItem.make(item: item) {
#if os(iOS)
NavigationView {
PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem))
.navigationBarHidden(true)
.navigationViewStyle(.stack)
.accentColor(.appGrayTextContrast)
.onAppear { viewModel.trackReadEvent() }
}
#else
if let pdfURL = pdfItem.pdfURL {
PDFWrapperView(pdfURL: pdfURL)
@ -58,7 +60,7 @@ public struct WebReaderLoadingContainer: View {
} else if item.state == "CONTENT_NOT_FETCHED" {
ProgressView()
} else {
WebReaderContainerView(item: item, pop: { dismiss() })
WebReaderContainerView(item: item)
#if os(iOS)
.navigationViewStyle(.stack)
#endif

View file

@ -3,6 +3,7 @@ import Services
import SwiftUI
import Views
import WebKit
import Utils
struct SafariWebLink: Identifiable {
let id: UUID
@ -20,14 +21,6 @@ struct SafariWebLink: Identifiable {
@Published var showOperationToast: Bool = false
@Published var operationStatus: OperationStatus = .none
@Published var showSnackbar: Bool = false
var snackbarOperation: SnackbarOperation?
func snackbar(message: String) {
snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
showSnackbar = true
}
func hasOriginalUrl(_ item: Models.LibraryItem) -> Bool {
if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host {
if host == "omnivore.app" {
@ -39,7 +32,7 @@ struct SafariWebLink: Identifiable {
}
func downloadAudio(audioController: AudioController, item: Models.LibraryItem) {
snackbar(message: "Downloading Offline Audio")
Snackbar.show(message: "Downloading Offline Audio", dismissAfter: 2000)
isDownloadingAudio = true
if let audioDownloadTask = audioDownloadTask {
@ -53,7 +46,7 @@ struct SafariWebLink: Identifiable {
DispatchQueue.main.async {
self.isDownloadingAudio = false
if !canceled {
self.snackbar(message: downloaded ? "Audio file downloaded" : "Error downloading audio")
Snackbar.show(message: downloaded ? "Audio file downloaded" : "Error downloading audio", dismissAfter: 2000)
}
}
}
@ -214,12 +207,11 @@ struct SafariWebLink: Identifiable {
func saveLink(dataService: DataService, url: URL) {
Task {
do {
snackbar(message: "Saving link")
print("SAVING: ", url.absoluteString)
Snackbar.show(message: "Saving link", dismissAfter: 5000)
_ = try await dataService.createPageFromUrl(id: UUID().uuidString, url: url.absoluteString)
snackbar(message: "Link saved")
Snackbar.show(message: "Link saved", dismissAfter: 2000)
} catch {
snackbar(message: "Error saving link")
Snackbar.show(message: "Error saving link", dismissAfter: 2000)
}
}
}
@ -227,15 +219,30 @@ struct SafariWebLink: Identifiable {
func saveLinkAndFetch(dataService: DataService, username: String, url: URL) {
Task {
do {
snackbar(message: "Saving link")
Snackbar.show(message: "Saving link", dismissAfter: 5000)
let requestId = UUID().uuidString
_ = try await dataService.createPageFromUrl(id: requestId, url: url.absoluteString)
snackbar(message: "Link saved")
Snackbar.show(message: "Link saved", dismissAfter: 2000)
await loadContent(dataService: dataService, username: username, itemID: requestId, retryCount: 0)
} catch {
snackbar(message: "Error saving link")
Snackbar.show(message: "Error saving link", dismissAfter: 2000)
}
}
}
func trackReadEvent(item: Models.LibraryItem) {
let itemID = item.unwrappedID
let slug = item.unwrappedSlug
let originalArticleURL = item.unwrappedPageURLString
EventTracker.track(
.linkRead(
linkID: itemID,
slug: slug,
reader: "WEB",
originalArticleURL: originalArticleURL
)
)
}
}

View file

@ -80,7 +80,12 @@
startAudio(atIndex: itemAudioProperties.startIndex, andOffset: itemAudioProperties.startOffset)
EventTracker.track(
.audioSessionStart(linkID: itemAudioProperties.itemID)
.audioSessionStart(
linkID: itemAudioProperties.itemID,
voice: currentVoice.lowercased(),
voiceProvider: Voices.isUltraRealisticVoice(currentVoice) ? "ultra" :
Voices.isOpenAIVoice(currentVoice) ? "openai" : "default"
)
)
}

View file

@ -1,4 +1,3 @@
import AsyncAlgorithms
import CoreData
import CoreImage
import Foundation
@ -26,7 +25,6 @@ public final class DataService: ObservableObject {
public let networker: Networker
public let prefetchQueue = OperationQueue()
public let itemLoaderChannel = AsyncChannel<String>()
var persistentContainer: PersistentContainer
public var backgroundContext: NSManagedObjectContext

File diff suppressed because it is too large Load diff

View file

@ -14,11 +14,6 @@ extension DataService {
// Send update to server
self.syncLinkArchiveStatus(itemID: linkedItem.unwrappedID, archived: archived)
let message = archived ? "Link archived" : "Link unarchived"
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
showInLibrarySnackbar(message)
}
}
}

View file

@ -0,0 +1,79 @@
import CoreData
import Foundation
import Models
import SwiftGraphQL
public extension DataService {
func emptyTrash() async -> Bool {
enum MutationResult {
case result(success: Bool)
case error(errorMessage: String)
}
let selection = Selection<MutationResult, Unions.EmptyTrashResult> {
try $0.on(
emptyTrashError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") },
emptyTrashSuccess: .init {
.result(success: try $0.success() ?? false)
}
)
}
let mutation = Selection.Mutation {
try $0.emptyTrash(selection: selection)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
let context = backgroundContext
return await withCheckedContinuation { continuation in
send(mutation, to: path, headers: headers) { queryResult in
guard let payload = try? queryResult.get() else {
print("network error emptying trash")
continuation.resume(returning: false)
return
}
switch (payload.data) {
case let .result(success):
if !success {
print("server did not return success for emptying trash")
continuation.resume(returning: false)
return
}
default:
print("server did not return success for emptying trash")
continuation.resume(returning: false)
return
}
do {
try context.performAndWait {
let fetchRequest = LibraryItem.fetchRequest()
fetchRequest.predicate = NSPredicate(
format: "%K == %i OR %K == \"DELETED\"",
#keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue),
#keyPath(Models.LibraryItem.state)
)
for object in try context.fetch(fetchRequest) {
context.delete(object)
}
do {
try context.save()
logger.debug("Empty trash completed")
continuation.resume(returning: true)
} catch {
context.rollback()
logger.debug("Failed to sync library item move: \(error.localizedDescription)")
continuation.resume(returning: false)
}
}
} catch {
print("error emptying trash", error)
continuation.resume(returning: false)
}
}
}
}
}

View file

@ -5,8 +5,7 @@ import Models
public extension NSNotification {
static let PushJSONArticle = Notification.Name("PushJSONArticle")
static let PushReaderItem = Notification.Name("PushReaderItem")
static let LibrarySnackBar = Notification.Name("LibrarySnackBar")
static let ReaderSnackBar = Notification.Name("ReaderSnackBar")
static let SnackBar = Notification.Name("SnackBar")
static let OperationFailure = Notification.Name("OperationFailure")
static let ReaderSettingsChanged = Notification.Name("ReaderSettingsChanged")
static let SpeakingReaderItem = Notification.Name("SpeakingReaderItem")
@ -27,12 +26,8 @@ public extension NSNotification {
NotificationCenter.default.publisher(for: PushReaderItem)
}
static var readerSnackBarPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: ReaderSnackBar)
}
static var librarySnackBarPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: LibrarySnackBar)
static var snackBarPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: SnackBar)
}
static var operationFailedPublisher: NotificationCenter.Publisher {
@ -82,10 +77,12 @@ public extension NSNotification {
)
}
static func librarySnackBar(message: String, undoAction: (() -> Void)?) {
NotificationCenter.default.post(name: NSNotification.LibrarySnackBar,
static func snackBar(message: String, undoAction: (() -> Void)?, dismissAfter: Int?) {
NotificationCenter.default.post(name: NSNotification.SnackBar,
object: nil,
userInfo: ["message": message, "undoAction": undoAction as Any])
userInfo: ["message": message,
"undoAction": undoAction as Any,
"dismissAfter": dismissAfter as Any])
}
static func operationFailed(message: String) {

View file

@ -4,7 +4,7 @@ public enum TrackableEvent {
case linkRead(linkID: String, slug: String, reader: String, originalArticleURL: String)
case debugMessage(message: String)
case backgroundFetch(jobStatus: BackgroundFetchJobStatus, itemCount: Int, secondsElapsed: Int)
case audioSessionStart(linkID: String)
case audioSessionStart(linkID: String, voice: String, voiceProvider: String)
case audioSessionEnd(linkID: String, timeElapsed: Double)
}
@ -48,9 +48,11 @@ public extension TrackableEvent {
"seconds_elapsed": String(secondsElapsed),
"fetched_item_count": String(itemCount)
]
case let .audioSessionStart(linkID: linkID):
case let .audioSessionStart(linkID: linkID, voice: voice, voiceProvider: voiceProvider):
return [
"link": linkID
"link": linkID,
"voice": voice,
"voiceProvider": voiceProvider
]
case let .audioSessionEnd(linkID: linkID, timeElapsed: timeElapsed):
return [

View file

@ -1,18 +0,0 @@
//
// ShowInSnackbar.swift
//
//
// Created by Jackson Harper on 11/1/22.
//
import Foundation
public func showInLibrarySnackbar(_ message: String) {
let nname = Notification.Name("LibrarySnackBar")
NotificationCenter.default.post(name: nname, object: nil, userInfo: ["message": message])
}
public func showInReaderSnackbar(_ message: String) {
let nname = Notification.Name("ReaderSnackBar")
NotificationCenter.default.post(name: nname, object: nil, userInfo: ["message": message])
}

View file

@ -1,6 +1,7 @@
import Models
import Utils
import WebKit
// swiftlint:disable file_length
/// Describes actions that can be sent from the WebView back to native views.
@ -191,6 +192,14 @@ public final class OmnivoreWebView: WKWebView {
}
}
#endif
// Because all the snackbar stuff lives in app we just use notifications here
func showInReaderSnackbar(_ message: String) {
NotificationCenter.default.post(name: Notification.Name("SnackBar"),
object: nil,
userInfo: ["message": message,
"dismissAfter": 2000 as Any])
}
}
#if os(iOS)

File diff suppressed because one or more lines are too long