317 lines
13 KiB
Swift
317 lines
13 KiB
Swift
import SwiftUI
|
||
import Combine
|
||
import Foundation
|
||
import UIKit
|
||
|
||
struct PhotoView: View {
|
||
let albumId: String
|
||
@StateObject private var viewModel: PhotoViewModel
|
||
@State private var currentImageIndex: Int = 0 // 当前可见的图片索引
|
||
@State private var progress: Double = 0 // 进度条比例(0-1)
|
||
@State private var totalImages: Int = 0 // 总图片数量
|
||
@State private var isFullscreen = false // 全屏状态标记
|
||
@Environment(\.presentationMode) var presentationMode // 返回上级页面
|
||
@EnvironmentObject var tabState: TabBarState // 获取状态对象
|
||
|
||
|
||
// 定时器:降低保存频率,减少性能消耗
|
||
private let saveTimer = Timer.publish(every: 2, on: .main, in: .common)
|
||
private var timerCancellable: AnyCancellable? // 用于存储定时器订阅
|
||
|
||
init(albumId: String) {
|
||
self.albumId = albumId
|
||
self._viewModel = StateObject(wrappedValue: PhotoViewModel(albumId: albumId))
|
||
self.timerCancellable = saveTimer.autoconnect().sink(receiveValue: { _ in })
|
||
}
|
||
|
||
var body: some View {
|
||
ZStack(alignment: .bottom) {
|
||
// 背景色(根据全屏状态切换)
|
||
Color(isFullscreen ? .black : .systemBackground)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
if viewModel.isLoading {
|
||
ProgressView("加载中...")
|
||
.foregroundColor(isFullscreen ? .white : .black)
|
||
} else if let error = viewModel.error {
|
||
VStack(spacing: 16) {
|
||
Text("加载失败")
|
||
.font(.headline)
|
||
.foregroundColor(isFullscreen ? .white : .black)
|
||
Text(error.localizedDescription)
|
||
.font(.subheadline)
|
||
.foregroundColor(isFullscreen ? .gray : .secondary)
|
||
Button("重试") { viewModel.loadAlbumData() }
|
||
.foregroundColor(.blue)
|
||
.padding()
|
||
.background(Color.blue.opacity(0.1))
|
||
.cornerRadius(8)
|
||
}
|
||
} else if let album = viewModel.album {
|
||
ScrollView {
|
||
ScrollViewReader { proxy in
|
||
// 漫画信息头部(全屏时隐藏)
|
||
if !isFullscreen {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text(album.name)
|
||
.font(.headline)
|
||
Text("作者: \(album.authors.joined(separator: ", "))")
|
||
.font(.subheadline)
|
||
.foregroundColor(isFullscreen ? .gray : .secondary)
|
||
Text("标签: \(album.tags.joined(separator: ", "))")
|
||
.font(.caption)
|
||
.foregroundColor(isFullscreen ? .gray : .secondary)
|
||
|
||
// 收藏按钮
|
||
Button(action: toggleFavorite) {
|
||
Image(systemName: viewModel.isFavorite ? "heart.fill" : "heart")
|
||
.foregroundColor(viewModel.isFavorite ? .red : .gray)
|
||
Text(viewModel.isFavorite ? "取消收藏" : "收藏")
|
||
.foregroundColor(isFullscreen ? .white : .black)
|
||
}
|
||
.padding(.vertical, 8)
|
||
}
|
||
.padding()
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.background(isFullscreen ? Color.black : Color(.systemBackground))
|
||
.id("header")
|
||
}
|
||
|
||
// 图片列表
|
||
LazyVStack(spacing: 0) {
|
||
ForEach(Array(album.image_urls.enumerated()), id: \.offset) { index, url in
|
||
AsyncImageView(
|
||
url: url,
|
||
nums: album.nums,
|
||
index: index
|
||
)
|
||
.frame(maxWidth: .infinity)
|
||
.id(index)
|
||
// 当图片进入屏幕时更新当前索引
|
||
.onAppear {
|
||
// 只有当图片索引大于当前索引时才更新(避免回滚时误判)
|
||
if index > currentImageIndex || abs(index - currentImageIndex) > 3 {
|
||
currentImageIndex = index
|
||
updateProgressRatio()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.onAppear {
|
||
totalImages = album.image_urls.count
|
||
// 初始化:加载上次保存的图片索引
|
||
let savedIndex = ReadingProgressManager.shared.getProgress(albumId: albumId)
|
||
currentImageIndex = clamp(value: savedIndex, min: 0, max: totalImages - 1)
|
||
updateProgressRatio()
|
||
// 在PhotoView的onAppear中添加
|
||
UserDefaults.standard.addHistory(albumId: albumId)
|
||
// 自动跳转到上次位置
|
||
print("上次阅读位置: \(savedIndex)")
|
||
if totalImages > 0, currentImageIndex > 0 {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||
withAnimation {
|
||
proxy.scrollTo(savedIndex, anchor: .top)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 进度条拖动时同步到对应图片
|
||
.onChange(of: progress) { newValue in
|
||
let targetIndex = Int(newValue * Double(max(0, totalImages - 1)))
|
||
let clampedIndex = clamp(value: targetIndex, min: 0, max: totalImages - 1)
|
||
if clampedIndex != currentImageIndex {
|
||
currentImageIndex = clampedIndex
|
||
withAnimation {
|
||
proxy.scrollTo(currentImageIndex, anchor: .top)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.padding(.top, isFullscreen ? 0 : 60)
|
||
.edgesIgnoringSafeArea(isFullscreen ? .all : .vertical)
|
||
.onTapGesture {
|
||
// 点击切换全屏状态
|
||
withAnimation {
|
||
tabState.toggleTabBar()
|
||
isFullscreen.toggle()
|
||
adjustSystemUI()
|
||
}
|
||
}
|
||
// 定期保存进度
|
||
.onReceive(saveTimer) { _ in
|
||
guard totalImages > 0 else { return }
|
||
ReadingProgressManager.shared.saveProgress(albumId: albumId, imageIndex: currentImageIndex)
|
||
}
|
||
.onDisappear {
|
||
ReadingProgressManager.shared.saveProgress(albumId: albumId, imageIndex: currentImageIndex)
|
||
timerCancellable?.cancel() // 取消定时器订阅
|
||
// 恢复系统UI
|
||
UIApplication.shared.isStatusBarHidden = false
|
||
}
|
||
|
||
// 控制栏(全屏时显示)
|
||
if isFullscreen {
|
||
// 顶部控制栏
|
||
VStack {
|
||
HStack {
|
||
// 返回按钮
|
||
Button(action: { presentationMode.wrappedValue.dismiss() }) {
|
||
Image(systemName: "chevron.left")
|
||
.foregroundColor(.white)
|
||
.padding()
|
||
.background(Color.black.opacity(0.5))
|
||
.cornerRadius(8)
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
.padding()
|
||
Spacer()
|
||
}
|
||
}
|
||
|
||
if totalImages > 0 && !isFullscreen {
|
||
CustomSlider(
|
||
value: $progress,
|
||
trackHeight: 20, // 这里可以设置足够粗的高度(如20px)
|
||
tintColor: .blue
|
||
)
|
||
.background(Color(.systemBackground).opacity(0.9))
|
||
.cornerRadius(8) // 圆角配合高度,更美观
|
||
.shadow(radius: 5)
|
||
.padding(.horizontal, 30) // 左右留空
|
||
.padding(.bottom, tabState.isTabBarHidden ? 30 : 60) // 底部距离
|
||
.padding(.top, 10) // 向上偏移
|
||
}
|
||
|
||
}
|
||
}
|
||
.alert(isPresented: $viewModel.showAlert) {
|
||
Alert(
|
||
title: Text(viewModel.alertTitle),
|
||
message: Text(viewModel.alertMessage),
|
||
dismissButton: .default(Text("确定"))
|
||
)
|
||
}
|
||
.navigationBarHidden(isFullscreen)
|
||
.statusBarHidden(isFullscreen)
|
||
.onAppear {
|
||
// 初始化系统UI状态
|
||
adjustSystemUI()
|
||
}
|
||
.onDisappear {
|
||
// 离开阅读器时显示底部导航栏
|
||
tabState.showTabBar()
|
||
}
|
||
}
|
||
|
||
// 调整系统UI(状态栏和导航栏)
|
||
private func adjustSystemUI() {
|
||
UIApplication.shared.isStatusBarHidden = isFullscreen
|
||
// 强制刷新状态栏
|
||
UIApplication.shared.windows.first?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
|
||
}
|
||
|
||
// 更新进度条比例
|
||
private func updateProgressRatio() {
|
||
guard totalImages > 0 else {
|
||
progress = 0
|
||
return
|
||
}
|
||
let safeTotal = max(1, totalImages - 1) // 避免除以0
|
||
progress = Double(currentImageIndex) / Double(safeTotal)
|
||
}
|
||
|
||
// 切换收藏状态
|
||
private func toggleFavorite() {
|
||
FavoriteManager.shared.toggleFavorite(albumId: albumId)
|
||
viewModel.isFavorite = FavoriteManager.shared.isFavorite(albumId: albumId)
|
||
}
|
||
|
||
// 限制数值范围
|
||
private func clamp(value: Int, min: Int, max: Int) -> Int {
|
||
return Swift.max(min, Swift.min(value, max))
|
||
}
|
||
}
|
||
|
||
// 滚动偏移量偏好键
|
||
struct ScrollOffsetKey: PreferenceKey {
|
||
static var defaultValue: CGFloat = 0
|
||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||
value = nextValue()
|
||
}
|
||
}
|
||
|
||
class PhotoViewModel: ObservableObject {
|
||
let albumId: String
|
||
|
||
@Published var album: Album?
|
||
@Published var isLoading = false
|
||
@Published var error: Error?
|
||
@Published var isFavorite: Bool = false // 收藏状态
|
||
@Published var showAlert = false
|
||
@Published var alertTitle = ""
|
||
@Published var alertMessage = ""
|
||
|
||
private var cancellables = Set<AnyCancellable>()
|
||
|
||
init(albumId: String) {
|
||
self.albumId = albumId
|
||
checkIfFavorite()
|
||
loadAlbumData()
|
||
}
|
||
|
||
// 检查是否已收藏
|
||
private func checkIfFavorite() {
|
||
isFavorite = FavoriteManager.shared.isFavorite(albumId: albumId)
|
||
}
|
||
|
||
func loadAlbumData() {
|
||
isLoading = true
|
||
error = nil
|
||
album = nil // 重置之前的数据
|
||
|
||
let urlString = Config.shared.apiURL(path: "album/\(albumId)/")
|
||
guard let url = URL(string: urlString) else {
|
||
let error = NSError(domain: "PhotoViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: "无效的URL"])
|
||
handleError(error: error)
|
||
return
|
||
}
|
||
|
||
print("正在加载相册数据: \(urlString)")
|
||
|
||
URLSession.shared.dataTaskPublisher(for: url)
|
||
.map(\.data)
|
||
.decode(type: Album.self, decoder: JSONDecoder())
|
||
.receive(on: DispatchQueue.main)
|
||
.sink(receiveCompletion: { [weak self] completion in
|
||
self?.isLoading = false
|
||
if case .failure(let error) = completion {
|
||
self?.handleError(error: error)
|
||
}
|
||
}, receiveValue: { [weak self] album in
|
||
self?.isLoading = false
|
||
self?.album = album
|
||
print("成功加载相册: \(album.name), 包含 \(album.image_urls.count) 张图片")
|
||
})
|
||
.store(in: &cancellables)
|
||
}
|
||
|
||
// 错误处理封装
|
||
private func handleError(error: Error) {
|
||
self.error = error
|
||
self.alertTitle = "加载失败"
|
||
self.alertMessage = error.localizedDescription
|
||
self.showAlert = true
|
||
self.isLoading = false
|
||
print("相册加载失败: \(error.localizedDescription)")
|
||
}
|
||
|
||
func showAlert(title: String, message: String) {
|
||
alertTitle = title
|
||
alertMessage = message
|
||
showAlert = true
|
||
}
|
||
}
|