damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

SupporterBadge.swift (8702B)


      1 //
      2 //  SupporterBadge.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2023-05-15.
      6 //
      7 
      8 import SwiftUI
      9 
     10 struct SupporterBadge: View {
     11     let percent: Int?
     12     let purple_account: DamusPurple.Account?
     13     let style: Style
     14     let text_color: Color
     15     var badge_variant: BadgeVariant {
     16         if purple_account?.attributes.contains(.memberForMoreThanOneYear) == true {
     17             return .oneYearSpecial
     18         }
     19         else {
     20             return .normal
     21         }
     22     }
     23     
     24     init(percent: Int?, purple_account: DamusPurple.Account? = nil, style: Style, text_color: Color = .secondary) {
     25         self.percent = percent
     26         self.purple_account = purple_account
     27         self.style = style
     28         self.text_color = text_color
     29     }
     30     
     31     let size: CGFloat = 17
     32     
     33     var body: some View {
     34         HStack {
     35             if let purple_account, purple_account.active == true {
     36                 HStack(spacing: 1) {
     37                     switch self.badge_variant {
     38                     case .normal:
     39                         StarShape()
     40                             .frame(width:size, height:size)
     41                             .foregroundStyle(GoldGradient)
     42                     case .oneYearSpecial:
     43                         DoubleStar(size: size)
     44                     }
     45                     
     46                     if self.style == .full,
     47                        let ordinal = self.purple_account?.ordinal() {
     48                         Text(ordinal)
     49                             .foregroundStyle(text_color)
     50                             .font(.caption)
     51                     }
     52                 }
     53             }
     54             else if let percent, percent < 100 {
     55                 Image("star.fill")
     56                     .resizable()
     57                     .frame(width:size, height:size)
     58                     .foregroundColor(support_level_color(percent))
     59             } else if let percent, percent == 100 {
     60                 Image("star.fill")
     61                     .resizable()
     62                     .frame(width:size, height:size)
     63                     .foregroundStyle(GoldGradient)
     64             }
     65         }
     66     }
     67     
     68     enum Style {
     69         case full       // Shows the entire badge with a purple subscriber number if present
     70         case compact    // Does not show purple subscriber number. Only shows the star (if applicable)
     71     }
     72     
     73     enum BadgeVariant {
     74         /// A normal badge that people are used to
     75         case normal
     76         /// A special badge for users who have been members for more than a year
     77         case oneYearSpecial
     78     }
     79 }
     80 
     81 
     82 struct StarShape: Shape {
     83     func path(in rect: CGRect) -> Path {
     84         var path = Path()
     85         let center = CGPoint(x: rect.midX, y: rect.midY)
     86         let radius: CGFloat = min(rect.width, rect.height) / 2
     87         let points = 5
     88         let adjustment: CGFloat = .pi / 2
     89         
     90         for i in 0..<points * 2 {
     91             let angle = (CGFloat(i) * .pi / CGFloat(points)) - adjustment
     92             let pointRadius = i % 2 == 0 ? radius : radius * 0.4
     93             let point = CGPoint(x: center.x + pointRadius * cos(angle), y: center.y + pointRadius * sin(angle))
     94             if i == 0 {
     95                 path.move(to: point)
     96             } else {
     97                 path.addLine(to: point)
     98             }
     99         }
    100         path.closeSubpath()
    101         return path
    102     }
    103 }
    104 
    105 struct DoubleStar: View {
    106     let size: CGFloat
    107     var starOffset: CGFloat = 5
    108     
    109     var body: some View {
    110         if #available(iOS 17.0, *) {
    111             DoubleStarShape(starOffset: starOffset)
    112                 .frame(width: size, height: size)
    113                 .foregroundStyle(GoldGradient)
    114                 .padding(.trailing, starOffset)
    115         } else {
    116             Fallback(size: size, starOffset: starOffset)
    117         }
    118     }
    119     
    120     @available(iOS 17.0, *)
    121     struct DoubleStarShape: Shape {
    122         var strokeSize: CGFloat = 3
    123         var starOffset: CGFloat
    124 
    125         func path(in rect: CGRect) -> Path {
    126             let normalSizedStarPath = StarShape().path(in: rect)
    127             let largerStarPath = StarShape().path(in: rect.insetBy(dx: -strokeSize, dy: -strokeSize))
    128             
    129             let finalPath = normalSizedStarPath
    130                 .subtracting(
    131                     largerStarPath.offsetBy(dx: starOffset, dy: 0)
    132                 )
    133                 .union(
    134                     normalSizedStarPath.offsetBy(dx: starOffset, dy: 0)
    135                 )
    136             
    137             return finalPath
    138         }
    139     }
    140     
    141     /// A fallback view for those who cannot run iOS 17
    142     struct Fallback: View {
    143         var size: CGFloat
    144         var starOffset: CGFloat
    145         
    146         var body: some View {
    147             HStack {
    148                 StarShape()
    149                     .frame(width: size, height: size)
    150                     .foregroundStyle(GoldGradient)
    151                 
    152                 StarShape()
    153                     .fill(GoldGradient)
    154                     .overlay(
    155                         StarShape()
    156                             .stroke(Color.damusAdaptableWhite, lineWidth: 1)
    157                     )
    158                     .frame(width: size + 1, height: size + 1)
    159                     .padding(.leading, -size - starOffset)
    160             }
    161             .padding(.trailing, -3)
    162         }
    163     }
    164 }
    165 
    166 
    167 
    168 func support_level_color(_ percent: Int) -> Color {
    169     if percent == 0 {
    170         return .gray
    171     }
    172     
    173     let percent_f = Double(percent) / 100.0
    174     let cutoff = 0.5
    175     let h = cutoff + (percent_f * cutoff); // Hue (note 0.2 = Green, see huge chart below)
    176     let s = 0.9; // Saturation
    177     let b = 0.9; // Brightness
    178     
    179     return Color(hue: h, saturation: s, brightness: b)
    180 }
    181 
    182 struct SupporterBadge_Previews: PreviewProvider {
    183     static func Level(_ p: Int) -> some View {
    184         HStack(alignment: .center) {
    185             SupporterBadge(percent: p, style: .full)
    186                 .frame(width: 50)
    187             Text(verbatim: p.formatted())
    188                 .frame(width: 50)
    189         }
    190     }
    191     
    192     static func Purple(_ subscriber_number: Int) -> some View {
    193         HStack(alignment: .center) {
    194             SupporterBadge(
    195                 percent: nil,
    196                 purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true, attributes: []),
    197                 style: .full
    198             )
    199                 .frame(width: 100)
    200         }
    201     }
    202     
    203     static var previews: some View {
    204         VStack(spacing: 0) {
    205             VStack(spacing: 0) {
    206                 Level(1)
    207                 Level(10)
    208                 Level(20)
    209                 Level(30)
    210                 Level(40)
    211                 Level(50)
    212             }
    213             Level(60)
    214             Level(70)
    215             Level(80)
    216             Level(90)
    217             Level(100)
    218             Purple(1)
    219             Purple(2)
    220             Purple(3)
    221             Purple(99)
    222             Purple(100)
    223             Purple(1971)
    224         }
    225     }
    226 }
    227 
    228 #Preview("1 yr badge") {
    229     VStack {
    230         HStack(alignment: .center) {
    231             SupporterBadge(
    232                 percent: nil,
    233                 purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: []),
    234                 style: .full
    235             )
    236                 .frame(width: 100)
    237         }
    238         
    239         HStack(alignment: .center) {
    240             SupporterBadge(
    241                 percent: nil,
    242                 purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: [.memberForMoreThanOneYear]),
    243                 style: .full
    244             )
    245                 .frame(width: 100)
    246         }
    247         
    248         Text(verbatim: "Double star (just shape itself, with alt background color, to show it adapts to background well)")
    249             .multilineTextAlignment(.center)
    250         
    251         if #available(iOS 17.0, *) {
    252             HStack(alignment: .center) {
    253                 DoubleStar.DoubleStarShape(starOffset: 5)
    254                     .frame(width: 17, height: 17)
    255                     .padding(.trailing, -8)
    256             }
    257             .background(Color.blue)
    258         }
    259         
    260         Text(verbatim: "Double star (fallback for iOS 16 and below)")
    261 
    262         HStack(alignment: .center) {
    263             DoubleStar.Fallback(size: 17, starOffset: 5)
    264         }
    265         
    266         Text(verbatim: "Double star (fallback for iOS 16 and below, with alt color limitation shown)")
    267             .multilineTextAlignment(.center)
    268         
    269         HStack(alignment: .center) {
    270             DoubleStar.Fallback(size: 17, starOffset: 5)
    271         }
    272         .background(Color.blue)
    273     }
    274 }
    275 
    276