wake-ios/wake/View/Blind/BlindBoxViewModel.swift

175 lines
6.6 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.

import Foundation
import Combine
@MainActor
final class BlindBoxViewModel: ObservableObject {
// Inputs
let mediaType: BlindBoxMediaType
let currentBoxId: String?
// Published state
@Published var isMember: Bool = false
@Published var memberDate: String = ""
@Published var memberProfile: MemberProfile? = nil
@Published var blindCount: BlindCount? = nil
@Published var blindGenerate: BlindBoxData? = nil
@Published var videoURL: String = ""
@Published var imageURL: String = ""
@Published var didBootstrap: Bool = false
@Published var countdownText: String = ""
// Tasks
private var pollingTask: Task<Void, Never>? = nil
private var countdownTask: Task<Void, Never>? = nil
private var remainingSeconds: Int = 0
init(mediaType: BlindBoxMediaType, currentBoxId: String?) {
self.mediaType = mediaType
self.currentBoxId = currentBoxId
}
func load() async {
await bootstrapInitialState()
await startPolling()
loadMemberProfile()
await loadBlindCount()
}
func startPolling() async {
// Unopened
if blindGenerate?.status == "Unopened" { return }
stopPolling()
if let boxId = currentBoxId {
// Poll a single box until unopened
pollingTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
for try await data in BlindBoxPolling.singleBox(boxId: boxId, intervalSeconds: 2.0) {
print("[VM] SingleBox polled status: \(data.status)")
self.blindGenerate = data
if self.mediaType == .image {
self.imageURL = data.resultFile?.url ?? ""
} else {
self.videoURL = data.resultFile?.url ?? ""
}
self.applyStatusSideEffects()
break
}
} catch is CancellationError {
// cancelled
} catch {
print("❌ BlindBoxViewModel polling error (single): \(error)")
}
}
} else {
// Poll list and yield first unopened
pollingTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
for try await item in BlindBoxPolling.firstUnopened(intervalSeconds: 2.0) {
print("[VM] List polled first unopened: id=\(item.id ?? "nil"), status=\(item.status)")
self.blindGenerate = item
if self.mediaType == .image {
self.imageURL = item.resultFile?.url ?? ""
} else {
self.videoURL = item.resultFile?.url ?? ""
}
self.applyStatusSideEffects()
break
}
} catch is CancellationError {
// cancelled
} catch {
print("❌ BlindBoxViewModel polling error (list): \(error)")
}
}
}
}
private func bootstrapInitialState() async {
if let boxId = currentBoxId {
do {
let data = try await BlindBoxApi.shared.getBlindBox(boxId: boxId)
if let data = data {
self.blindGenerate = data
if mediaType == .image {
self.imageURL = data.resultFile?.url ?? ""
} else {
self.videoURL = data.resultFile?.url ?? ""
}
self.applyStatusSideEffects()
}
} catch {
print("❌ bootstrapInitialState (single) failed: \(error)")
}
} else {
do {
let list = try await BlindBoxApi.shared.getBlindBoxList()
//
let count = (list ?? []).filter { $0.status == "Unopened" }.count
self.blindCount = BlindCount(availableQuantity: count)
if let item = list?.first(where: { $0.status == "Unopened" }) {
self.blindGenerate = item
if mediaType == .image {
self.imageURL = item.resultFile?.url ?? ""
} else {
self.videoURL = item.resultFile?.url ?? ""
}
self.applyStatusSideEffects()
} else if let first = list?.first {
// Unopened Preparing
self.blindGenerate = first
self.applyStatusSideEffects()
}
} catch {
print("❌ bootstrapInitialState (list) failed: \(error)")
}
}
// loading/ready
self.didBootstrap = true
}
func stopPolling() {
pollingTask?.cancel()
pollingTask = nil
}
func openBlindBox(for id: String) async throws {
try await BlindBoxApi.shared.openBlindBox(boxId: id)
}
private func loadMemberProfile() {
NetworkService.shared.get(
path: "/membership/personal-center-info",
parameters: nil
) { [weak self] (result: Result<MemberProfileResponse, NetworkError>) in
Task { @MainActor in
guard let self else { return }
switch result {
case .success(let response):
self.memberProfile = response.data
self.isMember = response.data.membershipLevel == "Pioneer"
self.memberDate = response.data.membershipEndAt ?? ""
print("✅ 成功获取会员信息:", response.data)
print("✅ 用户ID:", response.data.userInfo.userId)
case .failure(let error):
print("❌ 获取会员信息失败:", error)
}
}
}
}
private func loadBlindCount() async {
do {
let list = try await BlindBoxApi.shared.getBlindBoxList()
let count = (list ?? []).filter { $0.status == "Unopened" }.count
self.blindCount = BlindCount(availableQuantity: count)
} catch {
print("❌ 获取盲盒列表失败: \(error)")
}
}
}