From 5b14d559bfccf2a231b99199217de3c7b0af96d1 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Tue, 8 Apr 2025 15:39:28 +0800 Subject: [PATCH] fix index view --- dimensionhub.xcodeproj/project.pbxproj | 26 + .../Views/Index/IndexExceptionView.swift | 48 ++ dimensionhub/Views/Index/IndexMainView.swift | 262 +++++++++ dimensionhub/Views/Index/IndexModel.swift | 214 +++++++ dimensionhub/Views/Index/IndexView.swift | 61 ++ dimensionhub/Views/IndexView.swift | 530 ------------------ 6 files changed, 611 insertions(+), 530 deletions(-) create mode 100644 dimensionhub/Views/Index/IndexExceptionView.swift create mode 100644 dimensionhub/Views/Index/IndexMainView.swift create mode 100644 dimensionhub/Views/Index/IndexModel.swift create mode 100644 dimensionhub/Views/Index/IndexView.swift delete mode 100644 dimensionhub/Views/IndexView.swift diff --git a/dimensionhub.xcodeproj/project.pbxproj b/dimensionhub.xcodeproj/project.pbxproj index cede1f9..1ad2d43 100644 --- a/dimensionhub.xcodeproj/project.pbxproj +++ b/dimensionhub.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + C87233D82DA3D2D7006A6CDC /* Refresh in Frameworks */ = {isa = PBXBuildFile; productRef = C87233D72DA3D2D7006A6CDC /* Refresh */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ C85F58D32D64D11000D761E9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -65,6 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C87233D82DA3D2D7006A6CDC /* Refresh in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +130,7 @@ ); name = dimensionhub; packageProductDependencies = ( + C87233D72DA3D2D7006A6CDC /* Refresh */, ); productName = dimensionhub; productReference = C85F58C02D64D10F00D761E9 /* dimensionhub.app */; @@ -209,6 +215,7 @@ mainGroup = C85F58B72D64D10F00D761E9; minimizedProjectReferenceProxies = 1; packageReferences = ( + C87233D62DA3D2D7006A6CDC /* XCRemoteSwiftPackageReference "Refresh" */, ); preferredProjectObjectVersion = 77; productRefGroup = C85F58C12D64D10F00D761E9 /* Products */; @@ -585,6 +592,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + C87233D62DA3D2D7006A6CDC /* XCRemoteSwiftPackageReference "Refresh" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/wxxsw/Refresh.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.2.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + C87233D72DA3D2D7006A6CDC /* Refresh */ = { + isa = XCSwiftPackageProductDependency; + package = C87233D62DA3D2D7006A6CDC /* XCRemoteSwiftPackageReference "Refresh" */; + productName = Refresh; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = C85F58B82D64D10F00D761E9 /* Project object */; } diff --git a/dimensionhub/Views/Index/IndexExceptionView.swift b/dimensionhub/Views/Index/IndexExceptionView.swift new file mode 100644 index 0000000..073eca0 --- /dev/null +++ b/dimensionhub/Views/Index/IndexExceptionView.swift @@ -0,0 +1,48 @@ +// +// IndexExceptionView.swift +// dimensionhub +// +// Created by 安礼成 on 2025/4/8. +// + +import SwiftUI + +struct IndexExceptionView: View { + let onRetry: () -> Void + + var body: some View { + HStack { + Spacer() + VStack(alignment: .center, spacing: 20) { + Spacer() + Image("lost_network") + + Text("网络状态待提升,点击重试") + .font(.system(size: 13)) + .foregroundColor(Color(hex: "#333333")) + + Rectangle() + .frame(width: 100, height: 25) + .foregroundColor(Color(hex: "#F2F2F2")) + .overlay { + Text("重新加载") + .font(.system(size: 13)) + .foregroundColor(Color(hex: "#999999")) + .fontWeight(.regular) + } + .onTapGesture { + onRetry() + } + Spacer() + } + Spacer() + } + .background(Color(hex: "#F6F6F6"), ignoresSafeAreaEdges: .all) + } +} + +#Preview { + IndexExceptionView() { + print("call me retry") + } +} diff --git a/dimensionhub/Views/Index/IndexMainView.swift b/dimensionhub/Views/Index/IndexMainView.swift new file mode 100644 index 0000000..0e776f8 --- /dev/null +++ b/dimensionhub/Views/Index/IndexMainView.swift @@ -0,0 +1,262 @@ +// +// IndexMainView.swift +// dimensionhub +// +// Created by 安礼成 on 2025/4/8. +// +import SwiftUI +import Refresh + +// 首页的主要窗口 +struct IndexMainView: View { + @Environment(\.modelContext) private var modelContext + @AppStorage("userId") private var userId: String = Utils.defaultUserId() + + @State var indexModel = IndexModel() + @State var isMoreLoading: Bool = false + // 前向刷新 + @State var isPrevLoading: Bool = false + + // 提示信息 + @State var showPrompt: Bool = false + @State var promptMessage: String = "" + + // 是否显示日期弹出层 + @State private var selectGroupId: String = "" + @State private var showDateNavPopover: Bool = false + + // 刷新逻辑 + @State private var headerRefreshing: Bool = false + @State private var footerRefreshing: Bool = false + @State private var noMore: Bool = false + + var body: some View { + VStack(alignment: .center) { + + HStack(alignment: .center) { + Color.clear + .overlay { + HStack(alignment: .center) { + Text("亚次元") + .font(.system(size: 18, weight: .bold)) + .padding([.top, .bottom], 5) + Spacer() + + NavigationLink(destination: FollowListView()) { + HStack { + Text("♡ \(indexModel.follow_num)") + .font(.system(size: 17)) + .foregroundColor(.black) + .padding([.top, .bottom], 5) + } + } + } + .padding([.leading, .trailing], 15) + } + } + .frame(height: 50) + .background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .top) + + ScrollView(.vertical, showsIndicators: false) { + + RefreshHeader(refreshing: $headerRefreshing, action: { + print("call me head headerRefreshing") + }) { progress in + print("progress is: \(progress)") + + return ProgressView() + } + + // 基于日期的更新列表 + LazyVStack(alignment: .center, spacing: 10) { + ForEach(indexModel.updateDramaGroups, id: \.group_id) { group in + DramaGroupView(group: group, model: indexModel) { + selectGroupId = group.group_id + indexModel.selectedDate = group.group_id + showDateNavPopover = true + } + } + } + +// Rectangle() +// .frame(height: 0) +// .background(GeometryReader { geometry in +// Color.clear.onChange(of: geometry.frame(in: .global).minY) {_, offset in +// let frame = geometry.frame(in: .global) +// let screenBounds = UIScreen.main.bounds +// let contextFrame = geometry.frame(in: .named("indexScrollView")) +// +// if screenBounds.height - frame.minY > 50 && contextFrame.minY > 0 && !isMoreLoading { +// Task { +// self.isMoreLoading = true +// await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .next) +// self.isMoreLoading = false +// } +// } +// } +// }) + + RefreshFooter(refreshing: $footerRefreshing, action: { + print("call me here $footerRefreshing") + Task { + self.footerRefreshing = true + await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .next) + self.footerRefreshing = false + } + }) { + ProgressView() + } + .noMore(noMore) + .preload(offset: 50) + + if self.isMoreLoading { + ProgressView() + } + } + .enableRefresh() + .frame(width: 370) + .coordinateSpace(name: "indexScrollView") +// .refreshable { +// guard !self.isPrevLoading && !self.showDateNavPopover else { +// return +// } +// +// // 上拉刷新功能 +// self.isPrevLoading = true +// await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .prev) +// self.isPrevLoading = false +// } + .overlay(alignment: .topTrailing) { + HStack(alignment: .center) { + NavigationLink { + SearchView() + } label: { + Image(systemName: "magnifyingglass") + .font(.system(size: 20)) + } + + Spacer() + if let fixedDramaGroup = indexModel.fixedDramaGroup { + Text(fixedDramaGroup.group_name) + .font(.system(size: 18)) + .fontWeight(.regular) + .onTapGesture { + selectGroupId = fixedDramaGroup.group_id + indexModel.selectedDate = fixedDramaGroup.group_id + showDateNavPopover = true + } + } + } + .padding([.top, .bottom], 8) + .background(.white) + } + } + .ignoresSafeArea(edges: .bottom) + .popover(isPresented: $showDateNavPopover) { + DateNavView(selectGroupId: self.$selectGroupId, showDateNavPopover: $showDateNavPopover) { selectedDate in + Task { + await indexModel.loadDateUpdateDramas(userId: self.userId, date: selectedDate) + } + } + } + .alert(isPresented: $showPrompt) { + Alert(title: Text("提示"), message: Text(self.promptMessage), dismissButton: .default(Text("OK"))) + } + .task { + await self.indexModel.loadData(userId: self.userId) + } + .onPreferenceChange(DramaGroupElementPreferenceKey.self) { frames in + let visibleFrames = frames.filter { $0.value >= 0} + if let minFrame = visibleFrames.min(by: { $0.value <= $1.value}) { + indexModel.setFixedDrameGroup(groupId: minFrame.key) + } + } + } +} + +extension IndexMainView { + // 显示分组信息 + struct DramaGroupView: View { + let group: IndexModel.UpdateDramaGroup + let model: IndexModel + + var onTap: () -> Void + + var body: some View { + VStack(alignment: .center, spacing: 10) { + HStack { + Spacer() + Text(group.group_name) + .font(.system(size: 18)) + .fontWeight(.regular) + .onTapGesture { + onTap() + } + } + + ForEach(group.items, id: \.id) { item in + NavigationLink(destination: DetailView(id: item.id)) { + AsyncImage(url: URL(string: item.thumb)) { phase in + switch phase { + case .empty: + ProgressView() + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 370, height: 180) + .clipped() + default: + Image("ph_img_big") + .resizable() + .aspectRatio(contentMode: .fill) + .clipped() + } + + } + .frame(width: 370, height: 180) + .overlay(alignment: .topLeading) { + VStack(alignment: .leading, spacing: 8) { + Text(item.name) + .font(.system(size: 16)) + .foregroundColor(.white) + .lineLimit(1) + + Text(item.status) + .font(.system(size: 12)) + .foregroundColor(.white) + .lineLimit(1) + } + .padding(5) + .background( + Color.black.opacity(0.6) + ) + .cornerRadius(5) + .padding(8) + } + .background(GeometryReader { geometry in + let height = geometry.size.height + let minY = geometry.frame(in: .named("indexScrollView")).minY + let y = minY >= 0 ? minY : minY + height + + Color.clear.preference(key: DramaGroupElementPreferenceKey.self, value: [ group.group_id : y]) + }) + } + } + } + } + } + + // 元素的坐标的距离变化, [groupId : minY] + struct DramaGroupElementPreferenceKey: PreferenceKey { + static var defaultValue: [String: CGFloat] = [:] + + static func reduce(value: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) { + value.merge(nextValue()) { $1 } + } + } +} + +#Preview { + IndexMainView() +} diff --git a/dimensionhub/Views/Index/IndexModel.swift b/dimensionhub/Views/Index/IndexModel.swift new file mode 100644 index 0000000..fa86fb8 --- /dev/null +++ b/dimensionhub/Views/Index/IndexModel.swift @@ -0,0 +1,214 @@ +// +// IndexModel.swift +// dimensionhub +// +// Created by 安礼成 on 2025/4/8. +// + +import Foundation +import Observation + +@Observable +final class IndexModel { + + struct DramaItem: Codable { + struct Episode: Codable, Identifiable { + let id = UUID().uuidString + let name: String + let thumb: String + let num_name: String + let play: String + + enum CodingKeys: String, CodingKey { + case name, thumb, num_name, play + } + } + + let id: Int + let title: String + let episodes: [Episode] + } + + struct UpdateDramaGroup: Codable { + struct Item: Codable { + let id: Int + let name: String + let time: Int + let thumb: String + let status: String + } + + let group_id: String + let group_name: String + let items: [Item] + } + + struct IndexResponse: Codable { + let update_dramas: [UpdateDramaGroup] + let follow_num: Int + } + + var selectedDate: String + + // 保存原始的更新数据 + var updateDramaGroups: [UpdateDramaGroup] = [] + var follow_num: String = "0" + + // 用来显示固定栏目的group_name + var fixedDramaGroup: UpdateDramaGroup? = nil + + @ObservationIgnored + private var isLoaded = false + + init() { + self.selectedDate = "" + } + + func loadData(userId: String) async { + guard !isLoaded else { + return + } + + let response = await API.getIndexData(userId: userId, as: IndexResponse.self) + switch response { + case .error(let code, let message): + print("index load data get error_code: \(code), message: \(message)") + case .result(let result): + await MainActor.run { + self.updateDramaGroups = result.update_dramas + self.fixedDramaGroup = result.update_dramas.first + self.follow_num = result.follow_num >= 100 ? "99+" : "\(result.follow_num)" + } + } + self.isLoaded = true + } + + func setFixedDrameGroup(groupId: String) { + if let newFixedDramaGroup = self.updateDramaGroups.first(where: {$0.group_id == groupId}), + newFixedDramaGroup.group_id != self.fixedDramaGroup?.group_id { + self.fixedDramaGroup = newFixedDramaGroup + } + } + + func loadMoreUpdateDramas(userId: String, mode: API.LoadMode) async { + // 按照id来判断不一定正确,需要借助其他值 + let dramaIds = self.getDramaIds(self.updateDramaGroups) + print("current ids: \(dramaIds)") + + switch mode { + case .prev: + // 查找最小的id + if let firstId = dramaIds.first { + let response = await API.loadMoreUpdateDramas(userId: userId, mode: mode, id: firstId, as: [UpdateDramaGroup].self) + if case let .result(groups) = response { + if groups.count > 0 { + + print("--------- before ------------") + displayDramaGroups(self.updateDramaGroups) + await MainActor.run { + self.updateDramaGroups = preappendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups) + } + print("--------- after ------------") + displayDramaGroups(self.updateDramaGroups) + print("--------- ------------") + } + } + } + case .next: + if let lastId = dramaIds.last { + let response = await API.loadMoreUpdateDramas(userId: userId, mode: mode, id: lastId, as: [UpdateDramaGroup].self) + if case let .result(groups) = response { + if groups.count > 0 { + print("--------- before ------------") + displayDramaGroups(self.updateDramaGroups) + await MainActor.run { + self.updateDramaGroups = appendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups) + } + + print("----------after-----------") + displayDramaGroups(self.updateDramaGroups) + print("---------------------") + } + } + } + } + } + + // 指定日期,并更新日期下对应的数据 + func loadDateUpdateDramas(userId: String, date: String) async { + self.updateDramaGroups.removeAll() + let response = await API.loadDateUpdateDramas(userId: userId, date: date, as: [UpdateDramaGroup].self) + if case let .result(groups) = response { + await MainActor.run { + self.updateDramaGroups = groups + self.fixedDramaGroup = groups.first + } + } + } + + // 合并groups + private func preappendMergeDramaGroups(groups: [UpdateDramaGroup], mergeGroups: [UpdateDramaGroup]) -> [UpdateDramaGroup] { + var targetGroups = groups + + for group in mergeGroups { + if let idx = targetGroups.firstIndex(where: { $0.group_id == group.group_id}) { + var newItems = group.items + newItems.append(contentsOf: targetGroups[idx].items) + + targetGroups[idx] = UpdateDramaGroup(group_id: group.group_id, group_name: group.group_name, items: newItems) + } else { + targetGroups.insert(group, at: 0) + } + } + + return sortDramaGroups(groups: targetGroups) + } + + private func appendMergeDramaGroups(groups: [UpdateDramaGroup], mergeGroups: [UpdateDramaGroup]) -> [UpdateDramaGroup] { + var targetGroups = groups + + for group in mergeGroups { + if let idx = targetGroups.firstIndex(where: { $0.group_id == group.group_id}) { + var newItems = targetGroups[idx].items + newItems.append(contentsOf: group.items) + + targetGroups[idx] = UpdateDramaGroup(group_id: group.group_id, group_name: group.group_name, items: newItems) + } else { + targetGroups.append(group) + } + } + + return sortDramaGroups(groups: targetGroups) + } + + // 按照日期进行排序 + private func sortDramaGroups(groups: [UpdateDramaGroup]) -> [UpdateDramaGroup] { + return groups.sorted { g0, g1 in + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM" + + if let date0 = dateFormatter.date(from: g0.group_id), + let date1 = dateFormatter.date(from: g1.group_id) { + return date0 > date1 + } else { + return g0.group_id > g1.group_id + } + } + } + + private func getDramaIds(_ updateDramaGroups: [UpdateDramaGroup]) -> [Int] { + return self.updateDramaGroups.flatMap { group in + return group.items.map { item in + return item.id + } + } + } + + private func displayDramaGroups(_ groups: [UpdateDramaGroup]) { + for group in groups { + let ids = group.items.map { $0.id} + print("group_id: \(group.group_id), items: \(ids)") + } + } + +} diff --git a/dimensionhub/Views/Index/IndexView.swift b/dimensionhub/Views/Index/IndexView.swift new file mode 100644 index 0000000..9367e93 --- /dev/null +++ b/dimensionhub/Views/Index/IndexView.swift @@ -0,0 +1,61 @@ +// +// ContentView.swift +// dimensionhub +// +// Created by 安礼成 on 2025/2/18. +// + +import SwiftUI +import SwiftData +import Observation +import Network + +struct IndexView: View { + // 网络状态检测, 第一次进入app时,如果网络没有授权,网络请求会失败 + enum NetworkStatus { + case satisfied + case unsatisfied + } + + @State private var networkStatus: NetworkStatus = .satisfied + + private static let queue = DispatchQueue(label: "NetworkMonitorQueue") + + var body: some View { + ZStack { + switch self.networkStatus { + case .unsatisfied: + IndexExceptionView { + self.checkNetworkStatus() + } + case .satisfied: + IndexMainView() + .id("indexMainView") + } + } + .onAppear { + self.checkNetworkStatus() + } + } + + private func checkNetworkStatus() { + let monitor = NWPathMonitor() + monitor.pathUpdateHandler = { path in + DispatchQueue.main.async { + switch path.status { + case .satisfied: + self.networkStatus = .satisfied + default: + self.networkStatus = .unsatisfied + } + } + } + monitor.start(queue: Self.queue) + } + +} + +#Preview { + IndexView() + .modelContainer(for: Item.self, inMemory: true) +} diff --git a/dimensionhub/Views/IndexView.swift b/dimensionhub/Views/IndexView.swift deleted file mode 100644 index 8795ea3..0000000 --- a/dimensionhub/Views/IndexView.swift +++ /dev/null @@ -1,530 +0,0 @@ -// -// ContentView.swift -// dimensionhub -// -// Created by 安礼成 on 2025/2/18. -// - -import SwiftUI -import SwiftData -import Observation -import Network - -@Observable -final class IndexModel { - - struct DramaItem: Codable { - struct Episode: Codable, Identifiable { - let id = UUID().uuidString - let name: String - let thumb: String - let num_name: String - let play: String - - enum CodingKeys: String, CodingKey { - case name, thumb, num_name, play - } - } - - let id: Int - let title: String - let episodes: [Episode] - } - - struct UpdateDramaGroup: Codable { - struct Item: Codable { - let id: Int - let name: String - let time: Int - let thumb: String - let status: String - } - - let group_id: String - let group_name: String - let items: [Item] - } - - struct IndexResponse: Codable { - let update_dramas: [UpdateDramaGroup] - let follow_num: Int - } - - var selectedDate: String - - // 保存原始的更新数据 - var updateDramaGroups: [UpdateDramaGroup] = [] - var follow_num: String = "0" - - // 用来显示固定栏目的group_name - var fixedDramaGroup: UpdateDramaGroup? = nil - - @ObservationIgnored - private var isLoaded = false - - init() { - self.selectedDate = "" - } - - func loadData(userId: String) async { - guard !isLoaded else { - return - } - - let response = await API.getIndexData(userId: userId, as: IndexResponse.self) - switch response { - case .error(let code, let message): - print("index load data get error_code: \(code), message: \(message)") - case .result(let result): - await MainActor.run { - self.updateDramaGroups = result.update_dramas - self.fixedDramaGroup = result.update_dramas.first - self.follow_num = result.follow_num >= 100 ? "99+" : "\(result.follow_num)" - } - } - self.isLoaded = true - } - - func setFixedDrameGroup(groupId: String) { - if let newFixedDramaGroup = self.updateDramaGroups.first(where: {$0.group_id == groupId}), - newFixedDramaGroup.group_id != self.fixedDramaGroup?.group_id { - self.fixedDramaGroup = newFixedDramaGroup - } - } - - func loadMoreUpdateDramas(userId: String, mode: API.LoadMode) async { - // 按照id来判断不一定正确,需要借助其他值 - let dramaIds = self.getDramaIds(self.updateDramaGroups) - print("current ids: \(dramaIds)") - - switch mode { - case .prev: - // 查找最小的id - if let firstId = dramaIds.first { - let response = await API.loadMoreUpdateDramas(userId: userId, mode: mode, id: firstId, as: [UpdateDramaGroup].self) - if case let .result(groups) = response { - if groups.count > 0 { - - print("--------- before ------------") - displayDramaGroups(self.updateDramaGroups) - await MainActor.run { - self.updateDramaGroups = preappendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups) - } - print("--------- after ------------") - displayDramaGroups(self.updateDramaGroups) - print("--------- ------------") - } - } - } - case .next: - if let lastId = dramaIds.last { - let response = await API.loadMoreUpdateDramas(userId: userId, mode: mode, id: lastId, as: [UpdateDramaGroup].self) - if case let .result(groups) = response { - if groups.count > 0 { - print("--------- before ------------") - displayDramaGroups(self.updateDramaGroups) - await MainActor.run { - self.updateDramaGroups = appendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups) - } - - print("----------after-----------") - displayDramaGroups(self.updateDramaGroups) - print("---------------------") - } - } - } - } - } - - // 指定日期,并更新日期下对应的数据 - func loadDateUpdateDramas(userId: String, date: String) async { - self.updateDramaGroups.removeAll() - let response = await API.loadDateUpdateDramas(userId: userId, date: date, as: [UpdateDramaGroup].self) - if case let .result(groups) = response { - await MainActor.run { - self.updateDramaGroups = groups - self.fixedDramaGroup = groups.first - } - } - } - - // 合并groups - private func preappendMergeDramaGroups(groups: [UpdateDramaGroup], mergeGroups: [UpdateDramaGroup]) -> [UpdateDramaGroup] { - var targetGroups = groups - - for group in mergeGroups { - if let idx = targetGroups.firstIndex(where: { $0.group_id == group.group_id}) { - var newItems = group.items - newItems.append(contentsOf: targetGroups[idx].items) - - targetGroups[idx] = UpdateDramaGroup(group_id: group.group_id, group_name: group.group_name, items: newItems) - } else { - targetGroups.insert(group, at: 0) - } - } - - return sortDramaGroups(groups: targetGroups) - } - - private func appendMergeDramaGroups(groups: [UpdateDramaGroup], mergeGroups: [UpdateDramaGroup]) -> [UpdateDramaGroup] { - var targetGroups = groups - - for group in mergeGroups { - if let idx = targetGroups.firstIndex(where: { $0.group_id == group.group_id}) { - var newItems = targetGroups[idx].items - newItems.append(contentsOf: group.items) - - targetGroups[idx] = UpdateDramaGroup(group_id: group.group_id, group_name: group.group_name, items: newItems) - } else { - targetGroups.append(group) - } - } - - return sortDramaGroups(groups: targetGroups) - } - - // 按照日期进行排序 - private func sortDramaGroups(groups: [UpdateDramaGroup]) -> [UpdateDramaGroup] { - return groups.sorted { g0, g1 in - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM" - - if let date0 = dateFormatter.date(from: g0.group_id), - let date1 = dateFormatter.date(from: g1.group_id) { - return date0 > date1 - } else { - return g0.group_id > g1.group_id - } - } - } - - private func getDramaIds(_ updateDramaGroups: [UpdateDramaGroup]) -> [Int] { - return self.updateDramaGroups.flatMap { group in - return group.items.map { item in - return item.id - } - } - } - - private func displayDramaGroups(_ groups: [UpdateDramaGroup]) { - for group in groups { - let ids = group.items.map { $0.id} - print("group_id: \(group.group_id), items: \(ids)") - } - } - -} - -struct IndexView: View { - - // 网络状态检测, 第一次进入app时,如果网络没有授权,网络请求会失败 - enum NetworkStatus { - case satisfied - case unsatisfied - } - @State private var networkStatus: NetworkStatus = .satisfied - - private static let queue = DispatchQueue(label: "NetworkMonitorQueue") - - var body: some View { - ZStack { - switch self.networkStatus { - case .unsatisfied: - IndexExceptionView { - self.checkNetworkStatus() - } - case .satisfied: - IndexMainView() - .id("indexMainView") - } - } - .onAppear { - self.checkNetworkStatus() - } - } - - private func checkNetworkStatus() { - let monitor = NWPathMonitor() - monitor.pathUpdateHandler = { path in - DispatchQueue.main.async { - switch path.status { - case .satisfied: - self.networkStatus = .satisfied - default: - self.networkStatus = .unsatisfied - } - } - } - monitor.start(queue: Self.queue) - } - -} - -extension IndexView { - - struct IndexExceptionView: View { - let onRetry: () -> Void - - var body: some View { - HStack { - Spacer() - VStack(alignment: .center, spacing: 20) { - Spacer() - Image("lost_network") - - Text("网络状态待提升,点击重试") - .font(.system(size: 13)) - .foregroundColor(Color(hex: "#333333")) - - Rectangle() - .frame(width: 100, height: 25) - .foregroundColor(Color(hex: "#F2F2F2")) - .overlay { - Text("重新加载") - .font(.system(size: 13)) - .foregroundColor(Color(hex: "#999999")) - .fontWeight(.regular) - } - .onTapGesture { - onRetry() - } - Spacer() - } - Spacer() - } - .background(Color(hex: "#F6F6F6"), ignoresSafeAreaEdges: .all) - } - } - - - // 首页的主要窗口 - struct IndexMainView: View { - @Environment(\.modelContext) private var modelContext - @AppStorage("userId") private var userId: String = Utils.defaultUserId() - - @State var indexModel = IndexModel() - @State var isMoreLoading: Bool = false - // 前向刷新 - @State var isPrevLoading: Bool = false - - // 提示信息 - @State var showPrompt: Bool = false - @State var promptMessage: String = "" - - // 是否显示日期弹出层 - @State private var selectGroupId: String = "" - @State private var showDateNavPopover: Bool = false - - var body: some View { - VStack(alignment: .center) { - - HStack(alignment: .center) { - Color.clear - .overlay { - HStack(alignment: .center) { - Text("亚次元") - .font(.system(size: 18, weight: .bold)) - .padding([.top, .bottom], 5) - Spacer() - - NavigationLink(destination: FollowListView()) { - HStack { - Text("♡ \(indexModel.follow_num)") - .font(.system(size: 17)) - .foregroundColor(.black) - .padding([.top, .bottom], 5) - } - } - } - .padding([.leading, .trailing], 15) - } - } - .frame(height: 50) - .background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .top) - - ScrollView(.vertical, showsIndicators: false) { - - // 基于日期的更新列表 - LazyVStack(alignment: .center, spacing: 10) { - ForEach(indexModel.updateDramaGroups, id: \.group_id) { group in - DramaGroupView(group: group, model: indexModel) { - selectGroupId = group.group_id - indexModel.selectedDate = group.group_id - showDateNavPopover = true - } - } - } - - Rectangle() - .frame(height: 0) - .background(GeometryReader { geometry in - Color.clear.onChange(of: geometry.frame(in: .global).minY) {_, offset in - let frame = geometry.frame(in: .global) - let screenBounds = UIScreen.main.bounds - let contextFrame = geometry.frame(in: .named("indexScrollView")) - - if screenBounds.height - frame.minY > 50 && contextFrame.minY > 0 && !isMoreLoading { - Task { - self.isMoreLoading = true - await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .next) - self.isMoreLoading = false - } - } - } - }) - - if self.isMoreLoading { - ProgressView() - } - } - .frame(width: 370) - .coordinateSpace(name: "indexScrollView") - .refreshable { - guard !self.isPrevLoading && !self.showDateNavPopover else { - return - } - - // 上拉刷新功能 - self.isPrevLoading = true - await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .prev) - self.isPrevLoading = false - } - .overlay(alignment: .topTrailing) { - HStack(alignment: .center) { - NavigationLink { - SearchView() - } label: { - Image(systemName: "magnifyingglass") - .font(.system(size: 20)) - } - - Spacer() - if let fixedDramaGroup = indexModel.fixedDramaGroup { - Text(fixedDramaGroup.group_name) - .font(.system(size: 18)) - .fontWeight(.regular) - .onTapGesture { - selectGroupId = fixedDramaGroup.group_id - indexModel.selectedDate = fixedDramaGroup.group_id - showDateNavPopover = true - } - } - } - .padding([.top, .bottom], 8) - .background(.white) - } - } - .ignoresSafeArea(edges: .bottom) - .popover(isPresented: $showDateNavPopover) { - DateNavView(selectGroupId: self.$selectGroupId, showDateNavPopover: $showDateNavPopover) { selectedDate in - Task { - await indexModel.loadDateUpdateDramas(userId: self.userId, date: selectedDate) - } - } - } - .alert(isPresented: $showPrompt) { - Alert(title: Text("提示"), message: Text(self.promptMessage), dismissButton: .default(Text("OK"))) - } - .task { - await self.indexModel.loadData(userId: self.userId) - } - .onPreferenceChange(DramaGroupElementPreferenceKey.self) { frames in - let visibleFrames = frames.filter { $0.value >= 0} - if let minFrame = visibleFrames.min(by: { $0.value <= $1.value}) { - indexModel.setFixedDrameGroup(groupId: minFrame.key) - } - } - } - } - - // 显示分组信息 - struct DramaGroupView: View { - let group: IndexModel.UpdateDramaGroup - let model: IndexModel - - var onTap: () -> Void - - var body: some View { - VStack(alignment: .center, spacing: 10) { - HStack { - Spacer() - Text(group.group_name) - .font(.system(size: 18)) - .fontWeight(.regular) - .onTapGesture { - onTap() - } - } - - ForEach(group.items, id: \.id) { item in - NavigationLink(destination: DetailView(id: item.id)) { - AsyncImage(url: URL(string: item.thumb)) { phase in - switch phase { - case .empty: - ProgressView() - case .success(let image): - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 370, height: 180) - .clipped() - default: - Image("ph_img_big") - .resizable() - .aspectRatio(contentMode: .fill) - .clipped() - } - - } - .frame(width: 370, height: 180) - .overlay(alignment: .topLeading) { - VStack(alignment: .leading, spacing: 8) { - Text(item.name) - .font(.system(size: 16)) - .foregroundColor(.white) - .lineLimit(1) - - Text(item.status) - .font(.system(size: 12)) - .foregroundColor(.white) - .lineLimit(1) - } - .padding(5) - .background( - Color.black.opacity(0.6) - ) - .cornerRadius(5) - .padding(8) - } - .background(GeometryReader { - geometry in - - let height = geometry.size.height - let minY = geometry.frame(in: .named("indexScrollView")).minY - let y = minY >= 0 ? minY : minY + height - - Color.clear - .preference(key: DramaGroupElementPreferenceKey.self, value: [ group.group_id : y]) - - }) - } - } - } - } - } - - // 元素的坐标的距离变化, [groupId : minY] - struct DramaGroupElementPreferenceKey: PreferenceKey { - static var defaultValue: [String: CGFloat] = [:] - - static func reduce(value: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) { - value.merge(nextValue()) { $1 } - } - } - -} - -#Preview { - IndexView() - .modelContainer(for: Item.self, inMemory: true) -}