dimensionhub/dimensionhub/Views/IndexView.swift
2025-03-03 23:35:03 +08:00

499 lines
19 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
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 dramas: [DramaItem]
}
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 {
// 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("--------- ------------")
}
}
}
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("---------------------")
}
}
}
}
}
//
@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 {
// , 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()
}
}
.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 {
Text("亚次元")
.font(.system(size: 16))
.padding([.top, .bottom], 5)
}
}
.frame(height: 50)
.background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .top)
VStack(alignment: .center) {
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
await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .next)
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
await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .prev)
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)
print(UIScreen.main.bounds.width)
}
}
}
//
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()
.aspectRatio(contentMode: .fill)
.clipped()
} 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()
.aspectRatio(contentMode: .fill)
.frame(width: 370, height: 180)
.clipped()
} placeholder: {
ProgressView()
}
.frame(width: 370, height: 180)
.overlay {
HStack {
VStack {
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)
Spacer()
}
Spacer()
}
.padding([.top, .leading], 10)
}
}
}
}
}
}
}
#Preview {
IndexView()
.modelContainer(for: Item.self, inMemory: true)
}