@@ -14,12 +14,12 @@ struct ContentView: View {
1414 @EnvironmentObject private var loginState : LoginState
1515 @State private var selection : RootDestination ? = . home
1616 #if os(macOS)
17- private let homeBoards = Board . defaultBoard ( )
18- @State private var sidebarSelection : SidebarSelection ? = Board . defaultBoard ( ) . first. map { SidebarSelection . board ( $0) } ?? . home
17+ @State private var sidebarSelection : SidebarSelection ? = . home
1918 @State private var columnVisibility : NavigationSplitViewVisibility = . all
2019 @State private var searchText = " "
2120 @StateObject private var favoritesViewModel = FavoritesViewModel ( )
2221 @StateObject private var profileViewModel = ProfileViewModel ( )
22+ @StateObject private var macHomeNavigationViewModel = HomeNavigationViewModel ( )
2323 @State private var detailTopic : Topic ?
2424 @State private var detailBoard : Board ?
2525 @State private var detailMessage : MessagePreview ?
@@ -129,6 +129,9 @@ struct ContentView: View {
129129 . onAppear {
130130 initializeSidebarIfNeeded ( )
131131 handleLoginStateChange ( isLoggedIn: loginState. isLoggedIn)
132+ Task {
133+ await macHomeNavigationViewModel. loadNavigationsIfNeeded ( )
134+ }
132135 }
133136 . onChange ( of: loginState. isLoggedIn) { _, newValue in
134137 handleLoginStateChange ( isLoggedIn: newValue, forceReload: true )
@@ -145,12 +148,18 @@ struct ContentView: View {
145148 VStack ( spacing: 0 ) {
146149 sidebarSearchBar
147150 List ( selection: $sidebarSelection) {
148- OutlineGroup ( sidebarNodes, children: \. children) { node in
149- Label ( node. title, systemImage: node. iconName)
150- . tag ( node. selection)
151- . font ( . system( . body, design: . rounded) )
152- . padding ( . vertical, 4 )
153- . padding ( . leading, 8 )
151+ ForEach ( sidebarNodes) { node in
152+ if let children = node. children, !children. isEmpty {
153+ Section {
154+ ForEach ( children) { child in
155+ sidebarRow ( for: child, isChild: true )
156+ }
157+ } header: {
158+ sidebarRow ( for: node, isChild: false )
159+ }
160+ } else {
161+ sidebarRow ( for: node, isChild: false )
162+ }
154163 }
155164 }
156165 . listStyle ( . sidebar)
@@ -243,19 +252,28 @@ struct ContentView: View {
243252 . buttonStyle ( . plain)
244253 }
245254
255+ @ViewBuilder
256+ private func sidebarRow( for node: SidebarNode , isChild: Bool ) -> some View {
257+ Label ( node. title, systemImage: node. iconName)
258+ . tag ( node. selection)
259+ . font ( . system( . body, design: . rounded) )
260+ . padding ( . vertical, 4 )
261+ . padding ( . leading, isChild ? 16 : 8 )
262+ }
263+
246264 private var sidebarNodes : [ SidebarNode ] {
247265 [
248266 SidebarNode (
249267 id: " home " ,
250268 title: " 首页 " ,
251269 iconName: " house " ,
252270 selection: . home,
253- children: filteredHomeBoards . map { board in
271+ children: filteredNavigations . map { navigation in
254272 SidebarNode (
255- id: " home.board . \( board . id) " ,
256- title: board . title ,
257- iconName: " text.justify.leading " ,
258- selection: . board ( board )
273+ id: " home.navigation . \( navigation . id) " ,
274+ title: navigation . name ,
275+ iconName: navigationIconName ( for : navigation ) ,
276+ selection: . homeNavigation ( navigation )
259277 )
260278 }
261279 ) ,
@@ -286,13 +304,28 @@ struct ContentView: View {
286304 ]
287305 }
288306
289- private var filteredHomeBoards : [ Board ] {
307+ private var filteredNavigations : [ Navigation ] {
308+ let navigations = macHomeNavigationViewModel. navigations
290309 guard searchText. isEmpty == false else {
291- return homeBoards
310+ return navigations
292311 }
293- return homeBoards. filter { board in
294- board. title. localizedCaseInsensitiveContains ( searchText) ||
295- board. name. localizedCaseInsensitiveContains ( searchText)
312+ return navigations. filter { navigation in
313+ navigation. name. localizedCaseInsensitiveContains ( searchText)
314+ }
315+ }
316+
317+ private func navigationIconName( for navigation: Navigation ) -> String {
318+ switch navigation. type {
319+ case " top " :
320+ return " flame "
321+ case " global " :
322+ return " globe "
323+ case " channel " :
324+ return " square.grid.2x2 "
325+ case " album " :
326+ return " photo.on.rectangle "
327+ default :
328+ return " list.bullet "
296329 }
297330 }
298331
@@ -301,8 +334,9 @@ struct ContentView: View {
301334 switch selection {
302335 case . home:
303336 HomeView ( )
304- case let . board( board) :
305- TopicListView ( board: board, onTopicSelected: showTopicDetail)
337+ case let . homeNavigation( navigation) :
338+ NavigationTopicColumnView ( navigation: navigation, onTopicSelected: showTopicDetail)
339+ . id ( navigation. id)
306340 case . favorites:
307341 FavoritesColumnView (
308342 viewModel: favoritesViewModel,
@@ -335,7 +369,7 @@ struct ContentView: View {
335369
336370 private func initializeSidebarIfNeeded( ) {
337371 if sidebarSelection == nil {
338- sidebarSelection = homeBoards . first . map { SidebarSelection . board ( $0 ) } ?? . home
372+ sidebarSelection = . home
339373 }
340374 }
341375
@@ -408,7 +442,7 @@ private enum RootDestination: String, CaseIterable, Hashable, Identifiable {
408442 case . mine: return " person.circle "
409443 }
410444 }
411-
445+
412446 /// iOS 平台显示的 tab 列表(不包含"我的")
413447 static var iOSCases : [ RootDestination ] {
414448 [ . home, . favorites, . messages]
@@ -418,7 +452,7 @@ private enum RootDestination: String, CaseIterable, Hashable, Identifiable {
418452#if os(macOS)
419453private enum SidebarSelection : Hashable {
420454 case home
421- case board ( Board )
455+ case homeNavigation ( Navigation )
422456 case favorites
423457 case favoriteBoards
424458 case favoriteTopics
@@ -427,6 +461,56 @@ private enum SidebarSelection: Hashable {
427461 case profile
428462}
429463
464+ private struct NavigationTopicColumnView : View {
465+ @EnvironmentObject private var browsingHistory : BrowsingHistoryStore
466+ @Environment ( \. colorScheme) private var colorScheme
467+
468+ let navigation : Navigation
469+ let onTopicSelected : ( ( Topic ) -> Void ) ?
470+
471+ @StateObject private var viewModel = NaviTopicListViewModel ( )
472+
473+ private var isAlbumView : Bool {
474+ navigation. type == " album "
475+ }
476+
477+ var body : some View {
478+ ScrollView {
479+ LazyVStack ( spacing: AppTheme . compactSpacing) {
480+ ForEach ( viewModel. topics) { topic in
481+ Button {
482+ onTopicSelected ? ( topic)
483+ } label: {
484+ if isAlbumView {
485+ AlbumTopicRowView (
486+ topic: topic,
487+ isVisited: browsingHistory. visitedTopicIDs. contains ( topic. id)
488+ )
489+ } else {
490+ TopicRowView (
491+ topic: topic,
492+ isVisited: browsingHistory. visitedTopicIDs. contains ( topic. id)
493+ )
494+ }
495+ }
496+ . buttonStyle ( . plain)
497+ . frame ( maxWidth: . infinity, alignment: . leading)
498+ . onAppear {
499+ viewModel. loadNextPageIfNeeded ( currentItem: topic)
500+ }
501+ }
502+ }
503+ . padding ( . top, AppTheme . verticalSpacing)
504+ . padding ( . horizontal, AppTheme . verticalSpacing)
505+ }
506+ . smthScaffoldBackground ( )
507+ . tint ( AppTheme . accentColor ( for: colorScheme) )
508+ . task {
509+ await viewModel. switchNavigation ( to: navigation)
510+ }
511+ }
512+ }
513+
430514private struct SidebarNode : Identifiable {
431515 let id : String
432516 let title : String
0 commit comments