Merge pull request #2756 from omnivore-app/fix/ios-ios17-fixes

iOS17 navigation fixes
This commit is contained in:
Jackson Harper
2023-09-12 16:55:46 +08:00
committed by GitHub
24 changed files with 683 additions and 417 deletions

View File

@ -82,7 +82,7 @@ struct BriefingView: View {
var body: some View {
ZStack { // Using ZStack so .task can be used on if/else body
if let item = viewModel.item {
WebReaderContainerView(item: item)
WebReaderContainerView(item: item, pop: {})
}
}
.task {

View File

@ -12,24 +12,21 @@ struct MacFeedCardNavigationLink: View {
@ObservedObject var viewModel: HomeFeedViewModel
var body: some View {
ZStack {
NavigationLink(
destination: LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
),
tag: item.objectID,
selection: $viewModel.selectedLinkItem
) {
EmptyView()
}
.opacity(0)
.buttonStyle(PlainButtonStyle())
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}
NavigationLink(
destination: LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
),
tag: item.objectID,
selection: $viewModel.selectedLinkItem
) {
LibraryItemCard(item: item, viewer: dataService.currentViewer)
}
.opacity(0)
.buttonStyle(PlainButtonStyle())
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}
}
}
@ -43,20 +40,16 @@ struct FeedCardNavigationLink: View {
var body: some View {
ZStack {
Button {
viewModel.selectedItem = item
viewModel.linkIsActive = true
} label: {
NavigationLink(destination: EmptyView()) {
EmptyView()
}
.opacity(0)
.buttonStyle(PlainButtonStyle())
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}
LibraryItemCard(item: item, viewer: dataService.currentViewer)
}
LibraryItemCard(item: item, viewer: dataService.currentViewer)
NavigationLink(destination: LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
), label: {
EmptyView()
}).opacity(0)
}
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}
}
}
@ -75,28 +68,16 @@ struct GridCardNavigationLink: View {
@ObservedObject var viewModel: HomeFeedViewModel
var body: some View {
ZStack {
Button {
if isContextMenuOpen {
isContextMenuOpen = false
} else {
viewModel.selectedItem = item
viewModel.linkIsActive = true
}
} label: {
NavigationLink(destination: EmptyView()) {
EmptyView()
}
.opacity(0)
.buttonStyle(PlainButtonStyle())
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}
GridCard(item: item, isContextMenuOpen: $isContextMenuOpen, actionHandler: actionHandler)
}
NavigationLink(destination: LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
)) {
GridCard(item: item, isContextMenuOpen: $isContextMenuOpen, actionHandler: actionHandler)
}
.aspectRatio(1.8, contentMode: .fill)
.scaleEffect(scale)
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}
.aspectRatio(1.0, contentMode: .fill)
.background(
Color.secondarySystemGroupedBackground
.onTapGesture {

View File

@ -20,49 +20,41 @@ struct LibraryFeatureCardNavigationLink: View {
@State var showFeatureActions = false
var body: some View {
ZStack {
Button {
NavigationLink(destination: LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
)) {
LibraryFeatureCard(item: item, viewer: dataService.currentViewer)
}
.confirmationDialog("", isPresented: $showFeatureActions) {
if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .pinned {
Button("Unpin", action: {
viewModel.unpinItem(dataService: dataService, item: item)
})
}
Button("Pin", action: {
viewModel.pinItem(dataService: dataService, item: item)
})
Button("Archive", action: {
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true)
})
Button("Remove", action: {
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
})
if FeaturedItemFilter(rawValue: viewModel.featureFilter) != .pinned {
Button("Mark Read", action: {
viewModel.markRead(dataService: dataService, item: item)
})
Button("Mark Unread", action: {
viewModel.markUnread(dataService: dataService, item: item)
})
}
Button("Dismiss", role: .cancel, action: {
showFeatureActions = false
viewModel.selectedItem = item
viewModel.linkIsActive = true
} label: {
NavigationLink(destination: EmptyView()) {
EmptyView()
}
.opacity(0)
.buttonStyle(PlainButtonStyle())
LibraryFeatureCard(item: item, viewer: dataService.currentViewer)
}
.confirmationDialog("", isPresented: $showFeatureActions) {
if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .pinned {
Button("Unpin", action: {
viewModel.unpinItem(dataService: dataService, item: item)
})
}
Button("Pin", action: {
viewModel.pinItem(dataService: dataService, item: item)
})
Button("Archive", action: {
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true)
})
Button("Remove", action: {
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
})
if FeaturedItemFilter(rawValue: viewModel.featureFilter) != .pinned {
Button("Mark Read", action: {
viewModel.markRead(dataService: dataService, item: item)
})
Button("Mark Unread", action: {
viewModel.markUnread(dataService: dataService, item: item)
})
}
Button("Dismiss", role: .cancel, action: {
showFeatureActions = false
})
}
.delayedGesture(LongPressGesture().onEnded { _ in
showFeatureActions = true
})
}
.delayedGesture(LongPressGesture().onEnded { _ in
showFeatureActions = true
})
}
}

View File

@ -44,121 +44,110 @@ struct AnimatingCellHeight: AnimatableModifier {
}
var body: some View {
ZStack {
if let linkRequest = viewModel.linkRequest {
NavigationLink(
destination: WebReaderLoadingContainer(requestID: linkRequest.serverID),
tag: linkRequest,
selection: $viewModel.linkRequest
) {
HomeFeedView(
listTitle: $listTitle,
isListScrolled: $isListScrolled,
prefersListLayout: $prefersListLayout,
viewModel: viewModel
)
.refreshable {
loadItems(isRefresh: true)
}
.onChange(of: viewModel.searchTerm) { _ in
// Maybe we should debounce this, but
// it feels like it works ok without
loadItems(isRefresh: true)
}
.onChange(of: viewModel.selectedLabels) { _ in
loadItems(isRefresh: true)
}
.onChange(of: viewModel.negatedLabels) { _ in
loadItems(isRefresh: true)
}
.onChange(of: viewModel.appliedFilter) { _ in
loadItems(isRefresh: true)
}
.onChange(of: viewModel.appliedSort) { _ in
loadItems(isRefresh: true)
}
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
ApplyLabelsView(mode: .item(item), onSave: nil)
}
.sheet(item: $viewModel.itemUnderTitleEdit) { item in
LinkedItemMetadataEditView(item: item)
}
.sheet(item: $viewModel.itemForHighlightsView) { item in
NotebookView(itemObjectID: item.objectID, hasHighlightMutations: $hasHighlightMutations)
}
.sheet(isPresented: $viewModel.showFiltersModal) {
NavigationView {
FilterSelectorView(viewModel: viewModel)
}
}
// .navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .barLeading) {
VStack(alignment: .leading) {
let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName
Text(title)
.font(Font.system(size: isListScrolled ? 10 : 18, weight: .semibold))
if prefersListLayout, isListScrolled {
Text(listTitle)
.font(Font.system(size: 15, weight: .regular))
.foregroundColor(Color.appGrayText)
}
}.frame(maxWidth: .infinity, alignment: .leading)
}
ToolbarItem(placement: .barTrailing) {
Button("", action: {})
.disabled(true)
.overlay {
if viewModel.isLoading, !prefersListLayout, enableGrid {
ProgressView()
}
}
}
ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) {
if enableGrid {
Button(
action: { prefersListLayout.toggle() },
label: {
Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet")
}
)
} else {
EmptyView()
}
}
HomeFeedView(
listTitle: $listTitle,
isListScrolled: $isListScrolled,
prefersListLayout: $prefersListLayout,
viewModel: viewModel
)
.refreshable {
loadItems(isRefresh: true)
}
.onChange(of: viewModel.searchTerm) { _ in
// Maybe we should debounce this, but
// it feels like it works ok without
loadItems(isRefresh: true)
}
.onChange(of: viewModel.selectedLabels) { _ in
loadItems(isRefresh: true)
}
.onChange(of: viewModel.negatedLabels) { _ in
loadItems(isRefresh: true)
}
.onChange(of: viewModel.appliedFilter) { _ in
loadItems(isRefresh: true)
}
.onChange(of: viewModel.appliedSort) { _ in
loadItems(isRefresh: true)
}
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
ApplyLabelsView(mode: .item(item), onSave: nil)
}
.sheet(item: $viewModel.itemUnderTitleEdit) { item in
LinkedItemMetadataEditView(item: item)
}
.sheet(item: $viewModel.itemForHighlightsView) { item in
NotebookView(itemObjectID: item.objectID, hasHighlightMutations: $hasHighlightMutations)
}
.sheet(isPresented: $viewModel.showFiltersModal) {
NavigationView {
FilterSelectorView(viewModel: viewModel)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .barLeading) {
VStack(alignment: .leading) {
let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName
Text(title)
.font(Font.system(size: isListScrolled ? 10 : 18, weight: .semibold))
if isListScrolled {
Text(listTitle)
.font(Font.system(size: 15, weight: .regular))
.foregroundColor(Color.appGrayText)
}
}.frame(maxWidth: .infinity, alignment: .leading)
}
ToolbarItem(placement: .barTrailing) {
Button("", action: {})
.disabled(true)
.overlay {
if viewModel.isLoading, !prefersListLayout, enableGrid {
ProgressView()
}
}
}
ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) {
if enableGrid {
Button(
action: { prefersListLayout.toggle() },
label: {
Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet")
}
)
} else {
EmptyView()
}
}
ToolbarItem(placement: .barTrailing) {
Button(
action: { searchPresented = true },
label: {
Image(systemName: "magnifyingglass")
.resizable()
.frame(width: 18, height: 18)
.padding(.vertical)
.foregroundColor(.appGrayTextContrast)
}
)
}
ToolbarItem(placement: .barTrailing) {
if UIDevice.isIPhone {
Menu(content: {
Button(action: { settingsPresented = true }, label: {
Label(LocalText.genericProfile, systemImage: "person.circle")
})
Button(action: { addLinkPresented = true }, label: {
Label("Add Link", systemImage: "plus.circle")
})
}, label: {
Image.utilityMenu
})
ToolbarItem(placement: .barTrailing) {
Button(
action: { searchPresented = true },
label: {
Image(systemName: "magnifyingglass")
.resizable()
.frame(width: 18, height: 18)
.padding(.vertical)
.foregroundColor(.appGrayTextContrast)
} else {
EmptyView()
}
)
}
ToolbarItem(placement: .barTrailing) {
if UIDevice.isIPhone {
Menu(content: {
Button(action: { settingsPresented = true }, label: {
Label(LocalText.genericProfile, systemImage: "person.circle")
})
Button(action: { addLinkPresented = true }, label: {
Label("Add Link", systemImage: "plus.circle")
})
}, label: {
Image.utilityMenu
})
.foregroundColor(.appGrayTextContrast)
} else {
EmptyView()
}
}
}
@ -173,11 +162,6 @@ struct AnimatingCellHeight: AnimatableModifier {
viewModel.selectedItem = linkedItem
viewModel.linkIsActive = true
}
.onReceive(NSNotification.pushReaderItemPublisher) { notification in
if let objectID = notification.userInfo?["objectID"] as? NSManagedObjectID {
viewModel.handleReaderItemNotification(objectID: objectID, dataService: dataService)
}
}
.onOpenURL { url in
viewModel.linkRequest = nil
if let deepLink = DeepLink.make(from: url) {
@ -244,10 +228,23 @@ struct AnimatingCellHeight: AnimatableModifier {
var body: some View {
VStack(spacing: 0) {
if let linkRequest = viewModel.linkRequest {
NavigationLink(
destination: WebReaderLoadingContainer(requestID: linkRequest.serverID),
tag: linkRequest,
selection: $viewModel.linkRequest
) {
EmptyView()
}
}
NavigationLink(destination: LinkDestination(selectedItem: viewModel.selectedItem), isActive: $viewModel.linkIsActive) {
EmptyView()
}
if prefersListLayout || !enableGrid {
HomeFeedListView(listTitle: $listTitle, isListScrolled: $isListScrolled, prefersListLayout: $prefersListLayout, viewModel: viewModel)
} else {
HomeFeedGridView(viewModel: viewModel)
HomeFeedGridView(viewModel: viewModel, isListScrolled: $isListScrolled)
}
}.sheet(isPresented: $viewModel.showLabelsSheet) {
FilterByLabelsView(
@ -312,7 +309,8 @@ struct AnimatingCellHeight: AnimatableModifier {
},
label: {
TextChipButton.makeMenuButton(
title: LinkedItemFilter(rawValue: viewModel.appliedFilter)?.displayName ?? "Filter"
title: LinkedItemFilter(rawValue: viewModel.appliedFilter)?.displayName ?? "Filter",
color: .systemGray6
)
}
)
@ -325,13 +323,12 @@ struct AnimatingCellHeight: AnimatableModifier {
},
label: {
TextChipButton.makeMenuButton(
title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort"
title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort",
color: .systemGray6
)
}
)
TextChipButton.makeAddLabelButton {
viewModel.showLabelsSheet = true
}
TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true })
ForEach(viewModel.selectedLabels, id: \.self) { label in
TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) {
viewModel.selectedLabels.removeAll { $0.id == label.id }
@ -636,6 +633,7 @@ struct AnimatingCellHeight: AnimatableModifier {
@State var isContextMenuOpen = false
@ObservedObject var viewModel: HomeFeedViewModel
@Binding var isListScrolled: Bool
func contextMenuActionHandler(item: LinkedItem, action: GridCardAction) {
switch action {
@ -673,7 +671,8 @@ struct AnimatingCellHeight: AnimatableModifier {
},
label: {
TextChipButton.makeMenuButton(
title: LinkedItemFilter(rawValue: viewModel.appliedFilter)?.displayName ?? "Filter"
title: LinkedItemFilter(rawValue: viewModel.appliedFilter)?.displayName ?? "Filter",
color: .systemGray6
)
}
)
@ -686,13 +685,12 @@ struct AnimatingCellHeight: AnimatableModifier {
},
label: {
TextChipButton.makeMenuButton(
title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort"
title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort",
color: .systemGray6
)
}
)
TextChipButton.makeAddLabelButton {
viewModel.showLabelsSheet = true
}
TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true })
ForEach(viewModel.selectedLabels, id: \.self) { label in
TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) {
viewModel.selectedLabels.removeAll { $0.id == label.id }
@ -709,16 +707,33 @@ struct AnimatingCellHeight: AnimatableModifier {
}
.listRowSeparator(.hidden)
}
.dynamicTypeSize(.small ... .accessibility1)
}
var body: some View {
ZStack {
ScrollView {
filtersHeader
.padding(.leading, 16)
.padding(.bottom, 25)
VStack(alignment: .leading) {
if viewModel.showLoadingBar {
ShimmeringLoader()
} else {
Spacer(minLength: 2)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 325), spacing: 16)], alignment: .leading, spacing: 16) {
filtersHeader
.onAppear {
withAnimation {
isListScrolled = false
}
}
.onDisappear {
withAnimation {
isListScrolled = true
}
}
.padding(.horizontal, 20)
.frame(maxHeight: 35)
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 325, maximum: 400), spacing: 16)], alignment: .center, spacing: 30) {
ForEach(viewModel.items) { item in
GridCardNavigationLink(
item: item,
@ -726,9 +741,6 @@ struct AnimatingCellHeight: AnimatableModifier {
isContextMenuOpen: $isContextMenuOpen,
viewModel: viewModel
)
.contextMenu {
libraryItemMenu(dataService: dataService, viewModel: viewModel, item: item)
}
}
Spacer()
}
@ -754,6 +766,9 @@ struct AnimatingCellHeight: AnimatableModifier {
LoadingSection()
}
}
.background(Color(.systemGroupedBackground))
Spacer()
}
}
}

View File

@ -79,35 +79,6 @@ import Views
}
}
func handleReaderItemNotification(objectID: NSManagedObjectID, dataService: DataService) {
// Pop the current selected item if needed
if selectedItem != nil, selectedItem?.objectID != objectID {
// Temporarily disable animation to avoid excessive animations
#if os(iOS)
UIView.setAnimationsEnabled(false)
#endif
linkIsActive = false
selectedItem = nil
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.selectedLinkItem = objectID
self.selectedItem = dataService.viewContext.object(with: objectID) as? LinkedItem
self.linkIsActive = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
#if os(iOS)
UIView.setAnimationsEnabled(true)
#endif
}
} else {
selectedLinkItem = objectID
selectedItem = dataService.viewContext.object(with: objectID) as? LinkedItem
linkIsActive = true
}
}
func itemAppeared(item: LinkedItem, dataService: DataService) async {
if isLoading { return }
let itemIndex = items.firstIndex(where: { $0.id == item.id })

View File

@ -38,23 +38,21 @@ struct LibraryListView: View {
)
var body: some View {
ZStack {
NavigationLink(
destination: LinkDestination(selectedItem: libraryViewModel.selectedItem),
isActive: $libraryViewModel.linkIsActive
) {
EmptyView()
}
HomeView(viewModel: libraryViewModel)
.tabItem {
Label {
Text("Library")
} icon: {
Image.tabLibrary
}
// ZStack {
// NavigationLink(
// destination: LinkDestination(selectedItem: libraryViewModel.selectedItem),
// isActive: $libraryViewModel.linkIsActive
// ) {
// EmptyView()
// }
HomeView(viewModel: libraryViewModel)
.tabItem {
Label {
Text("Library")
} icon: {
Image.tabLibrary
}
}
.navigationViewStyle(.stack)
.navigationBarTitleDisplayMode(.inline)
}
// }
}
}

View File

@ -43,45 +43,14 @@ struct LibraryTabView: View {
)
var body: some View {
NavigationView {
ZStack {
NavigationLink(
destination: LinkDestination(selectedItem: libraryViewModel.selectedItem),
isActive: $libraryViewModel.linkIsActive
) {
EmptyView()
}
// TabView(selection: $selection) {
// BriefingView(
// articleId: "98e017a3-79d5-4049-97bc-ff170153792a"
// )
// .tabItem {
// Label {
// Text("Your Briefing")
// } icon: {
// Image.tabBriefing.padding(.trailing, 5)
// }
// }.tag(0)
if #available(iOS 16.0, *) {
NavigationView {
HomeView(viewModel: libraryViewModel)
// .tabItem {
// Label {
// Text("Library")
// } icon: {
// Image.tabLibrary
// }
// }.tag(1)
// HomeView(viewModel: highlightsViewModel)
// .tabItem {
// Label {
// Text("Highlights")
// } icon: {
// Image.tabHighlights
// }
// }.tag(2)
// }
.navigationBarHidden(false)
}
} else {
// Fallback on earlier versions
EmptyView()
}
}
}

View File

@ -75,25 +75,29 @@ struct LinkItemDetailView: View {
@StateObject private var viewModel = LinkItemDetailViewModel()
@State var isEnabled = true
@Environment(\.dismiss) var dismiss
init(linkedItemObjectID: NSManagedObjectID, isPDF: Bool) {
self.linkedItemObjectID = linkedItemObjectID
self.isPDF = isPDF
}
var body: some View {
ZStack { // Using ZStack so .task can be used on if/else body
ZStack {
if isPDF {
pdfContainerView
} else if let item = viewModel.item {
WebReaderContainerView(item: item)
WebReaderContainerView(item: item, pop: { dismiss() })
.navigationBarHidden(true)
.lazyPop(pop: {
dismiss()
}, isEnabled: $isEnabled)
}
}
.task {
await viewModel.loadItem(linkedItemObjectID: linkedItemObjectID, dataService: dataService)
}
#if os(iOS)
.navigationBarHidden(true)
#endif
}
@ViewBuilder private var pdfContainerView: some View {
@ -113,17 +117,3 @@ struct LinkItemDetailView: View {
}
}
}
#if os(iOS)
// Enable swipe to go back behavior if nav bar is hidden
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_: UIGestureRecognizer) -> Bool {
viewControllers.count > 1
}
}
#endif

