fix index view

This commit is contained in:
anlicheng 2025-04-08 15:39:28 +08:00
parent d411eb28b1
commit 5b14d559bf
6 changed files with 611 additions and 530 deletions

View File

@ -6,6 +6,10 @@
objectVersion = 77; objectVersion = 77;
objects = { objects = {
/* Begin PBXBuildFile section */
C87233D82DA3D2D7006A6CDC /* Refresh in Frameworks */ = {isa = PBXBuildFile; productRef = C87233D72DA3D2D7006A6CDC /* Refresh */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
C85F58D32D64D11000D761E9 /* PBXContainerItemProxy */ = { C85F58D32D64D11000D761E9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
@ -65,6 +69,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C87233D82DA3D2D7006A6CDC /* Refresh in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -125,6 +130,7 @@
); );
name = dimensionhub; name = dimensionhub;
packageProductDependencies = ( packageProductDependencies = (
C87233D72DA3D2D7006A6CDC /* Refresh */,
); );
productName = dimensionhub; productName = dimensionhub;
productReference = C85F58C02D64D10F00D761E9 /* dimensionhub.app */; productReference = C85F58C02D64D10F00D761E9 /* dimensionhub.app */;
@ -209,6 +215,7 @@
mainGroup = C85F58B72D64D10F00D761E9; mainGroup = C85F58B72D64D10F00D761E9;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
C87233D62DA3D2D7006A6CDC /* XCRemoteSwiftPackageReference "Refresh" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = C85F58C12D64D10F00D761E9 /* Products */; productRefGroup = C85F58C12D64D10F00D761E9 /* Products */;
@ -585,6 +592,25 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
C87233D62DA3D2D7006A6CDC /* XCRemoteSwiftPackageReference "Refresh" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/wxxsw/Refresh.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.2.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
C87233D72DA3D2D7006A6CDC /* Refresh */ = {
isa = XCSwiftPackageProductDependency;
package = C87233D62DA3D2D7006A6CDC /* XCRemoteSwiftPackageReference "Refresh" */;
productName = Refresh;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = C85F58B82D64D10F00D761E9 /* Project object */; rootObject = C85F58B82D64D10F00D761E9 /* Project object */;
} }

View File

@ -0,0 +1,48 @@
//
// IndexExceptionView.swift
// dimensionhub
//
// Created by on 2025/4/8.
//
import SwiftUI
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)
}
}
#Preview {
IndexExceptionView() {
print("call me retry")
}
}

View File

