diff --git a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift index 106d992..9cb7ec1 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift @@ -11,131 +11,185 @@ struct ReaderTabView: View { var body: some View { NavigationStack { - VStack(spacing: 0) { - // Sub-tab selector — ALWAYS rendered (prevents layout jitter) - 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 + ZStack(alignment: .bottom) { + 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)) + } } } } } - } - .padding(.horizontal) - .padding(.top, 8) - - // Feed filter bar — ALWAYS rendered, empty during load (stable height) - 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) + .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) } - .frame(height: 44) // Fixed height prevents layout shift + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.canvas) - // Entry list - EntryListView(vm: vm, isCardView: isCardView, isAutoScrolling: $isAutoScrolling, scrollSpeed: scrollSpeed) + // MARK: - Liquid Glass Control Bar + glassControlBar + .padding(.bottom, 2) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.canvas) .navigationBarHidden(true) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - // Auto-scroll controls - HStack(spacing: 8) { - if isAutoScrolling { - Button { - scrollSpeed = max(0.25, scrollSpeed - 0.25) - } label: { - Image(systemName: "minus") - .font(.caption2.weight(.bold)) - .foregroundStyle(Color.accentWarm) - } + .sheet(isPresented: $showFeedSheet) { + AddFeedSheet(vm: vm) + } + .sheet(isPresented: $showFeedManagement) { + FeedManagementSheet(vm: vm) + } + } + .onAppear { + ArticleRenderer.shared.reWarmIfNeeded() + } + .onChange(of: selectedSubTab) { _, _ in + isAutoScrolling = false + } + .onChange(of: vm.currentFilter) { _, _ in + isAutoScrolling = false + } + } - Text(String(format: "%.2fx", scrollSpeed)) - .font(.caption.weight(.semibold).monospacedDigit()) - .foregroundStyle(Color.textPrimary) - .frame(width: 42) + // MARK: - Glass Control Bar - Button { - scrollSpeed = min(3.0, scrollSpeed + 0.25) - } label: { - Image(systemName: "plus") - .font(.caption2.weight(.bold)) - .foregroundStyle(Color.accentWarm) - } - } - - Button { - isAutoScrolling.toggle() - } label: { - Image(systemName: isAutoScrolling ? "stop.fill" : "play.fill") - .foregroundStyle(isAutoScrolling ? .red : Color.accentWarm) + 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) } } - ToolbarItem(placement: .topBarTrailing) { + .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) } - } - ToolbarItem(placement: .topBarTrailing) { + + // More menu Menu { Button { Task { await vm.markAllRead() } @@ -163,29 +217,27 @@ struct ReaderTabView: View { Label("Add Feed", systemImage: "plus") } } label: { - Image(systemName: "ellipsis.circle") + Image(systemName: "ellipsis") + .font(.caption) .foregroundStyle(Color.accentWarm) + .frame(width: 36, height: 36) } } - } - .sheet(isPresented: $showFeedSheet) { - AddFeedSheet(vm: vm) - } - .sheet(isPresented: $showFeedManagement) { - FeedManagementSheet(vm: vm) + .transition(.asymmetric( + insertion: .scale(scale: 0.8).combined(with: .opacity), + removal: .scale(scale: 0.8).combined(with: .opacity) + )) } } - .onAppear { - ArticleRenderer.shared.reWarmIfNeeded() - } - .onChange(of: selectedSubTab) { _, _ in - isAutoScrolling = false - } - .onChange(of: vm.currentFilter) { _, _ in - isAutoScrolling = false - } + .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"] } private var isAllSelected: Bool {