diff --git a/Jetson Media/ContentView.swift b/Jetson Media/ContentView.swift deleted file mode 100644 index b99224d..0000000 --- a/Jetson Media/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContentView.swift -// Jetson Media -// -// Created by Spasol Reisa on 2025/8/16. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - } -} - -#Preview { - ContentView() -} diff --git a/Jetson Media/been/Album.swift b/Jetson Media/been/Album.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/been/AlbumItem.swift b/Jetson Media/been/AlbumItem.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/cache/ImageCacheManager.swift b/Jetson Media/cache/ImageCacheManager.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/cache/ReadingProgressManager.swift b/Jetson Media/cache/ReadingProgressManager.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/ui/AsyncImageView.swift b/Jetson Media/ui/AsyncImageView.swift new file mode 100644 index 0000000..7096844 --- /dev/null +++ b/Jetson Media/ui/AsyncImageView.swift @@ -0,0 +1,106 @@ +import SwiftUI + +struct AsyncImageView: View { + let url: String + let nums: [Int] + let index: Int + @State private var image: UIImage? + @State private var isLoading = false + @State private var error: Error? + + // 静态属性,用于维护当前加载的索引范围 + private static var currentlyLoadingIndices: Set = [] + private static let maxConcurrentLoads = 4 + + var body: some View { + Group { + if let image = image { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + } else if isLoading { + ProgressView() + } else if error != nil { + VStack { + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.red) + Text(error?.localizedDescription ?? "未知错误") + .font(.caption) + } + } else { + Color.gray.opacity(0.3) + } + } + .onAppear { + loadImage() + } + } + + private func loadImage() { + // 如果已经在加载中,或者超出最大加载限制,直接返回 + guard !Self.currentlyLoadingIndices.contains(index), + Self.currentlyLoadingIndices.count < Self.maxConcurrentLoads else { + return + } + + // 添加当前索引到加载集合 + Self.currentlyLoadingIndices.insert(index) + isLoading = true + error = nil + + // 确保 nums 和 index 对齐,避免越界 + guard let decodeNum = nums[safe: index] else { + print("错误:nums 中未找到索引 \(index) 对应的值,可能 API 数据不一致") + Self.currentlyLoadingIndices.remove(index) + return + } + + print("调试信息:图片 URL: \(url), 解密参数 num: \(decodeNum), 索引: \(index)") + + guard let imageUrl = URL(string: url) else { + self.error = NSError(domain: "无效的URL", code: 1, userInfo: nil) + isLoading = false + Self.currentlyLoadingIndices.remove(index) + return + } + + // 加载图片数据 + URLSession.shared.dataTask(with: imageUrl) { data, response, error in + DispatchQueue.main.async { + // 移除当前索引 + Self.currentlyLoadingIndices.remove(index) + self.isLoading = false + + if let error = error { + self.error = error + print("图片加载失败,错误: \(error.localizedDescription)") + return + } + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode), + let data = data, !data.isEmpty else { + self.error = NSError(domain: "无效响应", code: 2, userInfo: nil) + print("图片加载失败,无效响应或数据为空") + return + } + + if let loadedImage = UIImage(data: data) { + print("图片加载成功,开始解密...") + let decodedImage = loadedImage.decodeImage(num: decodeNum) + self.image = decodedImage + print("解密完成,图片已更新") + } else { + self.error = NSError(domain: "图片数据解码失败", code: 4, userInfo: nil) + print("图片数据解码失败") + } + } + }.resume() + } +} + +extension Array { + subscript(safe index: Int) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Jetson Media/ui/MainView.swift b/Jetson Media/ui/MainView.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/ui/PhotoView.swift b/Jetson Media/ui/PhotoView.swift new file mode 100644 index 0000000..6469d45 --- /dev/null +++ b/Jetson Media/ui/PhotoView.swift @@ -0,0 +1,130 @@ +import SwiftUI +import Combine + +struct PhotoView: View { + let albumId: String + @StateObject private var viewModel: PhotoViewModel + + init(albumId: String) { + self.albumId = albumId + self._viewModel = StateObject(wrappedValue: PhotoViewModel(albumId: albumId)) + } + + var body: some View { + ZStack { + if viewModel.isLoading { + ProgressView() + } 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() // 防止图片超出边界 + } + } + } + .edgesIgnoringSafeArea(.all) // 让内容填充整个屏幕 + } else if let error = viewModel.error { + Text("加载失败: \(error.localizedDescription)") + } + } + .alert(isPresented: $viewModel.showAlert) { + Alert( + title: Text(viewModel.alertTitle), + message: Text(viewModel.alertMessage), + dismissButton: .default(Text("确定")) + ) + } + } +} +// 其余代码保持不变(PhotoView, PhotoViewModel, Album结构体等) +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 showAlert = false + @Published var alertTitle = "" + @Published var alertMessage = "" + + private var cancellables = Set() + + init(albumId: String) { + self.albumId = albumId + loadAlbumData() + } + + func loadAlbumData() { + isLoading = true + error = nil + + let urlString = "https://jms.godserver.cn/album/\(albumId)/" + guard let url = URL(string: urlString) else { + error = NSError(domain: "无效的URL", code: 0, userInfo: nil) + isLoading = false + 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?.error = error + print("相册加载失败: \(error.localizedDescription)") + } + }, receiveValue: { [weak self] album in + self?.album = album + print("成功加载相册: \(album.name), 包含 \(album.image_urls.count) 张图片") + }) + .store(in: &cancellables) + } + + 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 + } +} diff --git a/Jetson Media/ui/ProfileView.swift b/Jetson Media/ui/ProfileView.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/ui/RankingsView.swift b/Jetson Media/ui/RankingsView.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/ui/SearchView.swift b/Jetson Media/ui/SearchView.swift new file mode 100644 index 0000000..9a6164f --- /dev/null +++ b/Jetson Media/ui/SearchView.swift @@ -0,0 +1,98 @@ +import SwiftUI + +struct SearchView: View { + @State private var searchQuery: String = "" + @State private var searchResults: [AlbumItem] = [] + @State private var isSearching: Bool = false + @State private var showAlert: Bool = false + @State private var alertMessage: String = "" + + var body: some View { + Form { + Section(header: Text("搜索")) { + HStack { + TextField("输入搜索内容", text: $searchQuery) + .textFieldStyle(RoundedBorderTextFieldStyle()) + + Button(action: performSearch) { + if isSearching { + ProgressView() + } else { + Image(systemName: "magnifyingglass") + } + } + .disabled(searchQuery.isEmpty || isSearching) + } + } + + if !searchResults.isEmpty { + Section(header: Text("搜索结果")) { + ForEach(searchResults, id: \.album_id) { item in + NavigationLink(destination: PhotoView(albumId: item.album_id)) { + VStack(alignment: .leading) { + Text(item.title) + .font(.headline) + Text("ID: \(item.album_id)") + .font(.subheadline) + .foregroundColor(.gray) + } + } + } + } + } + } + .alert(isPresented: $showAlert) { + Alert(title: Text("提示"), message: Text(alertMessage), dismissButton: .default(Text("确定"))) + } + } + + private func performSearch() { + guard !searchQuery.isEmpty else { return } + + isSearching = true + searchResults = [] + + let encodedQuery = searchQuery.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? searchQuery + let urlString = "https://jms.godserver.cn/search/?search_query=\(encodedQuery)&page=1" + + guard let url = URL(string: urlString) else { + showAlert(message: "无效的URL") + isSearching = false + return + } + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + DispatchQueue.main.async { + self.isSearching = false + + if let error = error { + self.showAlert(message: "搜索失败: \(error.localizedDescription)") + return + } + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + self.showAlert(message: "服务器返回错误") + return + } + + guard let data = data else { + self.showAlert(message: "没有接收到数据") + return + } + + do { + let decoder = JSONDecoder() + self.searchResults = try decoder.decode([AlbumItem].self, from: data) + } catch { + print("解码错误: \(error)") + self.showAlert(message: "解析数据失败: \(error.localizedDescription)") + } + } + } + task.resume() + } + private func showAlert(message: String) { + alertMessage = message + showAlert = true + } +} diff --git a/Jetson Media/ui/UIImage.swift b/Jetson Media/ui/UIImage.swift new file mode 100644 index 0000000..f270f56 --- /dev/null +++ b/Jetson Media/ui/UIImage.swift @@ -0,0 +1,138 @@ +import UIKit + +extension UIImage { + func decodeImage(num: Int) -> UIImage { + guard let decodedImage = decodeImage2(num: num) else { + print("解密失败,返回原图") + return self + } + + // 将解密后的图片进行上下翻转 + // 2. 上下翻转图片 + //let flippedImage = decodedImage.flipVertically() + + // 3. 左右镜像图片 + let mirroredImage = decodedImage.wrongFlipHorizontally().correctFlipHorizontally() + return mirroredImage + } + // 当前错误的水平翻转(保持原样) + private func wrongFlipHorizontally() -> UIImage { + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + defer { UIGraphicsEndImageContext() } + + guard let context = UIGraphicsGetCurrentContext(), + let cgImage = self.cgImage else { + print("镜像失败:无法获取 CGImage") + return self + } + + // 当前错误的翻转逻辑(实际会产生垂直翻转效果) + context.translateBy(x: self.size.width, y: 0) + context.scaleBy(x: -1.0, y: 1.0) + + context.draw(cgImage, in: CGRect(origin: .zero, size: self.size)) + + return UIGraphicsGetImageFromCurrentImageContext() ?? self + } + + // 新增:正确的水平翻转方法 + private func correctFlipHorizontally() -> UIImage { + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + defer { UIGraphicsEndImageContext() } + + guard let context = UIGraphicsGetCurrentContext(), + let cgImage = self.cgImage else { + print("镜像失败:无法获取 CGImage") + return self + } + + // 正确的水平翻转逻辑 + context.translateBy(x: self.size.width, y: self.size.height) + context.scaleBy(x: -1.0, y: -1.0) // 只翻转x轴 + + context.draw(cgImage, in: CGRect(origin: .zero, size: self.size)) + + return UIGraphicsGetImageFromCurrentImageContext() ?? self + } + // 解密图片逻辑(与之前一致) + private func decodeImage2(num: Int) -> UIImage? { + guard num > 0 else { + print("解密跳过:num 参数为 0,直接返回原图") + return self + } + + let width = Int(self.size.width * self.scale) + let height = Int(self.size.height * self.scale) + let totalHeight = height // 总高度为整数 + let totalWidth = width + + UIGraphicsBeginImageContextWithOptions(CGSize(width: totalWidth, height: totalHeight), false, self.scale) + defer { UIGraphicsEndImageContext() } + + guard let context = UIGraphicsGetCurrentContext(), + let cgImage = self.cgImage else { + print("解密失败:无法创建图形上下文或获取 CGImage") + return nil + } + + let segmentHeight = totalHeight / num // 每段高度 + let remainder = totalHeight % num // 余数 + + print("图片总高度: \(totalHeight),每段高度: \(segmentHeight),余数: \(remainder)") + + for i in 0.. UIImage { + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + defer { UIGraphicsEndImageContext() } + + guard let context = UIGraphicsGetCurrentContext(), + let cgImage = self.cgImage else { + print("翻转失败:无法获取 CGImage") + return self + } + + // 翻转坐标系 + context.translateBy(x: 0, y: self.size.height) + context.scaleBy(x: 1.0, y: -1.0) + + // 绘制翻转后的图片 + context.draw(cgImage, in: CGRect(origin: .zero, size: self.size)) + + return UIGraphicsGetImageFromCurrentImageContext() ?? self + } +} diff --git a/Jetson Media/ui/Untitled.swift b/Jetson Media/ui/Untitled.swift new file mode 100644 index 0000000..e69de29 diff --git a/Jetson Media/ui/UserDefaults.swift b/Jetson Media/ui/UserDefaults.swift new file mode 100644 index 0000000..e69de29