taler-ios

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

RiverHistoryView.swift (10090B)


      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                         // width/height can be 0 (or smaller than treeSpace) during the first
    175                         // layout pass or when the tile is zoomed very small - skip the trees
    176                         // then instead of handing Double.random an inverted range (crash).
    177                         if width > treeSpace + 3 && height > treeSpace + 3 {
    178                             let treeCount = Int.random(in: 23...57)
    179                             ForEach(0..<treeCount, id: \.self) { treeIndex in
    180                                 let xOffset = Double.random(in: 3...width-treeSpace)
    181                                 let yOffset = Double.random(in: 3...height-treeSpace)
    182                                 let treeSize = Double.random(in: 10...20)
    183                                 tree.frame(width: treeSize, height: treeSize)
    184                                     .offset(x: xOffset, y: yOffset)
    185                             }
    186                         }
    187                     }
    188                 }
    189             Color.yellow
    190         }
    191         let background2 = VStack(spacing: 0) {
    192             Color.brown
    193             Color.yellow
    194         }
    195 
    196         // transactions
    197         if let historyItem {
    198             if let talerTX = historyItem.talerTX {
    199                 let common = talerTX.common
    200                 let amount = common.amountEffective
    201                 let (dateString, date) = TalerDater.dateString(common.timestamp, true)
    202                 let (day, month) = getDate(date)
    203                 let sunImage = Image(systemName: SUNFILL)
    204                 let moonImage = Image(systemName: MOONFILL)
    205                 let sunText = Text("\(sunImage) \(day)")        // verbatim: doesn't work here, will not show the image. Thus we must set this to "Don't translate"
    206                 let moonText = Text("\(moonImage) \(month)")    //   "
    207                 if common.isIncoming {
    208                     let sizeIndex = sizeIn(for: amount.value)
    209                     VStack {
    210                         sunText
    211                         moonText
    212                         Image("River-Lake-" + String(sizeIndex))
    213                             .resizable()
    214                             .scaledToFit()
    215                             .onTapGesture { selectTX() }
    216                             .background { background }
    217                     }
    218                 } else if common.isOutgoing {
    219                     let sizeIndex = sizeOut(for: amount.value)
    220                     VStack {
    221                         sunText
    222                         moonText
    223                         Image("River-Pond-" + String(sizeIndex))
    224                             .resizable()
    225                             .scaledToFit()
    226                             .onTapGesture { selectTX() }
    227                             .background { background }
    228                     }
    229                 }
    230                 if historyItem.marker != .none {
    231                     let markerIndex = historyItem.marker.rawValue
    232                     VStack {
    233                         Text(verbatim: " ")
    234                         Text(verbatim: " ")
    235                         Image("River-Mark-" + String(markerIndex))
    236                             .resizable()
    237                             .scaledToFit()
    238                             .background { background2 }
    239                             .overlay {
    240                                 LinearGradient(colors: [.clear, .black, .clear], startPoint: .leading, endPoint: .trailing)
    241                                     .opacity(0.5)
    242                             }
    243                     }
    244                 }
    245             } else {
    246                 let _ = print("no talerTX")
    247             }
    248         } else {
    249             let _ = print("no historyItem")
    250         }
    251         //.border(.green)
    252         //.border(.blue)
    253     }
    254 }