对齐旅拍相册详情页 Android UI,并修复网格预览与布局问题。

重构相册详情为信息卡片、Tab 筛选、排序、批量删除与底部上传栏;修复网格重叠、禁用按钮蒙层,并支持点击预览大图。同步扩展素材列表 API 与 ViewModel 分页逻辑,并优化有线传图缩略图与传输性能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 13:53:59 +08:00
parent c71b45bdfd
commit 5e620623eb
23 changed files with 2000 additions and 288 deletions

View File

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