还原顶栏、浮动卡片、时间侧栏与底部交互;照片路径改为 CameraDownloads 相对路径并兼容旧数据;格式 Chip 固定 JPG 且不可点击。 Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
9.0 KiB
Swift
248 lines
9.0 KiB
Swift
//
|
||
// WiredCameraTransferView.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/29.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// 有线传图页,连接相机并上传照片到旅拍相册。
|
||
struct WiredCameraTransferView: View {
|
||
let context: WiredTransferContext
|
||
|
||
@EnvironmentObject private var accountContext: AccountContext
|
||
@Environment(\.travelAlbumAPI) private var travelAlbumAPI
|
||
@Environment(\.ossUploadService) private var ossUploadService
|
||
@EnvironmentObject private var toastCenter: ToastCenter
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
@StateObject private var viewModel: WiredCameraTransferViewModel
|
||
@State private var previewSources: [String]?
|
||
@State private var previewStartIndex = 0
|
||
|
||
init(context: WiredTransferContext) {
|
||
self.context = context
|
||
_viewModel = StateObject(
|
||
wrappedValue: WiredCameraTransferViewModel(
|
||
context: context,
|
||
cameraService: CameraServiceFactory.make(brand: .sony)
|
||
)
|
||
)
|
||
}
|
||
|
||
private let tabTitles = ["全部", "已上传", "失败"]
|
||
private var scenicID: Int { accountContext.currentScenic?.id ?? 0 }
|
||
private var scenicSpotLabel: String? {
|
||
accountContext.currentScenic?.name
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
WiredTransferHeaderView(
|
||
albumTitle: context.albumName,
|
||
headerPhone: context.phone,
|
||
onBack: { dismiss() }
|
||
)
|
||
|
||
WiredTransferControlCard(
|
||
scenicSpotLabel: scenicSpotLabel,
|
||
deviceStorageInfo: viewModel.deviceStorageInfo,
|
||
isCameraConnected: viewModel.isCameraConnected,
|
||
isRefreshingCameraFiles: viewModel.isRefreshingCameraFiles,
|
||
cameraDeviceName: viewModel.cameraDeviceName,
|
||
transferModeOption: viewModel.transferModeOption,
|
||
tabTitles: tabTitles,
|
||
tabCounts: viewModel.photos.transferTabCounts,
|
||
selectedTabIndex: viewModel.selectedTabIndex,
|
||
onDeviceUsageClick: viewModel.onDeviceUsageClick,
|
||
onRefreshCameraFiles: {
|
||
Task { await viewModel.refreshCameraFiles() }
|
||
},
|
||
onHelpClick: viewModel.openHelpDoc,
|
||
onTransferModeSelected: { option in
|
||
viewModel.selectTransferMode(
|
||
option,
|
||
api: travelAlbumAPI,
|
||
ossService: ossUploadService,
|
||
scenicID: scenicID
|
||
)
|
||
},
|
||
onTabSelected: viewModel.selectTab
|
||
)
|
||
|
||
if viewModel.selectUploadMode, !visiblePhotos.isEmpty {
|
||
selectAllBar
|
||
}
|
||
|
||
contentSection
|
||
|
||
WiredTransferBottomBar(
|
||
selectUploadMode: viewModel.selectUploadMode,
|
||
selectedCount: viewModel.selectedPhotoIDs.count,
|
||
onBatchUploadClick: {
|
||
Task {
|
||
await viewModel.onBatchUploadButtonClick(
|
||
api: travelAlbumAPI,
|
||
ossService: ossUploadService,
|
||
scenicID: scenicID
|
||
)
|
||
}
|
||
},
|
||
onSpecifyUploadClick: viewModel.onSpecifyUploadButtonClick
|
||
)
|
||
}
|
||
.background(WiredTransferDesign.pageBackground.ignoresSafeArea())
|
||
.toolbar(.hidden, for: .navigationBar)
|
||
.task {
|
||
await viewModel.start(
|
||
api: travelAlbumAPI,
|
||
ossService: ossUploadService,
|
||
scenicID: scenicID,
|
||
accountContext: accountContext
|
||
)
|
||
}
|
||
.onDisappear {
|
||
Task { await viewModel.stop() }
|
||
}
|
||
.onChange(of: viewModel.errorMessage) { message in
|
||
guard let message, !message.isEmpty else { return }
|
||
toastCenter.show(message)
|
||
viewModel.clearErrorMessage()
|
||
}
|
||
.onChange(of: viewModel.connectionState) { state in
|
||
switch state {
|
||
case .connected(let name):
|
||
toastCenter.show("已连接 \(name)")
|
||
case .error(let message):
|
||
toastCenter.show(message)
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
.alert("手机内存不足", isPresented: $viewModel.showDeviceStorageAlert) {
|
||
Button("我知道了", role: .cancel) {}
|
||
} message: {
|
||
Text("请及时清理内存")
|
||
}
|
||
.alert("相机已断开", isPresented: $viewModel.showCameraDisconnectedAlert) {
|
||
Button("我知道了", role: .cancel) {}
|
||
} message: {
|
||
Text("相机已断开,请检查 USB 连接。")
|
||
}
|
||
.sheet(isPresented: $viewModel.showSpecifyUploadSheet) {
|
||
SpecifyUploadBottomSheet(isPresented: $viewModel.showSpecifyUploadSheet) { option in
|
||
Task {
|
||
await viewModel.onSpecifyUploadOptionSelected(
|
||
option,
|
||
api: travelAlbumAPI,
|
||
ossService: ossUploadService,
|
||
scenicID: scenicID
|
||
)
|
||
}
|
||
}
|
||
.presentationDetents([.height(340)])
|
||
.presentationDragIndicator(.hidden)
|
||
}
|
||
.sheet(isPresented: $viewModel.showHelpDoc) {
|
||
NavigationStack {
|
||
WiredTransferHelpDocView(url: WiredCameraTransferViewModel.helpDocURL)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button("关闭") { viewModel.showHelpDoc = false }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.sheet(item: previewBinding) { item in
|
||
WiredTransferPhotoPreviewSheet(
|
||
imageSources: item.sources,
|
||
startIndex: item.startIndex,
|
||
onDismiss: { previewSources = nil }
|
||
)
|
||
}
|
||
}
|
||
|
||
private var visiblePhotos: [WiredTransferPhotoItem] {
|
||
viewModel.visiblePhotos
|
||
}
|
||
|
||
private var sidebarGroups: [WiredTransferDateGroup] {
|
||
visiblePhotos.buildSidebarGroups()
|
||
}
|
||
|
||
private var photoSections: [WiredTransferPhotoSection] {
|
||
visiblePhotos.buildPhotoSections()
|
||
}
|
||
|
||
private var selectAllBar: some View {
|
||
let pendingIDs = visiblePhotos.selectablePendingPhotoIDs
|
||
let allSelected = !pendingIDs.isEmpty && pendingIDs.isSubset(of: viewModel.selectedPhotoIDs)
|
||
return WiredTransferSelectAllBar(
|
||
selectedCount: viewModel.selectedPhotoIDs.count,
|
||
selectableCount: pendingIDs.count,
|
||
allSelected: allSelected,
|
||
onToggleSelectAll: viewModel.toggleSelectAllPendingInVisible
|
||
)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var contentSection: some View {
|
||
if visiblePhotos.isEmpty {
|
||
WiredTransferEmptyState()
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
} else {
|
||
WiredTransferPhotoListPanel(
|
||
sidebarExpanded: viewModel.sidebarExpanded,
|
||
sidebarGroups: sidebarGroups,
|
||
photoSections: photoSections,
|
||
selectedTimeSlotID: viewModel.selectedTimeSlotID,
|
||
selectUploadMode: viewModel.selectUploadMode,
|
||
selectedPhotoIDs: viewModel.selectedPhotoIDs,
|
||
onToggleSidebar: viewModel.toggleSidebar,
|
||
onTimeSlotSelected: viewModel.selectTimeSlot,
|
||
onPhotoClick: openPhotoPreview,
|
||
onRetry: { id in Task { await viewModel.retryPhoto(id: id) } },
|
||
onDelete: viewModel.deletePhoto,
|
||
onTogglePhotoSelection: viewModel.togglePhotoSelection
|
||
)
|
||
}
|
||
}
|
||
|
||
private var previewBinding: Binding<PreviewPayload?> {
|
||
Binding(
|
||
get: {
|
||
guard let previewSources else { return nil }
|
||
return PreviewPayload(sources: previewSources, startIndex: previewStartIndex)
|
||
},
|
||
set: { newValue in
|
||
if newValue == nil { previewSources = nil }
|
||
}
|
||
)
|
||
}
|
||
|
||
private func openPhotoPreview(photoID: String) {
|
||
let entries = visiblePhotos.compactMap { photo -> String? in
|
||
let url = viewModel.previewURL(for: photo.id)
|
||
return url.isEmpty ? nil : url
|
||
}
|
||
guard let startIndex = visiblePhotos.firstIndex(where: { $0.id == photoID }),
|
||
!viewModel.previewURL(for: photoID).isEmpty else {
|
||
toastCenter.show("暂无法预览该照片")
|
||
return
|
||
}
|
||
let mappedIndex = visiblePhotos.prefix(startIndex + 1).filter {
|
||
!viewModel.previewURL(for: $0.id).isEmpty
|
||
}.count - 1
|
||
previewStartIndex = max(mappedIndex, 0)
|
||
previewSources = entries
|
||
}
|
||
}
|
||
|
||
/// 预览 Sheet 载荷。
|
||
private struct PreviewPayload: Identifiable {
|
||
let id = UUID()
|
||
let sources: [String]
|
||
let startIndex: Int
|
||
}
|