taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }