RiverHistoryView.swift (9675B)
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 RiverHistoryView: View { 20 let stack: CallStack 21 private let logger = Logger(subsystem: "net.taler.gnu", category: "River") 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("RiverHistoryView \(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 RiverTileView(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 RiverTileView: 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 func getDate(_ date: Date?) -> (Int, Int) { 153 if let date { 154 let components = Calendar.current.dateComponents([.day, .year, .month], from: date) 155 return (components.day ?? 1, components.month ?? 1) 156 } 157 return(1, 1) 158 } 159 160 var body: some View { 161 let txValue = historyItem?.talerTX?.common.amountEffective.value ?? 0 162 // let width = width(for: txValue) 163 let tree = Image("Tree-without-shadow") 164 .resizable() 165 166 let treeSpace = 15.0 167 let background = VStack(spacing: 0) { 168 Color.brown // add some random trees 169 .overlay(alignment: .topLeading) { 170 GeometryReader { geo in 171 let height = geo.size.height 172 let width = geo.size.width 173 // let _ = print("width = \(width), height = \(height)") 174 let treeCount = Int.random(in: 23...57) 175 ForEach(0..<treeCount, id: \.self) { treeIndex in 176 let xOffset = Double.random(in: 3...width-treeSpace) 177 let yOffset = Double.random(in: 3...height-treeSpace) 178 let treeSize = Double.random(in: 10...20) 179 tree.frame(width: treeSize, height: treeSize) 180 .offset(x: xOffset, y: yOffset) 181 } 182 } 183 } 184 Color.yellow 185 } 186 let background2 = VStack(spacing: 0) { 187 Color.brown 188 Color.yellow 189 } 190 191 // transactions 192 if let historyItem { 193 if let talerTX = historyItem.talerTX { 194 let common = talerTX.common 195 let amount = common.amountEffective 196 let (dateString, date) = TalerDater.dateString(common.timestamp, true) 197 let (day, month) = getDate(date) 198 let sunImage = Image(systemName: SUNFILL) 199 let moonImage = Image(systemName: MOONFILL) 200 let sunText = Text("\(sunImage) \(day)") // verbatim: doesn't work here, will not show the image. Thus we must set this to "Don't translate" 201 let moonText = Text("\(moonImage) \(month)") // " 202 if common.isIncoming { 203 let sizeIndex = sizeIn(for: amount.value) 204 VStack { 205 sunText 206 moonText 207 Image("River-Lake-" + String(sizeIndex)) 208 .resizable() 209 .scaledToFit() 210 .onTapGesture { selectTX() } 211 .background { background } 212 } 213 } else if common.isOutgoing { 214 let sizeIndex = sizeOut(for: amount.value) 215 VStack { 216 sunText 217 moonText 218 Image("River-Pond-" + String(sizeIndex)) 219 .resizable() 220 .scaledToFit() 221 .onTapGesture { selectTX() } 222 .background { background } 223 } 224 } 225 if historyItem.marker != .none { 226 let markerIndex = historyItem.marker.rawValue 227 VStack { 228 Text(verbatim: " ") 229 Text(verbatim: " ") 230 Image("River-Mark-" + String(markerIndex)) 231 .resizable() 232 .scaledToFit() 233 .background { background2 } 234 .overlay { 235 LinearGradient(colors: [.clear, .black, .clear], startPoint: .leading, endPoint: .trailing) 236 .opacity(0.5) 237 } 238 } 239 } 240 } else { 241 let _ = print("no talerTX") 242 } 243 } else { 244 let _ = print("no historyItem") 245 } 246 //.border(.green) 247 //.border(.blue) 248 } 249 }