Initial Create
基础框架实现
This commit is contained in:
@@ -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()
|
||||
}
|
||||
0
Jetson Media/been/Album.swift
Normal file
0
Jetson Media/been/Album.swift
Normal file
0
Jetson Media/been/AlbumItem.swift
Normal file
0
Jetson Media/been/AlbumItem.swift
Normal file
0
Jetson Media/cache/ImageCacheManager.swift
vendored
Normal file
0
Jetson Media/cache/ImageCacheManager.swift
vendored
Normal file
0
Jetson Media/cache/ReadingProgressManager.swift
vendored
Normal file
0
Jetson Media/cache/ReadingProgressManager.swift
vendored
Normal file
106
Jetson Media/ui/AsyncImageView.swift
Normal file
106
Jetson Media/ui/AsyncImageView.swift
Normal file
@@ -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<Int> = []
|
||||
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
|
||||
}
|
||||
}
|
||||
0
Jetson Media/ui/MainView.swift
Normal file
0
Jetson Media/ui/MainView.swift
Normal file
130
Jetson Media/ui/PhotoView.swift
Normal file
130
Jetson Media/ui/PhotoView.swift
Normal file
@@ -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<AnyCancellable>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
0
Jetson Media/ui/ProfileView.swift
Normal file
0
Jetson Media/ui/ProfileView.swift
Normal file
0
Jetson Media/ui/RankingsView.swift
Normal file
0
Jetson Media/ui/RankingsView.swift
Normal file
98
Jetson Media/ui/SearchView.swift
Normal file
98
Jetson Media/ui/SearchView.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
138
Jetson Media/ui/UIImage.swift
Normal file
138
Jetson Media/ui/UIImage.swift
Normal file
@@ -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..<num {
|
||||
var currentSegmentHeight = segmentHeight
|
||||
let ySrc = segmentHeight * i // 从顶部开始裁剪
|
||||
var yDst = segmentHeight * i // 从顶部开始绘制
|
||||
|
||||
if i == 0 {
|
||||
currentSegmentHeight += remainder // 第一段包含余数
|
||||
}
|
||||
|
||||
let srcRect = CGRect(
|
||||
x: 0,
|
||||
y: CGFloat(ySrc),
|
||||
width: CGFloat(totalWidth),
|
||||
height: CGFloat(currentSegmentHeight)
|
||||
)
|
||||
|
||||
let dstRect = CGRect(
|
||||
x: 0,
|
||||
y: CGFloat(yDst),
|
||||
width: CGFloat(totalWidth),
|
||||
height: CGFloat(currentSegmentHeight)
|
||||
)
|
||||
|
||||
print("段 \(i): 源矩形: \(srcRect),目标矩形: \(dstRect)")
|
||||
|
||||
if let croppedImage = cgImage.cropping(to: srcRect) {
|
||||
context.draw(croppedImage, in: dstRect)
|
||||
} else {
|
||||
print("段 \(i): 裁剪失败")
|
||||
}
|
||||
}
|
||||
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
|
||||
// 上下翻转图片逻辑
|
||||
private func flipVertically() -> 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
|
||||
}
|
||||
}
|
||||
0
Jetson Media/ui/Untitled.swift
Normal file
0
Jetson Media/ui/Untitled.swift
Normal file
0
Jetson Media/ui/UserDefaults.swift
Normal file
0
Jetson Media/ui/UserDefaults.swift
Normal file
Reference in New Issue
Block a user