From d8dae2fa3c8cbc979da0cebc0bab48a9f0091cb4 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Tue, 18 Mar 2025 11:29:03 +0800 Subject: [PATCH] add favor view --- dimensionhub/Core/API.swift | 8 + dimensionhub/Views/FavorView.swift | 235 +++++++++++++++++++++++++++++ dimensionhub/Views/IndexView.swift | 10 +- 3 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 dimensionhub/Views/FavorView.swift diff --git a/dimensionhub/Core/API.swift b/dimensionhub/Core/API.swift index 06446c4..de3ffe9 100644 --- a/dimensionhub/Core/API.swift +++ b/dimensionhub/Core/API.swift @@ -36,6 +36,7 @@ struct API { // 服务器地址 static let baseUrl = "https://dimensionhub.s5s8.com" + // 获取首页的数据 static func getIndexData(userId: String, as: T.Type) async -> APIResponse { let request = URLRequest(url: URL(string: baseUrl + "/api/index?user_id=\(userId)")!) @@ -49,6 +50,13 @@ struct API { return await doRequest(request: request, as: T.self) } + // 获取用户收藏的数据列表 + static func getFavorData(userId: String, id: Int, as: T.Type) async -> APIResponse { + let request = URLRequest(url: URL(string: baseUrl + "/api/favor?user_id=\(userId)&id=\(id)")!) + + return await doRequest(request: request, as: T.self) + } + // 指定时间索引 static func loadDateUpdateDramas(userId: String, date: String, as: T.Type) async -> APIResponse { let request = URLRequest(url: URL(string: baseUrl + "/api/load_date_dramas?user_id=\(userId)&date=\(date)")!) diff --git a/dimensionhub/Views/FavorView.swift b/dimensionhub/Views/FavorView.swift new file mode 100644 index 0000000..83dc5c3 --- /dev/null +++ b/dimensionhub/Views/FavorView.swift @@ -0,0 +1,235 @@ +// +// FavorView.swift +// dimensionhub +// +// Created by 安礼成 on 2025/3/18. +// + +import Foundation +import SwiftUI +import Observation + +@Observable +final class FavorModel { + + 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 FavorResponse: Codable { + let dramas: [DramaItem] + let has_more: Bool + } + + var dramas: [DramaItem] + + // 标记是否还有新数据,避免空请求 + @ObservationIgnored + private var hasMore: Bool + + init() { + self.dramas = [] + self.hasMore = true + } + + @MainActor + func loadData(userId: String) async { + let response = await API.getFavorData(userId: userId, id: 0, as: FavorResponse.self) + switch response { + case .error(let code, let message): + print("index load data get error_code: \(code), message: \(message)") + case .result(let result): + self.dramas = result.dramas + self.hasMore = result.has_more + } + } + + @MainActor + func loadMoreFavorDramas(userId: String) async { + guard self.hasMore else { + return + } + + let lastId = self.dramas.last?.id ?? 0 + let response = await API.getFavorData(userId: userId, id: lastId, as: FavorResponse.self) + switch response { + case .result(let result): + self.dramas.append(contentsOf: result.dramas) + self.hasMore = result.has_more + case .error(let code, let string): + print("load more favor get error code: \(code), message: \(string)") + } + } +} + +struct FavorView: View { + @AppStorage("userId") private var userId: String = Utils.defaultUserId() + + @State var favorModel = FavorModel() + @State var isMoreLoading: Bool = false + + var body: some View { + VStack(alignment: .center) { + + HStack(alignment: .center) { + Color.clear + .overlay { + HStack(alignment: .center) { + Text("亚次元") + .font(.system(size: 16)) + .padding([.top, .bottom], 5) + Spacer() + + HStack { + Text("♡ 12") + .font(.system(size: 16)) + .padding([.top, .bottom], 5) + } + } + .padding([.leading, .trailing], 15) + } + } + .frame(height: 50) + .background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .top) + + VStack(alignment: .center) { + + ScrollView(.vertical, showsIndicators: false) { + HStack(alignment: .center) { + Spacer() + Text("番剧补完计划") + .font(.system(size: 24)) + .foregroundColor(Color(hex: "#999999")) + } + + ForEach(favorModel.dramas, id: \.id) { drama in + DramaCellView(dramaItem: drama) + } + + 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.favorModel.loadMoreFavorDramas(userId: userId) + self.isMoreLoading = false + } + } + } + }) + + if self.isMoreLoading { + ProgressView() + } + } + .coordinateSpace(name: "indexScrollView") + } + .frame(width: 370) + } + .ignoresSafeArea(edges: .bottom) + .task { + await self.favorModel.loadData(userId: self.userId) + } + } + +} + +extension FavorView { + // 显示剧集的列表信息 + struct DramaCellView: View { + let dramaItem: FavorModel.DramaItem + + var body: some View { + VStack(alignment: .leading) { + + NavigationLink(destination: DetailView(id: dramaItem.id)) { + Text(dramaItem.title) + .font(.system(size: 20)) + .foregroundColor(Color(hex: "#333333")) + } + + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(alignment: .center, spacing: 5) { + ForEach(dramaItem.episodes) { item in + VStack(alignment: .center) { + GeometryReader { geometry in + + AsyncImage(url: URL(string: item.thumb)) { phase in + switch phase { + case .empty: + ProgressView() + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.frame(in: .local).width, height: 80) + .clipped() + default: + Image("ph_img_medium") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.frame(in: .local).width, height: 80) + .clipped() + } + } + .frame(width: geometry.frame(in: .local).width, height: 80) + .overlay(alignment: .topLeading) { + if !item.num_name.isEmpty { + HStack(alignment: .center) { + Text(item.num_name) + .font(.system(size: 12)) + .foregroundColor(.white) + .lineLimit(1) + } + .padding(3) + .background( + Color.black.opacity(0.6) + ) + .cornerRadius(3) + .padding(3) + } else { + EmptyView() + } + } + } + + Text(item.name) + .font(.system(size: 12)) + .foregroundColor(Color(hex: "#333333")) + .lineLimit(1) + } + .frame(width: 120, height: 100) + .onTapGesture { + if let playUrl = URL(string: item.play) { + UIApplication.shared.open(playUrl) + } + } + } + } + } + } + } + } +} diff --git a/dimensionhub/Views/IndexView.swift b/dimensionhub/Views/IndexView.swift index 3e83ec8..af3d71a 100644 --- a/dimensionhub/Views/IndexView.swift +++ b/dimensionhub/Views/IndexView.swift @@ -296,10 +296,12 @@ extension IndexView { .padding([.top, .bottom], 5) Spacer() - HStack { - Text("♡ 12") - .font(.system(size: 16)) - .padding([.top, .bottom], 5) + NavigationLink(destination: FavorView()) { + HStack { + Text("♡ 12") + .font(.system(size: 16)) + .padding([.top, .bottom], 5) + } } } .padding([.leading, .trailing], 15)