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