ArrowHistoryView.swift (8955B)
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 _ = logger.log("ArrowHistoryView \(width.pTwo)") 67 68 ZStack(alignment: .top) { 69 HStack(spacing: 0) { 70 VStack { 71 Image("Request") 72 .resizable() 73 .scaledToFit() 74 .frame(width: OIMbuttonSize, height: OIMbuttonSize) 75 .padding(.bottom, 30) 76 Image("SendMoney") 77 .resizable() 78 .scaledToFit() 79 .frame(width: OIMbuttonSize, height: OIMbuttonSize) 80 .padding(.vertical, 30) 81 } 82 ScrollViewReader { scrollProxy in 83 OptimalSize(.horizontal) { // keep it small if we have only a few tx 84 ScrollView(.horizontal) { 85 if !shownItems.isEmpty { 86 HStack(spacing: 0) { 87 ForEach(-maxXValue...0, id: \.self) { xVal in 88 ArrowTileView(historyItem: historyItem(for: xVal)) { 89 selectTX(xVal) 90 } 91 } 92 }.id(riverID) 93 } 94 } 95 //.border(.blue) 96 .scrollBounceBehavior(.basedOnSize, axes: .horizontal) // don't bounce if it's small 97 .task(id: scrollBack) { 98 logger.log("Task \(scrollBack)") 99 if scrollBack { 100 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 101 logger.log("Scrolling") 102 withAnimation() { // .easeOut(duration: 3.0) doesn't work, always uses standard timing 103 scrollProxy.scrollTo(riverID, anchor: .bottomTrailing) 104 } 105 } 106 } 107 } 108 .onTapGesture(count: 2) { 109 withAnimation(.easeOut(duration: 0.6)) { 110 dataPointWidth = 100 // reset the scale first 111 scrollProxy.scrollTo(riverID, anchor: .bottomTrailing) 112 } 113 } 114 } 115 } // ScrollViewReader 116 } 117 if let selectedTX { 118 if let item = historyItem(for: selectedTX) { 119 if let talerTX = item.talerTX { 120 HistoryDetailView(stack: stack.push(), 121 currency: currency, 122 balance: item.balance, 123 talerTX: talerTX) 124 .onTapGesture { selectTX(nil) } 125 } 126 } 127 } 128 } // ZStack 129 } 130 } 131 // MARK: - 132 struct ArrowTileView: View { 133 let historyItem: HistoryItem? 134 let selectTX: () -> Void 135 136 func sizeIn(for value: Double) -> Int { 137 if value < 100 { return 0 } 138 if value < 300 { return 1 } 139 if value < 500 { return 2 } 140 if value < 750 { return 3 } 141 else { return 4 } 142 } 143 144 func sizeOut(for value: Double) -> Int { 145 if value < 31 { return 0 } 146 if value < 80 { return 1 } 147 if value < 200 { return 2 } 148 if value < 500 { return 3 } 149 else { return 4 } 150 } 151 152 var body: some View { 153 let txValue = historyItem?.talerTX?.common.amountEffective.value ?? 0 154 // let width = width(for: txValue) 155 let tree = Image("Tree-without-shadow") 156 .resizable() 157 158 let treeSpace = 15.0 159 let background = VStack(spacing: 0) { 160 Color.brown // add some random trees 161 .overlay(alignment: .topLeading) { 162 GeometryReader { geo in 163 let height = geo.size.height 164 let width = geo.size.width 165 // let _ = print("width = \(width), height = \(height)") 166 // width/height can be 0 (or smaller than treeSpace) during the first 167 // layout pass or when the tile is zoomed very small - skip the trees 168 // then instead of handing Double.random an inverted range (crash). 169 if width > treeSpace + 3 && height > treeSpace + 3 { 170 let treeCount = Int.random(in: 23...57) 171 ForEach(0..<treeCount, id: \.self) { treeIndex in 172 let xOffset = Double.random(in: 3...width-treeSpace) 173 let yOffset = Double.random(in: 3...height-treeSpace) 174 let treeSize = Double.random(in: 10...20) 175 tree.frame(width: treeSize, height: treeSize) 176 .offset(x: xOffset, y: yOffset) 177 } 178 } 179 } 180 } 181 Color.yellow 182 } 183 let background2 = VStack(spacing: 0) { 184 Color.brown 185 Color.yellow 186 } 187 188 // transactions 189 if let historyItem { 190 if let talerTX = historyItem.talerTX { 191 let common = talerTX.common 192 let amount = common.amountEffective 193 if common.isIncoming { 194 let sizeIndex = sizeIn(for: amount.value) 195 Image("Empty-" + String(sizeIndex)) 196 .resizable() 197 .scaledToFit() 198 // .border(.red) 199 .onTapGesture { selectTX() } 200 .background { background } 201 } else if common.isOutgoing { 202 let sizeIndex = sizeOut(for: amount.value) 203 Image("Empty-" + String(sizeIndex)) 204 .resizable() 205 .scaledToFit() 206 // .border(.red) 207 .onTapGesture { selectTX() } 208 .background { background } 209 } 210 if historyItem.marker != .none { 211 let markerIndex = historyItem.marker.rawValue 212 Image("Empty-" + String(markerIndex)) 213 .resizable() 214 .scaledToFit() 215 .background { background2 } 216 .overlay { 217 LinearGradient(colors: [.clear, .black, .clear], startPoint: .leading, endPoint: .trailing) 218 .opacity(0.5) 219 } 220 } 221 } else { 222 let _ = print("no talerTX") 223 } 224 } else { 225 let _ = print("no historyItem") 226 } 227 //.border(.green) 228 //.border(.blue) 229 } 230 }