taler-ios

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

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 }