From 5d2262e17aa5dcca404b9aa61cd1b64893380e03 Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Sat, 4 Apr 2026 07:28:12 -0500 Subject: [PATCH] feat: auto-scroll play button at tab bar level (like Photos search icon) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved auto-scroll control from ReaderTabView to MainTabView so it sits at the tab bar level, trailing side — matching the iOS Photos search icon placement. Idle: 48px glass circle with play icon (bottom-right, tab bar row) Playing: expands to capsule with [ - ] 1.00x [ + ] [ stop ] Spring animation between states. Grid/list toggle and ellipsis menu moved inline to the sub-tab header row (next to Unread/Starred/All) so they're always visible without needing a toolbar. ReaderTabView now receives isAutoScrolling and scrollSpeed as bindings from MainTabView. Co-Authored-By: Claude Opus 4.6 (1M context) --- ios/Platform/Platform/ContentView.swift | 90 ++++- .../Features/Reader/Views/ReaderTabView.swift | 317 +++++++----------- 2 files changed, 201 insertions(+), 206 deletions(-) diff --git a/ios/Platform/Platform/ContentView.swift b/ios/Platform/Platform/ContentView.swift index c4178a7..6129106 100644 --- a/ios/Platform/Platform/ContentView.swift +++ b/ios/Platform/Platform/ContentView.swift @@ -28,6 +28,8 @@ struct MainTabView: View { @State private var showAssistant = false @State private var confettiTrigger = 0 @State private var readerVM = ReaderViewModel() + @State private var isAutoScrolling = false + @State private var scrollSpeed: Double = 1.0 private var showReader: Bool { auth.currentUser?.id != 4 // Madiha doesn't see Reader @@ -45,7 +47,7 @@ struct MainTabView: View { .tag(1) if showReader { - ReaderTabView(vm: readerVM) + ReaderTabView(vm: readerVM, isAutoScrolling: $isAutoScrolling, scrollSpeed: $scrollSpeed) .tabItem { Label("Reader", systemImage: "newspaper.fill") } .tag(2) } @@ -53,16 +55,23 @@ struct MainTabView: View { .tint(Color.accentWarm) .modifier(TabBarMinimizeModifier()) - // Floating buttons (hidden on Reader tab) - if selectedTab != 2 { - VStack { - Spacer() - HStack(alignment: .bottom) { + // Floating buttons + VStack { + Spacer() + HStack(alignment: .bottom) { + if selectedTab != 2 { FeedbackButton() .padding(.leading, 20) + } - Spacer() + Spacer() + if selectedTab == 2 { + // Reader: auto-scroll control at tab bar level + readerAutoScrollPill + .padding(.trailing, 8) + .padding(.bottom, 0) + } else { Button { showAssistant = true } label: { Image(systemName: "plus") .font(.title2.weight(.semibold)) @@ -74,8 +83,8 @@ struct MainTabView: View { } .padding(.trailing, 20) } - .padding(.bottom, 70) } + .padding(.bottom, 70) } } .confettiCannon( @@ -108,6 +117,71 @@ struct MainTabView: View { confettiTrigger += 1 UINotificationFeedbackGenerator().notificationOccurred(.success) } + + // MARK: - Reader Auto-Scroll Pill (tab bar level) + + private var readerAutoScrollPill: some View { + Group { + if isAutoScrolling { + // Expanded: speed + stop + HStack(spacing: 8) { + Button { + scrollSpeed = max(0.25, scrollSpeed - 0.25) + } label: { + Image(systemName: "minus") + .font(.caption2.weight(.bold)) + .foregroundStyle(Color.accentWarm) + .frame(width: 28, height: 28) + } + + Text(String(format: "%.2fx", scrollSpeed)) + .font(.system(size: 11, weight: .bold, design: .monospaced)) + .foregroundStyle(Color.textPrimary) + + Button { + scrollSpeed = min(3.0, scrollSpeed + 0.25) + } label: { + Image(systemName: "plus") + .font(.caption2.weight(.bold)) + .foregroundStyle(Color.accentWarm) + .frame(width: 28, height: 28) + } + + Button { + withAnimation(.spring(duration: 0.3)) { + isAutoScrolling = false + } + } label: { + Image(systemName: "stop.fill") + .font(.system(size: 11)) + .foregroundStyle(.white) + .frame(width: 32, height: 32) + .background(Color.red.opacity(0.8)) + .clipShape(Circle()) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(.ultraThinMaterial, in: Capsule()) + .transition(.scale(scale: 0.5).combined(with: .opacity)) + } else { + // Collapsed: play button circle (like Photos search icon) + Button { + withAnimation(.spring(duration: 0.3)) { + isAutoScrolling = true + } + } label: { + Image(systemName: "play.fill") + .font(.system(size: 15)) + .foregroundStyle(Color.accentWarm) + .frame(width: 48, height: 48) + .background(.ultraThinMaterial, in: Circle()) + } + .transition(.scale(scale: 0.5).combined(with: .opacity)) + } + } + .animation(.spring(duration: 0.3), value: isAutoScrolling) + } } // MARK: - Assistant Sheet diff --git a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift index 753d46d..6cb2af2 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift @@ -2,100 +2,143 @@ import SwiftUI struct ReaderTabView: View { @Bindable var vm: ReaderViewModel + @Binding var isAutoScrolling: Bool + @Binding var scrollSpeed: Double @State private var selectedSubTab = 0 @State private var showFeedSheet = false @State private var showFeedManagement = false @State private var isCardView = true - @State private var isAutoScrolling = false - @State private var scrollSpeed: Double = 1.0 var body: some View { NavigationStack { - ZStack(alignment: .bottomTrailing) { - VStack(spacing: 0) { - // Sub-tab selector - HStack(spacing: 0) { - ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in - Button { - withAnimation(.easeInOut(duration: 0.2)) { - selectedSubTab = index - switch index { - case 0: vm.applyFilter(.unread) - case 1: vm.applyFilter(.starred) - case 2: vm.applyFilter(.all) - default: break - } + VStack(spacing: 0) { + // Sub-tab selector + HStack(spacing: 0) { + ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + selectedSubTab = index + switch index { + case 0: vm.applyFilter(.unread) + case 1: vm.applyFilter(.starred) + case 2: vm.applyFilter(.all) + default: break } - } label: { - HStack(spacing: 4) { - Text(tab) - .font(.subheadline.weight(selectedSubTab == index ? .semibold : .regular)) + } + } label: { + HStack(spacing: 4) { + Text(tab) + .font(.subheadline.weight(selectedSubTab == index ? .semibold : .regular)) - if index == 0, let counters = vm.counters, counters.totalUnread > 0 { - Text("\(counters.totalUnread)") - .font(.caption2.weight(.bold)) - .foregroundStyle(.white) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.accentWarm) - .clipShape(Capsule()) - } + if index == 0, let counters = vm.counters, counters.totalUnread > 0 { + Text("\(counters.totalUnread)") + .font(.caption2.weight(.bold)) + .foregroundStyle(.white) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.accentWarm) + .clipShape(Capsule()) } - .foregroundStyle(selectedSubTab == index ? Color.accentWarm : Color.textSecondary) - .padding(.vertical, 10) - .padding(.horizontal, 16) - .background { - if selectedSubTab == index { - Capsule() - .fill(Color.accentWarm.opacity(0.12)) - } + } + .foregroundStyle(selectedSubTab == index ? Color.accentWarm : Color.textSecondary) + .padding(.vertical, 10) + .padding(.horizontal, 16) + .background { + if selectedSubTab == index { + Capsule() + .fill(Color.accentWarm.opacity(0.12)) } } } } + + Spacer() + + // Inline controls: grid/list + more menu + HStack(spacing: 4) { + Button { + withAnimation(.easeInOut(duration: 0.2)) { + isCardView.toggle() + } + } label: { + Image(systemName: isCardView ? "list.bullet" : "square.grid.2x2") + .font(.subheadline) + .foregroundStyle(Color.accentWarm) + .frame(width: 32, height: 32) + } + + Menu { + Button { + Task { await vm.markAllRead() } + } label: { + Label("Mark All Read", systemImage: "checkmark.circle") + } + + Button { + Task { await vm.refresh() } + } label: { + Label("Refresh Feeds", systemImage: "arrow.clockwise") + } + + Divider() + + Button { + showFeedManagement = true + } label: { + Label("Manage Feeds", systemImage: "list.bullet") + } + + Button { + showFeedSheet = true + } label: { + Label("Add Feed", systemImage: "plus") + } + } label: { + Image(systemName: "ellipsis") + .font(.subheadline) + .foregroundStyle(Color.accentWarm) + .frame(width: 32, height: 32) + } + } + .padding(.trailing, 8) + } + .padding(.leading) + .padding(.top, 8) + + // Feed filter bar + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + feedFilterChip("All", isSelected: isAllSelected) { + let tab = selectedSubTab + switch tab { + case 0: vm.applyFilter(.unread) + case 1: vm.applyFilter(.starred) + case 2: vm.applyFilter(.all) + default: vm.applyFilter(.unread) + } + } + + ForEach(vm.feeds) { feed in + let count = vm.counters?.count(forFeed: feed.id) ?? 0 + feedFilterChip( + feed.title, + count: selectedSubTab == 0 ? count : nil, + isSelected: vm.currentFilter == .feed(feed.id) + ) { + vm.applyFilter(.feed(feed.id)) + } + } + } .padding(.horizontal) - .padding(.top, 8) - - // Feed filter bar - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - feedFilterChip("All", isSelected: isAllSelected) { - let tab = selectedSubTab - switch tab { - case 0: vm.applyFilter(.unread) - case 1: vm.applyFilter(.starred) - case 2: vm.applyFilter(.all) - default: vm.applyFilter(.unread) - } - } - - ForEach(vm.feeds) { feed in - let count = vm.counters?.count(forFeed: feed.id) ?? 0 - feedFilterChip( - feed.title, - count: selectedSubTab == 0 ? count : nil, - isSelected: vm.currentFilter == .feed(feed.id) - ) { - vm.applyFilter(.feed(feed.id)) - } - } - } - .padding(.horizontal) - .padding(.vertical, 8) - } - .frame(height: 44) - - // Entry list - EntryListView(vm: vm, isCardView: isCardView, isAutoScrolling: $isAutoScrolling, scrollSpeed: scrollSpeed) + .padding(.vertical, 8) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.canvas) + .frame(height: 44) - // MARK: - Liquid Glass Control Bar - glassControlBar - .padding(.trailing, 16) - .padding(.bottom, 4) + // Entry list + EntryListView(vm: vm, isCardView: isCardView, isAutoScrolling: $isAutoScrolling, scrollSpeed: scrollSpeed) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.canvas) .navigationBarHidden(true) .sheet(isPresented: $showFeedSheet) { AddFeedSheet(vm: vm) @@ -115,128 +158,6 @@ struct ReaderTabView: View { } } - // MARK: - Glass Control Bar - - private var glassControlBar: some View { - HStack(spacing: 0) { - if isAutoScrolling { - // Expanded: speed controls - HStack(spacing: 12) { - Button { - scrollSpeed = max(0.25, scrollSpeed - 0.25) - } label: { - Image(systemName: "minus") - .font(.caption.weight(.bold)) - .foregroundStyle(Color.accentWarm) - .frame(width: 32, height: 32) - } - - Text(String(format: "%.2fx", scrollSpeed)) - .font(.caption.weight(.bold).monospacedDigit()) - .foregroundStyle(Color.textPrimary) - .frame(width: 44) - - Button { - scrollSpeed = min(3.0, scrollSpeed + 0.25) - } label: { - Image(systemName: "plus") - .font(.caption.weight(.bold)) - .foregroundStyle(Color.accentWarm) - .frame(width: 32, height: 32) - } - - Divider() - .frame(height: 20) - - Button { - withAnimation(.spring(duration: 0.3)) { - isAutoScrolling = false - } - } label: { - Image(systemName: "stop.fill") - .font(.caption) - .foregroundStyle(.red) - .frame(width: 32, height: 32) - } - } - .transition(.asymmetric( - insertion: .scale(scale: 0.8).combined(with: .opacity), - removal: .scale(scale: 0.8).combined(with: .opacity) - )) - } else { - // Collapsed: action buttons - HStack(spacing: 4) { - // Auto-scroll play - Button { - withAnimation(.spring(duration: 0.3)) { - isAutoScrolling = true - } - } label: { - Image(systemName: "play.fill") - .font(.caption) - .foregroundStyle(Color.accentWarm) - .frame(width: 36, height: 36) - } - - // Card/list toggle - Button { - withAnimation(.easeInOut(duration: 0.2)) { - isCardView.toggle() - } - } label: { - Image(systemName: isCardView ? "list.bullet" : "square.grid.2x2") - .font(.caption) - .foregroundStyle(Color.accentWarm) - .frame(width: 36, height: 36) - } - - // More menu - Menu { - Button { - Task { await vm.markAllRead() } - } label: { - Label("Mark All Read", systemImage: "checkmark.circle") - } - - Button { - Task { await vm.refresh() } - } label: { - Label("Refresh Feeds", systemImage: "arrow.clockwise") - } - - Divider() - - Button { - showFeedManagement = true - } label: { - Label("Manage Feeds", systemImage: "list.bullet") - } - - Button { - showFeedSheet = true - } label: { - Label("Add Feed", systemImage: "plus") - } - } label: { - Image(systemName: "ellipsis") - .font(.caption) - .foregroundStyle(Color.accentWarm) - .frame(width: 36, height: 36) - } - } - .transition(.asymmetric( - insertion: .scale(scale: 0.8).combined(with: .opacity), - removal: .scale(scale: 0.8).combined(with: .opacity) - )) - } - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(.ultraThinMaterial, in: Capsule()) - .shadow(color: .black.opacity(0.1), radius: 8, y: 2) - .animation(.spring(duration: 0.3), value: isAutoScrolling) - } - // MARK: - Helpers private var subTabs: [String] { ["Unread", "Starred", "All"] }