将 otg_swift OTG 引擎迁入 Core/CameraOTG,并接入旅拍有线传图与历史导入。
恢复 USB 相机连接、边拍边传、三品牌驱动与 SwiftUI 历史导入页,通过适配层对接现有本地上传与 OSS 管道。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,61 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 历史导入照片网格单元格。
|
||||
struct CameraHistoryImportPhotoCell: View {
|
||||
let photo: CameraObject
|
||||
let isImported: Bool
|
||||
let isSelected: Bool
|
||||
let thumbnailProvider: () async -> Data?
|
||||
let onToggle: () -> Void
|
||||
|
||||
@State private var thumbnail: UIImage?
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Group {
|
||||
if let thumbnail {
|
||||
Image(uiImage: thumbnail)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
Color.gray.opacity(0.15)
|
||||
.overlay {
|
||||
ProgressView()
|
||||
.scaleEffect(0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.clipped()
|
||||
|
||||
if isImported {
|
||||
Text("已导入")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.background(Color.black.opacity(0.55))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.padding(6)
|
||||
} else {
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.font(.system(size: 22))
|
||||
.foregroundStyle(isSelected ? AppDesign.primary : .white)
|
||||
.shadow(radius: 2)
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
guard !isImported else { return }
|
||||
onToggle()
|
||||
}
|
||||
.task(id: photo.id) {
|
||||
guard thumbnail == nil else { return }
|
||||
if let data = await thumbnailProvider(), let image = UIImage(data: data) {
|
||||
thumbnail = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 历史导入分组标题栏。
|
||||
struct CameraHistoryImportSectionHeader: View {
|
||||
let title: String
|
||||
let isFullySelected: Bool
|
||||
let hasImportable: Bool
|
||||
let onToggleAll: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(WiredTransferDesign.text333)
|
||||
Spacer()
|
||||
if hasImportable {
|
||||
Button(isFullySelected ? "取消全选" : "全选") {
|
||||
onToggleAll()
|
||||
}
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(AppDesign.primary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(WiredTransferDesign.pageBackground)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 相机历史导入主页面。
|
||||
struct CameraHistoryImportView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var viewModel: CameraHistoryImportViewModel
|
||||
@State private var showSonyHelp = false
|
||||
|
||||
init(viewModel: CameraHistoryImportViewModel) {
|
||||
_viewModel = StateObject(wrappedValue: viewModel)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
content
|
||||
if case .importing = viewModel.state {
|
||||
importingOverlay
|
||||
}
|
||||
}
|
||||
.background(WiredTransferDesign.pageBackground.ignoresSafeArea())
|
||||
.navigationTitle("选择照片")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("取消") { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(importButtonTitle) {
|
||||
viewModel.importSelected()
|
||||
}
|
||||
.disabled(selectedCount == 0)
|
||||
}
|
||||
if viewModel.shouldShowSonyMTPHelp {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
showSonyHelp = true
|
||||
} label: {
|
||||
Image(systemName: "questionmark.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task { viewModel.loadPhotos() }
|
||||
.navigationDestination(isPresented: $showSonyHelp) {
|
||||
SonyMTPHelpView()
|
||||
}
|
||||
.onChange(of: viewModel.state) { state in
|
||||
if case .finished = state {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
switch viewModel.state {
|
||||
case .idle, .loading:
|
||||
ProgressView("正在加载相机照片…")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
case .loaded, .importing:
|
||||
photoList(sections: viewModel.sections)
|
||||
case .finished:
|
||||
EmptyView()
|
||||
case .failed(let message):
|
||||
emptyView(message: message)
|
||||
}
|
||||
}
|
||||
|
||||
private func photoList(sections: [CameraPhotoSection]) -> some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12, pinnedViews: [.sectionHeaders]) {
|
||||
ForEach(sections) { section in
|
||||
Section {
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 2) {
|
||||
ForEach(section.photos, id: \.id) { photo in
|
||||
CameraHistoryImportPhotoCell(
|
||||
photo: photo,
|
||||
isImported: viewModel.isPhotoAlreadyInAlbum(id: photo.id),
|
||||
isSelected: viewModel.isPhotoSelected(id: photo.id),
|
||||
thumbnailProvider: { await viewModel.thumbnailData(for: photo, maxPixelSize: 160) },
|
||||
onToggle: { viewModel.togglePhoto(id: photo.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
CameraHistoryImportSectionHeader(
|
||||
title: section.title,
|
||||
isFullySelected: viewModel.isSectionFullySelected(section),
|
||||
hasImportable: viewModel.hasImportablePhotos(in: section),
|
||||
onToggleAll: { viewModel.toggleSection(section) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 2)
|
||||
}
|
||||
}
|
||||
|
||||
private func emptyView(message: String) -> some View {
|
||||
VStack(spacing: 16) {
|
||||
Text(message)
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(WiredTransferDesign.text666)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
if viewModel.shouldShowSonyMTPHelp {
|
||||
Button("如何切换到 MTP 模式") {
|
||||
showSonyHelp = true
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
private var importingOverlay: some View {
|
||||
Group {
|
||||
if case .importing(let current, let total) = viewModel.state {
|
||||
VStack(spacing: 12) {
|
||||
ProgressView()
|
||||
Text("正在导入 \(current)/\(total)")
|
||||
.font(.system(size: 15))
|
||||
}
|
||||
.padding(24)
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedCount: Int {
|
||||
viewModel.selectedPhotoIDs.count
|
||||
}
|
||||
|
||||
private var importButtonTitle: String {
|
||||
"导入 (\(selectedCount))"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 索尼 MTP 模式帮助页。
|
||||
struct SonyMTPHelpView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(SonyMTPHelpContent.introduction)
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(WiredTransferDesign.text333)
|
||||
|
||||
ForEach(SonyMTPHelpContent.sections) { section in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(section.title)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(WiredTransferDesign.text333)
|
||||
Text(section.body)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(WiredTransferDesign.text666)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(12)
|
||||
.background(Color.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.background(WiredTransferDesign.pageBackground.ignoresSafeArea())
|
||||
.navigationTitle(SonyMTPHelpContent.navigationTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("关闭") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user