dimensionhub/dimensionhub/Views/IndexView.swift

438 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ContentView.swift
// dimensionhub
//
// Created by on 2025/2/18.
//
import SwiftUI
import SwiftData
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 dramas: [DramaItem]
}
enum LoadMoreResult {
case success
case error(String)
}
var dramas: [DramaItem]
var selectedDate: String
//
var updateDramaGroups: [UpdateDramaGroup] = []
init() {
self.dramas = []
self.selectedDate = ""
}
@MainActor
func loadData(userId: String) async {
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):
self.dramas = result.dramas
self.updateDramaGroups = result.update_dramas
}
}
@MainActor
func loadMoreUpdateDramas(userId: String, mode: API.LoadMode) async -> LoadMoreResult {
// 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)
self.updateDramaGroups = preappendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups)
print("--------- after ------------")
displayDramaGroups(self.updateDramaGroups)
print("--------- ------------")
return .success
} else {
return .error("没有更多数据")
}
} else {
return .error("加载失败")
}
}
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)
self.updateDramaGroups = appendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups)
print("----------after-----------")
displayDramaGroups(self.updateDramaGroups)
print("---------------------")
return .success
} else {
return .error("没有更多数据")
}
} else {
return .error("加载失败")
}
}
}
return .success
}
//
@MainActor
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 {
self.updateDramaGroups = preappendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups)
}
}
// 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 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 targetGroups
}
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 {
@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 {
Text("亚次元")
.font(.system(size: 16))
.padding([.top, .bottom], 5)
}
}
.frame(height: 50)
.background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .bottom)
HStack(alignment: .center) {
Spacer()
Text("番剧补完计划")
.font(.system(size: 24))
.foregroundColor(Color(hex: "#999999"))
}
ForEach(indexModel.dramas, id: \.id) { drama in
DramaCellView(dramaItem: drama)
}
//
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 10) {
ForEach(indexModel.updateDramaGroups, id: \.group_id) { group in
DramaGroupView(group: group) {
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("scrollView"))
if screenBounds.height - frame.minY > 50 && contextFrame.minY > 0 && !isMoreLoading {
Task {
self.isMoreLoading = true
let result = await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .next)
switch result {
case .success:
()
case .error(let message):
DispatchQueue.main.async {
self.showPrompt = true
self.promptMessage = message
}
}
self.isMoreLoading = false
}
}
}
})
if self.isMoreLoading {
ProgressView()
}
}
.coordinateSpace(name: "scrollView")
.popover(isPresented: $showDateNavPopover) {
DateNavView(selectGroupId: self.$selectGroupId, showDateNavPopover: $showDateNavPopover) { selectedDate in
Task {
await indexModel.loadDateUpdateDramas(userId: self.userId, date: selectedDate)
}
}
}
.refreshable {
guard !self.isPrevLoading else {
return
}
//
self.isPrevLoading = true
let result = await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .prev)
switch result {
case .success:
()
case .error(let message):
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showPrompt = true
self.promptMessage = message
}
}
self.isPrevLoading = false
}
}
.frame(width: 370)
.ignoresSafeArea(edges: .bottom)
.alert(isPresented: $showPrompt) {
Alert(title: Text("提示"), message: Text(self.promptMessage), dismissButton: .default(Text("OK")))
}
.task {
await self.indexModel.loadData(userId: self.userId)
}
}
}
extension IndexView {
//
struct DramaCellView: View {
let dramaItem: IndexModel.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) {
HStack(alignment: .center, spacing: 5) {
ForEach(dramaItem.episodes) { item in
VStack(alignment: .center) {
GeometryReader { geometry in
AsyncImage(url: URL(string: item.thumb)) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.frame(width: geometry.frame(in: .local).width, height: 80)
.overlay {
VStack {
HStack(alignment: .center) {
Text(item.num_name)
.font(.system(size: 12))
.foregroundColor(Color(hex: "#333333"))
Spacer()
}
Spacer()
}
.padding([.top, .leading], 5)
}
}
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)
}
}
}
}
}
}
}
}
//
struct DramaGroupView: View {
let group: IndexModel.UpdateDramaGroup
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)) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.frame(width: 370, height: 180)
.overlay {
HStack {
VStack(alignment: .leading, spacing: 8) {
Text(item.name)
.font(.system(size: 16))
.foregroundColor(Color(hex: "#333333"))
.lineLimit(1)
Text(item.status)
.font(.system(size: 12))
.foregroundColor(Color(hex: "#333333"))
.lineLimit(1)
Spacer()
}
Spacer()
}
.padding([.top, .leading], 10)
}
}
}
}
}
}
}
#Preview {
IndexView()
.modelContainer(for: Item.self, inMemory: true)
}