* charts (wip)

* charts (wip)

* wip

* add asset grid

* split files

* logic wip

* proper getPrices logic

* viewmodel (wip)

* charts

* remove unused import

* remove restricted import...?

* fix background gradient when scrolling

* add safeguards for broken db

* save new pin on removal

* add safeguards for concurrency

* add haptic feedback

* fix haptic feedback

* fix fiat ticker

* reduce chart padding

* add line touch indicator

* improve touch target size on range selector

* add date/time display when viewing past amount

* merge

* remove iconSvgPath reference

* merge

* fix state for date display

* merge

* auto-reformat

* only load charts when page is opened

* merge

* reformat and apply lints

* refactor to use `Money` instead of storing amount in `String`

* add slop comment

* fix exception on division by zero

* ci

* workaround toDouble being removed

* disable strict parsing and rounding

* fix abs()

* use new currency picker sheet

* safeguard for max flutter-supported decimals

* fix amount precision

* adjust to new buy/sell

* Update lib/new-ui/pages/charts_page.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* adjust buy/sell di

* replace -> ignore

* reformat

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
malik1004x
2026-09-22 21:32:38 +03:00
committed by GitHub
co-authored by Omar Hatem
parent d38c748127
commit b88fbf32ae
94 changed files with 2544 additions and 270 deletions
+18 -4
View File
@@ -52,6 +52,7 @@ import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/haven/cw_haven.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/new-ui/model/charts/price_store.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
@@ -59,6 +60,9 @@ import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import 'package:cake_wallet/new-ui/pages/charts_page.dart';
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
@@ -68,6 +72,7 @@ import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart";
import "package:cake_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
import 'package:cake_wallet/new-ui/viewmodels/charts/charts_bloc.dart';
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
@@ -552,6 +557,15 @@ Future<void> setup({
(displayMode == BitcoinAmountDisplayMode.satoshiForLightning && lightningMode)));
});
getIt.registerLazySingleton<PriceStore>(() => PriceStore());
getIt.registerFactory<ChartsBloc>(
() => ChartsBloc(appStore: getIt.get<AppStore>(), priceStore: getIt.get<PriceStore>()));
getIt.registerFactory<ChartsPage>(() => ChartsPage(
chartsBloc: getIt.get<ChartsBloc>(),
));
getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
@@ -1321,11 +1335,11 @@ Future<void> setup({
getIt.registerFactory(() => BuyAmountViewModel());
getIt.registerFactoryParam<BuySellViewModel, BuySellPageMode, void>((mode, _) => BuySellViewModel(
mode: mode, getIt.get<AppStore>(), fiatConversionStore: getIt.get<FiatConversionStore>()));
getIt.registerFactoryParam<BuySellViewModel, NewBuySellParams, void>((params, _) => BuySellViewModel(
params: params, getIt.get<AppStore>(), fiatConversionStore: getIt.get<FiatConversionStore>()));
getIt.registerFactoryParam<NewBuySellAmountPage, BuySellPageMode, void>((mode, _) =>
NewBuySellAmountPage(buySellViewModel: getIt.get<BuySellViewModel>(param1: mode)));
getIt.registerFactoryParam<NewBuySellAmountPage, NewBuySellParams, void>((params, _) =>
NewBuySellAmountPage(buySellViewModel: getIt.get<BuySellViewModel>(param1: params)));
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) {
@@ -13,8 +13,10 @@ import 'package:cake_wallet/entities/haven_seed_store.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/model/charts/charts_asset.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:cake_wallet/entities/sync_status_display_mode.dart';
import 'package:cw_core/node_list.dart';
@@ -648,6 +650,9 @@ Future<void> defaultSettingsMigration(
enabled: false,
);
break;
case 72:
await createDefaultChartsData();
break;
default:
break;
}
@@ -1338,6 +1343,12 @@ Future<void> _addXaut0TokenToExistingSolanaWallets() async {
}
}
Future<void> createDefaultChartsData() async {
await ChartsAsset(asset: CryptoCurrency.btc, isFavorite: true).insert();
await ChartsAsset(asset: CryptoCurrency.xmr, isFavorite: false).insert();
await ChartsAsset(asset: CryptoCurrency.eth, isFavorite: false).insert();
}
Future<void> _addTbbTokenToExistingSolanaWallets() async {
try {
final tbbToken = SPLToken(
+3
View File
@@ -19,6 +19,9 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
@override
final int decimals;
@override
String get apiString => "fiat.$title";
@override
final String symbol;
+1 -1
View File
@@ -82,7 +82,7 @@ class MainActions {
canShow: (viewModel) => viewModel.hasTradeAction,
onTap: (BuildContext context, DashboardViewModel viewModel) async {
if (!viewModel.isEnabledTradeAction) return;
await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
await Navigator.of(context).pushNamed(Routes.buySellPage);
},
);
}
+24 -24
View File
@@ -1,14 +1,8 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:flutter/material.dart";
class NewMainActions {
final Key key;
final String Function(BuildContext context) name;
final String image;
final VoidCallback onTap;
final bool Function(DashboardViewModel viewModel)? isEnabled;
final bool Function(DashboardViewModel viewModel)? canShow;
NewMainActions._({
required this.key,
@@ -18,48 +12,54 @@ class NewMainActions {
this.isEnabled,
this.canShow,
});
final Key key;
final String Function(BuildContext context) name;
final String image;
final VoidCallback onTap;
final bool Function(DashboardViewModel viewModel)? isEnabled;
final bool Function(DashboardViewModel viewModel)? canShow;
static List<NewMainActions> all = [
homeAction,
walletsAction,
contactsAction,
appsAction,
//chartsAction,
chartsAction,
];
static NewMainActions homeAction = NewMainActions._(
name: (context) => S.of(context).home,
image: 'assets/new-ui/navbar/home.svg',
key: ValueKey('dashboard_page_home_action_button_key'),
image: "assets/new-ui/navbar/home.svg",
key: const ValueKey("dashboard_page_home_action_button_key"),
onTap: () {},
);
static NewMainActions walletsAction = NewMainActions._(
name: (context) => S.of(context).wallets,
image: 'assets/new-ui/navbar/wallets.svg',
key: ValueKey('dashboard_page_wallets_action_button_key'),
image: "assets/new-ui/navbar/wallets.svg",
key: const ValueKey("dashboard_page_wallets_action_button_key"),
onTap: () {},
);
static NewMainActions contactsAction = NewMainActions._(
name: (context) => S.of(context).contacts,
image: 'assets/new-ui/navbar/contacts.svg',
key: ValueKey('dashboard_page_contacts_action_button_key'),
image: "assets/new-ui/navbar/contacts.svg",
key: const ValueKey("dashboard_page_contacts_action_button_key"),
onTap: () {},
);
static NewMainActions appsAction = NewMainActions._(
name: (context) => S.of(context).apps,
image: 'assets/new-ui/navbar/apps.svg',
key: ValueKey('dashboard_page_apps_action_button_key'),
image: "assets/new-ui/navbar/apps.svg",
key: const ValueKey("dashboard_page_apps_action_button_key"),
canShow: (dashboardVM) => dashboardVM.showApps,
onTap: () {},
);
// static NewMainActions chartsAction = NewMainActions._(
// name: (context) => 'Charts', //TODO S.of(context).charts,
// image: 'assets/new-ui/navbar/charts.svg',
// key: ValueKey('dashboard_page_charts_action_button_key'),
// onTap: () {},
// );
static NewMainActions chartsAction = NewMainActions._(
name: (context) => S.of(context).charts,
image: "assets/new-ui/navbar/charts.svg",
key: const ValueKey("dashboard_page_charts_action_button_key"),
onTap: () {},
);
}
+1 -1
View File
@@ -309,7 +309,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 71,
initialMigrationVersion: 72,
);
}
-58
View File
@@ -1,58 +0,0 @@
import 'dart:ui';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
class LongPressPopupBuilder extends StatelessWidget {
const LongPressPopupBuilder(
{super.key, required this.child, required this.popup, this.spacing = 8});
final Widget child;
final Widget popup;
final double spacing;
@override
Widget build(BuildContext context) {
return GestureDetector(
onLongPress: () {
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
showPopUp(
context: context,
builder: (context) => _buildPopup(context, offset, size),
);
},
child: child,
);
}
Widget _buildPopup(BuildContext context, Offset offset, Size size) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy,
width: size.width,
height: size.height,
child: Material(
color: Colors.transparent,
child: child,
),
),
Positioned(
left: offset.dx * 2,
top: offset.dy + size.height + spacing,
child: Material(
color: Colors.transparent,
child: popup,
),
),
],
),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import "package:cake_wallet/new-ui/model/charts/price_data.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/db/sqlite.dart";
import "package:sqflite/sqflite.dart";
class ChartsAsset {
const ChartsAsset({required this.asset, required this.isFavorite});
factory ChartsAsset.fromJson(Map<String, dynamic> json) => ChartsAsset(
asset: currencyFromApiString(json["asset"] as String) as CryptoCurrency,
isFavorite: json["isFavorite"] != 0,
);
final CryptoCurrency asset;
final bool isFavorite;
static const tableName = "ChartsAssets";
Map<String, dynamic> toJson() => {"asset": asset.apiString, "isFavorite": isFavorite ? 1 : 0};
static Future<List<ChartsAsset>> get() async {
final json = await db!.query(tableName);
return List.generate(json.length, (index) => ChartsAsset.fromJson(json[index]));
}
Future<void> insert() async {
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<void> remove() async {
await db!.delete(tableName, where: "asset = ?", whereArgs: [asset.apiString]);
}
}
@@ -0,0 +1,6 @@
extension DateTimeX on DateTime {
int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000;
static DateTime fromSecondsSinceEpoch(int seconds) =>
DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
}
@@ -0,0 +1,78 @@
import "dart:convert";
import "package:cake_wallet/.secrets.g.dart" as secrets;
import "package:cake_wallet/new-ui/model/charts/datetime_extension.dart";
import "package:cake_wallet/new-ui/model/charts/price_data.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/utils/proxy_wrapper.dart";
const priceApiHost = "prices.cakewallet.com";
class PriceRequest {
const PriceRequest({
required this.interval,
required this.from,
required this.to,
this.beginTime,
this.count,
});
final DateTime? beginTime;
final Duration interval;
final int? count;
final Currency from;
final Currency to;
Uri get uri => Uri.https(priceApiHost, "/v3/rates", {
"start": (beginTime?.secondsSinceEpoch ?? 0).toString(),
"end": DateTime.now().secondsSinceEpoch.toString(),
"interval": "${interval.inSeconds}",
if (count != null) "count": count.toString(),
"quote": from.apiString,
"base": to.apiString,
});
}
class PriceApiClient {
static Future<Map<String, dynamic>?> _getJson(Uri uri) async {
final resp =
await ProxyWrapper().get(headers: {"x-api-key": secrets.fiatApiKey}, clearnetUri: uri);
if (!(resp.statusCode >= 200 && resp.statusCode < 300)) {
printV("server returned code: ${resp.statusCode}\nuri: ${uri}\nresp body: ${resp.body}");
return null;
}
try {
return jsonDecode(resp.body) as Map<String, dynamic>?;
} catch (e) {
printV("failed to decode response for ${uri.host}/${uri.path}: $e");
return null;
}
}
static Future<List<PriceData>> getPrices(PriceRequest request) async {
final List<PriceData> ret = [];
printV(request.uri);
final data = await _getJson(request.uri);
if (data == null) {
return [];
}
final results = data["results"] as Map<String, dynamic>?;
if (results == null) {
printV(data.toString());
return [];
}
printV(data.toString());
for (final time in results.keys) {
ret.add(
PriceData(
time: DateTimeX.fromSecondsSinceEpoch(int.parse(time)),
base: request.from,
quote: Money.parse((results[time] as num).toStringAsFixed(20), request.to, strictParsing: false),
),
);
}
return ret;
}
}
+104
View File
@@ -0,0 +1,104 @@
import "package:cake_wallet/entities/fiat_currency.dart";
import "package:cake_wallet/new-ui/model/charts/datetime_extension.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/db/sqlite.dart";
import "package:sqflite/sqflite.dart";
Currency currencyFromApiString(String key) {
final parts = key.split(".");
final type = parts[0];
final id = parts[1];
switch (type) {
case "fiat":
return FiatCurrency.deserialize(raw: id);
case "crypto":
return CryptoCurrency.fromString(id);
case "evm":
throw UnimplementedError();
case "sol":
throw UnimplementedError();
}
throw Exception("unknown api string");
}
class PriceData implements Comparable<PriceData> {
const PriceData({required this.time, required this.base, required this.quote,});
factory PriceData.fromJson(Map<String, dynamic> json) => PriceData(
time: DateTimeX.fromSecondsSinceEpoch(json["timestamp"] as int),
base: currencyFromApiString(json["fromCurrency"] as String),
quote: Money.parse(
json["price"] as String, currencyFromApiString(json["toCurrency"] as String),
strictParsing: false),
);
final DateTime time;
final Currency base;
final Money quote;
static const tableName = "PriceData";
Map<String, dynamic> toJson() => {
"timestamp": time.secondsSinceEpoch.toString(),
"price": quote.toString(),
"fromCurrency": base.apiString,
"toCurrency": quote.currency.apiString,
};
static Future<List<PriceData>> get(
Currency from,
Currency to,
DateTime? start,
DateTime? end,
) async {
final json = await db!.query(
tableName,
where: "fromCurrency = ? AND toCurrency = ? AND timestamp >= ? AND timestamp <= ?",
whereArgs: [
from.apiString,
to.apiString,
start?.secondsSinceEpoch ?? 0,
// dart doesn't have INT_MAX, apparently.
end?.secondsSinceEpoch ?? 0x7FFFFFFFFFFFFFFF,
],
);
return List.generate(json.length, (index) => PriceData.fromJson(json[index]));
}
Future<void> insert() async {
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
}
static Future<void> insertMany(Iterable<PriceData> data) async {
if (data.isEmpty) {
return;
}
final batch = db!.batch();
for (final datum in data) {
batch.insert(
tableName,
datum.toJson(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
}
@override
bool operator ==(Object other) =>
other is PriceData &&
time == other.time &&
base == other.base &&
quote.currency == other.quote.currency;
@override
int get hashCode => Object.hash(time, base, quote.currency);
@override
int compareTo(PriceData other) => time.compareTo(other.time);
}
+133
View File
@@ -0,0 +1,133 @@
import "package:cake_wallet/new-ui/model/charts/price_api_client.dart";
import "package:cake_wallet/new-ui/model/charts/price_data.dart";
import "package:cake_wallet/new-ui/model/charts/util/chart_range.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/utils/print_verbose.dart";
abstract class PriceSource {
const PriceSource();
Future<List<PriceData>> get(
DateTime start,
DateTime end,
Currency from,
Currency to,
Duration interval,
);
}
mixin UpdatablePriceSource {
Future<void> update(Iterable<PriceData> newData);
}
class DatabasePriceSource extends PriceSource with UpdatablePriceSource {
const DatabasePriceSource();
@override
Future<List<PriceData>> get(
DateTime start,
DateTime end,
Currency from,
Currency to,
Duration interval,
) =>
PriceData.get(from, to, start, end);
@override
Future<void> update(Iterable<PriceData> newData) async {
await PriceData.insertMany(newData);
}
}
class ApiPriceSource extends PriceSource {
const ApiPriceSource();
@override
Future<List<PriceData>> get(
DateTime start,
DateTime end,
Currency from,
Currency to,
Duration interval,
) =>
PriceApiClient.getPrices(
PriceRequest(beginTime: start, interval: interval, from: from, to: to),
);
}
class PriceStore {
static const priceSources = [
// TODO InMemoryPriceSource() - easily implementable w this pattern but idk if we need to optimize this that much
DatabasePriceSource(),
ApiPriceSource(),
];
Future<List<PriceData>> getPrices(Currency from, Currency to, ChartRange range) async {
final Set<PriceData> data = {};
final end = DateTime.now();
DateTime? start;
if (range.duration == null) {
start = DateTime.fromMillisecondsSinceEpoch(0);
} else {
start = end.subtract(range.duration!);
}
start = _alignedStart(start, range.dataPrecision);
final alignedStart = start;
for (final source in priceSources) {
final sourceData = await source.get(start!, end, from, to, range.dataPrecision);
data.addAll(sourceData);
start = _firstUnavailablePrice(data.toList(), range.dataPrecision, start, end);
if (start == null) {
break;
}
}
for (final source in priceSources) {
if (source case final UpdatablePriceSource s) {
await s.update(data);
}
}
return _alignedData(alignedStart, end, data, range.dataPrecision);
}
static List<PriceData> _alignedData(
DateTime start,
DateTime end,
Iterable<PriceData> data,
Duration precision,
) {
final ret = data
.where((datum) => datum.time.millisecondsSinceEpoch % precision.inMilliseconds == 0)
.toList();
ret.sort((a, b) => a.time.compareTo(b.time));
return ret;
}
static DateTime _alignedStart(DateTime start, Duration precision) {
final alignedStartMs =
(start.millisecondsSinceEpoch ~/ precision.inMilliseconds) * precision.inMilliseconds;
return DateTime.fromMillisecondsSinceEpoch(alignedStartMs);
}
static DateTime? _firstUnavailablePrice(
List<PriceData> prices,
Duration precision,
DateTime start,
DateTime end,
) {
if (prices.isEmpty) {
return start;
}
prices.sort();
final last = prices.last;
printV("last.time: ${last.time.toIso8601String()} end: ${end.toIso8601String()}");
if (last.time.isAfter(end.subtract(precision)) ||
last.time.isAtSameMomentAs(end.subtract(precision))) {
return null;
}
return last.time.add(precision);
}
}
@@ -0,0 +1,21 @@
class ChartRange {
const ChartRange._(this.duration, this.displayText, this.dataPrecision);
final Duration? duration;
final String displayText;
// api can handle any precision up to 5min, regardless of data range.
// we set a "preferred" precision for 2 reasons:
// - less precision means less data sent, less bandwidth used, and thus less load on the backend
// - less data points means the chart is easier to look through on a small screen
final Duration dataPrecision;
static const oneHour = ChartRange._(Duration(hours: 1), "1H", Duration(minutes: 5));
static const oneDay = ChartRange._(Duration(days: 1), "1D", Duration(minutes: 15));
static const sevenDays = ChartRange._(Duration(days: 7), "7D", Duration(hours: 1));
static const thirtyDays = ChartRange._(Duration(days: 30), "30D", Duration(hours: 4));
static const oneYear = ChartRange._(Duration(days: 365), "1Y", Duration(days: 1));
static const all = ChartRange._(null, "ALL", Duration(days: 1));
static const ranges = [oneHour, oneDay, sevenDays, thirtyDays, oneYear, all];
}
@@ -0,0 +1,19 @@
import "package:cake_wallet/new-ui/model/charts/util/price_change_direction.dart";
import "package:cw_core/amount/money.dart";
class PriceChangeData implements Comparable<PriceChangeData> {
const PriceChangeData({required this.direction, required this.amount, required this.percentage});
final PriceChangeDirection direction;
final Money amount;
final String percentage;
@override
int compareTo(PriceChangeData other) {
final double thisValue =
double.parse(percentage) * (direction == PriceChangeDirection.up ? 1 : -1);
final double otherValue =
double.parse(other.percentage) * (other.direction == PriceChangeDirection.up ? 1 : -1);
return thisValue.compareTo(otherValue);
}
}
@@ -0,0 +1,11 @@
import "dart:ui";
class PriceChangeDirection {
const PriceChangeDirection._(this.color, this.symbol);
final Color color;
final String symbol;
static const up = PriceChangeDirection._(Color(0xFF6FC84E), "+");
static const down = PriceChangeDirection._(Color(0xFFEA696F), "-");
}
@@ -0,0 +1,141 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_change_data.dart";
import "package:cw_core/crypto_currency.dart";
const List<CryptoCurrency> cryptoCurrenciesByMarketcap = [
CryptoCurrency.btc,
CryptoCurrency.eth,
CryptoCurrency.usdt,
CryptoCurrency.xrp,
CryptoCurrency.bnb,
CryptoCurrency.sol,
CryptoCurrency.usdc,
CryptoCurrency.doge,
CryptoCurrency.trx,
CryptoCurrency.ton,
CryptoCurrency.bch,
CryptoCurrency.ltc,
CryptoCurrency.xmr,
CryptoCurrency.matic,
CryptoCurrency.arb,
CryptoCurrency.dai,
CryptoCurrency.paxg,
CryptoCurrency.zec,
CryptoCurrency.dcr,
CryptoCurrency.nano,
CryptoCurrency.zano,
CryptoCurrency.deuro,
CryptoCurrency.wow,
];
abstract class PriceDataSortCriterium {
const PriceDataSortCriterium();
String get name;
String get iconPath;
int comparator(
PriceChangeData changeDataA,
PriceChangeData changeDataB,
CryptoCurrency a,
CryptoCurrency b,
);
static const all = [
MarketcapSortCriterium(),
GainsSortCriterium(),
LossesSortCriterium(),
AlphabeticalSortCriterium(),
];
}
class AlphabeticalSortCriterium extends PriceDataSortCriterium {
const AlphabeticalSortCriterium();
@override
String get name => S.current.alphabetical;
@override
String get iconPath => "assets/new-ui/charts_sort_criteria/alpha.svg";
@override
int comparator(
PriceChangeData changeDataA,
PriceChangeData changeDataB,
CryptoCurrency a,
CryptoCurrency b,
) =>
(a.fullName ?? a.title).compareTo(b.fullName ?? b.title);
}
class MarketcapSortCriterium extends PriceDataSortCriterium {
const MarketcapSortCriterium();
@override
String get name => S.current.marketcap;
@override
String get iconPath => "assets/new-ui/charts_sort_criteria/marketcap.svg";
@override
int comparator(
PriceChangeData changeDataA,
PriceChangeData changeDataB,
CryptoCurrency a,
CryptoCurrency b,
) {
final aIndex = cryptoCurrenciesByMarketcap.indexOf(a);
final bIndex = cryptoCurrenciesByMarketcap.indexOf(b);
if (aIndex != -1 && bIndex != -1) {
return aIndex.compareTo(bIndex);
}
if (aIndex != -1) {
return -1;
}
if (bIndex != -1) {
return 1;
}
return (a.fullName ?? a.title).compareTo(b.fullName ?? b.title);
}
}
class GainsSortCriterium extends PriceDataSortCriterium {
const GainsSortCriterium();
@override
String get name => S.current.gains;
@override
String get iconPath => "assets/new-ui/charts_sort_criteria/gains.svg";
@override
int comparator(
PriceChangeData changeDataA,
PriceChangeData changeDataB,
CryptoCurrency a,
CryptoCurrency b,
) =>
changeDataB.compareTo(changeDataA);
}
class LossesSortCriterium extends PriceDataSortCriterium {
const LossesSortCriterium();
@override
String get name => S.current.losses;
@override
String get iconPath => "assets/new-ui/charts_sort_criteria/losses.svg";
@override
int comparator(
PriceChangeData changeDataA,
PriceChangeData changeDataB,
CryptoCurrency a,
CryptoCurrency b,
) =>
changeDataA.compareTo(changeDataB);
}
+3 -1
View File
@@ -3,6 +3,8 @@ import 'dart:io';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import "package:cake_wallet/new-ui/page_open_listener.dart";
import "package:cake_wallet/new-ui/page_open_listener.dart";
import 'package:cake_wallet/new-ui/pages/charts_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/widgets/changelog_modal.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
@@ -36,7 +38,7 @@ class NewDashboard extends StatefulWidget {
getIt.get<WalletListPage>(),
getIt.get<ContactListPage>(),
getIt.get<CakeFeaturesPage>(),
Placeholder(),
getIt.get<ChartsPage>()
];
@override
+2
View File
@@ -1,3 +1,5 @@
abstract interface class PageOpenListener {
void onPageOpen();
}
+2 -2
View File
@@ -3,11 +3,11 @@ import 'dart:ui';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/long_press_popup.dart';
import 'package:cake_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_info.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
import 'package:cake_wallet/new-ui/widgets/long_press_menu.dart';
import 'package:cake_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -27,6 +27,13 @@ import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
class NewBuySellParams {
NewBuySellParams({required this.mode, this.initialCurrency});
final BuySellPageMode mode;
final CryptoCurrency? initialCurrency;
}
class NewBuySellAmountPage extends StatefulWidget {
const NewBuySellAmountPage({required this.buySellViewModel, super.key});
+103
View File
@@ -0,0 +1,103 @@
import "package:cake_wallet/new-ui/page_open_listener.dart";
import "package:cake_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/asset_grid.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/asset_grid_header.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/chart_header.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
class ChartsPage extends StatelessWidget implements PageOpenListener {
const ChartsPage({required this.chartsBloc, super.key});
final ChartsBloc chartsBloc;
@override
void onPageOpen() {
chartsBloc.add(Init());
}
@override
Widget build(BuildContext context) => BlocProvider(
create: (context) => chartsBloc,
child: BlocBuilder<ChartsBloc, ChartsState>(
builder: (context, state) {
if (state is ChartsInitial) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 24,
children: [
CakeImageWidget(
width: 36,
height: 36,
imageUrl: "assets/new-ui/navbar/charts.svg",
),
CupertinoActivityIndicator(),
if (kDebugMode)
Text(
"devs: if this doesn't go away in a few seconds, either your device is painfully slow or the db is messed up",
),
],
),
);
}
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: CustomScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: [
SliverPadding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
sliver: CupertinoSliverRefreshControl(
refreshTriggerPullDistance: 160,
refreshIndicatorExtent: 90,
onRefresh: () async => chartsBloc.add(PageRefreshed()),
),
),
SliverToBoxAdapter(
child: SafeArea(
child: Column(
spacing: 24,
children: [
if (state is ChartsStateWithData)
ChartHeader(
currency: state.pinnedCurrency,
chartHeight: 100,
chartPadding: 22,
centered: false,
favorite: true,
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 18),
child: Column(
spacing: 24,
children: [ChartsAssetGridHeader(), ChartsAssetGrid()],
),
),
const SizedBox(
height: 96,
),
],
),
),
),
],
),
);
},
),
);
}
@@ -0,0 +1,173 @@
import "dart:async";
import "package:bloc/bloc.dart";
import "package:bloc_concurrency/bloc_concurrency.dart";
import "package:cake_wallet/core/utilities.dart";
import "package:cake_wallet/entities/fiat_currency.dart";
import "package:cake_wallet/new-ui/model/charts/charts_asset.dart";
import "package:cake_wallet/new-ui/model/charts/price_data.dart";
import "package:cake_wallet/new-ui/model/charts/price_store.dart";
import "package:cake_wallet/new-ui/model/charts/util/chart_range.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_change_data.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_change_direction.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_data_sort_criteria.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_amount_format.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/foundation.dart";
part "charts_event.dart";
part "charts_state.dart";
class ChartsBloc extends Bloc<ChartsEvent, ChartsState> {
ChartsBloc({required this.priceStore, required this.appStore}) : super(const ChartsInitial()) {
on<RangeChanged>(_onRangeChanged, transformer: sequential());
on<SortingCriteriumChanged>(_onSortingCriteriumChanged);
on<CurrencyAdded>(_onCurrencyAdded, transformer: sequential());
on<CurrencyRemoved>(_onCurrencyRemoved, transformer: sequential());
on<CurrencyPinned>(_onCurrencyPinned, transformer: sequential());
on<PageRefreshed>(_onPageRefreshed, transformer: sequential());
on<PageLoadStarted>(_onPageLoadStarted, transformer: restartable());
on<Init>(_init);
}
final PriceStore priceStore;
final AppStore appStore;
Future<void> _init(Init event, Emitter<ChartsState> emit) async {
if (state is! ChartsInitial) {
return;
}
final assets = await ChartsAsset.get();
if (assets.isEmpty) {
// generally this shouldn't happen, but i wanna make sure we can recover from a broken db
assets.add(const ChartsAsset(asset: CryptoCurrency.btc, isFavorite: true));
unawaited(assets.first.insert());
}
if (assets.firstWhereOrNull((item) => item.isFavorite) == null) {
assets.first = ChartsAsset(asset: assets.first.asset, isFavorite: true);
unawaited(assets.first.insert());
}
emit(
ChartsLoading(
pinnedCurrency: assets.firstWhere((item) => item.isFavorite).asset,
currencies: assets.map((item) => item.asset).toList(),
range: ChartRange.all,
sortCriterium: PriceDataSortCriterium.all.first,
),
);
add(PageLoadStarted());
}
Future<void> _onPageLoadStarted(PageLoadStarted event, Emitter<ChartsState> emit) async {
if (state case final ChartsStateWithData s) {
final Map<CryptoCurrency, List<PriceData>> data = {};
final currencies = s.currencies.toList();
final range = s.range;
for (final curr in currencies) {
data[curr] = await priceStore.getPrices(appStore.settingsStore.fiatCurrency, curr, range);
}
emit(
ChartsLoaded(
pinnedCurrency: s.pinnedCurrency,
prices: data,
range: s.range,
sortCriterium: s.sortCriterium,
),
);
} else {
throw Exception("attempted price load without currency data");
}
}
Future<void> _onRangeChanged(
RangeChanged event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
emit(s.toLoading().copyWith(range: event.newRange));
add(PageLoadStarted());
}
}
Future<void> _onSortingCriteriumChanged(
SortingCriteriumChanged event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
emit(s.copyWith(sortCriterium: event.newCriterium));
}
}
Future<void> _onCurrencyAdded(
CurrencyAdded event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
final newCurrencies = s.currencies..add(event.currency);
await ChartsAsset(asset: event.currency, isFavorite: false).insert();
emit(s.toLoading().copyWith(currencies: newCurrencies));
add(PageLoadStarted());
}
}
Future<void> _onCurrencyRemoved(
CurrencyRemoved event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
final newCurrencies = s.currencies..remove(event.currency);
if (newCurrencies.isEmpty) {
throw Exception(
"removed the last currency ${kDebugMode ? "- your ui should block this! what did you do?" : ""}",
);
}
final CryptoCurrency newPin;
if (s.pinnedCurrency == event.currency) {
newPin = newCurrencies.first;
await ChartsAsset(asset: s.pinnedCurrency, isFavorite: false).insert();
await ChartsAsset(asset: newPin, isFavorite: true).insert();
} else {
newPin = s.pinnedCurrency;
}
await ChartsAsset(asset: event.currency, isFavorite: false).remove();
emit(s.toLoading().copyWith(currencies: newCurrencies, pinnedCurrency: newPin));
add(PageLoadStarted());
}
}
Future<void> _onCurrencyPinned(
CurrencyPinned event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
if (s.pinnedCurrency == event.currency) {
return;
}
await ChartsAsset(asset: s.pinnedCurrency, isFavorite: false).insert();
await ChartsAsset(asset: event.currency, isFavorite: true).insert();
emit(s.toLoading().copyWith(pinnedCurrency: event.currency));
add(PageLoadStarted());
}
}
Future<void> _onPageRefreshed(
PageRefreshed event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
emit(s.toLoading());
add(PageLoadStarted());
}
}
}
@@ -0,0 +1,42 @@
part of 'charts_bloc.dart';
@immutable
sealed class ChartsEvent {
const ChartsEvent();
}
class RangeChanged extends ChartsEvent {
const RangeChanged({required this.newRange});
final ChartRange newRange;
}
class SortingCriteriumChanged extends ChartsEvent {
const SortingCriteriumChanged({required this.newCriterium});
final PriceDataSortCriterium newCriterium;
}
class CurrencyAdded extends ChartsEvent {
const CurrencyAdded({required this.currency});
final CryptoCurrency currency;
}
class CurrencyRemoved extends ChartsEvent {
const CurrencyRemoved({required this.currency});
final CryptoCurrency currency;
}
class CurrencyPinned extends ChartsEvent {
const CurrencyPinned({required this.currency});
final CryptoCurrency currency;
}
class PageRefreshed extends ChartsEvent {}
class PageLoadStarted extends ChartsEvent {}
class Init extends ChartsEvent {}
@@ -0,0 +1,132 @@
part of "charts_bloc.dart";
@immutable
sealed class ChartsState {
const ChartsState();
}
final class ChartsInitial extends ChartsState {
const ChartsInitial();
}
abstract final class ChartsStateWithData extends ChartsState {
const ChartsStateWithData({
required this.pinnedCurrency,
required this.range,
required this.sortCriterium,
});
final CryptoCurrency pinnedCurrency;
List<CryptoCurrency> get currencies;
final PriceDataSortCriterium sortCriterium;
final ChartRange range;
Money? priceFor(CryptoCurrency curr) => null;
String get fiatTicker => "";
bool get hasSingleCurrency => currencies.length == 1;
ChartsStateWithData copyWith({
CryptoCurrency? pinnedCurrency,
ChartRange? range,
PriceDataSortCriterium? sortCriterium,
});
ChartsLoading toLoading() => ChartsLoading(
pinnedCurrency: pinnedCurrency,
currencies: currencies,
range: range,
sortCriterium: sortCriterium,
);
}
final class ChartsLoading extends ChartsStateWithData {
const ChartsLoading({
required super.pinnedCurrency,
required this.currencies,
required super.range,
required super.sortCriterium,
});
@override
final List<CryptoCurrency> currencies;
@override
ChartsLoading copyWith({
CryptoCurrency? pinnedCurrency,
List<CryptoCurrency>? currencies,
ChartRange? range,
PriceDataSortCriterium? sortCriterium,
}) =>
ChartsLoading(
pinnedCurrency: pinnedCurrency ?? this.pinnedCurrency,
currencies: currencies ?? this.currencies,
range: range ?? this.range,
sortCriterium: sortCriterium ?? this.sortCriterium,
);
}
final class ChartsLoaded extends ChartsStateWithData {
const ChartsLoaded({
required super.pinnedCurrency,
required Map<CryptoCurrency, List<PriceData>> prices,
required super.range,
required super.sortCriterium,
}) : _prices = prices;
final Map<CryptoCurrency, List<PriceData>> _prices;
@override
String get fiatTicker => _prices[_prices.keys.first]?.firstOrNull?.base.name ?? "";
@override
List<CryptoCurrency> get currencies {
final list = _prices.keys.toList();
list.sort((a, b) => sortCriterium.comparator(changeDataFor(a), changeDataFor(b), a, b));
return list;
}
@override
Money? priceFor(CryptoCurrency curr) => _prices[curr]?.lastOrNull?.quote;
PriceChangeData changeDataFor(CryptoCurrency curr) {
final latestPrice = _prices[curr]?.lastOrNull?.quote ?? Money.zero(FiatCurrency.usd);
final secondLatestPrice = _prices[curr]?.firstOrNull?.quote ?? Money.zero(FiatCurrency.usd);
final direction =
latestPrice >= secondLatestPrice ? PriceChangeDirection.up : PriceChangeDirection.down;
final double percentage;
if(secondLatestPrice.isZero) {
percentage = 0;
} else {
percentage =
(((double.parse(latestPrice.toString()) - double.parse(secondLatestPrice.toString())) /
double.parse(secondLatestPrice.toString())) * 100).abs();
}
final amount = (latestPrice - secondLatestPrice).abs();
return PriceChangeData(
direction: direction,
amount: amount,
percentage: percentage.toStringAsFixed(2),
);
}
List<PriceData> dataFor(CryptoCurrency curr) => _prices[curr] ?? [];
@override
ChartsLoaded copyWith({
CryptoCurrency? pinnedCurrency,
Map<CryptoCurrency, List<PriceData>>? prices,
ChartRange? range,
PriceDataSortCriterium? sortCriterium,
}) =>
ChartsLoaded(
pinnedCurrency: pinnedCurrency ?? this.pinnedCurrency,
prices: prices ?? _prices,
range: range ?? this.range,
sortCriterium: sortCriterium ?? this.sortCriterium,
);
}
@@ -3,6 +3,7 @@ import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
import "package:cake_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/themes/core/theme_extension.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
@@ -68,7 +69,7 @@ class BuySellSelectorModal extends StatelessWidget {
void openBuySellPage(BuildContext context, BuySellPageMode mode) {
Navigator.of(context).pop();
final page = getIt.get<NewBuySellAmountPage>(param1: mode);
final page = getIt.get<NewBuySellAmountPage>(param1: NewBuySellParams(mode: mode));
showModalBottomSheet(
useSafeArea: true,
isScrollControlled: true,
@@ -0,0 +1,222 @@
import "dart:async";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_change_data.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_change_direction.dart";
import "package:cake_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/change_pill.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/chart_modal.dart";
import "package:cake_wallet/new-ui/widgets/long_press_menu/long_press_footer.dart";
import "package:cake_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart";
import "package:cake_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/themes/core/theme_extension.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_bloc/flutter_bloc.dart";
class ChartsAssetGrid extends StatelessWidget {
const ChartsAssetGrid({super.key});
@override
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
builder: (context, state) {
if (state is ChartsStateWithData) {
final currencies = state.currencies;
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: currencies.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
mainAxisExtent: 105,),
itemBuilder: (context, index) {
final curr = currencies[index];
return ChartsAssetCard(
currency: curr,
price: state.priceFor(curr)?.toStringWithPrecision(fractionalDigits: 2) ?? "...",
ticker: state.fiatTicker,
changeData: state is ChartsLoaded ? state.changeDataFor(curr) : null,
favorite: curr == state.pinnedCurrency,
isSingleCurrency: state.hasSingleCurrency,
);
},);
} else {
return const SizedBox.shrink();
}
},
);
}
class ChartsAssetCard extends StatelessWidget {
const ChartsAssetCard(
{super.key,
required this.currency,
required this.price,
required this.ticker,
this.changeData,
required this.favorite,
required this.isSingleCurrency,});
final CryptoCurrency currency;
final String price;
final String ticker;
final PriceChangeData? changeData;
final bool favorite;
final bool isSingleCurrency;
String get displayPrice {
try {
final priceDouble = double.parse(price);
if (priceDouble > 10000) {
return priceDouble.toStringAsFixed(0);
} else {
return priceDouble.toStringAsFixed(2);
}
} catch (_) {
return price;
}
}
@override
Widget build(BuildContext context) => LongPressPopupBuilder(
popup: LongPressMenu(items: [
LongPressMenuItem(
label: S.of(context).favorite,
iconPath: "assets/new-ui/favorite.svg",
onSelected: () {
context.read<ChartsBloc>().add(CurrencyPinned(currency: currency));
Navigator.of(context).pop();
},
color: favorite ? Theme.of(context).colorScheme.error : null,),
LongPressMenuItem(
label: S.of(context).remove,
iconPath: "assets/new-ui/address_hide.svg",
onSelected: () {
if (!isSingleCurrency) {
context.read<ChartsBloc>().add(CurrencyRemoved(currency: currency));
Navigator.of(context).pop();
}
},
color: isSingleCurrency ? Theme.of(context).colorScheme.onSurfaceVariant : null,),
],),
footer:
isSingleCurrency ? LongPressFooter(text: S.of(context).cannot_remove_last_asset) : null,
child: GestureDetector(
onTap: () async {
unawaited(HapticFeedback.mediumImpact());
final res = await showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => ChartModal(
currency: currency,
isFavorite: favorite,
),);
if (res != null && res is bool && res) {
context.read<ChartsBloc>().add(CurrencyPinned(currency: currency));
}
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
width: 1, color: Theme.of(context).colorScheme.surfaceContainerHighest,),
gradient: LinearGradient(colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary,
], begin: Alignment.topCenter, end: Alignment.bottomCenter,),),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
spacing: 12,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 5,
children: [
if (changeData != null)
RotatedBox(
quarterTurns:
changeData!.direction == PriceChangeDirection.up ? 0 : 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/price_change_arrow.svg",
width: 8,
height: 8,
colorFilter: ColorFilter.mode(
changeData!.direction.color, BlendMode.srcIn,),
),
),
Text(
currency.title.toUpperCase(),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
],
),
Text(
currency.fullName ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,),
),
],
),
),
if (changeData != null)
ChangePill(
changePercentage: changeData!.percentage,
direction: changeData!.direction,),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8,
children: [
CakeImageWidget(
imageUrl: currency.iconPath,
width: 24,
height: 24,
),
Expanded(
child: FittedBox(
alignment: Alignment.centerRight,
fit: BoxFit.scaleDown,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
spacing: 4,
children: [
Text(
displayPrice,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
Text(
ticker,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
fontSize: 20,),
),
],
),
),
),
],
),
],
),
),
),
),
);
}
@@ -0,0 +1,85 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_data_sort_criteria.dart";
import "package:cake_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
import "package:cake_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart";
import "package:cake_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
class ChartsAssetGridHeader extends StatelessWidget {
const ChartsAssetGridHeader({
super.key,
});
@override
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
builder: (context, state) {
final bloc = context.read<ChartsBloc>();
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).followed_assets,
style:
TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12),
),
Row(
spacing: 8,
children: [
ModernButton.svg(
size: 36,
iconSize: 16,
svgPath: "assets/new-ui/add.svg",
onPressed: () => _showCurrencyPicker(context, bloc),
),
LongPressPopupBuilder(
showOnTap: true,
popup: LongPressMenu(
items: PriceDataSortCriterium.all.map((item) {
final isSelected =
state is ChartsStateWithData && state.sortCriterium == item;
return LongPressMenuItem(
label: item.name,
iconPath: item.iconPath,
color: isSelected ? Theme.of(context).colorScheme.primary : null,
onSelected: () {
bloc.add(SortingCriteriumChanged(newCriterium: item));
Navigator.of(context).pop();
},
);
}).toList(),
),
child: ModernButton.svg(
size: 36,
iconSize: 12,
svgPath: "assets/new-ui/sort.svg",
onPressed: () {},
),
),
],
),
],
);
},
);
void _showCurrencyPicker(BuildContext context, ChartsBloc bloc) {
CurrencyPickerSheet.show(
context: context,
args: CurrencyPickerArgs(
items: CryptoCurrency.all,
onSelected: (item) {
bloc.add(CurrencyAdded(currency: item));
},
showStablesHeader: false,
symbolResolver: (item) => item.symbol,
),
);
}
}
@@ -0,0 +1,23 @@
import 'package:cake_wallet/new-ui/model/charts/util/price_change_data.dart';
import 'package:cake_wallet/new-ui/widgets/charts_page/change_pill.dart';
import 'package:flutter/material.dart';
class ChangeDisplay extends StatelessWidget {
const ChangeDisplay({super.key, required this.changeData, required this.ticker});
final String ticker;
final PriceChangeData changeData;
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Text(
"${changeData.direction.symbol}${ticker} ${changeData.amount.toStringWithPrecision(fractionalDigits: 2)}",
style: TextStyle(fontSize: 16, color: changeData.direction.color),
),
ChangePill(changePercentage: changeData.percentage, direction: changeData.direction)
],
);
}
@@ -0,0 +1,21 @@
import "package:cake_wallet/new-ui/model/charts/util/price_change_direction.dart";
import "package:flutter/material.dart";
class ChangePill extends StatelessWidget {
const ChangePill({super.key, required this.changePercentage, required this.direction});
final String changePercentage;
final PriceChangeDirection direction;
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
color: direction.color.withAlpha(52), borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.only(top: 2.5, bottom: 2.5, left: 4, right: 8),
child: Text(
"${direction.symbol} $changePercentage%",
style: TextStyle(color: direction.color),
),
));
}
@@ -0,0 +1,162 @@
import "package:cake_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/change_display.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/chart_view.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/coin_header.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/price_header.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/range_selector.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/amount/money_double.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_bloc/flutter_bloc.dart";
import "package:intl/intl.dart";
class ChartHeader extends StatefulWidget {
const ChartHeader({
super.key,
required this.currency,
required this.chartHeight,
required this.chartPadding,
required this.centered,
required this.favorite,
});
final CryptoCurrency currency;
final double chartHeight;
final double chartPadding;
final bool centered;
final bool favorite;
@override
State<ChartHeader> createState() => _ChartHeaderState();
}
class _ChartHeaderState extends State<ChartHeader> {
String? _viewedPrice;
String? _viewedTime;
@override
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
builder: (context, state) {
if (state case final ChartsStateWithData s) {
return Column(
crossAxisAlignment:
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
spacing: 10,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
crossAxisAlignment:
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
spacing: 10,
children: [
if (!widget.favorite)
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 60,
height: 60,
),
ChartViewCoinHeader(currency: widget.currency, isFavorite: widget.favorite),
ChartViewPriceHeader(
price: _viewedPrice ?? s.priceFor(widget.currency)?.toStringWithPrecision(fractionalDigits: 2) ?? "...",
ticker: s.fiatTicker,
highlight: _viewedPrice != null,
),
Column(
crossAxisAlignment:
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
children: [
Container(
// has to be constant-size, otherwise will jump around when loading
height: 36,
alignment: widget.centered ? Alignment.center : Alignment.centerLeft,
child: (s is ChartsLoaded)
? (_viewedPrice != null && _viewedTime != null)
? Text(
_viewedTime!,
style: TextStyle(
fontFamily: "IBM Plex Mono",
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 16,
),
)
: ChangeDisplay(
changeData: s.changeDataFor(widget.currency),
ticker: s.fiatTicker,
)
: const SizedBox.shrink(),
),
if (s is ChartsLoaded)
Padding(
padding: EdgeInsets.symmetric(vertical: widget.chartPadding),
child: PriceChart(
height: widget.chartHeight,
prices: s.dataFor(widget.currency),
direction: s.changeDataFor(widget.currency).direction,
touchCallback: (event, response) {
if (!event.isInterestedForInteractions) {
setState(() {
_viewedPrice = null;
_viewedTime = null;
});
return;
}
final newPrice =
response?.lineBarSpots?.firstOrNull?.y.toStringAsFixed(2);
final newTime = DateFormat("M/d/y HH:mm").format(
DateTime.fromMillisecondsSinceEpoch(
response?.lineBarSpots?.firstOrNull?.x.toInt() ?? 0,
),
);
if (_viewedPrice != newPrice) {
HapticFeedback.selectionClick();
setState(() {
_viewedPrice = newPrice;
});
}
if (_viewedTime != newTime) {
setState(() {
_viewedTime = newTime;
});
}
},
),
)
else
SizedBox(
height: widget.chartHeight + widget.chartPadding * 2,
child: const Center(child: CupertinoActivityIndicator()),
),
],
),
],
),
),
Container(
width: double.infinity,
height: 1,
color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(128),
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ChartRangeSelector(
selectedRange: s.range,
onRangeSelected: (range) =>
context.read<ChartsBloc>().add(RangeChanged(newRange: range)),
),
],
),
],
);
}
return const CupertinoActivityIndicator();
},
);
}
@@ -0,0 +1,157 @@
import "package:cake_wallet/di.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
import "package:cake_wallet/new-ui/pages/swap_page.dart";
import "package:cake_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
import "package:cake_wallet/new-ui/widgets/charts_page/chart_header.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/buy/buy_sell_page.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class ChartModal extends StatelessWidget {
const ChartModal({required this.currency, required this.isFavorite, super.key});
final CryptoCurrency currency;
final bool isFavorite;
@override
Widget build(BuildContext context) => BlocProvider(
create: (context) => getIt.get<ChartsBloc>()..add(Init()),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: "",
onLeadingPressed: Navigator.of(context).pop,
leadingIcon: const Icon(Icons.close),
padding: const EdgeInsets.only(top: 18, left: 18),
),
ChartHeader(
currency: currency,
chartHeight: 220,
chartPadding: 32,
centered: true,
favorite: false,
),
const SizedBox(
height: 48,
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: MediaQuery.of(context).size.width * 0.05,
children: [
if (canOpenBuySell) ...[
CoinActionButton(
icon: CakeImageWidget(
width: 36,
height: 36,
imageUrl: "assets/new-ui/plus.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
label: S.of(context).buy,
action: () => openBuySellPage(context, BuySellPageMode.buy),
),
CoinActionButton(
icon: CakeImageWidget(
width: 36,
height: 36,
imageUrl: "assets/new-ui/sell.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
label: S.of(context).sell,
action: () => openBuySellPage(context, BuySellPageMode.sell),
),
],
CoinActionButton(
icon: CakeImageWidget(
imageUrl: "assets/new-ui/exchange.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
label: S.of(context).swap,
action: () {
final page = getIt.get<NewSwapPage>(param2: currency);
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => Material(
child: ModalNavigator(
rootPage: page,
parentContext: context,
),
),
);
},
),
CoinActionButton(
icon: CakeImageWidget(
width: 36,
height: 36,
imageUrl: "assets/new-ui/favorite.svg",
colorFilter: ColorFilter.mode(
isFavorite
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
gradientColors:
isFavorite ? [const Color(0xFFDF2626), const Color(0xFF980F0F)] : null,
label: S.of(context).favorite,
action: () {
Navigator.of(context).pop(true);
},
),
],
),
const SizedBox(
height: 32,
),
],
),
),
),
);
void openBuySellPage(BuildContext context, BuySellPageMode mode) {
Navigator.of(context).pop();
final page = getIt.get<NewBuySellAmountPage>(
param1: NewBuySellParams(mode: mode, initialCurrency: currency));
showModalBottomSheet(
useSafeArea: true,
isScrollControlled: true,
context: navigatorKey.currentContext!,
builder: (modalContext) => ModalNavigator(
rootPage: page,
parentContext: modalContext,
),
);
}
bool get canOpenBuySell => getIt.get<AppStore>().wallet!.balance.containsKey(currency);
}
@@ -0,0 +1,70 @@
import "package:cake_wallet/new-ui/model/charts/price_data.dart";
import "package:cake_wallet/new-ui/model/charts/util/price_change_direction.dart";
import "package:fl_chart/fl_chart.dart";
import "package:flutter/material.dart";
class PriceChart extends StatelessWidget {
const PriceChart({
super.key,
required this.height,
required this.prices,
required this.direction,
required this.touchCallback,
});
final List<PriceData> prices;
final double height;
final PriceChangeDirection direction;
final Function(FlTouchEvent, LineTouchResponse?) touchCallback;
@override
Widget build(BuildContext context) {
final chartPoints = prices.map((entry) {
final x = entry.time.millisecondsSinceEpoch.toDouble();
final y = double.parse(entry.quote.toString());
return FlSpot(x, y);
}).toList();
return SizedBox(
height: height,
child: LineChart(
LineChartData(
gridData: const FlGridData(show: false),
titlesData: const FlTitlesData(show: false),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: chartPoints,
gradient: LinearGradient(colors: [direction.color.withAlpha(25), direction.color]),
barWidth: 1.5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
),
],
lineTouchData: LineTouchData(
enabled: true,
getTouchLineStart: (barData, spotIndex) => -double.infinity,
getTouchLineEnd: (barData, spotIndex) => double.infinity,
getTouchedSpotIndicator: (barData, spotIndexes) => spotIndexes
.map(
(index) => TouchedSpotIndicatorData(
FlLine(
strokeWidth: 1,
color: direction.color.withAlpha(80),
dashArray: [4, 4],
),
const FlDotData(),
),
)
.toList(),
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (touchedSpots) => touchedSpots.map((spot) => null).toList(),
),
touchCallback: touchCallback,
),
),
),
);
}
}
@@ -0,0 +1,67 @@
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/material.dart";
class ChartViewCoinHeader extends StatelessWidget {
const ChartViewCoinHeader({required this.currency, required this.isFavorite, super.key});
final CryptoCurrency currency;
final bool isFavorite;
@override
Widget build(BuildContext context) => Row(
mainAxisAlignment: isFavorite ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [
if (isFavorite)
CakeImageWidget(
imageUrl: currency.iconPath ?? "",
width: 30,
height: 30,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 5,
children: [
Text(
currency.fullName ?? currency.title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
currency.title,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
],
),
if (isFavorite)
CakeImageWidget(
imageUrl: "assets/new-ui/favorite.svg",
width: 16,
height: 16,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
);
}
@@ -0,0 +1,43 @@
import "package:flutter/material.dart";
class ChartViewPriceHeader extends StatelessWidget {
const ChartViewPriceHeader({
required this.price,
required this.ticker,
required this.highlight,
super.key,
});
final String price;
final String ticker;
final bool highlight;
@override
Widget build(BuildContext context) => FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Row(
spacing: 6,
children: [
Text(
price,
style: TextStyle(
fontSize: 36,
color: highlight
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
Text(
ticker,
style: TextStyle(
fontSize: 36,
color: highlight
? Theme.of(context).colorScheme.primary.withAlpha(128)
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
@@ -0,0 +1,72 @@
import "package:cake_wallet/new-ui/model/charts/util/chart_range.dart";
import "package:flutter/material.dart";
class ChartRangeSelector extends StatelessWidget {
const ChartRangeSelector({required this.selectedRange, required this.onRangeSelected, super.key});
final ChartRange selectedRange;
final Function(ChartRange) onRangeSelected;
static const double optionSize = 36;
static const double optionPadding = 24;
static const Duration switchDuration = Duration(milliseconds: 250);
double pillPosition(int selectedIndex) => selectedIndex * (optionSize + optionPadding);
@override
Widget build(BuildContext context) {
final selectedIndex = ChartRange.ranges.indexOf(selectedRange);
return Stack(
children: [
AnimatedPositioned(
curve: Curves.easeOutCubic,
left: pillPosition(selectedIndex),
duration: switchDuration,
child: Container(
height: optionSize,
width: optionSize,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface.withAlpha(25),
borderRadius: BorderRadius.circular(999999),
),
),
),
Row(
children: ChartRange.ranges.map((item) {
final selected = selectedRange == item;
final isFirst = ChartRange.ranges.indexOf(item) == 0;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (!selected) {
onRangeSelected(item);
}
},
child: Padding(
padding: EdgeInsets.only(
right: optionPadding / 2,
left: isFirst ? 0 : optionPadding / 2,
),
child: SizedBox(
width: optionSize,
height: optionSize,
child: AnimatedDefaultTextStyle(
duration: switchDuration,
style: TextStyle(
fontWeight: selected ? FontWeight.w400 : FontWeight.w500,
color: selected
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
),
child: Center(child: Text(item.displayText)),
),
),
),
);
}).toList(),
),
],
);
}
}
@@ -1,20 +1,22 @@
import 'dart:math';
import "dart:math";
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "package:cake_wallet/themes/core/theme_extension.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class CoinActionButton extends StatelessWidget {
const CoinActionButton({
super.key,
required this.icon,
required this.label,
required this.action,
super.key,
this.gradientColors,
});
final Widget icon;
final String label;
final VoidCallback action;
final List<Color>? gradientColors;
static const sizeFactor = 0.16;
@@ -37,10 +39,11 @@ class CoinActionButton extends StatelessWidget {
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary
],
colors: gradientColors ??
[
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
@@ -61,7 +64,7 @@ class CoinActionButton extends StatelessWidget {
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
padding: const EdgeInsets.only(top: 8),
child: ExcludeSemantics(
child: Text(
style: Theme.of(context).textTheme.bodySmall?.copyWith(
@@ -118,6 +118,7 @@ class CurrencyPickerArgs {
const CurrencyPickerArgs({
this.selected,
required this.items,
this.showStablesHeader = true,
this.balanceByAsset,
this.filterByNetwork,
required this.onSelected,
@@ -129,6 +130,7 @@ class CurrencyPickerArgs {
final CryptoCurrency? selected;
final List<CryptoCurrency> items;
final WalletType? filterByNetwork;
final bool showStablesHeader;
final void Function(CryptoCurrency) onSelected;
final String Function(CryptoCurrency) symbolResolver;
final Map<CryptoCurrency, CurrencyPickerBalance>? balanceByAsset;
@@ -166,6 +166,7 @@ class _MultiNetworkCurrencyPickerState extends State<MultiNetworkCurrencyPicker>
natives: _natives,
selected: widget.args.selected,
symbolResolver: widget.args.symbolResolver,
showStablesHeader: widget.args.showStablesHeader,
onSelect: _selectCurrency,
onStablecoinTap: _onStablecoinPillTapped,
),
@@ -190,10 +191,12 @@ class _MultiNetworkPickerBody extends StatefulWidget {
required this.symbolResolver,
required this.onSelect,
required this.onStablecoinTap,
required this.showStablesHeader,
});
final bool isSearching;
final bool recentsLoaded;
final bool showStablesHeader;
final CryptoCurrency? selected;
final List<CryptoCurrency> items;
final List<CryptoCurrency> recents;
@@ -363,7 +366,7 @@ class _MultiNetworkPickerBodyState extends State<_MultiNetworkPickerBody> {
onTap: onSelect,
),
),
if (stablecoins.isNotEmpty)
if (stablecoins.isNotEmpty && widget.showStablesHeader)
_PickerSection(
title: S.of(context).picker_section_stablecoins,
child: PillGrid(
-108
View File
@@ -1,108 +0,0 @@
import 'dart:ui';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class LongPressMenuItem {
final String label;
final String iconPath;
final VoidCallback onSelected;
final Color? color;
LongPressMenuItem(
{required this.label, required this.iconPath, required this.onSelected, this.color});
}
class LongPressMenu extends StatefulWidget {
const LongPressMenu({super.key, required this.items});
final List<LongPressMenuItem> items;
@override
State<LongPressMenu> createState() => _LongPressMenuState();
}
class _LongPressMenuState extends State<LongPressMenu> {
bool _isVisible = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isVisible = true;
});
});
}
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
alignment: Alignment.topCenter,
child: SizedBox(
height: _isVisible ? null : 0,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.primary.withAlpha(60),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.items.map((item) {
final color = item.color ?? Theme.of(context).colorScheme.onSurface;
return Material(
color: Colors.transparent,
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
onTap: item.onSelected,
child: Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12,
),
child: Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 8,
children: [
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: item.iconPath,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
),
),
Text(item.label,
style: TextStyle(
color: color, fontSize: 14, fontWeight: FontWeight.w500)),
],
),
),
),
),
),
),
);
}).toList(),
),
),
),
),
),
);
}
}
@@ -0,0 +1,42 @@
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
class LongPressFooter extends StatelessWidget {
const LongPressFooter({required this.text, super.key});
final String text;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.all(12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.primary.withAlpha(60),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/info.svg",
width: 18,
height: 18,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
Text(
text,
style: TextStyle(
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
),
),
);
}
@@ -0,0 +1,112 @@
import "dart:ui";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
class LongPressMenuItem {
LongPressMenuItem({
required this.label,
required this.iconPath,
required this.onSelected,
this.color,
});
final String label;
final String iconPath;
final VoidCallback onSelected;
final Color? color;
}
class LongPressMenu extends StatefulWidget {
const LongPressMenu({required this.items, super.key});
final List<LongPressMenuItem> items;
@override
State<LongPressMenu> createState() => _LongPressMenuState();
}
class _LongPressMenuState extends State<LongPressMenu> {
bool _isVisible = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isVisible = true;
});
});
}
@override
Widget build(BuildContext context) => AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
alignment: Alignment.topCenter,
child: SizedBox(
height: _isVisible ? null : 0,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.primary.withAlpha(60),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.items.map((item) {
final color = item.color ?? Theme.of(context).colorScheme.onSurface;
return Material(
color: Colors.transparent,
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
onTap: item.onSelected,
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 8,
children: [
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: item.iconPath,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
),
),
Text(
item.label,
style: TextStyle(
color: color,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
),
);
}).toList(),
),
),
),
),
),
);
}
@@ -0,0 +1,80 @@
import "dart:ui";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:flutter/material.dart";
class LongPressPopupBuilder extends StatelessWidget {
const LongPressPopupBuilder({
required this.child,
required this.popup,
super.key,
this.spacing = 8,
this.showOnTap = false,
this.footer,
});
final Widget child;
final Widget popup;
final Widget? footer;
final double spacing;
final bool showOnTap;
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () => _showMenu(context),
onTap: showOnTap ? () => _showMenu(context) : null,
child: IgnorePointer(ignoring: showOnTap, child: child),
);
void _showMenu(BuildContext context) {
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
showPopUp(
context: context,
builder: (context) => _buildPopup(context, offset, size),
);
}
Widget _buildPopup(BuildContext context, Offset offset, Size size) {
final screenWidth = MediaQuery.of(context).size.width;
final bool isOnRightHalf = offset.dx + (size.width / 2) > screenWidth / 2;
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy,
width: size.width,
height: size.height,
child: Material(
color: Colors.transparent,
child: child,
),
),
Positioned(
left: isOnRightHalf ? null : offset.dx,
right: isOnRightHalf ? (screenWidth - (offset.dx + size.width)) : null,
top: offset.dy + size.height + spacing,
child: Material(
color: Colors.transparent,
child: popup,
),
),
if (footer != null)
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).viewPadding.bottom,
child: footer!,
),
],
),
);
}
}
+1 -1
View File
@@ -663,7 +663,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.buySellPage:
return handleRouteWithPlatformAwareness(
(context) => Material(child: getIt.get<NewBuySellAmountPage>(param1: BuySellPageMode.buy)),
(context) => Material(child: getIt.get<NewBuySellAmountPage>(param1: NewBuySellParams(mode: BuySellPageMode.buy))),
);
@@ -51,7 +51,7 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
static const iconColorChangeDuration = Duration(milliseconds: 200);
static const pillTextStyle = TextStyle(
fontSize: 14,
fontSize: 16,
fontWeight: FontWeight.w500,
);
+7 -1
View File
@@ -12,6 +12,7 @@ import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/app_store.dart';
@@ -30,9 +31,10 @@ enum BuySellPageMode { buy, sell }
class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
BuySellViewModelBase(AppStore appStore, {required this.mode, required this.fiatConversionStore})
BuySellViewModelBase(AppStore appStore, {required NewBuySellParams params, required this.fiatConversionStore})
: _cryptoAmount = '',
fiatAmount = '',
mode = params.mode,
cryptoCurrencyAddress = '',
isCryptoCurrencyAddressEnabled = false,
cryptoCurrencies = <CryptoCurrency>[],
@@ -58,6 +60,10 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
_initialize();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
if(params.initialCurrency != null) {
changeCryptoCurrency(currency: params.initialCurrency!);
}
}
late Timer bestRateSync;