-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
640 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 80 additions & 22 deletions
102
UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/HorizontalDivider.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import SwiftUI | ||
import ThemeKit | ||
|
||
struct HorizontalDivider: View { | ||
private let color: Color | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import Foundation | ||
import SwiftUI | ||
import WidgetKit | ||
|
||
struct CoinPriceListEntry: TimelineEntry { | ||
let date: Date | ||
let title: String | ||
let sortType: String | ||
let items: [Item] | ||
|
||
struct Item { | ||
let uid: String | ||
let icon: Image? | ||
let code: String | ||
let name: String | ||
let price: String | ||
let priceChange: String | ||
let priceChangeType: PriceChangeType | ||
} | ||
} |
107 changes: 107 additions & 0 deletions
107
UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import Foundation | ||
import SwiftUI | ||
import WidgetKit | ||
|
||
struct CoinPriceListProvider: IntentTimelineProvider { | ||
func placeholder(in context: Context) -> CoinPriceListEntry { | ||
let count: Int | ||
|
||
switch context.family { | ||
case .systemSmall, .systemMedium: count = 3 | ||
default: count = 6 | ||
} | ||
|
||
return CoinPriceListEntry( | ||
date: Date(), | ||
title: "Top Coins", | ||
sortType: "Highest Cap", | ||
items: (1 ... count).map { index in | ||
CoinPriceListEntry.Item( | ||
uid: "coin\(index)", | ||
icon: nil, | ||
code: "COD\(index)", | ||
name: "Coin Name \(index)", | ||
price: "$1234", | ||
priceChange: "1.23", | ||
priceChangeType: .unknown | ||
) | ||
} | ||
) | ||
} | ||
|
||
func getSnapshot(for _: CoinPriceListIntent, in context: Context, completion: @escaping (CoinPriceListEntry) -> Void) { | ||
Task { | ||
let entry = try await fetch(sortType: .highestCap, family: context.family) | ||
completion(entry) | ||
} | ||
} | ||
|
||
func getTimeline(for configuration: CoinPriceListIntent, in context: Context, completion: @escaping (Timeline<CoinPriceListEntry>) -> Void) { | ||
Task { | ||
let entry = try await fetch(sortType: configuration.sortBy, family: context.family) | ||
|
||
if let nextUpdate = Calendar.current.date(byAdding: DateComponents(minute: 15), to: Date()) { | ||
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) | ||
completion(timeline) | ||
} | ||
} | ||
} | ||
|
||
private func fetch(sortType: SortType, family: WidgetFamily) async throws -> CoinPriceListEntry { | ||
let currency = CurrencyManager(storage: SharedLocalStorage()).baseCurrency | ||
let apiProvider = ApiProvider(baseUrl: "https://api-dev.blocksdecoded.com") | ||
|
||
let listType: ApiProvider.ListType | ||
let listOrder: ApiProvider.ListOrder | ||
let limit: Int | ||
|
||
switch sortType { | ||
case .highestCap, .lowestCap, .unknown: listType = .mcap | ||
case .highestVolume, .lowestVolume: listType = .volume | ||
case .topGainers, .topLosers: listType = .price | ||
} | ||
|
||
switch sortType { | ||
case .highestCap, .highestVolume, .topGainers, .unknown: listOrder = .desc | ||
case .lowestCap, .lowestVolume, .topLosers: listOrder = .asc | ||
} | ||
|
||
switch family { | ||
case .systemSmall, .systemMedium: limit = 3 | ||
default: limit = 6 | ||
} | ||
|
||
let coins = try await apiProvider.listCoins(type: listType, order: listOrder, limit: limit, currencyCode: currency.code) | ||
|
||
return CoinPriceListEntry( | ||
date: Date(), | ||
title: "Top Coins", | ||
sortType: title(sortType: sortType), | ||
items: coins.map { coin in | ||
let iconUrl = "https://cdn.blocksdecoded.com/coin-icons/32px/\(coin.uid)@3x.png" | ||
let coinIcon = URL(string: iconUrl).flatMap { try? Data(contentsOf: $0) }.flatMap { UIImage(data: $0) }.map { Image(uiImage: $0) } | ||
|
||
return CoinPriceListEntry.Item( | ||
uid: coin.uid, | ||
icon: coinIcon, | ||
code: coin.code, | ||
name: coin.name, | ||
price: coin.price.flatMap { ValueFormatter.format(currency: currency, value: $0) } ?? "n/a", | ||
priceChange: coin.priceChange24h.flatMap { ValueFormatter.format(percentValue: $0) } ?? "n/a", | ||
priceChangeType: coin.priceChange24h.map { $0 >= 0 ? .up : .down } ?? .unknown | ||
) | ||
} | ||
) | ||
} | ||
|
||
private func title(sortType: SortType) -> String { | ||
switch sortType { | ||
case .highestCap, .unknown: return "Highest Cap" | ||
case .lowestCap: return "Lowest Cap" | ||
case .highestVolume: return "Highest Volume" | ||
case .lowestVolume: return "Lowest Volume" | ||
case .topGainers: return "Top Gainers" | ||
case .topLosers: return "Top Losers" | ||
} | ||
} | ||
} |
128 changes: 128 additions & 0 deletions
128
UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import Charts | ||
import SwiftUI | ||
import WidgetKit | ||
|
||
struct CoinPriceListView: View { | ||
var entry: CoinPriceListProvider.Entry | ||
|
||
@Environment(\.widgetFamily) private var family | ||
|
||
var body: some View { | ||
switch family { | ||
case .systemSmall: smallView() | ||
case .systemMedium: mediumView() | ||
default: largeView() | ||
} | ||
} | ||
|
||
@ViewBuilder private func smallView() -> some View { | ||
ListSection { | ||
ForEach(entry.items, id: \.uid) { item in | ||
HStack(spacing: .margin8) { | ||
icon(image: item.icon) | ||
|
||
VStack(spacing: 1) { | ||
Text(item.price) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.font(.themeSubhead1) | ||
.foregroundColor(.themeLeah) | ||
Text(item.priceChange) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.font(.themeCaption) | ||
.foregroundColor(item.priceChangeType.color) | ||
} | ||
} | ||
.padding(.horizontal, .margin16) | ||
.frame(maxHeight: .infinity) | ||
} | ||
} | ||
.listStyle(.transparent) | ||
.frame(maxHeight: .infinity) | ||
.padding(.vertical, .margin4) | ||
} | ||
|
||
@ViewBuilder private func mediumView() -> some View { | ||
ListSection { | ||
ForEach(entry.items, id: \.uid) { item in | ||
row(item: item) | ||
} | ||
} | ||
.listStyle(.transparent) | ||
.frame(maxHeight: .infinity) | ||
.padding(.vertical, .margin4) | ||
} | ||
|
||
@ViewBuilder private func largeView() -> some View { | ||
VStack(spacing: 0) { | ||
HStack(spacing: .margin16) { | ||
Text(entry.title) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.font(.themeSubhead1) | ||
.foregroundColor(.themeLeah) | ||
|
||
Text(entry.sortType) | ||
.frame(maxWidth: .infinity, alignment: .trailing) | ||
.font(.themeSubhead2) | ||
.foregroundColor(.themeGray) | ||
} | ||
.padding(.margin16) | ||
|
||
HorizontalDivider() | ||
|
||
ListSection { | ||
ForEach(entry.items, id: \.uid) { item in | ||
row(item: item) | ||
} | ||
} | ||
.listStyle(.transparent) | ||
.frame(maxHeight: .infinity) | ||
} | ||
.padding(.vertical, .margin4) | ||
} | ||
|
||
@ViewBuilder private func icon(image: Image?) -> some View { | ||
if let image = image { | ||
image | ||
.resizable() | ||
.scaledToFit() | ||
.frame(width: .iconSize32, height: .iconSize32) | ||
} else { | ||
Circle() | ||
.fill(Color.themeGray) | ||
.frame(width: .iconSize32, height: .iconSize32) | ||
} | ||
} | ||
|
||
@ViewBuilder private func row(item: CoinPriceListEntry.Item) -> some View { | ||
HStack(spacing: .margin16) { | ||
icon(image: item.icon) | ||
|
||
VStack(spacing: 1) { | ||
HStack(spacing: .margin16) { | ||
Text(item.code.uppercased()) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.font(.themeSubhead1) | ||
.foregroundColor(.themeLeah) | ||
|
||
Text(item.price) | ||
.frame(maxWidth: .infinity, alignment: .trailing) | ||
.font(.themeSubhead1) | ||
.foregroundColor(.themeLeah) | ||
} | ||
|
||
HStack(spacing: .margin16) { | ||
Text(item.name) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.font(.themeSubhead2) | ||
.foregroundColor(.themeGray) | ||
|
||
Text(item.priceChange) | ||
.font(.themeSubhead2) | ||
.foregroundColor(item.priceChangeType.color) | ||
} | ||
} | ||
} | ||
.padding(.horizontal, .margin16) | ||
.frame(maxHeight: .infinity) | ||
} | ||
} |
Oops, something went wrong.