dimensionhub/dimensionhub/Views/Index/IndexMainView.swift

282 lines
9.6 KiB
Swift

//
// IndexMainView.swift
// dimensionhub
//
// Created by on 2025/4/8.
//
import SwiftUI
import Combine
//
struct IndexMainView: View {
@Environment(\.modelContext) private var modelContext
@EnvironmentObject var appNavigation: AppNavigation
@Environment(\.userId) private var userId
@State var indexModel = IndexModel()
@State private var scrollID: IndexModel.ScrollTarget? = nil
//
@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
@State private var scrollOffset: CGFloat = 0
@State private var showFloatGroupLabel: Bool = false
@State private var scrollHeight: Double = 0
var body: some View {
VStack(alignment: .center) {
HStack(alignment: .center) {
Text("亚次元")
.font(.system(size: 20, weight: .bold))
.padding([.top, .bottom], 5)
Spacer()
HStack {
Text("\(indexModel.follow_num)")
.font(.system(size: 18))
.foregroundColor(.black)
.padding([.top, .bottom], 5)
.padding(.leading, 10)
}
.contentShape(Rectangle())
.highPriorityGesture(
TapGesture().onEnded {
appNavigation.append(dest: .followList)
}
)
.zIndex(1)
.task {
await indexModel.reloadFollowNum(userId: userId)
}
}
.padding([.leading, .trailing], 15)
.frame(height: 50)
.background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .top)
ScrollView(.vertical, showsIndicators: false) {
ScrollViewOffsetReader(offset: $scrollOffset)
MixGroupLabelView(fixedDramaGroup: indexModel.fixedDramaGroup) {
if let groupId = indexModel.fixedDramaGroup?.group_id {
selectGroupId = groupId
indexModel.selectedDate = groupId
showDateNavPopover = true
}
}
.opacity(!showFloatGroupLabel ? 1 : 0)
//
LazyVStack(alignment: .center, spacing: 10) {
ForEach(indexModel.dramaGroupElements, id: \.id) { item in
switch item.data {
case .label(let groupId, let groupName, _):
DramaGroupLabelView(group_name: groupName) {
selectGroupId = groupId
indexModel.selectedDate = groupId
showDateNavPopover = true
}
.id(item.id)
case .item(let groupId, let item, _):
DramaGroupItemView(groupId: groupId, item: item)
.id(item.id)
}
}
}
.scrollTargetLayout()
ProgressView()
}
.frame(width: 370)
.coordinateSpace(name: "indexScrollView")
.scrollPosition(id: $scrollID)
.onChange(of: scrollOffset) {
self.showFloatGroupLabel = scrollOffset < 0
indexModel.offsetChanged(offset: scrollOffset)
}
.refreshable {
guard !self.showDateNavPopover && !self.headerRefreshing else {
return
}
//
self.headerRefreshing = true
Task {
await self.indexModel.loadPrevUpdateDramasTask(userId: self.userId) { anchorGroupElement in
DispatchQueue.main.async {
self.headerRefreshing = false
}
}
}
}
.onChange(of: scrollID) { _, newValue in
if let newValue {
indexModel.scrollIDPublisher.send((userId, newValue))
}
}
.overlay(alignment: .topTrailing) {
MixGroupLabelView(fixedDramaGroup: indexModel.fixedDramaGroup) {
if let groupId = indexModel.fixedDramaGroup?.group_id {
selectGroupId = groupId
indexModel.selectedDate = groupId
showDateNavPopover = true
}
}
.opacity(showFloatGroupLabel ? 1 : 0)
}
}
.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 ScrollViewOffsetReader: View {
@Binding var offset: CGFloat
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("indexScrollView")).origin.y
)
}
.frame(height: 0)
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
DispatchQueue.main.async {
self.offset = value
}
}
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
}
extension IndexMainView {
struct MixGroupLabelView: View {
@EnvironmentObject var appNav: AppNavigation
let fixedDramaGroup: IndexModel.UpdateDramaGroup?
let onTap: () -> Void
var body: some View {
HStack(alignment: .center) {
Button(action: {
appNav.append(dest: .search)
}) {
Image(systemName: "magnifyingglass")
.font(.system(size: 20))
}
Spacer()
if let fixedDramaGroup {
Text(fixedDramaGroup.group_name)
.font(.system(size: 18))
.fontWeight(.regular)
.onTapGesture {
onTap()
}
}
}
.padding([.top, .bottom], 8)
.background(.white)
}
}
//
struct DramaGroupLabelView: View {
let group_name: String
var onTap: () -> Void
var body: some View {
VStack(alignment: .center, spacing: 10) {
HStack {
Spacer()
Text(group_name)
.font(.system(size: 18))
.fontWeight(.regular)
.onTapGesture {
onTap()
}
}
}
}
}
// item
struct DramaGroupItemView: View {
@EnvironmentObject var appNav: AppNavigation
let groupId: String
let item: IndexModel.UpdateDramaGroup.Item
var body: some View {
FlexImage(urlString: item.thumb, width: 370, height: 180, placeholder: "ph_img_big")
.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)
}
.contentShape(Rectangle())
.onTapGesture {
appNav.append(dest: .detail(id: item.id))
}
}
}
}
#Preview {
IndexMainView()
}