对齐旅拍相册详情页 Android UI,并修复网格预览与布局问题。
重构相册详情为信息卡片、Tab 筛选、排序、批量删除与底部上传栏;修复网格重叠、禁用按钮蒙层,并支持点击预览大图。同步扩展素材列表 API 与 ViewModel 分页逻辑,并优化有线传图缩略图与传输性能。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -6,6 +6,8 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
/// 相机下载文件本地存储工具,负责目录创建、相对路径持久化与安全文件名处理。
|
||||
enum CameraDownloadStorage {
|
||||
@ -23,9 +25,21 @@ enum CameraDownloadStorage {
|
||||
return directory
|
||||
}
|
||||
|
||||
/// 相机照片列表缩略图目录。
|
||||
static var thumbnailsDirectory: URL {
|
||||
let directory = downloadsDirectory.appendingPathComponent("Thumbnails", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
return directory
|
||||
}
|
||||
|
||||
/// 将绝对路径转为相对 Documents 的路径,如 `CameraDownloads/1730_DSC.JPG`。
|
||||
static func relativePath(for url: URL) -> String {
|
||||
"\(cameraDownloadsFolderName)/\(url.lastPathComponent)"
|
||||
let standardizedURL = url.standardizedFileURL
|
||||
let thumbnailsPath = thumbnailsDirectory.standardizedFileURL.path
|
||||
if standardizedURL.path.hasPrefix(thumbnailsPath) {
|
||||
return "\(cameraDownloadsFolderName)/Thumbnails/\(url.lastPathComponent)"
|
||||
}
|
||||
return "\(cameraDownloadsFolderName)/\(url.lastPathComponent)"
|
||||
}
|
||||
|
||||
/// 将持久化的相对或旧版绝对路径解析为当前沙盒内的文件 URL。
|
||||
@ -82,6 +96,13 @@ enum CameraDownloadStorage {
|
||||
return downloadsDirectory.appendingPathComponent("\(timestamp)_\(safeName)")
|
||||
}
|
||||
|
||||
/// 生成带 asset ID 的唯一缩略图文件 URL。
|
||||
static func uniqueThumbnailURL(for assetID: String) -> URL {
|
||||
let safeName = sanitizeFilename(assetID)
|
||||
.replacingOccurrences(of: ".", with: "_")
|
||||
return thumbnailsDirectory.appendingPathComponent("\(safeName)_thumb.jpg")
|
||||
}
|
||||
|
||||
/// 将相机 PTP 文件名转为 iOS 文件系统可安全使用的 ASCII 名称。
|
||||
static func sanitizeFilename(_ filename: String) -> String {
|
||||
let trimmed = filename
|
||||
@ -111,4 +132,57 @@ enum CameraDownloadStorage {
|
||||
}
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
/// 后台读取文件大小,避免在列表刷新路径上阻塞主线程。
|
||||
static func fileSizeBytes(for url: URL?) async -> Int64 {
|
||||
guard let url else { return 0 }
|
||||
return await Task.detached(priority: .utility) {
|
||||
(try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64) ?? 0
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
||||
/// 相机本地照片缩略图生成器,负责为列表生成小尺寸预览图。
|
||||
enum CameraThumbnailGenerator {
|
||||
private static let maxPixelSize: CGFloat = 160
|
||||
|
||||
/// 为本地照片生成列表缩略图,成功时返回相对 Documents 的持久化路径。
|
||||
static func generateThumbnail(for imageURL: URL, assetID: String) async -> String? {
|
||||
await Task.detached(priority: .utility) {
|
||||
guard FileManager.default.fileExists(atPath: imageURL.path),
|
||||
let source = CGImageSourceCreateWithURL(imageURL as CFURL, nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let options: [CFString: Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceShouldCacheImmediately: false,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
|
||||
]
|
||||
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let destination = CameraDownloadStorage.uniqueThumbnailURL(for: assetID)
|
||||
if FileManager.default.fileExists(atPath: destination.path) {
|
||||
try? FileManager.default.removeItem(at: destination)
|
||||
}
|
||||
guard let imageDestination = CGImageDestinationCreateWithURL(
|
||||
destination as CFURL,
|
||||
UTType.jpeg.identifier as CFString,
|
||||
1,
|
||||
nil
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
CGImageDestinationAddImage(imageDestination, thumbnail, [
|
||||
kCGImageDestinationLossyCompressionQuality: 0.78
|
||||
] as CFDictionary)
|
||||
|
||||
guard CGImageDestinationFinalize(imageDestination) else { return nil }
|
||||
return CameraDownloadStorage.relativePath(for: destination)
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user