@ -0,0 +1,262 @@
//
// IndexMainView.swift
// dimensionhub
//
// Created by on 2025/4/8.
//
import SwiftUI
import Refresh
//
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
//
@State private var headerRefreshing: Bool = false
@State private var footerRefreshing: Bool = false
@State private var noMore: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack(alignment: .center) {
Color.clear
.overlay {
HStack(alignment: .center) {
Text("亚次元")
.font(.system(size: 18, weight: .bold))
.padding([.top, .bottom], 5)
Spacer()
NavigationLink(destination: FollowListView()) {
HStack {
Text("\(indexModel.follow_num)")
.font(.system(size: 17))
.foregroundColor(.black)
.padding([.top, .bottom], 5)
}
}
}
.padding([.leading, .trailing], 15)
}
}
.frame(height: 50)
.background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .top)
ScrollView(.vertical, showsIndicators: false) {
RefreshHeader(refreshing: $headerRefreshing, action: {
print("call me head headerRefreshing")
}) { progress in
print("progress is: \(progress)")
return ProgressView()
}
//
LazyVStack(alignment: .center, spacing: 10) {
ForEach(indexModel.updateDramaGroups, id: \.group_id) { group in
DramaGroupView(group: group, model: indexModel) {
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
// }
// }
// }
// })
RefreshFooter(refreshing: $footerRefreshing, action: {
print("call me here $footerRefreshing")
Task {
self.footerRefreshing = true
await self.indexModel.loadMoreUpdateDramas(userId: self.userId, mode: .next)
self.footerRefreshing = false
}
}) {
ProgressView()
}
.noMore(noMore)
.preload(offset: 50)
if self.isMoreLoading {
ProgressView()
}
}
.enableRefresh()
.frame(width: 370)
.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
// }
.overlay(alignment: .topTrailing) {
HStack(alignment: .center) {
NavigationLink {
SearchView()
} label: {
Image(systemName: "magnifyingglass")
.font(.system(size: 20))
}
Spacer()
if let fixedDramaGroup = indexModel.fixedDramaGroup {
Text(fixedDramaGroup.group_name)
.font(.system(size: 18))
.fontWeight(.regular)
.onTapGesture {
selectGroupId = fixedDramaGroup.group_id
indexModel.selectedDate = fixedDramaGroup.group_id
showDateNavPopover = true
}
}
}
.padding([.top, .bottom], 8)
.background(.white)
}
}
.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)
}
.onPreferenceChange(DramaGroupElementPreferenceKey.self) { frames in
let visibleFrames = frames.filter { $0.value >= 0}
if let minFrame = visibleFrames.min(by: { $0.value <= $1.value}) {
indexModel.setFixedDrameGroup(groupId: minFrame.key)
}
}
}
}
extension IndexMainView {
//
struct DramaGroupView: View {
let group: IndexModel.UpdateDramaGroup
let model: IndexModel
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)
}
.background(GeometryReader { geometry in
let height = geometry.size.height
let minY = geometry.frame(in: .named("indexScrollView")).minY
let y = minY >= 0 ? minY : minY + height
Color.clear.preference(key: DramaGroupElementPreferenceKey.self, value: [ group.group_id : y])
})
}
}
}
}
}
// , [groupId : minY]
struct DramaGroupElementPreferenceKey: PreferenceKey {
static var defaultValue: [String: CGFloat] = [:]
static func reduce(value: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) {
value.merge(nextValue()) { $1 }
}
}
}
#Preview {
IndexMainView()
}

View File

@ -0,0 +1,214 @@
//
// IndexModel.swift
// dimensionhub
//
// Created by on 2025/4/8.
//
import Foundation
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 follow_num: Int
}
var selectedDate: String
//
var updateDramaGroups: [UpdateDramaGroup] = []
var follow_num: String = "0"
// group_name
var fixedDramaGroup: UpdateDramaGroup? = nil
@ObservationIgnored
private var isLoaded = false
init() {
self.selectedDate = ""
}
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):
await MainActor.run {
self.updateDramaGroups = result.update_dramas
self.fixedDramaGroup = result.update_dramas.first
self.follow_num = result.follow_num >= 100 ? "99+" : "\(result.follow_num)"
}
}
self.isLoaded = true
}
func setFixedDrameGroup(groupId: String) {
if let newFixedDramaGroup = self.updateDramaGroups.first(where: {$0.group_id == groupId}),
newFixedDramaGroup.group_id != self.fixedDramaGroup?.group_id {
self.fixedDramaGroup = newFixedDramaGroup
}
}
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)
await MainActor.run {
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)
await MainActor.run {
self.updateDramaGroups = appendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups)
}
print("----------after-----------")
displayDramaGroups(self.updateDramaGroups)
print("---------------------")
}
}
}
}
}
//
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 {
await MainActor.run {
self.updateDramaGroups = groups
self.fixedDramaGroup = groups.first
}
}
}
// 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 sortDramaGroups(groups: 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 sortDramaGroups(groups: targetGroups)
}
//
private func sortDramaGroups(groups: [UpdateDramaGroup]) -> [UpdateDramaGroup] {
return groups.sorted { g0, g1 in
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM"
if let date0 = dateFormatter.date(from: g0.group_id),
let date1 = dateFormatter.date(from: g1.group_id) {
return date0 > date1
} else {
return g0.group_id > g1.group_id
}
}
}
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)")
}
}
}

View File

@ -0,0 +1,61 @@
//
// ContentView.swift
// dimensionhub
//
// Created by on 2025/2/18.
//
import SwiftUI
import SwiftData
import Observation
import Network
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)
}
}
#Preview {
IndexView()
.modelContainer(for: Item.self, inMemory: true)
}

View File

