Feedback #3 y #4: pegar/OCR por campo en Quick Update + swipe entre charts (build 60)

#3 Quick Update paste/scan por campo:
- Barra de teclado con 'Pegar €X' (si hay importe en portapapeles) y 'Escanear'
  (PhotosPicker → OCR on-device). Actúan sobre el campo enfocado.
- ImageAmountScanner nuevo (app target, Vision) espejo del scanner del Share
  Extension: candidatos rankeados por prominencia, filtra % y años.
- 1 candidato → rellena directo; varios → confirmationDialog para elegir.
- Strings ×7.

#4 Swipe entre charts (iPhone):
- Gesto horizontal en el área del chart avanza al chart anterior/siguiente en
  el orden del menú de título (respeta showForecast). Indicador de página con
  puntos. Transición direccional. selectChart ya no bloquea premium (muestra
  teaser), así que deslizar a uno premium funciona.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
alexandrev-tibco
2026-07-10 21:37:21 +02:00
parent 86c95a9e73
commit 6bdf2740c5
11 changed files with 301 additions and 10 deletions
@@ -604,6 +604,14 @@ struct ChartsContainerView: View {
}
iPhonePeriodControl
chartContent
// Swipe left/right to move to the adjacent chart type.
.gesture(chartSwipeGesture)
.id(viewModel.selectedChartType)
.transition(.asymmetric(
insertion: .move(edge: swipeInsertionEdge).combined(with: .opacity),
removal: .opacity
))
chartPageIndicator
if viewModel.hiddenHistoryMonths > 0 {
lockedHistoryTeaser
}
@@ -614,6 +622,45 @@ struct ChartsContainerView: View {
.sheet(isPresented: $viewModel.showingPaywall) { PaywallView() }
}
// MARK: - iPhone swipe between charts
/// Chart types in the same grouped order as the title switcher menu.
private var orderedChartTypes: [ChartsViewModel.ChartType] {
Self.chartGroups.flatMap { $0.types }.filter { showForecast || $0 != .prediction }
}
@State private var swipeInsertionEdge: Edge = .trailing
private var chartSwipeGesture: some Gesture {
DragGesture(minimumDistance: 30)
.onEnded { value in
// Horizontal-dominant swipes only, so vertical scroll still works.
guard abs(value.translation.width) > abs(value.translation.height) * 1.5 else { return }
let ordered = orderedChartTypes
guard let idx = ordered.firstIndex(of: viewModel.selectedChartType) else { return }
if value.translation.width < 0, idx < ordered.count - 1 {
swipeInsertionEdge = .trailing
withAnimation(.snappy) { viewModel.selectChart(ordered[idx + 1]) }
} else if value.translation.width > 0, idx > 0 {
swipeInsertionEdge = .leading
withAnimation(.snappy) { viewModel.selectChart(ordered[idx - 1]) }
}
}
}
private var chartPageIndicator: some View {
let ordered = orderedChartTypes
let current = ordered.firstIndex(of: viewModel.selectedChartType) ?? 0
return HStack(spacing: 5) {
ForEach(ordered.indices, id: \.self) { i in
Circle()
.fill(i == current ? Color.appPrimary : Color.secondary.opacity(0.25))
.frame(width: i == current ? 7 : 5, height: i == current ? 7 : 5)
}
}
.animation(.snappy, value: current)
}
/// Stocks-style segmented period control above the chart (the chart switcher
/// lives in the title menu, filters in the toolbar).
@ViewBuilder