// // PhoneTabView.swift // readeck // // Created by Ilyas Hallak on 01.07.25. // import SwiftUI struct PhoneTabView: View { private let mainTabs: [SidebarTab] = [.all, .unread, .favorite, .archived] private let moreTabs: [SidebarTab] = [.article, .videos, .pictures, .tags, .settings] @State private var selectedTab: SidebarTab = .unread @State private var offlineBookmarksViewModel = OfflineBookmarksViewModel() // Navigation paths for each tab @State private var allPath = NavigationPath() @State private var unreadPath = NavigationPath() @State private var favoritePath = NavigationPath() @State private var archivedPath = NavigationPath() @State private var morePath = NavigationPath() // Search functionality @State private var searchViewModel = SearchBookmarksViewModel() @FocusState private var searchFieldIsFocused: Bool @EnvironmentObject var appSettings: AppSettings private var cardLayoutStyle: CardLayoutStyle { appSettings.settings?.cardLayoutStyle ?? .compact } private var offlineBookmarksBadgeCount: Int { offlineBookmarksViewModel.state.localBookmarkCount > 0 ? offlineBookmarksViewModel.state.localBookmarkCount : 0 } var body: some View { GlobalPlayerContainerView { TabView(selection: $selectedTab) { Tab(value: SidebarTab.all) { NavigationStack(path: $allPath) { tabView(for: .all) } } label: { Label(SidebarTab.all.label, systemImage: SidebarTab.all.systemImage) } Tab(value: SidebarTab.unread) { NavigationStack(path: $unreadPath) { tabView(for: .unread) } } label: { Label(SidebarTab.unread.label, systemImage: SidebarTab.unread.systemImage) } Tab(value: SidebarTab.favorite) { NavigationStack(path: $favoritePath) { tabView(for: .favorite) } } label: { Label(SidebarTab.favorite.label, systemImage: SidebarTab.favorite.systemImage) } Tab(value: SidebarTab.archived) { NavigationStack(path: $archivedPath) { tabView(for: .archived) } } label: { Label(SidebarTab.archived.label, systemImage: SidebarTab.archived.systemImage) } // iOS 26+: Dedicated search tab with role if #available(iOS 26, *) { Tab("Search", systemImage: SidebarTab.search.systemImage, value: SidebarTab.search, role: .search) { NavigationStack { moreTabContent .searchable(text: $searchViewModel.searchQuery, prompt: "Search bookmarks...") } } .badge(offlineBookmarksBadgeCount) } else { Tab(value: SidebarTab.settings) { NavigationStack(path: $morePath) { VStack(spacing: 0) { // Classic search bar for iOS 18 HStack { Image(systemName: "magnifyingglass") .foregroundColor(.gray) TextField("Search...", text: $searchViewModel.searchQuery) .focused($searchFieldIsFocused) .textFieldStyle(PlainTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) if !searchViewModel.searchQuery.isEmpty { Button(action: { searchViewModel.searchQuery = "" searchFieldIsFocused = true }) { Image(systemName: "xmark.circle.fill") .foregroundColor(.gray) } .buttonStyle(.plain) } } .padding(10) .background(Color(.systemGray6)) .cornerRadius(12) .padding([.horizontal, .top]) moreTabContent moreTabsFooter } .navigationTitle("More") } } label: { Label("More", systemImage: "ellipsis") } .badge(offlineBookmarksBadgeCount) } } .tabBarMinimizeBehaviorIfAvailable() .accentColor(.accentColor) .searchToolbarBehaviorIfAvailable() } } // MARK: - Tab Content @ViewBuilder private var moreTabContent: some View { if searchViewModel.searchQuery.isEmpty { moreTabsList } else { searchResultsView } } @ViewBuilder private var searchResultsView: some View { if searchViewModel.isLoading { ProgressView("Searching...") .padding() } else if let error = searchViewModel.errorMessage { Text(error) .foregroundColor(.red) .padding() } else if let bookmarks = searchViewModel.bookmarks?.bookmarks, !bookmarks.isEmpty { List(bookmarks) { bookmark in ZStack { // Hidden NavigationLink to remove disclosure indicator NavigationLink { BookmarkDetailView(bookmarkId: bookmark.id) } label: { EmptyView() } .opacity(0) BookmarkCardView( bookmark: bookmark, currentState: .all, layout: cardLayoutStyle, onArchive: { _ in }, onDelete: { _ in }, onToggleFavorite: { _ in } ) .contentShape(Rectangle()) } .listRowInsets(EdgeInsets( top: cardLayoutStyle == .compact ? 8 : 12, leading: 16, bottom: cardLayoutStyle == .compact ? 8 : 12, trailing: 16 )) .listRowSeparator(.hidden) .listRowBackground(Color(R.color.bookmark_list_bg)) } .scrollContentBackground(.hidden) .background(Color(R.color.bookmark_list_bg)) .listStyle(.plain) } else if searchViewModel.searchQuery.isEmpty == false { ContentUnavailableView("No results", systemImage: "magnifyingglass", description: Text("No bookmarks found.")) .padding() } } @ViewBuilder private var moreTabsList: some View { List { ForEach(moreTabs, id: \.self) { tab in NavigationLink { tabView(for: tab) .navigationTitle(tab.label) .navigationBarTitleDisplayMode(.large) } label: { Label(tab.label, systemImage: tab.systemImage) } .listRowBackground(Color(R.color.bookmark_list_bg)) } if case .idle = offlineBookmarksViewModel.state { // Don't show anything for idle state } else { Section { VStack { LocalBookmarksSyncView(state: offlineBookmarksViewModel.state) { await offlineBookmarksViewModel.syncOfflineBookmarks() } } .listRowBackground(Color.clear) .listRowInsets(EdgeInsets()) } } } .navigationTitle("More") .scrollContentBackground(.hidden) .background(Color(R.color.bookmark_list_bg)) } @ViewBuilder private var moreTabsFooter: some View { if appSettings.enableTTS { PlayerQueueResumeButton() .padding(.top, 16) } } @ViewBuilder private func tabView(for tab: SidebarTab) -> some View { switch tab { case .all: BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: .constant(nil)) case .unread: BookmarksView(state: .unread, type: [.article, .video, .photo], selectedBookmark: .constant(nil)) case .favorite: BookmarksView(state: .favorite, type: [.article, .video, .photo], selectedBookmark: .constant(nil)) case .archived: BookmarksView(state: .archived, type: [.article, .video, .photo], selectedBookmark: .constant(nil)) case .search: EmptyView() // search is directly implemented case .settings: SettingsView() case .article: BookmarksView(state: .all, type: [.article], selectedBookmark: .constant(nil)) case .videos: BookmarksView(state: .all, type: [.video], selectedBookmark: .constant(nil)) case .pictures: BookmarksView(state: .all, type: [.photo], selectedBookmark: .constant(nil)) case .tags: LabelsView(selectedTag: .constant(nil)) } } } // MARK: - View Extension for iOS 26+ Compatibility extension View { @ViewBuilder func searchToolbarBehaviorIfAvailable() -> some View { if #available(iOS 26, *) { self .searchToolbarBehavior(.minimize) } else { self } } @ViewBuilder func tabBarMinimizeBehaviorIfAvailable() -> some View { if #available(iOS 26.0, *) { self .tabBarMinimizeBehavior(.onScrollDown) } else { self } } }