taler-ios

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

ArrowHistoryView.swift (8540B)


      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 ArrowHistoryView: View {
     20     let stack: CallStack
     21     private let logger = Logger(subsystem: "net.taler.gnu", category: "Arrow")
     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("ArrowHistoryView \(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                                         ArrowTileView(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 ArrowTileView: 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     var body: some View {
    153         let txValue = historyItem?.talerTX?.common.amountEffective.value ?? 0
    154 //        let width = width(for: txValue)
    155         let tree = Image("Tree-without-shadow")
    156             .resizable()
    157 
    158         let treeSpace = 15.0
    159         let background = VStack(spacing: 0) {
    160             Color.brown     // add some random trees
    161                 .overlay(alignment: .topLeading) {
    162                     GeometryReader { geo in
    163                         let height = geo.size.height
    164                         let width = geo.size.width
    165 //                        let _ = print("width = \(width), height = \(height)")
    166                         let treeCount = Int.random(in: 23...57)
    167                         ForEach(0..<treeCount, id: \.self) { treeIndex in
    168                             let xOffset = Double.random(in: 3...width-treeSpace)
    169                             let yOffset = Double.random(in: 3...height-treeSpace)
    170                             let treeSize = Double.random(in: 10...20)
    171                             tree.frame(width: treeSize, height: treeSize)
    172                                 .offset(x: xOffset, y: yOffset)
    173                         }
    174                     }
    175                 }
    176             Color.yellow
    177         }
    178         let background2 = VStack(spacing: 0) {
    179             Color.brown
    180             Color.yellow
    181         }
    182 
    183         // transactions
    184         if let historyItem {
    185             if let talerTX = historyItem.talerTX {
    186                 let common = talerTX.common
    187                 let amount = common.amountEffective
    188                 if common.isIncoming {
    189                     let sizeIndex = sizeIn(for: amount.value)
    190                     Image("Empty-" + String(sizeIndex))
    191                         .resizable()
    192                         .scaledToFit()
    193 //                        .border(.red)
    194                         .onTapGesture { selectTX() }
    195                         .background { background }
    196                 } else if common.isOutgoing {
    197                     let sizeIndex = sizeOut(for: amount.value)
    198                     Image("Empty-" + String(sizeIndex))
    199                         .resizable()
    200                         .scaledToFit()
    201 //                        .border(.red)
    202                         .onTapGesture { selectTX() }
    203                         .background { background }
    204                 }
    205                 if historyItem.marker != .none {
    206                     let markerIndex = historyItem.marker.rawValue
    207                     Image("Empty-" + String(markerIndex))
    208                         .resizable()
    209                         .scaledToFit()
    210                         .background { background2 }
    211                         .overlay {
    212                             LinearGradient(colors: [.clear, .black, .clear], startPoint: .leading, endPoint: .trailing)
    213                                 .opacity(0.5)
    214                         }
    215                 }
    216             } else {
    217                 let _ = print("no talerTX")
    218             }
    219         } else {
    220             let _ = print("no historyItem")
    221         }
    222         //.border(.green)
    223         //.border(.blue)
    224     }
    225 }