Files
suixinkan_ios_new/suixinkan/Features/TravelAlbum/Views/WiredTransfer/WiredTransferControlCard.swift
汉秋 90d135b5ac 完善 OTG 相机有线传图:支持 Sony/Canon 自动识别,页面 detach 保持会话以便再次进入复连。
引入进程级 CameraTetheringSession 与共享 USB Browser;离开传图页仅取消 UI 回调,App 终止时再完整 shutdown 释放 PTP 与 Browser 资源。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:45:35 +08:00

241 lines
8.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// WiredTransferControlCard.swift
// suixinkan
//
import SwiftUI
/// 线 + + Tab
struct WiredTransferControlCard: View {
let scenicSpotLabel: String?
let deviceStorageInfo: DeviceStorageSnapshot
let isCameraConnected: Bool
let isRefreshingCameraFiles: Bool
let isSearchingCamera: Bool
let cameraDeviceName: String
let transferModeOption: String
let tabTitles: [String]
let tabCounts: [Int]
let selectedTabIndex: Int
let onDeviceUsageClick: () -> Void
let onRefreshCameraFiles: () -> Void
let onHelpClick: () -> Void
let onTransferModeSelected: (String) -> Void
let onTabSelected: (Int) -> Void
private let modeOptions = [
WiredCameraTransferViewModel.modeLiveCapture,
WiredCameraTransferViewModel.modePostShootTransfer
]
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 12) {
connectionStatusRow
filterChipRow
}
.padding(.horizontal, 14)
.padding(.vertical, 14)
Divider().background(WiredTransferDesign.borderLight)
WiredTransferStatsTabRow(
tabTitles: tabTitles,
counts: tabCounts,
selectedTabIndex: selectedTabIndex,
onTabSelected: onTabSelected
)
}
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: WiredTransferDesign.cardCornerRadius))
.shadow(color: WiredTransferDesign.cardShadow, radius: 6, x: 0, y: 2)
.padding(.horizontal, WiredTransferDesign.cardHorizontalPadding)
.offset(y: -WiredTransferDesign.cardOverlap)
}
private var connectionStatusRow: some View {
HStack(alignment: .top, spacing: 10) {
deviceUsagePanel
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .center, spacing: 8) {
connectionChip
Spacer(minLength: 4)
refreshButton
}
if !isCameraConnected {
helpDocLine
}
if let scenicSpotLabel, !scenicSpotLabel.isEmpty {
Text(scenicSpotLabel)
.font(.system(size: 10))
.foregroundStyle(WiredTransferDesign.text999)
.lineLimit(1)
}
}
}
}
private var deviceUsagePanel: some View {
Button(action: onDeviceUsageClick) {
VStack(spacing: 6) {
Text(deviceStorageInfo.modelName)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(WiredTransferDesign.text333)
.multilineTextAlignment(.center)
.lineLimit(2)
.frame(maxWidth: 88)
Text(deviceStorageInfo.availableGbText)
.font(.system(size: 10))
.foregroundStyle(AppDesign.primary)
.lineLimit(1)
}
.frame(minWidth: WiredTransferDesign.statusRingMinWidth)
.padding(.horizontal, 8)
.padding(.vertical, 10)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(WiredTransferDesign.borderLight, lineWidth: 3)
}
}
.buttonStyle(.plain)
}
private var connectionChip: some View {
Text(connectionChipText)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(isCameraConnected ? AppDesign.primary : WiredTransferDesign.danger)
.lineLimit(1)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background((isCameraConnected ? AppDesign.primary : WiredTransferDesign.danger).opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 4))
}
private var connectionChipText: String {
if isCameraConnected {
let name = cameraDeviceName.isEmpty ? "已连接相机" : cameraDeviceName
return name
}
return "未连接相机"
}
private var refreshButton: some View {
Button(action: onRefreshCameraFiles) {
Text(refreshButtonTitle)
.font(.system(size: 12))
.foregroundStyle(.white)
.padding(.horizontal, 10)
.frame(minWidth: 64)
.frame(height: 34)
.background(refreshButtonColor)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
.disabled(isRefreshingCameraFiles || isSearchingCamera)
.opacity(isRefreshingCameraFiles || isSearchingCamera ? 0.85 : 1)
}
private var refreshButtonTitle: String {
if isSearchingCamera { return "搜索中" }
if isRefreshingCameraFiles { return "读取中" }
if isCameraConnected { return "刷新" }
return "重新搜索"
}
private var refreshButtonColor: Color {
if isSearchingCamera || isRefreshingCameraFiles { return WiredTransferDesign.text999 }
if !isCameraConnected { return AppDesign.primary }
return AppDesign.primary
}
private var helpDocLine: some View {
Button(action: onHelpClick) {
HStack(spacing: 0) {
Text("未连接,查看")
.foregroundStyle(WiredTransferDesign.text666)
Text("《帮助文档》")
.foregroundStyle(AppDesign.primary)
}
.font(.system(size: 10))
.frame(maxWidth: .infinity, alignment: .leading)
}
.buttonStyle(.plain)
}
private var filterChipRow: some View {
GeometryReader { geometry in
let totalWeight: CGFloat = 1 + 0.72 + 1.15
let spacing: CGFloat = 6
let available = geometry.size.width - spacing * 2
HStack(spacing: spacing) {
WiredTransferFilterChip(label: "不修图", options: [], enabled: false, onSelected: { _ in })
.frame(width: available * 1 / totalWeight)
WiredTransferFilterChip(
label: WiredCameraTransferViewModel.formatJPG,
options: [],
enabled: false,
onSelected: { _ in }
)
.frame(width: available * 0.72 / totalWeight)
WiredTransferFilterChip(
label: transferModeOption,
options: modeOptions,
onSelected: onTransferModeSelected
)
.frame(width: available * 1.15 / totalWeight)
}
}
.frame(height: WiredTransferDesign.filterChipHeight)
}
}
/// Tab Android TransferStatsTabRow
struct WiredTransferStatsTabRow: View {
let tabTitles: [String]
let counts: [Int]
let selectedTabIndex: Int
let onTabSelected: (Int) -> Void
var body: some View {
HStack(spacing: 0) {
ForEach(Array(tabTitles.enumerated()), id: \.offset) { index, title in
if index > 0 {
Divider()
.frame(height: 32)
.background(WiredTransferDesign.borderLight)
}
tabItem(index: index, title: title)
}
}
.frame(height: WiredTransferDesign.tabRowHeight)
}
private func tabItem(index: Int, title: String) -> some View {
Button {
onTabSelected(index)
} label: {
VStack(spacing: 2) {
Text("\(counts.indices.contains(index) ? counts[index] : 0)")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(countColor(for: index))
Text(title)
.font(.system(size: 13))
.foregroundStyle(selectedTabIndex == index ? AppDesign.primary : WiredTransferDesign.text666)
RoundedRectangle(cornerRadius: 1)
.fill(selectedTabIndex == index ? AppDesign.primary : Color.clear)
.frame(width: 24, height: 2)
.padding(.top, 2)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
}
.buttonStyle(.plain)
}
private func countColor(for index: Int) -> Color {
if selectedTabIndex == index { return AppDesign.primary }
if index == 2 { return WiredTransferDesign.danger }
return WiredTransferDesign.text333
}
}