mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3496 from omnivore-app/feat/ios-views-cleanup
iOS snackbar crash fixes
This commit is contained in:
commit
87928a517a
39 changed files with 27739 additions and 27251 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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 = "";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
apple/OmnivoreKit/Sources/App/Views/PDFContainerView.swift
Normal file
66
apple/OmnivoreKit/Sources/App/Views/PDFContainerView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue