Merge pull request #2756 from omnivore-app/fix/ios-ios17-fixes
iOS17 navigation fixes
This commit is contained in:
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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)
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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() }
|
||||
|
||||
@ -33,4 +33,5 @@ public enum UserDefaultKey: String {
|
||||
case userWordsPerMinute
|
||||
case hideFeatureSection
|
||||
case justifyText
|
||||
case prefersHideStatusBarInReader
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user