View File

@ -19,6 +19,8 @@ import Views
return AnyView(splitView)
} else {
return AnyView(LibraryTabView())
// .navigationViewStyle(.stack)
// .navigationBarTitleDisplayMode(.inline)
}
#else
return AnyView(splitView)
@ -44,6 +46,7 @@ import Views
// Second column is the Primary Nav Stack
PrimaryContentCategory.feed.destinationView
}
.navigationBarTitleDisplayMode(.inline)
.accentColor(.appGrayTextContrast)
.introspectSplitViewController {
$0.preferredSplitBehavior = .tile

View File

@ -49,22 +49,22 @@ struct InnerRootView: View {
@ViewBuilder private var innerBody: some View {
if authenticator.isLoggedIn {
GeometryReader { geo in
PrimaryContentView()
#if os(iOS)
.miniPlayer()
.formSheet(isPresented: $viewModel.showNewFeaturePrimer,
modalSize: CGSize(width: geo.size.width * 0.66, height: geo.size.width * 0.66)) {
FeaturePrimer.recommendationsPrimer
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
viewModel.showNewFeaturePrimer = viewModel.shouldShowNewFeaturePrimer
viewModel.shouldShowNewFeaturePrimer = false
}
}
#endif
}
// GeometryReader { geo in
PrimaryContentView()
// #if os(iOS)
// .miniPlayer()
// .formSheet(isPresented: $viewModel.showNewFeaturePrimer,
// modalSize: CGSize(width: geo.size.width * 0.66, height: geo.size.width * 0.66)) {
// FeaturePrimer.recommendationsPrimer
// }
// .onAppear {
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
// viewModel.showNewFeaturePrimer = viewModel.shouldShowNewFeaturePrimer
// viewModel.shouldShowNewFeaturePrimer = false
// }
// }
// #endif
// }
} else {
WelcomeView()
.accessibilityElement()

View File

@ -0,0 +1,63 @@
//
// SlideAnimatedTransitioning.swift
// SwipeRightToPopController
//
// Created by Warif Akhand Rishi on 2/19/16.
// Copyright © 2016 Warif Akhand Rishi. All rights reserved.
//
import UIKit
class SlideAnimatedTransitioning: NSObject {}
extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {
return
}
let width = containerView.frame.width
var offsetLeft = fromVC.view?.frame
offsetLeft?.origin.x = width
var offscreenRight = fromVC.view?.frame
offscreenRight?.origin.x = -width / 3.33
toVC.view?.frame = offscreenRight!
fromVC.view?.layer.shadowRadius = 5.0
fromVC.view?.layer.shadowOpacity = 1.0
toVC.view?.layer.opacity = 0.9
transitionContext.containerView.addSubview(toVC.view)
transitionContext.containerView.addSubview(fromVC.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: {
toVC.view?.frame = (fromVC.view?.frame)!
fromVC.view?.frame = offsetLeft!
toVC.view?.layer.opacity = 1.0
fromVC.view?.layer.shadowOpacity = 0.1
}, completion: { _ in
toVC.view?.layer.opacity = 1.0
toVC.view?.layer.shadowOpacity = 0
fromVC.view?.layer.opacity = 1.0
fromVC.view?.layer.shadowOpacity = 0
fromVC.view.removeFromSuperview()
// when cancelling or completing the animation, ios simulator seems to sometimes flash black backgrounds during the animation. on devices, this doesn't seem to happen though.
// containerView.backgroundColor = [UIColor whiteColor];
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
0.3
}
}

View File

@ -0,0 +1,187 @@
//
// SwipeRightToPopViewController.swift
// SwipeRightToPopController
//
// Created by Warif Akhand Rishi on 2/19/16.
// Copyright © 2016 Warif Akhand Rishi. All rights reserved.
//
// Modified by Joseph Hinkle on 12/1/19.
// Modified version allows use in SwiftUI by subclassing UIHostingController.
// Copyright © 2019 Joseph Hinkle. All rights reserved.
//
import SwiftUI
private func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (lll?, rrr?):
return lll < rrr
case (nil, _?):
return true
default:
return false
}
}
private func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (lll?, rrr?):
return lll > rrr
default:
return rhs < lhs
}
}
class SwipeRightToPopViewController<Content>: UIHostingController<Content>, UINavigationControllerDelegate where Content: View {
fileprivate var pop: (() -> Void)?
fileprivate var lazyPopContent: LazyPop<Content>?
private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition?
private var panGestureRecognizer: UIPanGestureRecognizer!
private var parentNavigationControllerToUse: UINavigationController?
private var gestureAdded = false
override func viewDidLayoutSubviews() {
// You need to add gesture events after every subview layout to protect against weird edge cases
// One notable edgecase is if you are in a splitview in landscape. In this case, there will be
// no nav controller with 2 vcs, so our addGesture will fail. After rotating back to portrait,
// the splitview will combine into one view with the details pushed on top. So only then would
// would the addGesture find a parent nav controller with 2 view controllers. I don't know if
// there are other edge cases, but running addGesture on every viewDidLayoutSubviews seems safe.
addGesture()
}
public func addGesture() {
if !gestureAdded {
// attempt to find a parent navigationController
var currentVc: UIViewController = self
while true {
if currentVc.navigationController != nil,
currentVc.navigationController?.viewControllers.count > 1
{
parentNavigationControllerToUse = currentVc.navigationController
break
}
guard let parent = currentVc.parent else {
return
}
currentVc = parent
}
guard parentNavigationControllerToUse?.viewControllers.count > 1 else {
return
}
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(SwipeRightToPopViewController.handlePanGesture(_:)))
view.addGestureRecognizer(panGestureRecognizer)
gestureAdded = true
}
}
@objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
// if the parentNavigationControllerToUse has a width value, use that because it's more accurate. Otherwise use this view's width as a backup
let total = parentNavigationControllerToUse?.view.frame.width ?? view.frame.width
let percent = max(panGesture.translation(in: view).x, 0) / total
switch panGesture.state {
case .began:
if lazyPopContent?.isEnabled == true {
parentNavigationControllerToUse?.delegate = self
if let pop = self.pop {
pop()
}
}
case .changed:
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
percentDrivenInteractiveTransition.update(percent)
}
case .ended:
let velocity = panGesture.velocity(in: view).x
// Continue if drag more than 50% of screen width or velocity is higher than 100
if percent > 0.5 || velocity > 100 {
percentDrivenInteractiveTransition?.finish()
} else {
percentDrivenInteractiveTransition?.cancel()
}
case .cancelled, .failed:
percentDrivenInteractiveTransition?.cancel()
default:
break
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func navigationController(_: UINavigationController,
animationControllerFor _: UINavigationController.Operation,
from _: UIViewController,
to _: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
if #available(iOS 17.0, *) {
return nil
} else if UIDevice.isIPad {
return nil
} else {
return SlideAnimatedTransitioning()
}
}
func navigationController(_: UINavigationController,
interactionControllerFor _: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning?
{
parentNavigationControllerToUse?.delegate = nil
// navigationController.delegate = nil
if panGestureRecognizer.state == .began {
percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()
percentDrivenInteractiveTransition?.completionCurve = .easeOut
} else {
percentDrivenInteractiveTransition = nil
}
return percentDrivenInteractiveTransition
}
}
//
// Lazy Pop SwiftUI Component
//
// Created by Joseph Hinkle on 12/1/19.
// Copyright © 2019 Joseph Hinkle. All rights reserved.
//
private struct LazyPop<Content: View>: UIViewControllerRepresentable {
let rootView: Content
let pop: () -> Void
@Binding var isEnabled: Bool
init(_ rootView: Content, pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) {
self.rootView = rootView
self.pop = pop
self._isEnabled = isEnabled ?? Binding<Bool>(get: { true }, set: { _ in })
}
func makeUIViewController(context _: Context) -> UIViewController {
let vc = SwipeRightToPopViewController(rootView: rootView)
vc.pop = pop
vc.lazyPopContent = self
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context _: Context) {
if let host = uiViewController as? UIHostingController<Content> {
host.rootView = rootView
}
}
}
public extension View {
func lazyPop(pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) -> some View {
LazyPop(self, pop: pop, isEnabled: isEnabled)
}
}

