选择图片分享功能

This commit is contained in:
2025-08-17 22:08:25 +08:00
parent e6afca5ba6
commit d80289fe73
19 changed files with 1402 additions and 110 deletions

View File

@@ -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()
// PhotoViewonAppear
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
}
}