diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 8714d9b52..b454125fc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -39,26 +39,6 @@ struct FeedCardNavigationLink: View { var body: some View { ZStack { LibraryItemCard(item: item, viewer: dataService.currentViewer) -// PresentationLink({ -// <#code#> -// } label: { -// EmptyView() -// }).opacity(0) - -// public init( -// edge: Edge = .bottom, -// prefersScaleEffect: Bool = true, -// preferredCornerRadius: CGFloat? = nil, -// isInteractive: Bool = true, -// options: Options = .init(modalPresentationCapturesStatusBarAppearance: true) -// ) { -// self.edge = edge -// self.prefersScaleEffect = prefersScaleEffect -// self.preferredCornerRadius = preferredCornerRadius -// self.isInteractive = isInteractive -// self.options = options -// } -// PresentationLink( transition: PresentationLinkTransition.slide( options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, @@ -75,7 +55,7 @@ struct FeedCardNavigationLink: View { }, label: { EmptyView() } - ) + ).opacity(0) } .onAppear { Task { await viewModel.itemAppeared(item: item, dataService: dataService) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift index c107f4d10..ce76a1310 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift @@ -8,6 +8,7 @@ import Models import Services import SwiftUI +import Transmission import Views struct LibraryFeatureCardNavigationLink: View { @@ -20,12 +21,23 @@ struct LibraryFeatureCardNavigationLink: View { @State var showFeatureActions = false var body: some View { - NavigationLink(destination: LinkItemDetailView( - linkedItemObjectID: item.objectID, - isPDF: item.isPDF - )) { - LibraryFeatureCard(item: item, viewer: dataService.currentViewer) - } + PresentationLink( + transition: PresentationLinkTransition.slide( + options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, + options: + PresentationLinkTransition.Options( + modalPresentationCapturesStatusBarAppearance: true + ))), + destination: { + LinkItemDetailView( + linkedItemObjectID: item.objectID, + isPDF: item.isPDF + ) + .background(ThemeManager.currentBgColor) + }, label: { + LibraryFeatureCard(item: item, viewer: dataService.currentViewer) + } + ) .confirmationDialog("", isPresented: $showFeatureActions) { if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .pinned { Button("Unpin", action: { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 252817b29..a54bc9790 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -2,6 +2,7 @@ import CoreData import Models import Services import SwiftUI +import Transmission import UserNotifications import Utils import Views @@ -80,6 +81,11 @@ struct AnimatingCellHeight: AnimatableModifier { .onChange(of: viewModel.filterState.appliedFilter?.id) { _ in loadItems(isRefresh: true) } + .onChange(of: viewModel.presentWebContainer) { _ in + if !viewModel.presentWebContainer { + viewModel.linkRequest = nil + } + } .onChange(of: viewModel.filterState.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without @@ -143,6 +149,7 @@ struct AnimatingCellHeight: AnimatableModifier { DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { withoutAnimation { viewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID) + viewModel.presentWebContainer = true } } } @@ -177,7 +184,7 @@ struct AnimatingCellHeight: AnimatableModifier { let showDate = isListScrolled && !listTitle.isEmpty if let title = viewModel.filterState.appliedFilter?.name { Text(title) - .font(Font.system(size: showDate ? 10 : 32, weight: .semibold)) + .font(Font.system(size: showDate ? 10 : 28, weight: .semibold)) if showDate, prefersListLayout, isListScrolled || !showFeatureCards { Text(listTitle) .font(Font.system(size: 15, weight: .regular)) @@ -280,21 +287,22 @@ 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() - } + PresentationLink( + transition: PresentationLinkTransition.slide( + options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, + options: + PresentationLinkTransition.Options( + modalPresentationCapturesStatusBarAppearance: true + ))), + isPresented: $viewModel.presentWebContainer, + destination: { + WebReaderLoadingContainer(requestID: linkRequest.serverID) + .background(ThemeManager.currentBgColor) + }, label: { + EmptyView() + } + ) } - NavigationLink( - destination: LinkDestination(selectedItem: viewModel.selectedItem), - isActive: $viewModel.linkIsActive - ) { - EmptyView() - } - if prefersListLayout || !enableGrid { HomeFeedListView( listTitle: $listTitle, @@ -363,6 +371,10 @@ struct AnimatingCellHeight: AnimatableModifier { let showFeatureCards: Bool + @State var shouldScrollToTop = false + @State var topItem: Models.LibraryItem? + @ObservedObject var networkMonitor = NetworkMonitor() + init(listTitle: Binding, isListScrolled: Binding, prefersListLayout: Binding, @@ -555,8 +567,6 @@ struct AnimatingCellHeight: AnimatableModifier { static func reduce(value _: inout CGPoint, nextValue _: () -> CGPoint) {} } - @State var topItem: Models.LibraryItem? - func setTopItem(_ item: Models.LibraryItem) { print("setting top item: ", item) if let date = item.savedAt, let daysAgo = Calendar.current.dateComponents([.day], from: date, to: Date()).day { @@ -592,8 +602,6 @@ struct AnimatingCellHeight: AnimatableModifier { } } - @ObservedObject var networkMonitor = NetworkMonitor() - var body: some View { let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) VStack(spacing: 0) { @@ -603,83 +611,96 @@ struct AnimatingCellHeight: AnimatableModifier { Spacer(minLength: 2) } - List(selection: $selection) { - Section(content: { - if let appliedFilter = viewModel.filterState.appliedFilter, - networkMonitor.status == .disconnected, - !appliedFilter.allowLocalFetch - { - HStack { - Text("This search requires an internet connection.") - .padding() - .foregroundColor(Color.white) - .frame(maxWidth: .infinity, alignment: .center) - } - .background(Color.blue) - .frame(maxWidth: .infinity, alignment: .center) - .listRowSeparator(.hidden, edges: .all) - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - } else { - if showFeatureCards { - featureCard - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowSeparator(.hidden, edges: .all) - .modifier(AnimatingCellHeight(height: 190 + 13)) - .onDisappear { - withAnimation { - isListScrolled = true + ScrollViewReader { reader in + List(selection: $selection) { + Section(content: { + if let appliedFilter = viewModel.filterState.appliedFilter, + networkMonitor.status == .disconnected, + !appliedFilter.allowLocalFetch + { + HStack { + Text("This search requires an internet connection.") + .padding() + .foregroundColor(Color.white) + .frame(maxWidth: .infinity, alignment: .center) + } + .background(Color.blue) + .frame(maxWidth: .infinity, alignment: .center) + .listRowSeparator(.hidden, edges: .all) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + } else { + if showFeatureCards { + featureCard + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowSeparator(.hidden, edges: .all) + .modifier(AnimatingCellHeight(height: 190 + 13)) + .onDisappear { + withAnimation { + isListScrolled = true + } } - } - .onAppear { - withAnimation { - isListScrolled = false + .onAppear { + withAnimation { + isListScrolled = false + } } - } - } + } - ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in - FeedCardNavigationLink( - item: item, - isInMultiSelectMode: viewModel.isInMultiSelectMode, - viewModel: viewModel - ) - .background(GeometryReader { geometry in - Color.clear - .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin) - }) - .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in - print("ScrollOffsetPreferenceKey.self", value, item) - if value.y < 100, value.y > 0 { - if item.savedAt != nil, topItem != item { - setTopItem(item) + ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in + FeedCardNavigationLink( + item: item, + isInMultiSelectMode: viewModel.isInMultiSelectMode, + viewModel: viewModel + ) + .background(GeometryReader { geometry in + Color.clear + .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin) + }) + .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in + if value.y < 100, value.y > 0 { + if item.savedAt != nil, topItem != item { + setTopItem(item) + } + } + } + .listRowSeparatorTint(Color.thBorderColor) + .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) + .contextMenu { + menuItems(for: item) + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in + swipeActionButton(action: action, item: item) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in + swipeActionButton(action: action, item: item) } } } - .listRowSeparatorTint(Color.thBorderColor) - .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) - .contextMenu { - menuItems(for: item) - } - .swipeActions(edge: .leading, allowsFullSwipe: true) { - ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in - swipeActionButton(action: action, item: item) - } - } - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in - swipeActionButton(action: action, item: item) - } - } + } + }, header: { + filtersHeader + }) + } + .padding(0) + .listStyle(.plain) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .coordinateSpace(name: "scroll") + .onChange(of: shouldScrollToTop) { _ in + if shouldScrollToTop, let topItem = viewModel.fetcher.items.first { + print("READING POISTION: ", topItem) + withAnimation { // add animation for scroll to top + reader.scrollTo(topItem.unwrappedID, anchor: .top) // scroll } } - }, header: { - filtersHeader - }) + shouldScrollToTop = false + } + } + .onReceive(NotificationCenter.default.publisher(for: Notification.Name("ScrollToTop"))) { _ in + shouldScrollToTop = true } - .padding(0) - .listStyle(.plain) - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .coordinateSpace(name: "scroll") } .alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.", isPresented: $showHideFeatureAlert) { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 2ca444e94..9ec6b4de5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -18,6 +18,7 @@ import Views @Published var snoozePresented = false @Published var itemToSnoozeID: String? @Published var linkRequest: LinkRequest? + @Published var presentWebContainer = false @Published var showLoadingBar = false @Published var isInMultiSelectMode = false diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 8ec1dc4f9..fa40fa01f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -88,7 +88,7 @@ struct ProfileView: View { ToolbarItem(placement: .barLeading) { VStack(alignment: .leading) { Text(LocalText.genericProfile) - .font(Font.system(size: 32, weight: .semibold)) + .font(Font.system(size: 28, weight: .semibold)) } .frame(maxWidth: .infinity, alignment: .bottomLeading) } diff --git a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift index 43d8dbe6c..984c72a13 100644 --- a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift @@ -22,6 +22,9 @@ struct TabBarButton: View { var body: some View { Button(action: { + if selectedTab == key { + NotificationCenter.default.post(Notification(name: Notification.Name("ScrollToTop"))) + } selectedTab = key }, label: { image diff --git a/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift b/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift index 254661e64..7abfd17e1 100644 --- a/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift +++ b/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift @@ -12,6 +12,7 @@ public extension NSNotification { static let SpeakingReaderItem = Notification.Name("SpeakingReaderItem") static let DisplayProfile = Notification.Name("DisplayProfile") static let Logout = Notification.Name("Logout") + static let ScrollToTop = Notification.Name("ScrollToTop") static var pushFeedItemPublisher: NotificationCenter.Publisher { NotificationCenter.default.publisher(for: PushJSONArticle) @@ -49,6 +50,10 @@ public extension NSNotification { NotificationCenter.default.publisher(for: Logout) } + static var scrollToTopPublisher: NotificationCenter.Publisher { + NotificationCenter.default.publisher(for: ScrollToTop) + } + internal var operationMessage: String? { if let message = userInfo?["message"] as? String { return message