View File

@ -10,6 +10,7 @@ import WebKit
// swiftlint:disable file_length type_body_length
struct WebReaderContainerView: View {
let item: LinkedItem
let pop: () -> Void
@State private var showPreferencesPopover = false
@State private var showPreferencesFormsheet = false
@ -40,9 +41,11 @@ struct WebReaderContainerView: View {
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@Environment(\.openURL) var openURL
@StateObject var viewModel = WebReaderViewModel()
@Environment(\.dismiss) var dismiss
@AppStorage(UserDefaultKey.prefersHideStatusBarInReader.rawValue) var prefersHideStatusBarInReader = false
func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) {
if let replyHandler = replyHandler {
@ -271,7 +274,9 @@ struct WebReaderContainerView: View {
HStack(alignment: .center, spacing: 10) {
#if os(iOS)
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
action: {
pop()
},
label: {
Image.chevronRight
.padding(.horizontal, 10)
@ -422,6 +427,7 @@ struct WebReaderContainerView: View {
showHighlightAnnotationModal: $showHighlightAnnotationModal
)
.background(ThemeManager.currentBgColor)
.statusBar(hidden: prefersHideStatusBarInReader)
.onAppear {
if item.isUnread {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0)
@ -620,7 +626,7 @@ struct WebReaderContainerView: View {
func archive() {
dataService.archiveLink(objectID: item.objectID, archived: !item.isArchived)
#if os(iOS)
presentationMode.wrappedValue.dismiss()
pop()
#endif
}
@ -651,7 +657,7 @@ struct WebReaderContainerView: View {
removeLibraryItemAction(dataService: dataService, objectID: item.objectID)
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
presentationMode.wrappedValue.dismiss()
pop()
}
#endif
}

View File

@ -34,8 +34,11 @@ import Views
public struct WebReaderLoadingContainer: View {
let requestID: String
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@State var lazyPopIsEnabled = true
@StateObject var viewModel = WebReaderLoadingContainerViewModel()
public var body: some View {
@ -53,10 +56,11 @@ public struct WebReaderLoadingContainer: View {
}
#endif
} else {
WebReaderContainerView(item: item)
WebReaderContainerView(item: item, pop: { dismiss() })
#if os(iOS)
.navigationBarHidden(true)
.navigationViewStyle(.stack)
.navigationBarHidden(true)
.lazyPop(pop: { dismiss() }, isEnabled: $lazyPopIsEnabled)
#endif
.accentColor(.appGrayTextContrast)
.task { viewModel.trackReadEvent() }

View File

@ -33,4 +33,5 @@ public enum UserDefaultKey: String {
case userWordsPerMinute
case hideFeatureSection
case justifyText
case prefersHideStatusBarInReader
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF9",
"green" : "0xF9",
"red" : "0xF9"
"blue" : "0xF5",
"green" : "0xF5",
"red" : "0xF5"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF7",
"green" : "0xF2",
"red" : "0xF2"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF7",
"green" : "0xF2",
"red" : "0xF2"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -69,20 +69,67 @@ public struct GridCard: View {
}
}
var imageBox: some View {
GeometryReader { geo in
ZStack(alignment: .bottomLeading) {
if let imageURL = item.imageURL {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
Color.systemBackground
.frame(maxWidth: .infinity, maxHeight: geo.size.height)
case let .success(image):
image.resizable()
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: geo.size.height)
.clipped()
case .failure:
fallbackImage
@unknown default:
// Since the AsyncImagePhase enum isn't frozen,
// we need to add this currently unused fallback
// to handle any new cases that might be added
// in the future:
Color.systemBackground
.frame(maxWidth: .infinity, maxHeight: geo.size.height)
}
}
} else {
fallbackImage
}
Color(hex: "#D9D9D9")?.opacity(0.65).frame(width: geo.size.width, height: 5)
Color(hex: "#FFD234").frame(width: geo.size.width * (item.readingProgress / 100), height: 5)
}
}
.cornerRadius(5)
}
var fallbackImage: some View {
GeometryReader { geo in
HStack {
Text(item.unwrappedTitle.prefix(1))
.font(Font.system(size: 128, weight: .bold))
.offset(CGSize(width: -48, height: 12))
.frame(alignment: .bottomLeading)
.foregroundColor(Gradient.randomColor(str: item.unwrappedTitle, offset: 1))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Gradient.randomColor(str: item.unwrappedTitle, offset: 0))
.background(LinearGradient(gradient: Gradient(fromStr: item.unwrappedTitle)!, startPoint: .top, endPoint: .bottom))
.frame(width: geo.size.width, height: geo.size.height)
}
}
public var body: some View {
GeometryReader { geo in
VStack(alignment: .leading, spacing: 0) {
// Progress Bar
Group {
ProgressView(value: min(abs(item.readingProgress) / 100, 1))
.tint(.appYellow48)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 16)
}
// .onTapGesture { tapHandler() }
VStack {
// Title, Subtitle, Menu Button
imageBox
.frame(height: geo.size.height / 2.0)
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(item.unwrappedTitle)
@ -108,42 +155,23 @@ public struct GridCard: View {
Spacer()
}
// .onTapGesture { tapHandler() }
}
.frame(height: 30)
.padding(.horizontal)
.padding(.bottom, 16)
.padding(.horizontal, 10)
.padding(.bottom, 10)
.padding(.top, 10)
// Link description and image
HStack(alignment: .top) {
Text(item.descriptionText ?? item.unwrappedTitle)
.font(.appSubheadline)
.foregroundColor(.appGrayTextContrast)
.lineLimit(nil)
.lineLimit(3)
.multilineTextAlignment(.leading)
Spacer()
if let imageURL = item.imageURL {
AsyncImage(url: imageURL) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width / 3, height: (geo.size.width * 2) / 9)
.cornerRadius(3)
} else if phase.error != nil {
EmptyView()
} else {
Color.appButtonBackground
.frame(width: geo.size.width / 3, height: (geo.size.width * 2) / 9)
.cornerRadius(3)
}
}
}
}
.padding(.horizontal)
// .onTapGesture { tapHandler() }
.padding(.horizontal, 10)
// Category Labels
if item.hasLabels {
@ -154,18 +182,12 @@ public struct GridCard: View {
}
Spacer()
}
.padding(.horizontal)
.padding(.horizontal, 10)
}
// .onTapGesture { tapHandler() }
}
if item.serverSyncStatus != ServerSyncStatus.isNSync.rawValue {
SyncStatusIcon(status: ServerSyncStatus(rawValue: Int(item.serverSyncStatus)) ?? ServerSyncStatus.isNSync)
}
}
.padding(.horizontal, 0)
.padding(.top, 0)
.padding(.bottom, 8)
}
.contextMenu { contextMenuView }
}

