将 otg_swift OTG 引擎迁入 Core/CameraOTG,并接入旅拍有线传图与历史导入。

恢复 USB 相机连接、边拍边传、三品牌驱动与 SwiftUI 历史导入页,通过适配层对接现有本地上传与 OSS 管道。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 11:22:07 +08:00
parent d7fec35715
commit be9c844bd3
50 changed files with 5721 additions and 20 deletions

View File

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

View File

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

View File

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

View File

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