feat: auto-scroll play button in tab bar using Tab(role: .search)
Migrated to new Tab API (iOS 18+). The auto-scroll play button uses Tab(role: .search) with systemImage: "play.fill" — this gives the separated circular placement on the trailing side of the tab bar, identical to the Photos app search icon. Tapping the play icon: - Switches to Reader tab - Starts auto-scroll When playing: a Liquid Glass speed control capsule appears above the tab bar with [ - ] 1.00x [ + ] [ stop ]. Removed the old floating glass pill implementation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,46 +32,53 @@ struct MainTabView: View {
|
|||||||
@State private var scrollSpeed: Double = 1.0
|
@State private var scrollSpeed: Double = 1.0
|
||||||
|
|
||||||
private var showReader: Bool {
|
private var showReader: Bool {
|
||||||
auth.currentUser?.id != 4 // Madiha doesn't see Reader
|
auth.currentUser?.id != 4
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
TabView(selection: $selectedTab) {
|
TabView(selection: $selectedTab) {
|
||||||
|
Tab("Home", systemImage: "house.fill", value: 0) {
|
||||||
HomeView(selectedTab: $selectedTab)
|
HomeView(selectedTab: $selectedTab)
|
||||||
.tabItem { Label("Home", systemImage: "house.fill") }
|
}
|
||||||
.tag(0)
|
|
||||||
|
|
||||||
|
Tab("Fitness", systemImage: "flame.fill", value: 1) {
|
||||||
FitnessTabView()
|
FitnessTabView()
|
||||||
.tabItem { Label("Fitness", systemImage: "flame.fill") }
|
}
|
||||||
.tag(1)
|
|
||||||
|
|
||||||
if showReader {
|
if showReader {
|
||||||
|
Tab("Reader", systemImage: "newspaper.fill", value: 2) {
|
||||||
ReaderTabView(vm: readerVM, isAutoScrolling: $isAutoScrolling, scrollSpeed: $scrollSpeed)
|
ReaderTabView(vm: readerVM, isAutoScrolling: $isAutoScrolling, scrollSpeed: $scrollSpeed)
|
||||||
.tabItem { Label("Reader", systemImage: "newspaper.fill") }
|
}
|
||||||
.tag(2)
|
|
||||||
|
// Auto-scroll button — uses .search role for the separated
|
||||||
|
// circular placement on the trailing side of the tab bar
|
||||||
|
Tab("Auto", systemImage: "play.fill", value: 3, role: .search) {
|
||||||
|
// When tapped, toggle auto-scroll instead of showing search
|
||||||
|
Color.clear
|
||||||
|
.onAppear {
|
||||||
|
// Switch to Reader tab and start auto-scroll
|
||||||
|
selectedTab = 2
|
||||||
|
withAnimation(.spring(duration: 0.3)) {
|
||||||
|
isAutoScrolling = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Color.accentWarm)
|
.tint(Color.accentWarm)
|
||||||
.modifier(TabBarMinimizeModifier())
|
.modifier(TabBarMinimizeModifier())
|
||||||
|
|
||||||
// Floating buttons
|
// Floating buttons (hidden on Reader tab)
|
||||||
|
if selectedTab != 2 {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack(alignment: .bottom) {
|
HStack(alignment: .bottom) {
|
||||||
if selectedTab != 2 {
|
|
||||||
FeedbackButton()
|
FeedbackButton()
|
||||||
.padding(.leading, 20)
|
.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: {
|
Button { showAssistant = true } label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
.font(.title2.weight(.semibold))
|
.font(.title2.weight(.semibold))
|
||||||
@@ -83,10 +90,63 @@ struct MainTabView: View {
|
|||||||
}
|
}
|
||||||
.padding(.trailing, 20)
|
.padding(.trailing, 20)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.padding(.bottom, 70)
|
.padding(.bottom, 70)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-scroll speed controls (overlay when playing)
|
||||||
|
if isAutoScrolling && selectedTab == 2 {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
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(.system(size: 13, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundStyle(Color.textPrimary)
|
||||||
|
|
||||||
|
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(.system(size: 12))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 34, height: 34)
|
||||||
|
.background(Color.red.opacity(0.8))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(.ultraThinMaterial, in: Capsule())
|
||||||
|
.shadow(color: .black.opacity(0.1), radius: 8, y: 2)
|
||||||
|
.padding(.bottom, 70)
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||||
|
}
|
||||||
|
.animation(.spring(duration: 0.3), value: isAutoScrolling)
|
||||||
|
}
|
||||||
|
}
|
||||||
.confettiCannon(
|
.confettiCannon(
|
||||||
trigger: $confettiTrigger,
|
trigger: $confettiTrigger,
|
||||||
num: 80,
|
num: 80,
|
||||||
@@ -104,8 +164,6 @@ struct MainTabView: View {
|
|||||||
.task {
|
.task {
|
||||||
guard showReader else { return }
|
guard showReader else { return }
|
||||||
let renderer = ArticleRenderer.shared
|
let renderer = ArticleRenderer.shared
|
||||||
// Window is now available — attach WKWebView to force GPU
|
|
||||||
// compositor launch during startup, not first article tap
|
|
||||||
renderer.attachToWindow()
|
renderer.attachToWindow()
|
||||||
await readerVM.loadInitial()
|
await readerVM.loadInitial()
|
||||||
}
|
}
|
||||||
@@ -117,71 +175,6 @@ struct MainTabView: View {
|
|||||||
confettiTrigger += 1
|
confettiTrigger += 1
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
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
|
// MARK: - Assistant Sheet
|
||||||
|
|||||||
Reference in New Issue
Block a user