View File

@ -36,7 +36,8 @@ public struct LibraryItemCard: View {
labels
}
}
.padding(.bottom, 5)
.padding(5)
.padding(.top, 10)
.draggableItem(item: item)
.dynamicTypeSize(.xSmall ... .accessibility1)
}
@ -149,25 +150,42 @@ public struct LibraryItemCard: View {
}
var imageBox: some View {
Group {
ZStack(alignment: .bottomLeading) {
if let imageURL = item.imageURL {
AsyncImage(url: imageURL) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 40, height: 40)
.frame(width: 50, height: 75)
.cornerRadius(5)
.padding(.top, 2)
} else {
Color.systemBackground
.frame(width: 40, height: 40)
.frame(width: 50, height: 75)
.cornerRadius(5)
.padding(.top, 2)
}
}
} else {
fallbackImage
}
}
.padding(.top, 10)
.cornerRadius(5)
}
var fallbackImage: some View {
HStack {
Text(item.unwrappedTitle.prefix(1))
.font(Font.system(size: 32, weight: .bold))
.frame(alignment: .bottomLeading)
.foregroundColor(Gradient.randomColor(str: item.unwrappedTitle, offset: 1))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Gradient.randomColor(str: item.unwrappedTitle, offset: 0))
.background(LinearGradient(gradient: Gradient(fromStr: item.unwrappedTitle)!, startPoint: .top, endPoint: .bottom))
.frame(width: 50, height: 50)
}
var bylineStr: String {

View File

@ -28,7 +28,7 @@ public struct LibraryItemLabelView: View {
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.isDarkMode ? Color.themeLabelBackground : Color.themeLabelOutline, lineWidth: 1)
.stroke(Color.isDarkMode ? Color.themeLabelBackground : Color.themeLabelBackground, lineWidth: 1)
)
}
}

