From f6a2e1ee799f41e0ad9697a0d8b2016211932604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=B4=E9=93=AD?= Date: Tue, 19 Nov 2024 20:11:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=87=E5=8D=97=E7=9A=84list=E8=83=BD?= =?UTF-8?q?=E5=A4=9F=E8=AE=B0=E5=BD=95=E5=B1=95=E5=BC=80=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/SwiftPamphletAppConfig.swift | 3 + .../Guide/View/GuideListView.swift | 181 ++++++++++-------- SwiftPamphletApp/HomeUI/DataLink.swift | 35 +++- SwiftPamphletApp/HomeUI/HomeView.swift | 38 +++- 4 files changed, 159 insertions(+), 98 deletions(-) diff --git a/SwiftPamphletApp/App/SwiftPamphletAppConfig.swift b/SwiftPamphletApp/App/SwiftPamphletAppConfig.swift index 752730ba7..ff5431352 100644 --- a/SwiftPamphletApp/App/SwiftPamphletAppConfig.swift +++ b/SwiftPamphletApp/App/SwiftPamphletAppConfig.swift @@ -29,6 +29,9 @@ struct SPC { static let isShowWWDCInspector = "isShowWWDCInspector" static let inspectorType = "inspectorType" + // MARK: GuideListView + static let expandedGuideItems = "expandedGuideItems" + // static func loadCustomIssues(jsonFileName: String) -> [CustomIssuesModel] { // let lc: [CustomIssuesModel] = SMFile.loadBundleJSONFile(jsonFileName + ".json") // return lc diff --git a/SwiftPamphletApp/Guide/View/GuideListView.swift b/SwiftPamphletApp/Guide/View/GuideListView.swift index 976d0b878..9a702ed10 100644 --- a/SwiftPamphletApp/Guide/View/GuideListView.swift +++ b/SwiftPamphletApp/Guide/View/GuideListView.swift @@ -12,94 +12,121 @@ import SMFile struct GuideListView: View { @Query(BookmarkModel.all) var bookmarks: [BookmarkModel] @State private var apBookmarks: [String] = [String]() - @State var listModel = GuideListModel() - @State private var limit: Int = 50 - @State private var trigger = false // 触发列表书签状态更新 + @State var listModel: GuideListModel + @Binding var selectedItem: L? // 改为 L? 类型 + @AppStorage(SPC.expandedGuideItems) private var expandedItemsString: String = "" + @State private var expandedItems: Set = [] var body: some View { - if listModel.searchText.isEmpty == false { - HStack { - Text("搜索”\(listModel.searchText)“结果如下") - Button { - listModel.searchText = "" - } label: { - Image(systemName: "xmark.circle") + List(selection: $selectedItem) { + ForEach(listModel.filtered()) { item in + OutlineGroup( + item: item, + bookmarks: apBookmarks, + selectedItem: $selectedItem, + expandedItems: $expandedItems + ) + } + } + .searchable( + text: $listModel.searchText, + prompt: "搜索 Apple 技术手册" + ) + #if os(macOS) + .listStyle(.sidebar) + #endif + .onAppear { + // 读取保存的展开状态 + expandedItems = Set(expandedItemsString.split(separator: ",").map(String.init)) + print("Loaded expanded items: \(expandedItems.count)") // Debug + updateApBookmarks() + } + .onChange(of: expandedItems) { _, newValue in + // 保存展开状态 + expandedItemsString = newValue.joined(separator: ",") + print("Saved expanded items: \(newValue.count)") // Debug + } + .overlay { + if listModel.filtered().isEmpty { + ContentUnavailableView { + Label("无结果", systemImage: "rectangle.and.text.magnifyingglass") + } description: { + Text("请再次输入") } } - .padding(.top, 10) } - NavigationStack { - SPOutlineListView(d: listModel.filtered(), c: \.sub) { i in - NavigationLink( - destination: GuideDetailView( - t: i.t, - icon: i.icon, - plName: listModel.plName, - limit: $limit, - trigger: $trigger - ) + } + + private struct OutlineGroup: View { + let item: L + let bookmarks: [String] + @Binding var selectedItem: L? + @Binding var expandedItems: Set + + var body: some View { + if let subItems = item.sub { + DisclosureGroup( + isExpanded: expandedBinding(for: item.t, in: $expandedItems) ) { - HStack(spacing:3) { - if i.icon.isEmpty == false { - Image(systemName: i.icon) - .foregroundStyle(i.sub == nil ? Color.secondary : .indigo) - } else if i.sub != nil { - Image(systemName: "folder.fill") - .foregroundStyle(.indigo) - } - Text(listModel.searchText.isEmpty == true ? GuideListModel.simpleTitle(i.t) : i.t) - Spacer() - if apBookmarks.contains(i.t) { - Image(systemName: "bookmark") - .foregroundStyle(.secondary) - .font(.footnote) - } + ForEach(subItems) { subItem in + OutlineGroup( + item: subItem, + bookmarks: bookmarks, + selectedItem: $selectedItem, + expandedItems: $expandedItems + ) } - .contentShape(Rectangle()) + } label: { + ItemRow(item: item, bookmarks: bookmarks) + .tag(item) } + } else { + ItemRow(item: item, bookmarks: bookmarks) + .tag(item) } - .searchable( - text: $listModel.searchText, - prompt: "搜索 Apple 技术手册" - ) - #if os(macOS) - .listStyle(.sidebar) - #endif - .onChange(of: trigger, { oldValue, newValue in - updateApBookmarks() - }) - .onAppear(perform: { - updateApBookmarks() - //导出内容 - // listModel.buildMDContent() - - }) - .overlay { - if listModel.filtered().isEmpty { - ContentUnavailableView { - Label( - "无结果", - systemImage: "rectangle.and.text.magnifyingglass" - ) - } description: { - Text("请再次输入") - } + } + } + + private static func expandedBinding(for id: String, in expandedItems: Binding>) -> Binding { + Binding( + get: { expandedItems.wrappedValue.contains(id) }, + set: { isExpanded in + if isExpanded { + expandedItems.wrappedValue.insert(id) + } else { + expandedItems.wrappedValue.remove(id) + } + } + ) + } + + private struct ItemRow: View { + let item: L + let bookmarks: [String] + + var body: some View { + HStack(spacing: 3) { + if !item.icon.isEmpty { + Image(systemName: item.icon) + .foregroundStyle(item.sub == nil ? Color.secondary : .indigo) + } else if item.sub != nil { + Image(systemName: "folder.fill") + .foregroundStyle(.indigo) + } + Text(GuideListModel.simpleTitle(item.t)) + Spacer() + if bookmarks.contains(item.t) { + Image(systemName: "bookmark") + .foregroundStyle(.secondary) + .font(.footnote) } - } // end overlay + } + .contentShape(Rectangle()) } - #if os(iOS) - .navigationTitle("Apple 开发手册") - #endif } func updateApBookmarks() { - apBookmarks = [String]() - for bm in bookmarks { -// if bm.pamphletName == "ap" { -// apBookmarks.append(bm.name) -// } - apBookmarks.append(bm.name) - } + apBookmarks = bookmarks.map(\.name) } } @@ -183,15 +210,11 @@ final class GuideListModel { } // end for one SMFile.writeToDownload(fileName: "read.md", content: md) } - - - } struct L: Hashable, Identifiable { - var id = UUID() + var id: String { t } var t: String var icon: String = "" var sub: [L]? } - diff --git a/SwiftPamphletApp/HomeUI/DataLink.swift b/SwiftPamphletApp/HomeUI/DataLink.swift index ea9d4a0d0..156c3479a 100644 --- a/SwiftPamphletApp/HomeUI/DataLink.swift +++ b/SwiftPamphletApp/HomeUI/DataLink.swift @@ -26,6 +26,10 @@ struct DataLink: Identifiable { selectDev:Binding, selectInfoBindable: IOInfo?, selectDevBindable: DeveloperModel?, + selectGuideItem: Binding, + selectGuideItemBindable: L?, + limit: Binding, + trigger: Binding, type: ShowType ) -> some View { switch title { @@ -92,17 +96,28 @@ struct DataLink: Identifiable { case "Apple技术": switch type { case .content: - GuideListView(listModel: GuideListModel(plModel: AppleGuide().outline, pplName: "ap")) + GuideListView( + listModel: GuideListModel(plModel: AppleGuide().outline, pplName: "ap"), + selectedItem: selectGuideItem + ) case .detail: - IntroView() - } - case "计算机科学": - switch type { - case .content: - GuideListView(listModel: GuideListModel(plModel: CSGuide().outline, pplName: "cs")) - case .detail: - IntroView() + if let item = selectGuideItemBindable { + GuideDetailView( + t: item.t, + icon: item.icon, + plName: "ap", + limit: limit, + trigger: trigger + ) + } } +// case "计算机科学": +// switch type { +// case .content: +// GuideListView(listModel: GuideListModel(plModel: CSGuide().outline, pplName: "cs")) +// case .detail: +// IntroView() +// } case "WWDC": switch type { case .content: @@ -121,7 +136,7 @@ struct DataLink: Identifiable { switch type { case .content: // 默认 - GuideListView(listModel: GuideListModel(plModel: AppleGuide().outline, pplName: "ap")) + InfoListView(selectInfo: selectInfo) case .detail: IntroView() } diff --git a/SwiftPamphletApp/HomeUI/HomeView.swift b/SwiftPamphletApp/HomeUI/HomeView.swift index f4eaab3e6..2c9b1591d 100644 --- a/SwiftPamphletApp/HomeUI/HomeView.swift +++ b/SwiftPamphletApp/HomeUI/HomeView.swift @@ -17,6 +17,9 @@ struct HomeView: View { @AppStorage(SPC.isFirstRun) var isFirstRun = true @Environment(\.scenePhase) var scenePhase + @State private var selectedGuideItem: L? = nil // 改为 L? 类型 + @State private var limit: Int = 50 // 为 GuideDetailView 添加 + @State private var trigger: Bool = false // 为 GuideDetailView 添加 var body: some View { #if os(macOS) @@ -33,6 +36,10 @@ struct HomeView: View { selectDev: $selectDev, selectInfoBindable: selectInfo, selectDevBindable: selectDev, + selectGuideItem: $selectedGuideItem, + selectGuideItemBindable: selectedGuideItem, + limit: $limit, + trigger: $trigger, type: .content ) } else { @@ -45,14 +52,28 @@ struct HomeView: View { } } detail: { if !selectedDataLinkString.isEmpty { - DataLink.viewToShow( - for: selectedDataLinkString, - selectInfo: $selectInfo, - selectDev: $selectDev, - selectInfoBindable: selectInfo, - selectDevBindable: selectDev, - type: .detail - ) + if selectedDataLinkString == "Apple技术", let item = selectedGuideItem { + GuideDetailView( + t: item.t, + icon: item.icon, + plName: "ap", + limit: $limit, + trigger: $trigger + ) + } else { + DataLink.viewToShow( + for: selectedDataLinkString, + selectInfo: $selectInfo, + selectDev: $selectDev, + selectInfoBindable: selectInfo, + selectDevBindable: selectDev, + selectGuideItem: $selectedGuideItem, + selectGuideItemBindable: selectedGuideItem, + limit: $limit, + trigger: $trigger, + type: .detail + ) + } } else { IntroView() } @@ -82,7 +103,6 @@ struct HomeView: View { .onOpenURL(perform: { url in // 处理外部链接 }) - #endif } }