对齐有线传图页 Android UI,并改用相对路径持久化本地照片。
还原顶栏、浮动卡片、时间侧栏与底部交互;照片路径改为 CameraDownloads 相对路径并兼容旧数据;格式 Chip 固定 JPG 且不可点击。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,238 @@
|
||||
//
|
||||
// WiredTransferControlCard.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 有线传图浮动控制卡片:连接区 + 筛选 + Tab。
|
||||
struct WiredTransferControlCard: View {
|
||||
let scenicSpotLabel: String?
|
||||
let deviceStorageInfo: DeviceStorageSnapshot
|
||||
let isCameraConnected: Bool
|
||||
let isRefreshingCameraFiles: 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 "未连接 USB"
|
||||
}
|
||||
|
||||
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(!isCameraConnected || isRefreshingCameraFiles)
|
||||
.opacity(isCameraConnected && !isRefreshingCameraFiles ? 1 : 0.85)
|
||||
}
|
||||
|
||||
private var refreshButtonTitle: String {
|
||||
if isRefreshingCameraFiles { return "读取中" }
|
||||
if isCameraConnected { return "刷新" }
|
||||
return "等待连接"
|
||||
}
|
||||
|
||||
private var refreshButtonColor: Color {
|
||||
if !isCameraConnected { return WiredTransferDesign.danger }
|
||||
if isRefreshingCameraFiles { return WiredTransferDesign.text999 }
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user