538 lines
21 KiB
Swift
538 lines
21 KiB
Swift
//
|
||
// 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] = []
|
||
|
||
@ObservationIgnored
|
||
private var isLoaded = false
|
||
|
||
init() {
|
||
self.dramas = []
|
||
self.selectedDate = ""
|
||
}
|
||
|
||
@MainActor
|
||
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):
|
||
self.dramas = result.dramas
|
||
self.updateDramaGroups = result.update_dramas
|
||
}
|
||
self.isLoaded = true
|
||
}
|
||
|
||
@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 = 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()
|
||
.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: 16))
|
||
.padding([.top, .bottom], 5)
|
||
Spacer()
|
||
|
||
NavigationLink(destination: FavorView()) {
|
||
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(indexModel.dramas, id: \.id) { drama in
|
||
DramaCellView(dramaItem: drama)
|
||
}
|
||
|
||
// 基于日期的更新列表
|
||
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("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()
|
||
}
|
||
}
|
||
.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
|
||
}
|
||
|
||
}
|
||
.frame(width: 370)
|
||
}
|
||
.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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 显示剧集的列表信息
|
||
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) {
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 显示分组信息
|
||
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)) { 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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
IndexView()
|
||
.modelContainer(for: Item.self, inMemory: true)
|
||
}
|