Structure tabviews in iOS
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -38,7 +38,8 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
|
||||
@ViewBuilder var destinationView: some View {
|
||||
switch self {
|
||||
case .feed:
|
||||
HomeView()
|
||||
// HomeView(viewModel: HomeFeedViewModel())
|
||||
EmptyView()
|
||||
case .profile:
|
||||
ProfileView()
|
||||
}
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
|
||||
import Introspect
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Views
|
||||
|
||||
@MainActor final class FilterSelectorViewModel: NSObject, ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage: String = ""
|
||||
@Published var showErrorMessage: Bool = false
|
||||
|
||||
func error(_ msg: String) {
|
||||
errorMessage = msg
|
||||
showErrorMessage = true
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
struct FilterSelectorView: View {
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
@ObservedObject var filterViewModel = FilterByLabelsViewModel()
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State var showLabelsSheet = false
|
||||
|
||||
init(viewModel: HomeFeedViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
List {
|
||||
innerBody
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
#elseif os(macOS)
|
||||
List {
|
||||
innerBody
|
||||
}
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
}
|
||||
.navigationBarTitle("Library")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing: doneButton)
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
Group {
|
||||
Section {
|
||||
ForEach(LinkedItemFilter.allCases, id: \.self) { filter in
|
||||
HStack {
|
||||
Text(filter.displayName)
|
||||
.foregroundColor(viewModel.appliedFilter == filter.rawValue ? Color.blue : Color.appTextDefault)
|
||||
Spacer()
|
||||
if viewModel.appliedFilter == filter.rawValue {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(Color.blue)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
viewModel.appliedFilter = filter.rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Labels") {
|
||||
Button(
|
||||
action: {
|
||||
showLabelsSheet = true
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Text("Select Labels (\(viewModel.selectedLabels.count))")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showLabelsSheet) {
|
||||
FilterByLabelsView(
|
||||
initiallySelected: viewModel.selectedLabels,
|
||||
initiallyNegated: viewModel.negatedLabels
|
||||
) {
|
||||
self.viewModel.selectedLabels = $0
|
||||
self.viewModel.negatedLabels = $1
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await filterViewModel.loadLabels(
|
||||
dataService: dataService,
|
||||
initiallySelectedLabels: viewModel.selectedLabels,
|
||||
initiallyNegatedLabels: viewModel.negatedLabels
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func isNegated(_ label: LinkedItemLabel) -> Bool {
|
||||
filterViewModel.negatedLabels.contains(where: { $0.id == label.id })
|
||||
}
|
||||
|
||||
func isSelected(_ label: LinkedItemLabel) -> Bool {
|
||||
filterViewModel.selectedLabels.contains(where: { $0.id == label.id })
|
||||
}
|
||||
|
||||
var doneButton: some View {
|
||||
Button(
|
||||
action: { dismiss() },
|
||||
label: { Text("Done") }
|
||||
)
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
@ -83,30 +83,24 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
.sheet(item: $viewModel.itemForHighlightsView) { item in
|
||||
NotebookView(itemObjectID: item.objectID, hasHighlightMutations: $hasHighlightMutations)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showCommunityModal) {
|
||||
CommunityModal()
|
||||
.onAppear {
|
||||
shouldPromptCommunityModal = false
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showFiltersModal) {
|
||||
NavigationView {
|
||||
FilterSelectorView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .barLeading) {
|
||||
Button(action: {
|
||||
viewModel.showCommunityModal = true
|
||||
viewModel.showFiltersModal = true
|
||||
}, label: {
|
||||
Image.smallOmnivoreLogo
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.overlay(alignment: .topTrailing, content: {
|
||||
if shouldPromptCommunityModal {
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 6, height: 6)
|
||||
}
|
||||
})
|
||||
HStack(alignment: .center) {
|
||||
let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName
|
||||
Text(title)
|
||||
.font(Font.system(size: 18, weight: .semibold))
|
||||
Image(systemName: "chevron.down")
|
||||
.font(Font.system(size: 13, weight: .regular))
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
@ -367,20 +361,20 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
|
||||
var featureCard: some View {
|
||||
VStack {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
VStack(alignment: .leading, spacing: 15) {
|
||||
Menu(content: {
|
||||
Button(action: {
|
||||
viewModel.updateFeatureFilter(.continueReading)
|
||||
viewModel.updateFeatureFilter(dataService: dataService, filter: .continueReading)
|
||||
}, label: {
|
||||
Text("Continue Reading")
|
||||
})
|
||||
Button(action: {
|
||||
viewModel.updateFeatureFilter(.pinned)
|
||||
viewModel.updateFeatureFilter(dataService: dataService, filter: .pinned)
|
||||
}, label: {
|
||||
Text("Pinned")
|
||||
})
|
||||
Button(action: {
|
||||
viewModel.updateFeatureFilter(.newsletters)
|
||||
viewModel.updateFeatureFilter(dataService: dataService, filter: .newsletters)
|
||||
}, label: {
|
||||
Text("Newsletters")
|
||||
})
|
||||
@ -392,7 +386,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
}, label: {
|
||||
HStack(alignment: .center) {
|
||||
Text((FeaturedItemFilter(rawValue: viewModel.featureFilter) ?? .continueReading).title)
|
||||
.font(Font.system(size: 13, weight: .regular))
|
||||
.font(Font.system(size: 13, weight: .medium))
|
||||
Image(systemName: "chevron.down")
|
||||
.font(Font.system(size: 13, weight: .regular))
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@ -401,14 +395,14 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
.padding(.top, 15)
|
||||
|
||||
GeometryReader { geo in
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
if viewModel.featureItems.count > 0 {
|
||||
LazyHStack(alignment: .top, spacing: 10) {
|
||||
LazyHStack(alignment: .top, spacing: 15) {
|
||||
ForEach(viewModel.featureItems) { item in
|
||||
LibraryFeatureCardNavigationLink(item: item, viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
.padding(.top, 0)
|
||||
} else {
|
||||
Text((FeaturedItemFilter(rawValue: viewModel.featureFilter) ?? .continueReading).emptyMessage)
|
||||
.font(Font.system(size: 14, weight: .regular))
|
||||
@ -428,104 +422,93 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
NavigationLink(
|
||||
destination: LinkDestination(selectedItem: viewModel.selectedItem),
|
||||
isActive: $viewModel.linkIsActive
|
||||
) {
|
||||
EmptyView()
|
||||
VStack(spacing: 0) {
|
||||
if viewModel.showLoadingBar {
|
||||
ShimmeringLoader()
|
||||
} else {
|
||||
Spacer(minLength: 2)
|
||||
}
|
||||
VStack(spacing: 0) {
|
||||
if viewModel.showLoadingBar {
|
||||
ShimmeringLoader()
|
||||
} else {
|
||||
Spacer(minLength: 2)
|
||||
|
||||
List {
|
||||
if !viewModel.hideFeatureSection, viewModel.items.count > 0,
|
||||
viewModel.searchTerm.isEmpty, viewModel.selectedLabels.isEmpty,
|
||||
viewModel.negatedLabels.isEmpty
|
||||
{
|
||||
featureCard
|
||||
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.modifier(AnimatingCellHeight(height: viewModel.featureItems.count > 0 ? 200 : 130))
|
||||
}
|
||||
|
||||
List {
|
||||
filtersHeader
|
||||
.listRowInsets(.init(top: 0, leading: 10, bottom: 10, trailing: 10))
|
||||
|
||||
if !viewModel.hideFeatureSection, viewModel.items.count > 0, viewModel.searchTerm.isEmpty, viewModel.selectedLabels.isEmpty, viewModel.negatedLabels.isEmpty {
|
||||
featureCard
|
||||
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.modifier(AnimatingCellHeight(height: viewModel.featureItems.count > 0 ? 200 : 130))
|
||||
ForEach(viewModel.items) { item in
|
||||
FeedCardNavigationLink(
|
||||
item: item,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.listRowSeparatorTint(Color.thBorderColor)
|
||||
.listRowInsets(.init(top: 0, leading: 10, bottom: 10, trailing: 10))
|
||||
.contextMenu {
|
||||
menuItems(for: item)
|
||||
}
|
||||
|
||||
ForEach(viewModel.items) { item in
|
||||
FeedCardNavigationLink(
|
||||
item: item,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.listRowSeparatorTint(Color.thBorderColor)
|
||||
.listRowInsets(.init(top: 0, leading: 10, bottom: 10, trailing: 10))
|
||||
.contextMenu {
|
||||
menuItems(for: item)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if !item.isArchived {
|
||||
Button(action: {
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true)
|
||||
}
|
||||
}, label: {
|
||||
Label("Archive", systemImage: "archivebox")
|
||||
}).tint(.green)
|
||||
} else {
|
||||
Button(action: {
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: false)
|
||||
}
|
||||
}, label: {
|
||||
Label("Unarchive", systemImage: "tray.and.arrow.down.fill")
|
||||
}).tint(.indigo)
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "trash")
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if !item.isArchived {
|
||||
Button(action: {
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true)
|
||||
}
|
||||
).tint(.red)
|
||||
}, label: {
|
||||
Label("Archive", systemImage: "archivebox")
|
||||
}).tint(.green)
|
||||
} else {
|
||||
Button(action: {
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: false)
|
||||
}
|
||||
}, label: {
|
||||
Label("Unarchive", systemImage: "tray.and.arrow.down.fill")
|
||||
}).tint(.indigo)
|
||||
}
|
||||
// .swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
// if FeatureFlag.enableSnooze {
|
||||
// Button {
|
||||
// viewModel.itemToSnoozeID = item.id
|
||||
// viewModel.snoozePresented = true
|
||||
// } label: {
|
||||
// Label { Text(LocalText.genericSnooze) } icon: { Image.moon }
|
||||
// }.tint(.appYellow48)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
.padding(0)
|
||||
.listStyle(PlainListStyle())
|
||||
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.alert("Are you sure you want to delete this item? All associated notes and highlights will be deleted.",
|
||||
isPresented: $confirmationShown) {
|
||||
Button("Remove Item", role: .destructive) {
|
||||
if let itemToRemove = itemToRemove {
|
||||
withAnimation {
|
||||
viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID)
|
||||
Button(
|
||||
action: {
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
self.itemToRemove = nil
|
||||
).tint(.red)
|
||||
}
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.itemToRemove = nil }
|
||||
// .swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
// Button {
|
||||
// viewModel.pinItem(dataService, item)
|
||||
// } label: {
|
||||
// Label { Text("Pin") } icon: { Image(systemName: "pin.fill") }
|
||||
// }.tint(Color(hex: "#0A84FF"))
|
||||
// }
|
||||
}
|
||||
}
|
||||
.alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.",
|
||||
isPresented: $showHideFeatureAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.hideFeatureSection = true
|
||||
.padding(0)
|
||||
.listStyle(PlainListStyle())
|
||||
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.alert("Are you sure you want to delete this item? All associated notes and highlights will be deleted.",
|
||||
isPresented: $confirmationShown) {
|
||||
Button("Remove Item", role: .destructive) {
|
||||
if let itemToRemove = itemToRemove {
|
||||
withAnimation {
|
||||
viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID)
|
||||
}
|
||||
}
|
||||
self.itemToRemove = nil
|
||||
}
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFeatureAlert = false }
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.itemToRemove = nil }
|
||||
}
|
||||
}
|
||||
.alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.",
|
||||
isPresented: $showHideFeatureAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.hideFeatureSection = true
|
||||
}
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFeatureAlert = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ import Views
|
||||
@Published var linkIsActive = false
|
||||
|
||||
@Published var showLabelsSheet = false
|
||||
@Published var showFiltersModal = false
|
||||
@Published var showCommunityModal = false
|
||||
@Published var featureItems = [LinkedItem]()
|
||||
|
||||
@ -49,12 +50,19 @@ import Views
|
||||
|
||||
func setItems(_ items: [LinkedItem]) {
|
||||
self.items = items
|
||||
updateFeatureFilter(FeaturedItemFilter(rawValue: featureFilter))
|
||||
}
|
||||
|
||||
func updateFeatureFilter(_ filter: FeaturedItemFilter?) {
|
||||
func updateFeatureFilter(dataService: DataService, filter: FeaturedItemFilter?) {
|
||||
Task {
|
||||
if let filter = filter {
|
||||
featureItems = await loadFeatureItems(dataService: dataService, predicate: filter.predicate, sort: filter.sortDescriptor)
|
||||
} else {
|
||||
featureItems = []
|
||||
}
|
||||
}
|
||||
if let filter = filter {
|
||||
// now try to update the continue reading items:
|
||||
|
||||
featureItems = (items.filter { item in
|
||||
filter.predicate.evaluate(with: item)
|
||||
} as NSArray)
|
||||
@ -223,6 +231,8 @@ import Views
|
||||
updateFetchController(dataService: dataService)
|
||||
}
|
||||
|
||||
updateFeatureFilter(dataService: dataService, filter: FeaturedItemFilter(rawValue: featureFilter))
|
||||
|
||||
isLoading = false
|
||||
showLoadingBar = false
|
||||
}
|
||||
@ -237,6 +247,17 @@ import Views
|
||||
showLoadingBar = false
|
||||
}
|
||||
|
||||
func pinItem(dataService _: DataService, item _: LinkedItem) async {}
|
||||
|
||||
func loadFeatureItems(dataService: DataService, predicate: NSPredicate, sort: NSSortDescriptor) async -> [LinkedItem] {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.fetchLimit = 25
|
||||
fetchRequest.predicate = predicate
|
||||
fetchRequest.sortDescriptors = [sort]
|
||||
|
||||
return (try? dataService.viewContext.fetch(fetchRequest)) ?? []
|
||||
}
|
||||
|
||||
private var fetchRequest: NSFetchRequest<Models.LinkedItem> {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
|
||||
|
||||
@ -3,7 +3,11 @@ import Utils
|
||||
import Views
|
||||
|
||||
struct HomeView: View {
|
||||
@StateObject private var viewModel = HomeFeedViewModel()
|
||||
@State private var viewModel: HomeFeedViewModel
|
||||
|
||||
init(viewModel: HomeFeedViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var navView: some View {
|
||||
|
||||
@ -6,41 +6,50 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryTabView: View {
|
||||
// @EnvironmentObject var authenticator: Authenticator
|
||||
// @EnvironmentObject var dataService: DataService
|
||||
// @Binding var selectedEnvironment: AppEnvironment
|
||||
|
||||
// let appEnvironments: [AppEnvironment] = [.local, .demo, .prod]
|
||||
@StateObject private var viewModel = HomeFeedViewModel()
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
HomeView()
|
||||
.tabItem {
|
||||
Label {
|
||||
Text("Subscriptions")
|
||||
} icon: {
|
||||
Image.tabSubscriptions
|
||||
}
|
||||
NavigationView {
|
||||
ZStack {
|
||||
NavigationLink(
|
||||
destination: LinkDestination(selectedItem: viewModel.selectedItem),
|
||||
isActive: $viewModel.linkIsActive
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
HomeView()
|
||||
.tabItem {
|
||||
Label {
|
||||
Text("Library")
|
||||
} icon: {
|
||||
Image.tabLibrary
|
||||
}
|
||||
}
|
||||
HomeView()
|
||||
.tabItem {
|
||||
Label {
|
||||
Text("Highlights")
|
||||
} icon: {
|
||||
Image.tabHighlights
|
||||
}
|
||||
TabView {
|
||||
HomeView(viewModel: viewModel)
|
||||
.tabItem {
|
||||
Label {
|
||||
Text("Subscriptions")
|
||||
} icon: {
|
||||
Image.tabSubscriptions
|
||||
}
|
||||
}
|
||||
HomeView(viewModel: viewModel)
|
||||
.tabItem {
|
||||
Label {
|
||||
Text("Library")
|
||||
} icon: {
|
||||
Image.tabLibrary
|
||||
}
|
||||
}
|
||||
HomeView(viewModel: viewModel)
|
||||
.tabItem {
|
||||
Label {
|
||||
Text("Highlights")
|
||||
} icon: {
|
||||
Image.tabHighlights
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +112,10 @@ struct LinkItemDetailView: View {
|
||||
if isPDF {
|
||||
pdfContainerView
|
||||
} else if let item = viewModel.item {
|
||||
WebReaderContainerView(item: item)
|
||||
GeometryReader { geo in
|
||||
let foo = print("READER", geo.size)
|
||||
WebReaderContainerView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
|
||||
@ -395,171 +395,174 @@ struct WebReaderContainerView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let articleContent = viewModel.articleContent {
|
||||
WebReader(
|
||||
item: item,
|
||||
articleContent: articleContent,
|
||||
openLinkAction: {
|
||||
#if os(macOS)
|
||||
NSWorkspace.shared.open($0)
|
||||
#elseif os(iOS)
|
||||
if UIDevice.current.userInterfaceIdiom == .phone, $0.absoluteString != item.unwrappedPageURLString {
|
||||
linkToOpen = $0
|
||||
displayLinkSheet = true
|
||||
} else {
|
||||
safariWebLink = SafariWebLink(id: UUID(), url: $0)
|
||||
}
|
||||
#endif
|
||||
},
|
||||
tapHandler: tapHandler,
|
||||
scrollPercentHandler: scrollPercentHandler,
|
||||
webViewActionHandler: webViewActionHandler,
|
||||
navBarVisibilityRatioUpdater: {
|
||||
navBarVisibilityRatio = $0
|
||||
},
|
||||
readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID,
|
||||
annotationSaveTransactionID: $annotationSaveTransactionID,
|
||||
showNavBarActionID: $showNavBarActionID,
|
||||
shareActionID: $shareActionID,
|
||||
annotation: $annotation,
|
||||
showBottomBar: $showBottomBar,
|
||||
showHighlightAnnotationModal: $showHighlightAnnotationModal
|
||||
)
|
||||
.background(ThemeManager.currentBgColor)
|
||||
.onAppear {
|
||||
if item.isUnread {
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0)
|
||||
}
|
||||
Task {
|
||||
await audioController.preload(itemIDs: [item.unwrappedID])
|
||||
}
|
||||
}
|
||||
.confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet) {
|
||||
Button(action: {
|
||||
if let linkToOpen = linkToOpen {
|
||||
safariWebLink = SafariWebLink(id: UUID(), url: linkToOpen)
|
||||
}
|
||||
}, label: { Text(LocalText.genericOpen) })
|
||||
Button(action: {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = item.unwrappedPageURLString
|
||||
#else
|
||||
// Pasteboard.general.string = item.unwrappedPageURLString TODO: fix for mac
|
||||
#endif
|
||||
showInSnackbar("Link Copied")
|
||||
}, label: { Text(LocalText.readerCopyLink) })
|
||||
Button(action: {
|
||||
if let linkToOpen = linkToOpen {
|
||||
viewModel.saveLink(dataService: dataService, url: linkToOpen)
|
||||
}
|
||||
}, label: { Text(LocalText.readerSave) })
|
||||
}
|
||||
#if os(iOS)
|
||||
.fullScreenCover(item: $safariWebLink) {
|
||||
SafariView(url: $0.url)
|
||||
}
|
||||
#endif
|
||||
.alert(errorAlertMessage ?? LocalText.readerError, isPresented: $showErrorAlertMessage) {
|
||||
Button(LocalText.genericOk, role: .cancel, action: {
|
||||
errorAlertMessage = nil
|
||||
showErrorAlertMessage = false
|
||||
})
|
||||
}
|
||||
#if os(iOS)
|
||||
.formSheet(isPresented: $showRecommendSheet) {
|
||||
let highlightCount = item.highlights.asArray(of: Highlight.self).filter(\.createdByMe).count
|
||||
|
||||
NavigationView {
|
||||
RecommendToView(
|
||||
dataService: dataService,
|
||||
viewModel: RecommendToViewModel(pageID: item.unwrappedID,
|
||||
highlightCount: highlightCount)
|
||||
)
|
||||
}.onDisappear {
|
||||
showRecommendSheet = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.sheet(isPresented: $showHighlightAnnotationModal) {
|
||||
NavigationView {
|
||||
HighlightAnnotationSheet(
|
||||
annotation: $annotation,
|
||||
onSave: {
|
||||
annotationSaveTransactionID = UUID()
|
||||
},
|
||||
onCancel: {
|
||||
showHighlightAnnotationModal = false
|
||||
},
|
||||
errorAlertMessage: $errorAlertMessage,
|
||||
showErrorAlertMessage: $showErrorAlertMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHighlightLabelsModal) {
|
||||
if let highlight = Highlight.lookup(byID: self.annotation, inContext: self.dataService.viewContext) {
|
||||
ApplyLabelsView(mode: .highlight(highlight), isSearchFocused: false) { selectedLabels in
|
||||
viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID,
|
||||
labelIDs: selectedLabels.map(\.unwrappedID),
|
||||
dataService: dataService)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let errorMessage = viewModel.errorMessage {
|
||||
VStack {
|
||||
Text(errorMessage).padding()
|
||||
if viewModel.allowRetry, viewModel.hasOriginalUrl(item) {
|
||||
Button("Open Original", action: {
|
||||
openOriginalURL(urlString: item.pageURLString)
|
||||
}).buttonStyle(RoundedRectButtonStyle())
|
||||
if let urlStr = item.pageURLString, let username = dataService.currentViewer?.username, let url = URL(string: urlStr) {
|
||||
Button("Attempt to Save Again", action: {
|
||||
viewModel.errorMessage = nil
|
||||
viewModel.saveLinkAndFetch(dataService: dataService, username: username, url: url)
|
||||
}).buttonStyle(RoundedRectButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
.opacity(progressViewOpacity)
|
||||
GeometryReader { geo in
|
||||
let foo = print("READER", geo.size)
|
||||
if let articleContent = viewModel.articleContent {
|
||||
WebReader(
|
||||
item: item,
|
||||
articleContent: articleContent,
|
||||
openLinkAction: {
|
||||
#if os(macOS)
|
||||
NSWorkspace.shared.open($0)
|
||||
#elseif os(iOS)
|
||||
if UIDevice.current.userInterfaceIdiom == .phone, $0.absoluteString != item.unwrappedPageURLString {
|
||||
linkToOpen = $0
|
||||
displayLinkSheet = true
|
||||
} else {
|
||||
safariWebLink = SafariWebLink(id: UUID(), url: $0)
|
||||
}
|
||||
#endif
|
||||
},
|
||||
tapHandler: tapHandler,
|
||||
scrollPercentHandler: scrollPercentHandler,
|
||||
webViewActionHandler: webViewActionHandler,
|
||||
navBarVisibilityRatioUpdater: {
|
||||
navBarVisibilityRatio = $0
|
||||
},
|
||||
readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID,
|
||||
annotationSaveTransactionID: $annotationSaveTransactionID,
|
||||
showNavBarActionID: $showNavBarActionID,
|
||||
shareActionID: $shareActionID,
|
||||
annotation: $annotation,
|
||||
showBottomBar: $showBottomBar,
|
||||
showHighlightAnnotationModal: $showHighlightAnnotationModal
|
||||
)
|
||||
.background(ThemeManager.currentBgColor)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
|
||||
progressViewOpacity = 1
|
||||
if item.isUnread {
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0)
|
||||
}
|
||||
Task {
|
||||
await audioController.preload(itemIDs: [item.unwrappedID])
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if let username = dataService.currentViewer?.username {
|
||||
await viewModel.loadContent(
|
||||
dataService: dataService,
|
||||
username: username,
|
||||
itemID: item.unwrappedID
|
||||
.confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet) {
|
||||
Button(action: {
|
||||
if let linkToOpen = linkToOpen {
|
||||
safariWebLink = SafariWebLink(id: UUID(), url: linkToOpen)
|
||||
}
|
||||
}, label: { Text(LocalText.genericOpen) })
|
||||
Button(action: {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = item.unwrappedPageURLString
|
||||
#else
|
||||
// Pasteboard.general.string = item.unwrappedPageURLString TODO: fix for mac
|
||||
#endif
|
||||
showInSnackbar("Link Copied")
|
||||
}, label: { Text(LocalText.readerCopyLink) })
|
||||
Button(action: {
|
||||
if let linkToOpen = linkToOpen {
|
||||
viewModel.saveLink(dataService: dataService, url: linkToOpen)
|
||||
}
|
||||
}, label: { Text(LocalText.readerSave) })
|
||||
}
|
||||
#if os(iOS)
|
||||
.fullScreenCover(item: $safariWebLink) {
|
||||
SafariView(url: $0.url)
|
||||
}
|
||||
#endif
|
||||
.alert(errorAlertMessage ?? LocalText.readerError, isPresented: $showErrorAlertMessage) {
|
||||
Button(LocalText.genericOk, role: .cancel, action: {
|
||||
errorAlertMessage = nil
|
||||
showErrorAlertMessage = false
|
||||
})
|
||||
}
|
||||
#if os(iOS)
|
||||
.formSheet(isPresented: $showRecommendSheet) {
|
||||
let highlightCount = item.highlights.asArray(of: Highlight.self).filter(\.createdByMe).count
|
||||
|
||||
NavigationView {
|
||||
RecommendToView(
|
||||
dataService: dataService,
|
||||
viewModel: RecommendToViewModel(pageID: item.unwrappedID,
|
||||
highlightCount: highlightCount)
|
||||
)
|
||||
}.onDisappear {
|
||||
showRecommendSheet = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.sheet(isPresented: $showHighlightAnnotationModal) {
|
||||
NavigationView {
|
||||
HighlightAnnotationSheet(
|
||||
annotation: $annotation,
|
||||
onSave: {
|
||||
annotationSaveTransactionID = UUID()
|
||||
},
|
||||
onCancel: {
|
||||
showHighlightAnnotationModal = false
|
||||
},
|
||||
errorAlertMessage: $errorAlertMessage,
|
||||
showErrorAlertMessage: $showErrorAlertMessage
|
||||
)
|
||||
} else {
|
||||
viewModel.errorMessage = "You are not logged in."
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
VStack(spacing: 0) {
|
||||
navBar
|
||||
Spacer()
|
||||
if showBottomBar {
|
||||
bottomButtons
|
||||
.frame(height: 48)
|
||||
.background(Color.webControlButtonBackground)
|
||||
.cornerRadius(6)
|
||||
.padding(.bottom, 34)
|
||||
.shadow(color: .gray.opacity(0.13), radius: 8, x: 0, y: 4)
|
||||
.opacity(bottomBarOpacity)
|
||||
.onAppear {
|
||||
withAnimation(Animation.linear(duration: 0.25)) { self.bottomBarOpacity = 1 }
|
||||
}
|
||||
.onDisappear {
|
||||
self.bottomBarOpacity = 0
|
||||
.sheet(isPresented: $showHighlightLabelsModal) {
|
||||
if let highlight = Highlight.lookup(byID: self.annotation, inContext: self.dataService.viewContext) {
|
||||
ApplyLabelsView(mode: .highlight(highlight), isSearchFocused: false) { selectedLabels in
|
||||
viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID,
|
||||
labelIDs: selectedLabels.map(\.unwrappedID),
|
||||
dataService: dataService)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let errorMessage = viewModel.errorMessage {
|
||||
VStack {
|
||||
Text(errorMessage).padding()
|
||||
if viewModel.allowRetry, viewModel.hasOriginalUrl(item) {
|
||||
Button("Open Original", action: {
|
||||
openOriginalURL(urlString: item.pageURLString)
|
||||
}).buttonStyle(RoundedRectButtonStyle())
|
||||
if let urlStr = item.pageURLString, let username = dataService.currentViewer?.username, let url = URL(string: urlStr) {
|
||||
Button("Attempt to Save Again", action: {
|
||||
viewModel.errorMessage = nil
|
||||
viewModel.saveLinkAndFetch(dataService: dataService, username: username, url: url)
|
||||
}).buttonStyle(RoundedRectButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
.opacity(progressViewOpacity)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
|
||||
progressViewOpacity = 1
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if let username = dataService.currentViewer?.username {
|
||||
await viewModel.loadContent(
|
||||
dataService: dataService,
|
||||
username: username,
|
||||
itemID: item.unwrappedID
|
||||
)
|
||||
} else {
|
||||
viewModel.errorMessage = "You are not logged in."
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if os(iOS)
|
||||
VStack(spacing: 0) {
|
||||
navBar
|
||||
Spacer()
|
||||
if showBottomBar {
|
||||
bottomButtons
|
||||
.frame(height: 48)
|
||||
.background(Color.webControlButtonBackground)
|
||||
.cornerRadius(6)
|
||||
.padding(.bottom, 34)
|
||||
.shadow(color: .gray.opacity(0.13), radius: 8, x: 0, y: 4)
|
||||
.opacity(bottomBarOpacity)
|
||||
.onAppear {
|
||||
withAnimation(Animation.linear(duration: 0.25)) { self.bottomBarOpacity = 1 }
|
||||
}
|
||||
.onDisappear {
|
||||
self.bottomBarOpacity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.onReceive(NSNotification.readerSettingsChangedPublisher) { _ in
|
||||
|
||||
@ -142,11 +142,11 @@ public extension FeaturedItemFilter {
|
||||
continueReadingPredicate, undeletedPredicate, notInArchivePredicate
|
||||
])
|
||||
case .pinned:
|
||||
let newsletterLabelPredicate = NSPredicate(
|
||||
let pinnedPredicate = NSPredicate(
|
||||
format: "SUBQUERY(labels, $label, $label.name == \"Pinned\").@count > 0"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
notInArchivePredicate, undeletedPredicate, newsletterLabelPredicate
|
||||
notInArchivePredicate, undeletedPredicate, pinnedPredicate
|
||||
])
|
||||
case .newsletters:
|
||||
// non-archived or deleted items with the Newsletter label
|
||||
|
||||
@ -29,9 +29,9 @@ struct LabelsFlowLayout: View {
|
||||
var height = CGFloat.zero
|
||||
|
||||
return ZStack(alignment: .topLeading) {
|
||||
ForEach(self.labelItems, id: \.self) { label in
|
||||
ForEach(Array(self.labelItems.enumerated()), id: \.offset) { index, label in
|
||||
self.item(for: label)
|
||||
.padding(.horizontal, 3)
|
||||
.padding(.horizontal, index == 0 ? 0 : 3)
|
||||
.padding(.vertical, 5)
|
||||
.alignmentGuide(.leading, computeValue: { dim in
|
||||
if abs(width - dim.width) > geom.size.width {
|
||||
|
||||
@ -30,7 +30,6 @@ public struct LibraryItemCard: View {
|
||||
articleInfo
|
||||
imageBox
|
||||
}
|
||||
.padding(5)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
if item.hasLabels {
|
||||
@ -196,7 +195,6 @@ public struct LibraryItemCard: View {
|
||||
byLine
|
||||
}
|
||||
.padding(0)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
|
||||
var labels: some View {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user