ArrowHistoryView.swift (8567B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import Charts 10 import os.log 11 import SymLog 12 import taler_swift 13 14 fileprivate let lineWidth = 6.0 15 fileprivate let river_back = "river-back" 16 17 // MARK: - 18 @available(iOS 16.4, *) 19 struct ArrowHistoryView: View { 20 let stack: CallStack 21 private let logger = Logger(subsystem: "net.taler.gnu", category: "Arrow") 22 let currency: OIMcurrency 23 @Binding var shownItems: [HistoryItem] 24 @Binding var dataPointWidth: CGFloat 25 let scrollBack: Bool 26 27 let maxXValue: Int // #of dataPoints in history, plus spacers 28 @State private var maxYValue: Double // max balance 29 30 @Namespace var riverID 31 @State private var scrollPosition: Double = 0 32 @State private var selectedTX: Int? = nil 33 @State private var selectedY: Double? = nil 34 @State private var selectedRange: ClosedRange<Double>? 35 @State private var lastDelta: Double? 36 37 init(stack: CallStack, 38 currency: OIMcurrency, shownItems: Binding<[HistoryItem]>, dataPointWidth: Binding<CGFloat>, 39 scrollBack: Bool, maxIndex: Int = 1, maxValue: Double = 200 40 ) { 41 self.stack = stack 42 self.currency = currency 43 self.scrollBack = scrollBack 44 self._shownItems = shownItems 45 self._dataPointWidth = dataPointWidth 46 self.maxYValue = maxValue 47 self.maxXValue = maxIndex 48 } 49 50 func selectTX(_ selected: Int?) { 51 withAnimation { 52 selectedTX = selected 53 } 54 } 55 56 func historyItem(for xVal: Int) -> HistoryItem? { 57 for item in shownItems { 58 if item.distance == -xVal { 59 return item 60 } 61 } 62 return nil 63 } 64 65 var body: some View { 66 let count = shownItems.count 67 // let _ = logger.log("ArrowHistoryView \(width.pTwo)") 68 69 ZStack(alignment: .top) { 70 HStack(spacing: 0) { 71 VStack { 72 Image("Request") 73 .resizable() 74 .scaledToFit() 75 .frame(width: OIMbuttonSize, height: OIMbuttonSize) 76 .padding(.bottom, 30) 77 Image("SendMoney") 78 .resizable() 79 .scaledToFit() 80 .frame(width: OIMbuttonSize, height: OIMbuttonSize) 81 .padding(.vertical, 30) 82 } 83 ScrollViewReader { scrollProxy in 84 OptimalSize(.horizontal) { // keep it small if we have only a few tx 85 ScrollView(.horizontal) { 86 if count > 0 { 87 HStack(spacing: 0) { 88 ForEach(-maxXValue...0, id: \.self) { xVal in 89 ArrowTileView(historyItem: historyItem(for: xVal)) { 90 selectTX(xVal) 91 } 92 } 93 }.id(riverID) 94 } 95 } 96 //.border(.blue) 97 .scrollBounceBehavior(.basedOnSize, axes: .horizontal) // don't bounce if it's small 98 .task(id: scrollBack) { 99 logger.log("Task \(scrollBack)") 100 if scrollBack { 101 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 102 logger.log("Scrolling") 103 withAnimation() { // .easeOut(duration: 3.0) doesn't work, always uses standard timing 104 scrollProxy.scrollTo(riverID, anchor: .bottomTrailing) 105 } 106 } 107 } 108 } 109 .onTapGesture(count: 2) { 110 withAnimation(.easeOut(duration: 0.6)) { 111 dataPointWidth = 100 // reset the scale first 112 scrollProxy.scrollTo(riverID, anchor: .bottomTrailing) 113 } 114 } 115 } 116 } // ScrollViewReader 117 } 118 if let selectedTX { 119 if let item = historyItem(for: selectedTX) { 120 if let talerTX = item.talerTX { 121 HistoryDetailView(stack: stack.push(), 122 currency: currency, 123 balance: item.balance, 124 talerTX: talerTX) 125 .onTapGesture { selectTX(nil) } 126 } 127 } 128 } 129 } // ZStack 130 } 131 } 132 // MARK: - 133 struct ArrowTileView: View { 134 let historyItem: HistoryItem? 135 let selectTX: () -> Void 136 137 func sizeIn(for value: Double) -> Int { 138 if value < 100 { return 0 } 139 if value < 300 { return 1 } 140 if value < 500 { return 2 } 141 if value < 750 { return 3 } 142 else { return 4 } 143 } 144 145 func sizeOut(for value: Double) -> Int { 146 if value < 31 { return 0 } 147 if value < 80 { return 1 } 148 if value < 200 { return 2 } 149 if value < 500 { return 3 } 150 else { return 4 } 151 } 152 153 var body: some View { 154 let txValue = historyItem?.talerTX?.common.amountEffective.value ?? 0 155 // let width = width(for: txValue) 156 let tree = Image("Tree-without-shadow") 157 .resizable() 158 159 let treeSpace = 15.0 160 let background = VStack(spacing: 0) { 161 Color.brown // add some random trees 162 .overlay(alignment: .topLeading) { 163 GeometryReader { geo in 164 let height = geo.size.height 165 let width = geo.size.width 166 // let _ = print("width = \(width), height = \(height)") 167 let treeCount = Int.random(in: 23...57) 168 ForEach(0..<treeCount, id: \.self) { treeIndex in 169 let xOffset = Double.random(in: 3...width-treeSpace) 170 let yOffset = Double.random(in: 3...height-treeSpace) 171 let treeSize = Double.random(in: 10...20) 172 tree.frame(width: treeSize, height: treeSize) 173 .offset(x: xOffset, y: yOffset) 174 } 175 } 176 } 177 Color.yellow 178 } 179 let background2 = VStack(spacing: 0) { 180 Color.brown 181 Color.yellow 182 } 183 184 // transactions 185 if let historyItem { 186 if let talerTX = historyItem.talerTX { 187 let common = talerTX.common 188 let amount = common.amountEffective 189 if common.isIncoming { 190 let sizeIndex = sizeIn(for: amount.value) 191 Image("Empty-" + String(sizeIndex)) 192 .resizable() 193 .scaledToFit() 194 // .border(.red) 195 .onTapGesture { selectTX() } 196 .background { background } 197 } else if common.isOutgoing { 198 let sizeIndex = sizeOut(for: amount.value) 199 Image("Empty-" + String(sizeIndex)) 200 .resizable() 201 .scaledToFit() 202 // .border(.red) 203 .onTapGesture { selectTX() } 204 .background { background } 205 } 206 if historyItem.marker != .none { 207 let markerIndex = historyItem.marker.rawValue 208 Image("Empty-" + String(markerIndex)) 209 .resizable() 210 .scaledToFit() 211 .background { background2 } 212 .overlay { 213 LinearGradient(colors: [.clear, .black, .clear], startPoint: .leading, endPoint: .trailing) 214 .opacity(0.5) 215 } 216 } 217 } else { 218 let _ = print("no talerTX") 219 } 220 } else { 221 let _ = print("no historyItem") 222 } 223 //.border(.green) 224 //.border(.blue) 225 } 226 }