Initial Create

基础框架实现
This commit is contained in:
2025-08-16 23:45:52 +08:00
parent de8ac20776
commit cb7294d722
14 changed files with 472 additions and 24 deletions

View File

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

View File

View File

View File

View File

View 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
}
}

View File

View 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
}
}

View File

View File

View 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
}
}

View 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
}
}

View File

View File