选择图片分享功能
This commit is contained in:
@@ -1,52 +1,190 @@
|
||||
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 {
|
||||
ZStack(alignment: .bottom) {
|
||||
// 背景色(根据全屏状态切换)
|
||||
Color(isFullscreen ? .black : .systemBackground)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
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 {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(album.name)
|
||||
.font(.headline)
|
||||
|
||||
Text("作者: \(album.authors.joined(separator: ", "))")
|
||||
.font(.subheadline)
|
||||
|
||||
Text("标签: \(album.tags.joined(separator: ", "))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.systemBackground))
|
||||
|
||||
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) // 强制图片填充父视图宽度
|
||||
.aspectRatio(contentMode: .fill) // 确保图片填充整个区域
|
||||
.clipped() // 防止图片超出边界
|
||||
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(currentImageIndex, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all) // 让内容填充整个屏幕
|
||||
} else if let error = viewModel.error {
|
||||
Text("加载失败: \(error.localizedDescription)")
|
||||
.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) {
|
||||
@@ -56,16 +194,62 @@ struct PhotoView: View {
|
||||
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))
|
||||
}
|
||||
}
|
||||
// 其余代码保持不变(PhotoView, PhotoViewModel, Album结构体等)
|
||||
|
||||
// 滚动偏移量偏好键
|
||||
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 scrollPosition: Double = 0
|
||||
@Published var isFavorite: Bool = false // 收藏状态
|
||||
@Published var showAlert = false
|
||||
@Published var alertTitle = ""
|
||||
@Published var alertMessage = ""
|
||||
@@ -74,17 +258,24 @@ class PhotoViewModel: ObservableObject {
|
||||
|
||||
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 = "https://jms.godserver.cn/album/\(albumId)/"
|
||||
let urlString = Config.shared.apiURL(path: "album/\(albumId)/")
|
||||
guard let url = URL(string: urlString) else {
|
||||
error = NSError(domain: "无效的URL", code: 0, userInfo: nil)
|
||||
isLoading = false
|
||||
let error = NSError(domain: "PhotoViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: "无效的URL"])
|
||||
handleError(error: error)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,34 +288,29 @@ class PhotoViewModel: ObservableObject {
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
self?.isLoading = false
|
||||
if case .failure(let error) = completion {
|
||||
self?.error = error
|
||||
print("相册加载失败: \(error.localizedDescription)")
|
||||
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
|
||||
}
|
||||
}
|
||||
struct Album: Codable {
|
||||
let album_id: String
|
||||
let name: String
|
||||
let authors: [String]
|
||||
let actors: [String]
|
||||
let tags: [String]
|
||||
let image_urls: [String]
|
||||
let nums: [Int]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case album_id, name, authors, actors, tags
|
||||
case image_urls = "image_urls"
|
||||
case nums
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user