taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 03ddab6775770d78181ff36686758ec75b31581b
parent a16b067865c58ea2c20230352842675e7b6d3fa6
Author: Marc Stibane <marc@taler.net>
Date:   Sat, 27 Sep 2025 18:19:43 +0200

ArrowHistoryView

Diffstat:
ATalerWallet1/Views/OIM/ArrowHistoryView.swift | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 226 insertions(+), 0 deletions(-)

diff --git a/TalerWallet1/Views/OIM/ArrowHistoryView.swift b/TalerWallet1/Views/OIM/ArrowHistoryView.swift @@ -0,0 +1,226 @@ +/* + * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. + * See LICENSE.md + */ +/** + * @author Marc Stibane + */ +import SwiftUI +import Charts +import os.log +import SymLog +import taler_swift + +fileprivate let lineWidth = 6.0 +fileprivate let river_back = "river-back" + +// MARK: - +@available(iOS 16.4, *) +struct ArrowHistoryView: View { + let stack: CallStack + private let logger = Logger(subsystem: "net.taler.gnu", category: "Arrow") + let currency: OIMcurrency + @Binding var shownItems: [HistoryItem] + @Binding var dataPointWidth: CGFloat + let scrollBack: Bool + + let maxXValue: Int // #of dataPoints in history, plus spacers + @State private var maxYValue: Double // max balance + + @Namespace var riverID + @State private var scrollPosition: Double = 0 + @State private var selectedTX: Int? = nil + @State private var selectedY: Double? = nil + @State private var selectedRange: ClosedRange<Double>? + @State private var lastDelta: Double? + + init(stack: CallStack, + currency: OIMcurrency, shownItems: Binding<[HistoryItem]>, dataPointWidth: Binding<CGFloat>, + scrollBack: Bool, maxIndex: Int = 1, maxValue: Double = 200 + ) { + self.stack = stack + self.currency = currency + self.scrollBack = scrollBack + self._shownItems = shownItems + self._dataPointWidth = dataPointWidth + self.maxYValue = maxValue + self.maxXValue = maxIndex + } + + func selectTX(_ selected: Int?) { + withAnimation { + selectedTX = selected + } + } + + func historyItem(for xVal: Int) -> HistoryItem? { + for item in shownItems { + if item.distance == -xVal { + return item + } + } + return nil + } + + var body: some View { + let count = shownItems.count +// let _ = logger.log("ArrowHistoryView \(width.pTwo)") + + ZStack(alignment: .top) { + HStack(spacing: 0) { + VStack { + Image("Request") + .resizable() + .scaledToFit() + .frame(width: OIMbuttonSize, height: OIMbuttonSize) + .padding(.bottom, 30) + Image("SendMoney") + .resizable() + .scaledToFit() + .frame(width: OIMbuttonSize, height: OIMbuttonSize) + .padding(.vertical, 30) + } + ScrollViewReader { scrollProxy in + OptimalSize(.horizontal) { // keep it small if we have only a few tx + ScrollView(.horizontal) { + if count > 0 { + HStack(spacing: 0) { + ForEach(-maxXValue...0, id: \.self) { xVal in + ArrowTileView(historyItem: historyItem(for: xVal)) { + selectTX(xVal) + } + } + }.id(riverID) + } + } + //.border(.blue) + .scrollBounceBehavior(.basedOnSize, axes: .horizontal) // don't bounce if it's small + .task(id: scrollBack) { + logger.log("Task \(scrollBack)") + if scrollBack { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + logger.log("Scrolling") + withAnimation() { // .easeOut(duration: 3.0) doesn't work, always uses standard timing + scrollProxy.scrollTo(riverID, anchor: .bottomTrailing) + } + } + } + } + .onTapGesture(count: 2) { + withAnimation(.easeOut(duration: 0.6)) { + dataPointWidth = 100 // reset the scale first + scrollProxy.scrollTo(riverID, anchor: .bottomTrailing) + } + } + } + } // ScrollViewReader + } + if let selectedTX { + if let item = historyItem(for: selectedTX) { + if let talerTX = item.talerTX { + HistoryDetailView(stack: stack.push(), + currency: currency, + balance: item.balance, + talerTX: talerTX) + .onTapGesture { selectTX(nil) } + } + } + } + } // ZStack + } +} +// MARK: - +struct ArrowTileView: View { + let historyItem: HistoryItem? + let selectTX: () -> Void + + func sizeIn(for value: Double) -> Int { + if value < 100 { return 0 } + if value < 300 { return 1 } + if value < 500 { return 2 } + if value < 750 { return 3 } + else { return 4 } + } + + func sizeOut(for value: Double) -> Int { + if value < 31 { return 0 } + if value < 80 { return 1 } + if value < 200 { return 2 } + if value < 500 { return 3 } + else { return 4 } + } + + var body: some View { + let txValue = historyItem?.talerTX?.common.amountEffective.value ?? 0 +// let width = width(for: txValue) + let tree = Image("Tree-without-shadow") + .resizable() + + let treeSpace = 15.0 + let background = VStack(spacing: 0) { + Color.brown // add some random trees + .overlay(alignment: .topLeading) { + GeometryReader { geo in + let height = geo.size.height + let width = geo.size.width +// let _ = print("width = \(width), height = \(height)") + let treeCount = Int.random(in: 23...57) + ForEach(0..<treeCount, id: \.self) { treeIndex in + let xOffset = Double.random(in: 3...width-treeSpace) + let yOffset = Double.random(in: 3...height-treeSpace) + let treeSize = Double.random(in: 10...20) + tree.frame(width: treeSize, height: treeSize) + .offset(x: xOffset, y: yOffset) + } + } + } + Color.yellow + } + let background2 = VStack(spacing: 0) { + Color.brown + Color.yellow + } + + // transactions + if let historyItem { + if let talerTX = historyItem.talerTX { + let common = talerTX.common + let amount = common.amountEffective + if common.isIncoming { + let sizeIndex = sizeIn(for: amount.value) + Image("Empty-" + String(sizeIndex)) + .resizable() + .scaledToFit() +// .border(.red) + .onTapGesture { selectTX() } + .background { background } + } else if common.isOutgoing { + let sizeIndex = sizeOut(for: amount.value) + Image("Empty-" + String(sizeIndex)) + .resizable() + .scaledToFit() +// .border(.red) + .onTapGesture { selectTX() } + .background { background } + } + if historyItem.marker != .none { + let markerIndex = historyItem.marker.rawValue + Image("Empty-" + String(markerIndex)) + .resizable() + .scaledToFit() + .background { background2 } + .overlay { + LinearGradient(colors: [.clear, .black, .clear], startPoint: .leading, endPoint: .trailing) + .opacity(0.5) + } + } + } else { + let _ = print("no talerTX") + } + } else { + let _ = print("no historyItem") + } + //.border(.green) + //.border(.blue) + } +}