Structure tabviews in iOS

This commit is contained in:
Jackson Harper
2023-07-01 14:42:57 -07:00
parent 32d583e05c
commit 41443c70b5
13 changed files with 449 additions and 311 deletions

File diff suppressed because one or more lines are too long

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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 }
}
}
}

View File

@ -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()

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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