@ -1,530 +0,0 @@
//
// 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 follow_num: Int
}
var selectedDate: String
//
var updateDramaGroups: [UpdateDramaGroup] = []
var follow_num: String = "0"
// group_name
var fixedDramaGroup: UpdateDramaGroup? = nil
@ObservationIgnored
private var isLoaded = false
init() {
self.selectedDate = ""
}
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):
await MainActor.run {
self.updateDramaGroups = result.update_dramas
self.fixedDramaGroup = result.update_dramas.first
self.follow_num = result.follow_num >= 100 ? "99+" : "\(result.follow_num)"
}
}
self.isLoaded = true
}
func setFixedDrameGroup(groupId: String) {
if let newFixedDramaGroup = self.updateDramaGroups.first(where: {$0.group_id == groupId}),
newFixedDramaGroup.group_id != self.fixedDramaGroup?.group_id {
self.fixedDramaGroup = newFixedDramaGroup
}
}
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)
await MainActor.run {
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)
await MainActor.run {
self.updateDramaGroups = appendMergeDramaGroups(groups: self.updateDramaGroups, mergeGroups: groups)
}
print("----------after-----------")
displayDramaGroups(self.updateDramaGroups)
print("---------------------")
}
}
}
}
}
//
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 {
await MainActor.run {
self.updateDramaGroups = groups
self.fixedDramaGroup = groups.first
}
}
}
// 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 sortDramaGroups(groups: 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 sortDramaGroups(groups: targetGroups)
}
//
private func sortDramaGroups(groups: [UpdateDramaGroup]) -> [UpdateDramaGroup] {
return groups.sorted { g0, g1 in
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM"
if let date0 = dateFormatter.date(from: g0.group_id),
let date1 = dateFormatter.date(from: g1.group_id) {
return date0 > date1
} else {
return g0.group_id > g1.group_id
}
}
}
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: 18, weight: .bold))
.padding([.top, .bottom], 5)
Spacer()
NavigationLink(destination: FollowListView()) {
HStack {
Text("\(indexModel.follow_num)")
.font(.system(size: 17))
.foregroundColor(.black)
.padding([.top, .bottom], 5)
}
}
}
.padding([.leading, .trailing], 15)
}
}
.frame(height: 50)
.background(Color(hex: "#F2F2F2"), ignoresSafeAreaEdges: .top)
ScrollView(.vertical, showsIndicators: false) {
//
LazyVStack(alignment: .center, spacing: 10) {
ForEach(indexModel.updateDramaGroups, id: \.group_id) { group in
DramaGroupView(group: group, model: indexModel) {
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()
}
}
.frame(width: 370)
.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
}
.overlay(alignment: .topTrailing) {
HStack(alignment: .center) {
NavigationLink {
SearchView()
} label: {
Image(systemName: "magnifyingglass")
.font(.system(size: 20))
}
Spacer()
if let fixedDramaGroup = indexModel.fixedDramaGroup {
Text(fixedDramaGroup.group_name)
.font(.system(size: 18))
.fontWeight(.regular)
.onTapGesture {
selectGroupId = fixedDramaGroup.group_id
indexModel.selectedDate = fixedDramaGroup.group_id
showDateNavPopover = true
}
}
}
.padding([.top, .bottom], 8)
.background(.white)
}
}
.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)
}
.onPreferenceChange(DramaGroupElementPreferenceKey.self) { frames in
let visibleFrames = frames.filter { $0.value >= 0}
if let minFrame = visibleFrames.min(by: { $0.value <= $1.value}) {
indexModel.setFixedDrameGroup(groupId: minFrame.key)
}
}
}
}
//
struct DramaGroupView: View {
let group: IndexModel.UpdateDramaGroup
let model: IndexModel
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)
}
.background(GeometryReader {
geometry in
let height = geometry.size.height
let minY = geometry.frame(in: .named("indexScrollView")).minY
let y = minY >= 0 ? minY : minY + height
Color.clear
.preference(key: DramaGroupElementPreferenceKey.self, value: [ group.group_id : y])
})
}
}
}
}
}
// , [groupId : minY]
struct DramaGroupElementPreferenceKey: PreferenceKey {
static var defaultValue: [String: CGFloat] = [:]
static func reduce(value: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) {
value.merge(nextValue()) { $1 }
}
}
}
#Preview {
IndexView()
.modelContainer(for: Item.self, inMemory: true)
}