ArrowHistoryView.swift (8540B)
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 let treeCount = Int.random(in: 23...57) 167 ForEach(0..<treeCount, id: \.self) { treeIndex in 168 let xOffset = Double.random(in: 3...width-treeSpace) 169 let yOffset = Double.random(in: 3...height-treeSpace) 170 let treeSize = Double.random(in: 10...20) 171 tree.frame(width: treeSize, height: treeSize) 172 .offset(x: xOffset, y: yOffset) 173 } 174 } 175 } 176 Color.yellow 177 } 178 let background2 = VStack(spacing: 0) { 179 Color.brown 180 Color.yellow 181 } 182 183 // transactions 184 if let historyItem { 185 if let talerTX = historyItem.talerTX { 186 let common = talerTX.common 187 let amount = common.amountEffective 188 if common.isIncoming { 189 let sizeIndex = sizeIn(for: amount.value) 190 Image("Empty-" + String(sizeIndex)) 191 .resizable() 192 .scaledToFit() 193 // .border(.red) 194 .onTapGesture { selectTX() } 195 .background { background } 196 } else if common.isOutgoing { 197 let sizeIndex = sizeOut(for: amount.value) 198 Image("Empty-" + String(sizeIndex)) 199 .resizable() 200 .scaledToFit() 201 // .border(.red) 202 .onTapGesture { selectTX() } 203 .background { background } 204 } 205 if historyItem.marker != .none { 206 let markerIndex = historyItem.marker.rawValue 207 Image("Empty-" + String(markerIndex)) 208 .resizable() 209 .scaledToFit() 210 .background { background2 } 211 .overlay { 212 LinearGradient(colors: [.clear, .black, .clear], startPoint: .leading, endPoint: .trailing) 213 .opacity(0.5) 214 } 215 } 216 } else { 217 let _ = print("no talerTX") 218 } 219 } else { 220 let _ = print("no historyItem") 221 } 222 //.border(.green) 223 //.border(.blue) 224 } 225 }