View File

@ -90,6 +90,7 @@ public enum WebFont: String, CaseIterable {
@AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue
@AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true
@AppStorage(UserDefaultKey.justifyText.rawValue) var justifyText = false
@AppStorage(UserDefaultKey.prefersHideStatusBarInReader.rawValue) var prefersHideStatusBar = false
public init(
updateReaderPreferences: @escaping () -> Void,
@ -151,6 +152,13 @@ public enum WebFont: String, CaseIterable {
updateReaderPreferences()
}
Toggle("Hide Status Bar", isOn: $prefersHideStatusBar)
.frame(height: 40)
.padding(.trailing, 6)
.onChange(of: prefersHideStatusBar) { _ in
updateReaderPreferences()
}
Spacer()
}
.padding(.horizontal, 30)

View File

@ -92,12 +92,12 @@ public struct TextChip: View {
}
public struct TextChipButton: View {
public static func makeAddLabelButton(onTap: @escaping () -> Void) -> TextChipButton {
TextChipButton(title: LocalText.labelsGeneric, color: .systemGray6, actionType: .show, negated: false, onTap: onTap)
public static func makeAddLabelButton(color: Color, onTap: @escaping () -> Void) -> TextChipButton {
TextChipButton(title: LocalText.labelsGeneric, color: color, actionType: .show, negated: false, onTap: onTap)
}
public static func makeMenuButton(title: String) -> TextChipButton {
TextChipButton(title: title, color: .systemGray6, actionType: .show, negated: false, onTap: {})
public static func makeMenuButton(title: String, color: Color) -> TextChipButton {
TextChipButton(title: title, color: color, actionType: .show, negated: false, onTap: {})
}
public static func makeSearchFilterButton(title: String, onTap: @escaping () -> Void) -> TextChipButton {

View File

@ -1,12 +1,12 @@
#if os(iOS)
import UIKit
extension UINavigationController {
// Remove back button text
override open func viewWillLayoutSubviews() {
navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}
#endif
// #if os(iOS)
//
// import UIKit
//
// extension UINavigationController {
// // Remove back button text
// override open func viewWillLayoutSubviews() {
// navigationBar.topItem?.backButtonDisplayMode = .minimal
// }
// }
//
// #endif