From b88fbf32ae47cfe894703738d49fd35b263d7cc3 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Tue, 22 Sep 2026 20:32:38 +0200 Subject: [PATCH] charts (#3162) * 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 * adjust buy/sell di * replace -> ignore * reformat --------- Co-authored-by: Omar Hatem --- cw_core/lib/amount/money.dart | 3 + cw_core/lib/crypto_currency.dart | 2 + cw_core/lib/currency.dart | 1 + cw_core/lib/db/sqlite.dart | 128 ++++++---- cw_core/lib/erc20_token.dart | 3 + cw_core/lib/spl_token.dart | 3 + cw_core/lib/tron_token.dart | 3 + lib/di.dart | 22 +- lib/entities/default_settings_migration.dart | 11 + lib/entities/fiat_currency.dart | 3 + lib/entities/main_actions.dart | 2 +- lib/entities/new_main_actions.dart | 48 ++-- lib/main.dart | 2 +- lib/new-ui/long_press_popup.dart | 58 ----- lib/new-ui/model/charts/charts_asset.dart | 33 +++ .../model/charts/datetime_extension.dart | 6 + lib/new-ui/model/charts/price_api_client.dart | 78 ++++++ lib/new-ui/model/charts/price_data.dart | 104 ++++++++ lib/new-ui/model/charts/price_store.dart | 133 +++++++++++ lib/new-ui/model/charts/util/chart_range.dart | 21 ++ .../model/charts/util/price_change_data.dart | 19 ++ .../charts/util/price_change_direction.dart | 11 + .../charts/util/price_data_sort_criteria.dart | 141 +++++++++++ lib/new-ui/new_dashboard.dart | 4 +- lib/new-ui/page_open_listener.dart | 2 + lib/new-ui/pages/addresses_page.dart | 4 +- .../pages/buy_sell/buy_sell_amount_page.dart | 7 + lib/new-ui/pages/charts_page.dart | 103 ++++++++ lib/new-ui/viewmodels/charts/charts_bloc.dart | 173 ++++++++++++++ .../viewmodels/charts/charts_event.dart | 42 ++++ .../viewmodels/charts/charts_state.dart | 132 +++++++++++ .../buy_sell/buy_sell_selector_modal.dart | 3 +- .../widgets/charts_page/asset_grid.dart | 222 ++++++++++++++++++ .../charts_page/asset_grid_header.dart | 85 +++++++ .../widgets/charts_page/change_display.dart | 23 ++ .../widgets/charts_page/change_pill.dart | 21 ++ .../widgets/charts_page/chart_header.dart | 162 +++++++++++++ .../widgets/charts_page/chart_modal.dart | 157 +++++++++++++ .../widgets/charts_page/chart_view.dart | 70 ++++++ .../widgets/charts_page/coin_header.dart | 67 ++++++ .../widgets/charts_page/price_header.dart | 43 ++++ .../widgets/charts_page/range_selector.dart | 72 ++++++ .../action_row/coin_action_button.dart | 23 +- .../currency_picker/currency_picker_args.dart | 2 + .../multi_network_currency_picker.dart | 5 +- lib/new-ui/widgets/long_press_menu.dart | 108 --------- .../long_press_menu/long_press_footer.dart | 42 ++++ .../long_press_menu/long_press_menu.dart | 112 +++++++++ .../long_press_menu/long_press_popup.dart | 80 +++++++ lib/router.dart | 2 +- lib/src/screens/buy/buy_sell_page.dart | 0 .../widgets/new_main_navbar_widget.dart | 2 +- lib/view_model/buy/buy_sell_view_model.dart | 8 +- pubspec_base.yaml | 3 +- res/pictures/add.svg | 3 + res/pictures/buy.svg | 10 + res/pictures/charts_sort_criteria/alpha.svg | 1 + res/pictures/charts_sort_criteria/gains.svg | 1 + res/pictures/charts_sort_criteria/losses.svg | 1 + .../charts_sort_criteria/marketcap.svg | 1 + res/pictures/favorite.svg | 3 + res/pictures/info.svg | 8 +- res/pictures/price_change_arrow.svg | 3 + res/pictures/sort.svg | 3 + res/values/strings_ar.arb | 5 + res/values/strings_bg.arb | 5 + res/values/strings_cs.arb | 5 + res/values/strings_de.arb | 11 +- res/values/strings_en.arb | 9 + res/values/strings_es.arb | 5 + res/values/strings_fa.arb | 5 + res/values/strings_fr.arb | 5 + res/values/strings_gn.arb | 10 + res/values/strings_ha.arb | 5 + res/values/strings_hi.arb | 5 + res/values/strings_hr.arb | 5 + res/values/strings_hy.arb | 5 + res/values/strings_id.arb | 5 + res/values/strings_it.arb | 5 + res/values/strings_ja.arb | 5 + res/values/strings_ko.arb | 5 + res/values/strings_my.arb | 5 + res/values/strings_nl.arb | 5 + res/values/strings_pl.arb | 6 + res/values/strings_pt.arb | 5 + res/values/strings_ru.arb | 5 + res/values/strings_th.arb | 5 + res/values/strings_tl.arb | 5 + res/values/strings_tr.arb | 5 + res/values/strings_uk.arb | 5 + res/values/strings_ur.arb | 5 + res/values/strings_vi.arb | 5 + res/values/strings_yo.arb | 5 + res/values/strings_zh.arb | 5 + 94 files changed, 2544 insertions(+), 270 deletions(-) delete mode 100644 lib/new-ui/long_press_popup.dart create mode 100644 lib/new-ui/model/charts/charts_asset.dart create mode 100644 lib/new-ui/model/charts/datetime_extension.dart create mode 100644 lib/new-ui/model/charts/price_api_client.dart create mode 100644 lib/new-ui/model/charts/price_data.dart create mode 100644 lib/new-ui/model/charts/price_store.dart create mode 100644 lib/new-ui/model/charts/util/chart_range.dart create mode 100644 lib/new-ui/model/charts/util/price_change_data.dart create mode 100644 lib/new-ui/model/charts/util/price_change_direction.dart create mode 100644 lib/new-ui/model/charts/util/price_data_sort_criteria.dart create mode 100644 lib/new-ui/pages/charts_page.dart create mode 100644 lib/new-ui/viewmodels/charts/charts_bloc.dart create mode 100644 lib/new-ui/viewmodels/charts/charts_event.dart create mode 100644 lib/new-ui/viewmodels/charts/charts_state.dart create mode 100644 lib/new-ui/widgets/charts_page/asset_grid.dart create mode 100644 lib/new-ui/widgets/charts_page/asset_grid_header.dart create mode 100644 lib/new-ui/widgets/charts_page/change_display.dart create mode 100644 lib/new-ui/widgets/charts_page/change_pill.dart create mode 100644 lib/new-ui/widgets/charts_page/chart_header.dart create mode 100644 lib/new-ui/widgets/charts_page/chart_modal.dart create mode 100644 lib/new-ui/widgets/charts_page/chart_view.dart create mode 100644 lib/new-ui/widgets/charts_page/coin_header.dart create mode 100644 lib/new-ui/widgets/charts_page/price_header.dart create mode 100644 lib/new-ui/widgets/charts_page/range_selector.dart delete mode 100644 lib/new-ui/widgets/long_press_menu.dart create mode 100644 lib/new-ui/widgets/long_press_menu/long_press_footer.dart create mode 100644 lib/new-ui/widgets/long_press_menu/long_press_menu.dart create mode 100644 lib/new-ui/widgets/long_press_menu/long_press_popup.dart create mode 100644 lib/src/screens/buy/buy_sell_page.dart create mode 100644 res/pictures/add.svg create mode 100644 res/pictures/buy.svg create mode 100644 res/pictures/charts_sort_criteria/alpha.svg create mode 100644 res/pictures/charts_sort_criteria/gains.svg create mode 100644 res/pictures/charts_sort_criteria/losses.svg create mode 100644 res/pictures/charts_sort_criteria/marketcap.svg create mode 100644 res/pictures/favorite.svg create mode 100644 res/pictures/price_change_arrow.svg create mode 100644 res/pictures/sort.svg diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart index d4496c8e6..a4c258b54 100644 --- a/cw_core/lib/amount/money.dart +++ b/cw_core/lib/amount/money.dart @@ -310,4 +310,7 @@ class Money implements Comparable { } return currency.symbol; } + + /// new [Money] with absolute value of this instance + Money abs() => copyWith(amount: amount.abs()); } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 4954cad27..09ead9151 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -44,6 +44,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen @override String get symbol => title; + @override + String get apiString => "crypto.$title"; set enabled(bool value) => this.enabled = value; diff --git a/cw_core/lib/currency.dart b/cw_core/lib/currency.dart index 75fa854b7..1e84d42e3 100644 --- a/cw_core/lib/currency.dart +++ b/cw_core/lib/currency.dart @@ -20,4 +20,5 @@ abstract class Currency { @override int get hashCode => decimals.hashCode ^ symbol.hashCode ^ tag.hashCode; + String get apiString; } diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart index 07297449b..eb3784983 100644 --- a/cw_core/lib/db/sqlite.dart +++ b/cw_core/lib/db/sqlite.dart @@ -1,11 +1,11 @@ -import 'dart:io'; +import "dart:io"; -import 'package:cw_core/db/sqlite_debug.dart'; -import 'package:cw_core/root_dir.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:flutter/foundation.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:path/path.dart' as p; +import "package:cw_core/db/sqlite_debug.dart"; +import "package:cw_core/root_dir.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:flutter/foundation.dart"; +import "package:path/path.dart" as p; +import "package:sqflite_common_ffi/sqflite_ffi.dart"; Database? db; @@ -16,11 +16,11 @@ Future _addColumnIfNotExists( required String definition, }) async { final result = await db.rawQuery("PRAGMA table_info($table)"); - final columnExists = result.any((row) => row['name'] == column); + final columnExists = result.any((row) => row["name"] == column); if (!columnExists) { await db.execute( - 'ALTER TABLE $table ADD COLUMN $column $definition;', + "ALTER TABLE $table ADD COLUMN $column $definition;", ); } } @@ -63,11 +63,13 @@ Future _initDb({String? pathOverride}) async { } } await db?.close(); - db = await openDatabase(dbFile.path, version: 12, - onUpgrade: (Database db, int oldVersion, int newVersion) async { - printV("migrating: $oldVersion, $newVersion"); - if (oldVersion <= 1) { - await db.execute(''' + db = await openDatabase( + dbFile.path, + version: 13, + onUpgrade: (db, oldVersion, newVersion) async { + printV("migrating: $oldVersion, $newVersion"); + if (oldVersion <= 1) { + await db.execute(""" DELETE FROM WalletInfo WHERE walletInfoId NOT IN ( SELECT MIN(walletInfoId) @@ -77,10 +79,10 @@ WHERE walletInfoId NOT IN ( CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique ON WalletInfo (id); -'''); - } - if (oldVersion <= 2) { - await db.execute(''' +"""); + } + if (oldVersion <= 2) { + await db.execute(''' CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings ( walletInfoId INTEGER, accountIndex INTEGER DEFAULT -1, @@ -91,12 +93,12 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings ( FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) ); '''); - await _addColumnIfNotExists( - db, - table: 'WalletInfo', - column: 'receiveInfoboxDismissed', - definition: 'BOOLEAN DEFAULT FALSE', - ); + await _addColumnIfNotExists( + db, + table: "WalletInfo", + column: "receiveInfoboxDismissed", + definition: "BOOLEAN DEFAULT FALSE", + ); await _addColumnIfNotExists( db, @@ -164,6 +166,9 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings ( definition: "BOOLEAN DEFAULT FALSE", ); } + if(oldVersion <= 12) { + await _createChartsTables(db); + } }, onCreate: (Database db, int version) async { await db.execute(''' CREATE TABLE WalletInfo ( @@ -195,7 +200,7 @@ CREATE TABLE WalletInfo ( ); '''); - await db.execute(''' + await db.execute(""" CREATE TABLE WalletInfoDerivationInfo ( walletInfoDerivationInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, address TEXT NOT NULL, @@ -206,9 +211,9 @@ CREATE TABLE WalletInfoDerivationInfo ( scriptType TEXT, description TEXT ); -'''); +"""); - await db.execute(''' + await db.execute(''' CREATE TABLE WalletInfoAddress ( walletInfoAddressId INTEGER PRIMARY KEY AUTOINCREMENT, walletInfoId INTEGER, @@ -218,7 +223,7 @@ CREATE TABLE WalletInfoAddress ( ); '''); - await db.execute(''' + await db.execute(""" CREATE TABLE WalletInfoAddressInfo ( walletInfoAddressInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, walletInfoId INTEGER NOT NULL, @@ -228,9 +233,9 @@ CREATE TABLE WalletInfoAddressInfo ( mapValueLabel TEXT NOT NULL, CONSTRAINT WalletInfoAddressInfo_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) ); -'''); +"""); - await db.execute(''' + await db.execute(''' CREATE TABLE "WalletInfoAddressMap" ( walletInfoAddressMapId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, walletInfoId INTEGER NOT NULL, @@ -239,11 +244,11 @@ CREATE TABLE "WalletInfoAddressMap" ( CONSTRAINT WalletInfoAddress_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) ); '''); - await db.execute(''' + await db.execute(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique ON WalletInfo (id); -'''); - await db.execute(''' +"""); + await db.execute(''' CREATE TABLE BalanceCardStyleSettings ( walletInfoId INTEGER, accountIndex INTEGER DEFAULT -1, @@ -257,18 +262,39 @@ CREATE TABLE BalanceCardStyleSettings ( FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) ); '''); - await _createBridgeTransferTable(db); - await _createNodeTable(db); - await _createTradeTable(db); - await _createErc20TokenTable(db); - await _createSplTokenTable(db); - await _createTronTokenTable(db); - await _createImportedNFTTable(db); - }); + await _createBridgeTransferTable(db); + await _createNodeTable(db); + await _createTradeTable(db); + await _createChartsTables(db); + await _createErc20TokenTable(db); + await _createSplTokenTable(db); + await _createTronTokenTable(db); + await _createImportedNFTTable(db); + + }, + ); +} + +Future _createChartsTables(Database db) async { + await db.execute(""" +CREATE TABLE PriceData ( + fromCurrency TEXT NOT NULL, + toCurrency TEXT NOT NULL, + timestamp INTEGER NOT NULL, + price TEXT NOT NULL, + PRIMARY KEY (fromCurrency, toCurrency, timestamp) +); +"""); + await db.execute(""" +CREATE TABLE ChartsAssets ( + asset TEXT PRIMARY KEY, + isFavorite BOOLEAN DEFAULT FALSE +); + """); } Future _createTradeTable(Database db) async { - await db.execute(''' + await db.execute(""" CREATE TABLE IF NOT EXISTS Trade ( tradeId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL, @@ -323,11 +349,11 @@ CREATE TABLE IF NOT EXISTS Trade ( chainId INTEGER, fee REAL ); -'''); - await db.execute(''' +"""); + await db.execute(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_trade_id_unique ON Trade (id); -'''); +"""); } Future> dumpDb() async { @@ -343,7 +369,7 @@ Future> dumpDb() async { Future> _getTableNames(Database db) async { final tableNames = await db.rawQuery('SELECT name FROM sqlite_master WHERE type = "table"'); - return tableNames.map((e) => (e["name"]).toString()).toList(); + return tableNames.map((e) => e["name"].toString()).toList(); } Future> _dumpDb() async { @@ -366,7 +392,7 @@ Future> dumpCustomDb(String path) async { } Future _createBridgeTransferTable(Database db) async { - await db.execute(''' + await db.execute(""" CREATE TABLE IF NOT EXISTS BridgeTransfer ( id TEXT NOT NULL PRIMARY KEY, wallet_id TEXT NOT NULL, @@ -385,11 +411,11 @@ CREATE TABLE IF NOT EXISTS BridgeTransfer ( error_message TEXT, status_message TEXT ); -'''); - await db.execute(''' +"""); + await db.execute(""" CREATE INDEX IF NOT EXISTS idx_bridgetransfer_wallet_id ON BridgeTransfer(wallet_id); -'''); +"""); } Future _createErc20TokenTable(Database db) async { @@ -478,7 +504,7 @@ ON TronToken (walletName, contractAddress); } Future _createNodeTable(Database db) async { - db.execute(""" + await db.execute(""" CREATE TABLE Node ( NodeId INTEGER PRIMARY KEY, uri TEXT NOT NULL, diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart index a0c14173c..69c278e7b 100644 --- a/cw_core/lib/erc20_token.dart +++ b/cw_core/lib/erc20_token.dart @@ -187,4 +187,7 @@ class Erc20Token extends CryptoCurrency { @override int get hashCode => contractAddress.hashCode; + + @override + String get apiString => "evm.$contractAddress"; } diff --git a/cw_core/lib/spl_token.dart b/cw_core/lib/spl_token.dart index 140b50bb4..2c2285b67 100644 --- a/cw_core/lib/spl_token.dart +++ b/cw_core/lib/spl_token.dart @@ -197,4 +197,7 @@ class SPLToken extends CryptoCurrency { @override int get hashCode => mintAddress.hashCode; + + @override + String get apiString => "sol.$mintAddress"; } diff --git a/cw_core/lib/tron_token.dart b/cw_core/lib/tron_token.dart index 2e1b7ed1d..c2220d00d 100644 --- a/cw_core/lib/tron_token.dart +++ b/cw_core/lib/tron_token.dart @@ -175,4 +175,7 @@ class TronToken extends CryptoCurrency { @override int get hashCode => contractAddress.hashCode; + + @override + String get apiString => "trx.$contractAddress"; } diff --git a/lib/di.dart b/lib/di.dart index 34c9b2336..649b19a00 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -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 setup({ (displayMode == BitcoinAmountDisplayMode.satoshiForLightning && lightningMode))); }); + getIt.registerLazySingleton(() => PriceStore()); + + getIt.registerFactory( + () => ChartsBloc(appStore: getIt.get(), priceStore: getIt.get())); + + getIt.registerFactory(() => ChartsPage( + chartsBloc: getIt.get(), + )); + getIt.registerFactory(() => AccountCreationModal( accountEditOrCreateViewModel: getIt.get())); @@ -1321,11 +1335,11 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); - getIt.registerFactoryParam((mode, _) => BuySellViewModel( - mode: mode, getIt.get(), fiatConversionStore: getIt.get())); + getIt.registerFactoryParam((params, _) => BuySellViewModel( + params: params, getIt.get(), fiatConversionStore: getIt.get())); - getIt.registerFactoryParam((mode, _) => - NewBuySellAmountPage(buySellViewModel: getIt.get(param1: mode))); + getIt.registerFactoryParam((params, _) => + NewBuySellAmountPage(buySellViewModel: getIt.get(param1: params))); getIt.registerFactoryParam((order, _) { diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 6a304e946..5b919ccd5 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -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 defaultSettingsMigration( enabled: false, ); break; + case 72: + await createDefaultChartsData(); + break; default: break; } @@ -1338,6 +1343,12 @@ Future _addXaut0TokenToExistingSolanaWallets() async { } } +Future 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 _addTbbTokenToExistingSolanaWallets() async { try { final tbbToken = SPLToken( diff --git a/lib/entities/fiat_currency.dart b/lib/entities/fiat_currency.dart index 61d9ea4e0..56a836bf3 100644 --- a/lib/entities/fiat_currency.dart +++ b/lib/entities/fiat_currency.dart @@ -19,6 +19,9 @@ class FiatCurrency extends EnumerableItem with Serializable impl @override final int decimals; + @override + String get apiString => "fiat.$title"; + @override final String symbol; diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 743dbfaea..59246da9b 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -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); }, ); } diff --git a/lib/entities/new_main_actions.dart b/lib/entities/new_main_actions.dart index 519b914ff..94511b95a 100644 --- a/lib/entities/new_main_actions.dart +++ b/lib/entities/new_main_actions.dart @@ -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 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: () {}, + ); } diff --git a/lib/main.dart b/lib/main.dart index 53268a304..fb9c3665b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -309,7 +309,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 71, + initialMigrationVersion: 72, ); } diff --git a/lib/new-ui/long_press_popup.dart b/lib/new-ui/long_press_popup.dart deleted file mode 100644 index 08ae861dd..000000000 --- a/lib/new-ui/long_press_popup.dart +++ /dev/null @@ -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, - ), - ), - ], - ), - ); - } -} diff --git a/lib/new-ui/model/charts/charts_asset.dart b/lib/new-ui/model/charts/charts_asset.dart new file mode 100644 index 000000000..0edb26d4c --- /dev/null +++ b/lib/new-ui/model/charts/charts_asset.dart @@ -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 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 toJson() => {"asset": asset.apiString, "isFavorite": isFavorite ? 1 : 0}; + + static Future> get() async { + final json = await db!.query(tableName); + return List.generate(json.length, (index) => ChartsAsset.fromJson(json[index])); + } + + Future insert() async { + await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace); + } + + Future remove() async { + await db!.delete(tableName, where: "asset = ?", whereArgs: [asset.apiString]); + } +} diff --git a/lib/new-ui/model/charts/datetime_extension.dart b/lib/new-ui/model/charts/datetime_extension.dart new file mode 100644 index 000000000..f781a9154 --- /dev/null +++ b/lib/new-ui/model/charts/datetime_extension.dart @@ -0,0 +1,6 @@ +extension DateTimeX on DateTime { + int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000; + + static DateTime fromSecondsSinceEpoch(int seconds) => + DateTime.fromMillisecondsSinceEpoch(seconds * 1000); +} diff --git a/lib/new-ui/model/charts/price_api_client.dart b/lib/new-ui/model/charts/price_api_client.dart new file mode 100644 index 000000000..9afe64a61 --- /dev/null +++ b/lib/new-ui/model/charts/price_api_client.dart @@ -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?> _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?; + } catch (e) { + printV("failed to decode response for ${uri.host}/${uri.path}: $e"); + return null; + } + } + + static Future> getPrices(PriceRequest request) async { + final List ret = []; + printV(request.uri); + final data = await _getJson(request.uri); + if (data == null) { + return []; + } + final results = data["results"] as Map?; + 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; + } +} diff --git a/lib/new-ui/model/charts/price_data.dart b/lib/new-ui/model/charts/price_data.dart new file mode 100644 index 000000000..547dd1588 --- /dev/null +++ b/lib/new-ui/model/charts/price_data.dart @@ -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 { + const PriceData({required this.time, required this.base, required this.quote,}); + + factory PriceData.fromJson(Map 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 toJson() => { + "timestamp": time.secondsSinceEpoch.toString(), + "price": quote.toString(), + "fromCurrency": base.apiString, + "toCurrency": quote.currency.apiString, + }; + + static Future> 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 insert() async { + await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace); + } + + static Future insertMany(Iterable 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); +} diff --git a/lib/new-ui/model/charts/price_store.dart b/lib/new-ui/model/charts/price_store.dart new file mode 100644 index 000000000..b251f9482 --- /dev/null +++ b/lib/new-ui/model/charts/price_store.dart @@ -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> get( + DateTime start, + DateTime end, + Currency from, + Currency to, + Duration interval, + ); +} + +mixin UpdatablePriceSource { + Future update(Iterable newData); +} + +class DatabasePriceSource extends PriceSource with UpdatablePriceSource { + const DatabasePriceSource(); + + @override + Future> get( + DateTime start, + DateTime end, + Currency from, + Currency to, + Duration interval, + ) => + PriceData.get(from, to, start, end); + + @override + Future update(Iterable newData) async { + await PriceData.insertMany(newData); + } +} + +class ApiPriceSource extends PriceSource { + const ApiPriceSource(); + + @override + Future> 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> getPrices(Currency from, Currency to, ChartRange range) async { + final Set 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 _alignedData( + DateTime start, + DateTime end, + Iterable 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 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); + } +} diff --git a/lib/new-ui/model/charts/util/chart_range.dart b/lib/new-ui/model/charts/util/chart_range.dart new file mode 100644 index 000000000..096f6409a --- /dev/null +++ b/lib/new-ui/model/charts/util/chart_range.dart @@ -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]; +} diff --git a/lib/new-ui/model/charts/util/price_change_data.dart b/lib/new-ui/model/charts/util/price_change_data.dart new file mode 100644 index 000000000..0dd411fd9 --- /dev/null +++ b/lib/new-ui/model/charts/util/price_change_data.dart @@ -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 { + 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); + } +} diff --git a/lib/new-ui/model/charts/util/price_change_direction.dart b/lib/new-ui/model/charts/util/price_change_direction.dart new file mode 100644 index 000000000..5481f4255 --- /dev/null +++ b/lib/new-ui/model/charts/util/price_change_direction.dart @@ -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), "-"); +} diff --git a/lib/new-ui/model/charts/util/price_data_sort_criteria.dart b/lib/new-ui/model/charts/util/price_data_sort_criteria.dart new file mode 100644 index 000000000..419091ec0 --- /dev/null +++ b/lib/new-ui/model/charts/util/price_data_sort_criteria.dart @@ -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 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); +} diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart index e18e6187c..898d00829 100644 --- a/lib/new-ui/new_dashboard.dart +++ b/lib/new-ui/new_dashboard.dart @@ -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(), getIt.get(), getIt.get(), - Placeholder(), + getIt.get() ]; @override diff --git a/lib/new-ui/page_open_listener.dart b/lib/new-ui/page_open_listener.dart index 7914869ee..e8d2a4d89 100644 --- a/lib/new-ui/page_open_listener.dart +++ b/lib/new-ui/page_open_listener.dart @@ -1,3 +1,5 @@ + + abstract interface class PageOpenListener { void onPageOpen(); } diff --git a/lib/new-ui/pages/addresses_page.dart b/lib/new-ui/pages/addresses_page.dart index 8944f3feb..568cc1c90 100644 --- a/lib/new-ui/pages/addresses_page.dart +++ b/lib/new-ui/pages/addresses_page.dart @@ -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'; diff --git a/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart index 951e24674..b7c2d5f7d 100644 --- a/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart +++ b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.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}); diff --git a/lib/new-ui/pages/charts_page.dart b/lib/new-ui/pages/charts_page.dart new file mode 100644 index 000000000..3502ed2fd --- /dev/null +++ b/lib/new-ui/pages/charts_page.dart @@ -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( + 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, + ), + ], + ), + ), + ), + ], + ), + ); + }, + ), + ); +} diff --git a/lib/new-ui/viewmodels/charts/charts_bloc.dart b/lib/new-ui/viewmodels/charts/charts_bloc.dart new file mode 100644 index 000000000..d748c9de9 --- /dev/null +++ b/lib/new-ui/viewmodels/charts/charts_bloc.dart @@ -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 { + ChartsBloc({required this.priceStore, required this.appStore}) : super(const ChartsInitial()) { + on(_onRangeChanged, transformer: sequential()); + on(_onSortingCriteriumChanged); + on(_onCurrencyAdded, transformer: sequential()); + on(_onCurrencyRemoved, transformer: sequential()); + on(_onCurrencyPinned, transformer: sequential()); + on(_onPageRefreshed, transformer: sequential()); + on(_onPageLoadStarted, transformer: restartable()); + on(_init); + } + + final PriceStore priceStore; + final AppStore appStore; + + Future _init(Init event, Emitter 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 _onPageLoadStarted(PageLoadStarted event, Emitter emit) async { + if (state case final ChartsStateWithData s) { + final Map> 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 _onRangeChanged( + RangeChanged event, + Emitter emit, + ) async { + if (state case final ChartsStateWithData s) { + emit(s.toLoading().copyWith(range: event.newRange)); + add(PageLoadStarted()); + } + } + + Future _onSortingCriteriumChanged( + SortingCriteriumChanged event, + Emitter emit, + ) async { + if (state case final ChartsStateWithData s) { + emit(s.copyWith(sortCriterium: event.newCriterium)); + } + } + + Future _onCurrencyAdded( + CurrencyAdded event, + Emitter 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 _onCurrencyRemoved( + CurrencyRemoved event, + Emitter 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 _onCurrencyPinned( + CurrencyPinned event, + Emitter 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 _onPageRefreshed( + PageRefreshed event, + Emitter emit, + ) async { + if (state case final ChartsStateWithData s) { + emit(s.toLoading()); + add(PageLoadStarted()); + } + } +} diff --git a/lib/new-ui/viewmodels/charts/charts_event.dart b/lib/new-ui/viewmodels/charts/charts_event.dart new file mode 100644 index 000000000..ef04ba795 --- /dev/null +++ b/lib/new-ui/viewmodels/charts/charts_event.dart @@ -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 {} diff --git a/lib/new-ui/viewmodels/charts/charts_state.dart b/lib/new-ui/viewmodels/charts/charts_state.dart new file mode 100644 index 000000000..18496288a --- /dev/null +++ b/lib/new-ui/viewmodels/charts/charts_state.dart @@ -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 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 currencies; + + @override + ChartsLoading copyWith({ + CryptoCurrency? pinnedCurrency, + List? 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> prices, + required super.range, + required super.sortCriterium, + }) : _prices = prices; + final Map> _prices; + + @override + String get fiatTicker => _prices[_prices.keys.first]?.firstOrNull?.base.name ?? ""; + + @override + List 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 dataFor(CryptoCurrency curr) => _prices[curr] ?? []; + + @override + ChartsLoaded copyWith({ + CryptoCurrency? pinnedCurrency, + Map>? prices, + ChartRange? range, + PriceDataSortCriterium? sortCriterium, + }) => + ChartsLoaded( + pinnedCurrency: pinnedCurrency ?? this.pinnedCurrency, + prices: prices ?? _prices, + range: range ?? this.range, + sortCriterium: sortCriterium ?? this.sortCriterium, + ); +} diff --git a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart index ed1073398..a1a59cee6 100644 --- a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -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(param1: mode); + final page = getIt.get(param1: NewBuySellParams(mode: mode)); showModalBottomSheet( useSafeArea: true, isScrollControlled: true, diff --git a/lib/new-ui/widgets/charts_page/asset_grid.dart b/lib/new-ui/widgets/charts_page/asset_grid.dart new file mode 100644 index 000000000..3c7500fa4 --- /dev/null +++ b/lib/new-ui/widgets/charts_page/asset_grid.dart @@ -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( + 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().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().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().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,), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); +} diff --git a/lib/new-ui/widgets/charts_page/asset_grid_header.dart b/lib/new-ui/widgets/charts_page/asset_grid_header.dart new file mode 100644 index 000000000..a757b9e8b --- /dev/null +++ b/lib/new-ui/widgets/charts_page/asset_grid_header.dart @@ -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( + builder: (context, state) { + final bloc = context.read(); + + 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, + ), + ); + } +} diff --git a/lib/new-ui/widgets/charts_page/change_display.dart b/lib/new-ui/widgets/charts_page/change_display.dart new file mode 100644 index 000000000..b69405289 --- /dev/null +++ b/lib/new-ui/widgets/charts_page/change_display.dart @@ -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) + ], + ); +} diff --git a/lib/new-ui/widgets/charts_page/change_pill.dart b/lib/new-ui/widgets/charts_page/change_pill.dart new file mode 100644 index 000000000..e79a83ef6 --- /dev/null +++ b/lib/new-ui/widgets/charts_page/change_pill.dart @@ -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), + ), + )); +} diff --git a/lib/new-ui/widgets/charts_page/chart_header.dart b/lib/new-ui/widgets/charts_page/chart_header.dart new file mode 100644 index 000000000..6a21571e1 --- /dev/null +++ b/lib/new-ui/widgets/charts_page/chart_header.dart @@ -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 createState() => _ChartHeaderState(); +} + +class _ChartHeaderState extends State { + String? _viewedPrice; + String? _viewedTime; + + @override + Widget build(BuildContext context) => BlocBuilder( + 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().add(RangeChanged(newRange: range)), + ), + ], + ), + ], + ); + } + return const CupertinoActivityIndicator(); + }, + ); +} diff --git a/lib/new-ui/widgets/charts_page/chart_modal.dart b/lib/new-ui/widgets/charts_page/chart_modal.dart new file mode 100644 index 000000000..f8f46a581 --- /dev/null +++ b/lib/new-ui/widgets/charts_page/chart_modal.dart @@ -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()..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(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( + 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().wallet!.balance.containsKey(currency); +} diff --git a/lib/new-ui/widgets/charts_page/chart_view.dart b/lib/new-ui/widgets/charts_page/chart_view.dart new file mode 100644 index 000000000..fa1b92b88 --- /dev/null +++ b/lib/new-ui/widgets/charts_page/chart_view.dart @@ -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 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, + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/charts_page/coin_header.dart b/lib/new-ui/widgets/charts_page/coin_header.dart new file mode 100644 index 000000000..484cbaffd --- /dev/null +++ b/lib/new-ui/widgets/charts_page/coin_header.dart @@ -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), + ), + ], + ); +} diff --git a/lib/new-ui/widgets/charts_page/price_header.dart b/lib/new-ui/widgets/charts_page/price_header.dart new file mode 100644 index 000000000..56b4b6f7f --- /dev/null +++ b/lib/new-ui/widgets/charts_page/price_header.dart @@ -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, + ), + ), + ], + ), + ); +} diff --git a/lib/new-ui/widgets/charts_page/range_selector.dart b/lib/new-ui/widgets/charts_page/range_selector.dart new file mode 100644 index 000000000..be68cdffc --- /dev/null +++ b/lib/new-ui/widgets/charts_page/range_selector.dart @@ -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(), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart index cb2ff1015..d65aa33b8 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -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? 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( diff --git a/lib/new-ui/widgets/currency_picker/currency_picker_args.dart b/lib/new-ui/widgets/currency_picker/currency_picker_args.dart index 13a06523a..b49578691 100644 --- a/lib/new-ui/widgets/currency_picker/currency_picker_args.dart +++ b/lib/new-ui/widgets/currency_picker/currency_picker_args.dart @@ -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 items; final WalletType? filterByNetwork; + final bool showStablesHeader; final void Function(CryptoCurrency) onSelected; final String Function(CryptoCurrency) symbolResolver; final Map? balanceByAsset; diff --git a/lib/new-ui/widgets/currency_picker/multi_network_currency_picker.dart b/lib/new-ui/widgets/currency_picker/multi_network_currency_picker.dart index ffea261d2..aa6316d33 100644 --- a/lib/new-ui/widgets/currency_picker/multi_network_currency_picker.dart +++ b/lib/new-ui/widgets/currency_picker/multi_network_currency_picker.dart @@ -166,6 +166,7 @@ class _MultiNetworkCurrencyPickerState extends State 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 items; final List 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( diff --git a/lib/new-ui/widgets/long_press_menu.dart b/lib/new-ui/widgets/long_press_menu.dart deleted file mode 100644 index 6a97f0fdb..000000000 --- a/lib/new-ui/widgets/long_press_menu.dart +++ /dev/null @@ -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 items; - - @override - State createState() => _LongPressMenuState(); -} - -class _LongPressMenuState extends State { - 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(), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/new-ui/widgets/long_press_menu/long_press_footer.dart b/lib/new-ui/widgets/long_press_menu/long_press_footer.dart new file mode 100644 index 000000000..4064294df --- /dev/null +++ b/lib/new-ui/widgets/long_press_menu/long_press_footer.dart @@ -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), + ), + ], + ), + ), + ), + ); +} diff --git a/lib/new-ui/widgets/long_press_menu/long_press_menu.dart b/lib/new-ui/widgets/long_press_menu/long_press_menu.dart new file mode 100644 index 000000000..a145df0cc --- /dev/null +++ b/lib/new-ui/widgets/long_press_menu/long_press_menu.dart @@ -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 items; + + @override + State createState() => _LongPressMenuState(); +} + +class _LongPressMenuState extends State { + 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(), + ), + ), + ), + ), + ), + ); +} diff --git a/lib/new-ui/widgets/long_press_menu/long_press_popup.dart b/lib/new-ui/widgets/long_press_menu/long_press_popup.dart new file mode 100644 index 000000000..5bcccd96f --- /dev/null +++ b/lib/new-ui/widgets/long_press_menu/long_press_popup.dart @@ -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!, + ), + ], + ), + ); + } +} diff --git a/lib/router.dart b/lib/router.dart index 8f6325ca8..e35406b64 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -663,7 +663,7 @@ Route createRoute(RouteSettings settings) { case Routes.buySellPage: return handleRouteWithPlatformAwareness( - (context) => Material(child: getIt.get(param1: BuySellPageMode.buy)), + (context) => Material(child: getIt.get(param1: NewBuySellParams(mode: BuySellPageMode.buy))), ); diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 45883ae68..a1b3b6697 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -51,7 +51,7 @@ class _NEWNewMainNavBarState extends State { static const iconColorChangeDuration = Duration(milliseconds: 200); static const pillTextStyle = TextStyle( - fontSize: 14, + fontSize: 16, fontWeight: FontWeight.w500, ); diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 72cf87cab..978d48f71 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -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 = [], @@ -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; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index f020e5ee1..4c2a9dfde 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -166,7 +166,7 @@ dependencies: torch_dart: path: ./scripts/torch_dart quick_actions: ^1.1.0 - fl_chart: ^0.70.2 + fl_chart: ^1.2.0 dev_dependencies: flutter_test: @@ -313,6 +313,7 @@ flutter: - assets/new-ui/node_speed_badges/ - assets/new-ui/address_sources/ - assets/new-ui/buy_payment_methods/ + - assets/new-ui/charts_sort_criteria/ fonts: - family: Wix Madefor Text diff --git a/res/pictures/add.svg b/res/pictures/add.svg new file mode 100644 index 000000000..d52d63a16 --- /dev/null +++ b/res/pictures/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/buy.svg b/res/pictures/buy.svg new file mode 100644 index 000000000..dee24065f --- /dev/null +++ b/res/pictures/buy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/pictures/charts_sort_criteria/alpha.svg b/res/pictures/charts_sort_criteria/alpha.svg new file mode 100644 index 000000000..78bc7f441 --- /dev/null +++ b/res/pictures/charts_sort_criteria/alpha.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/pictures/charts_sort_criteria/gains.svg b/res/pictures/charts_sort_criteria/gains.svg new file mode 100644 index 000000000..0511e4d22 --- /dev/null +++ b/res/pictures/charts_sort_criteria/gains.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/pictures/charts_sort_criteria/losses.svg b/res/pictures/charts_sort_criteria/losses.svg new file mode 100644 index 000000000..09918e8c0 --- /dev/null +++ b/res/pictures/charts_sort_criteria/losses.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/pictures/charts_sort_criteria/marketcap.svg b/res/pictures/charts_sort_criteria/marketcap.svg new file mode 100644 index 000000000..8969f176c --- /dev/null +++ b/res/pictures/charts_sort_criteria/marketcap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/pictures/favorite.svg b/res/pictures/favorite.svg new file mode 100644 index 000000000..ea3697b59 --- /dev/null +++ b/res/pictures/favorite.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/info.svg b/res/pictures/info.svg index 945d67e52..c4978742d 100644 --- a/res/pictures/info.svg +++ b/res/pictures/info.svg @@ -1,3 +1,7 @@ - - + + + + + + diff --git a/res/pictures/price_change_arrow.svg b/res/pictures/price_change_arrow.svg new file mode 100644 index 000000000..0ef09fe67 --- /dev/null +++ b/res/pictures/price_change_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/sort.svg b/res/pictures/sort.svg new file mode 100644 index 000000000..f197cf01d --- /dev/null +++ b/res/pictures/sort.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 40897e66f..c79d6bedd 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -205,6 +205,7 @@ "change_username": "تغيير اسم المستخدم", "change_wallet_alert_content": "هل تريد تغيير المحفظة الحالية إلى ${wallet_name}؟", "change_wallet_alert_title": "تغيير المحفظة الحالية", + "charts": "الرسوم البيانية", "check_receiver_forms": "يرجى التحقق من نماذج المستلم", "choose_a_payment_method": "اختر طريقة دفع", "choose_a_provider": "اختر مزوّد خدمة", @@ -237,6 +238,8 @@ "color_theme": "سمة الألوان", "coming_soon_tag": "قريبًا", "commit_transaction_amount_fee": "تأكيد المعاملة\nالمبلغ: ${amount}\nالرسوم: ${fee}", + "compatible_services": "الخدمات المتوافقة", + "compatible_services_desc": "استخدم أداة المسح لقراءة الأنواع التالية من رموز QR المدعومة في Cake Wallet:", "compatible_networks": "الشبكات المتوافقة", "compatible_services": "الخدمات المتوافقة", "compatible_services_desc": "استخدم أداة المسح لقراءة الأنواع التالية من رموز QR المدعومة في Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "هذا الزوج الثابت غير مدعوم مع خدمات المبادلة المحددة", "fixed_rate": "سعر ثابت", "fixed_rate_alert": "ستتمكن من إدخال مبلغ الاستلام عند تفعيل وضع السعر الثابت. هل تريد التبديل إلى وضع السعر الثابت؟", + "followed_assets": "الأصول المتبعه", "forgot_password": "نسيت كلمة المرور", "freeze": "تجميد", "frequently_asked_questions": "الأسئلة الشائعة", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "جميع أحدث النماذج (GPT-5.3، Claude 4.6).\nبدون اشتراك، ادفع بالعملات المشفرة.", "narrow": "ضيق", "network": "الشبكة", + "networks": "الشبكات", "network_address_detected": "تم اكتشاف عنوان ${network}.", "network_prefix_on": "على", "network_switch_failed": "فشل التبديل إلى الشبكة المطلوبة. لم يتم تطبيق الدفع، يرجى المحاولة مرة أخرى.", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 510ce143b..eb9c596fa 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -205,6 +205,7 @@ "change_username": "Промяна на потребителското име", "change_wallet_alert_content": "Искате ли да смените текущия портфейл с ${wallet_name}?", "change_wallet_alert_title": "Смяна на текущия портфейл", + "charts": "Графики", "check_receiver_forms": "Моля, проверете полетата за получателя", "choose_a_payment_method": "Изберете метод на плащане", "choose_a_provider": "Изберете доставчик", @@ -237,6 +238,8 @@ "color_theme": "Цветова тема", "coming_soon_tag": "Очаквайте скоро", "commit_transaction_amount_fee": "Потвърдете транзакцията\nСума: ${amount}\nТакса: ${fee}", + "compatible_services": "Съвместими услуги", + "compatible_services_desc": "Използвайте инструмента за сканиране, за да четете следните типове QR кодове, поддържани в Cake Wallet:", "compatible_networks": "Съвместими мрежи", "compatible_services": "Съвместими услуги", "compatible_services_desc": "Използвайте инструмента за сканиране, за да четете следните типове QR кодове, поддържани в Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Тази фиксирана двойка не се поддържа от избраните услуги за обмен", "fixed_rate": "Фиксиран курс", "fixed_rate_alert": "Ще можете да въведете сума за получаване, когато е отметнат режимът с фиксиран курс. Искате ли да превключите към режим с фиксиран курс?", + "followed_assets": "Следени активи", "forgot_password": "Забравена парола", "freeze": "Замразяване", "frequently_asked_questions": "Често задавани въпроси", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Всички най-нови модели (GPT-5.3, Claude 4.6).\\nБез абонамент, плащайте с крипто.", "narrow": "Тесен", "network": "Мрежа", + "networks": "Мрежи", "network_address_detected": "Открит адрес на ${network}", "network_prefix_on": "на", "network_switch_failed": "Неуспешно превключване към заявената мрежа. Плащането не е приложено, моля опитайте отново.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 842518cb7..922c90f18 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -205,6 +205,7 @@ "change_username": "Změnit uživatelské jméno", "change_wallet_alert_content": "Chcete změnit aktuální peněženku na ${wallet_name}?", "change_wallet_alert_title": "Změnit aktuální peněženku", + "charts": "Grafy", "check_receiver_forms": "Zkontrolujte prosím údaje příjemce", "choose_a_payment_method": "Vyberte způsob platby", "choose_a_provider": "Vyberte poskytovatele", @@ -237,6 +238,8 @@ "color_theme": "Barevné téma", "coming_soon_tag": "Již brzy", "commit_transaction_amount_fee": "Potvrdit transakci\nČástka: ${amount}\nPoplatek: ${fee}", + "compatible_services": "Kompatibilní služby", + "compatible_services_desc": "Pomocí nástroje pro skenování načtěte následující typy QR kódů podporované v Cake Wallet:", "compatible_networks": "Kompatibilní sítě", "compatible_services": "Kompatibilní služby", "compatible_services_desc": "Pomocí nástroje pro skenování načtěte následující typy QR kódů podporované v Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Tento pevný pár není podporován u vybraných swapovacích služeb", "fixed_rate": "Fixní kurz", "fixed_rate_alert": "Když je zaškrtnutý režim pevného kurzu, budete moci zadat částku k přijetí. Chcete přepnout do režimu pevného kurzu?", + "followed_assets": "Sledovaná aktiva", "forgot_password": "Zapomenuté heslo", "freeze": "Zmrazit", "frequently_asked_questions": "Často kladené otázky", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Všechny nejnovější modely (GPT-5.3, Claude 4.6).\nBez předplatného, plaťte kryptem.", "narrow": "Úzké", "network": "Síť", + "networks": "Sítě", "network_address_detected": "Zjištěna adresa ${network}", "network_prefix_on": "na", "network_switch_failed": "Přepnutí na požadovanou síť se nezdařilo. Platba nebyla připsána, zkuste to prosím znovu.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 321dfe0c1..8c74c404c 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -206,6 +206,7 @@ "change_username": "Benutzernamen ändern", "change_wallet_alert_content": "Möchten Sie zur Wallet ${wallet_name} wechseln?", "change_wallet_alert_title": "Aktuelles Wallet ändern", + "charts": "Diagramme", "check_receiver_forms": "Bitte überprüfen Sie die Empfängerangaben", "choose_a_payment_method": "Wählen Sie eine Zahlungsmethode", "choose_a_provider": "Anbieter auswählen", @@ -238,6 +239,8 @@ "color_theme": "Farbschema", "coming_soon_tag": "Demnächst verfügbar", "commit_transaction_amount_fee": "Transaktion bestätigen\nBetrag: ${amount}\nGebühr: ${fee}", + "compatible_services": "Kompatible Dienste", + "compatible_services_desc": "Verwenden Sie das Scan-Tool, um die folgenden von Cake Wallet unterstützten QR-Code-Typen zu lesen:", "compatible_networks": "Kompatible Netzwerke", "compatible_services": "Kompatible Dienste", "compatible_services_desc": "Verwenden Sie das Scan-Tool, um die folgenden von Cake Wallet unterstützten QR-Code-Typen zu lesen:", @@ -528,6 +531,7 @@ "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Swap-Diensten nicht unterstützt", "fixed_rate": "Fester Kurs", "fixed_rate_alert": "Sie können den Empfangsbetrag eingeben, wenn der Festkursmodus aktiviert ist. Möchten Sie zum Festkursmodus wechseln?", + "followed_assets": "Gefolgte Vermögenswerte", "forgot_password": "Passwort vergessen", "freeze": "Sperren", "frequently_asked_questions": "Häufig gestellte Fragen", @@ -711,6 +715,7 @@ "nanogpt_subtitle": "Alle neuesten Modelle (GPT-5.3, Claude 4.6).\nKein Abonnement – bezahlen Sie mit Krypto.", "narrow": "Schmal", "network": "Netzwerk", + "networks": "Netzwerke", "network_address_detected": "${network}-Adresse erkannt", "network_prefix_on": "auf", "network_switch_failed": "Der Wechsel zum angeforderten Netzwerk ist fehlgeschlagen. Die Zahlung wurde nicht ausgeführt. Bitte versuchen Sie es erneut.", @@ -798,6 +803,7 @@ "pairing_error": "Kopplungsfehler", "pairingInvalidEvent": "Ungültiges Pairing-Ereignis", "passphrase": "Passphrase (Optional)", + "passphrase_view_keys": "Passphrase", "passphrase_entry": "Passphrase-Eingabe", "passphrase_raw": "Passphrase", "passphrase_view_keys": "Passphrase", @@ -847,6 +853,7 @@ "please_choose_one": "Bitte wählen Sie eine Option", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät angezeigt wird", "please_make_selection": "Bitte treffen Sie unten eine Auswahl, um Ihre Wallet zu erstellen oder wiederherzustellen.", + "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_reference_document": "Bitte beachten Sie die untenstehenden Dokumente für weitere Informationen.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie eine Sicherungsdatei aus und geben Sie das Sicherungskennwort ein.", @@ -1573,9 +1580,9 @@ "zcash_card_enable_later": "Sie können diese Karte jederzeit später in den Einstellungen aktivieren", "zcash_card_missing_funds": "Fehlendes Guthaben?", "zcash_card_scan": "Scannen", - "zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden.", "zcash_network_upgrade": "Zcash-Netzwerk-Upgrade", "zcash_network_upgrade_desc_1": "Das Zcash-Netzwerk wurde auf Ironwood aktualisiert. Damit Ihr Guthaben wieder ausgegeben werden kann, muss es migriert werden. Die Migration erfolgt automatisch und unter Wahrung Ihrer Privatsphäre. Sie beginnt, sobald Sie Ihre Zcash-Wallet öffnen.", "zcash_network_upgrade_desc_2": "Nach Abschluss der Migration wird Ihr verfügbares Guthaben wieder wie gewohnt angezeigt und Sie können Zcash wieder normal senden und empfangen.", - "zcash_network_upgrade_desc_3": "Bitte lassen Sie Ihre Zcash-Wallet vollständig synchronisieren und anschließend die App geöffnet, bis die Migration abgeschlossen ist. Dies kann je nach Guthaben einige Minuten dauern." + "zcash_network_upgrade_desc_3": "Bitte lassen Sie Ihre Zcash-Wallet vollständig synchronisieren und anschließend die App geöffnet, bis die Migration abgeschlossen ist. Dies kann je nach Guthaben einige Minuten dauern.", + "zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden." } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 8c5ea185f..06a380dbe 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -177,6 +177,9 @@ "camera_permission_is_required": "Camera permission is required. \nPlease enable it from app settings.", "cancel": "Cancel", "cannot_manage_accounts_during_sync": "You can't manage accounts while the wallet is still syncing. Please try again later.", + "cannot_remove_last_asset": "You can't remove this asset because it is your last.", + "cannot_verify": "Cannot Verify", + "cannot_verify_description": "This domain cannot be verified. Check the request carefully before approving.", "card_address": "Address:", "card_order_reset_desc": "Restore card order to default settings?", "card_style": "Card style", @@ -206,6 +209,7 @@ "change_username": "Change Username", "change_wallet_alert_content": "Do you want to change current wallet to ${wallet_name}?", "change_wallet_alert_title": "Change current wallet", + "charts": "Charts", "check_receiver_forms": "Please, check receiver forms", "choose_a_payment_method": "Choose a payment method", "choose_a_provider": "Choose a provider", @@ -507,6 +511,7 @@ "failed_authentication": "Failed authentication. ${state_error}", "failed_to_load_payment_methods": "Failed to load payment methods", "faq": "FAQ", + "favorite": "Favorite", "favorite_token": "Favorite token", "favorite_token_desc": "The favorite token's balance will show on the balance card.", "features": "Features", @@ -528,6 +533,7 @@ "fixed_pair_not_supported": "This fixed pair is not supported with the selected swap services", "fixed_rate": "Fixed rate", "fixed_rate_alert": "You will be able to enter receive amount when fixed rate mode is checked. Do you want to switch to fixed rate mode?", + "followed_assets": "Followed assets", "forgot_password": "Forgot Password", "freeze": "Freeze", "frequently_asked_questions": "Frequently asked questions", @@ -535,6 +541,7 @@ "frozen": "Frozen", "frozen_balance": "Frozen Balance", "full_balance": "Full Balance", + "gains": "Gains", "gallery": "Gallery", "gas_exceeds_allowance": "Gas required by transaction exceeds allowance.", "gas_price": "Gas price", @@ -656,6 +663,7 @@ "long_press_hide_balance": "Long press card to hide balance", "long_press_show_balance": "Long press card to show balance", "long_press_to_copy": "Long press to copy", + "losses": "Losses", "low_fee": "Low fee", "low_fee_alert": "You currently are using a low network fee priority. This could cause long waits, different rates, or canceled trades. We recommend setting a higher fee for a better experience.", "low_kyc": "Low KYC", @@ -667,6 +675,7 @@ "manual_input": "Manual Input", "mark_as_redeemed": "Mark As Redeemed", "market_place": "Marketplace", + "marketcap": "Marketcap", "mask": "Mask", "matrix_green_dark_theme": "Matrix Green Dark Theme", "max": "Max", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index e1e22a7b7..d1d5ae934 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -205,6 +205,7 @@ "change_username": "Cambiar nombre de usuario", "change_wallet_alert_content": "¿Quieres cambiar la cartera actual a ${wallet_name}?", "change_wallet_alert_title": "Cambiar la billetera actual", + "charts": "Gráficos", "check_receiver_forms": "Por favor, revisa los campos del destinatario.", "choose_a_payment_method": "Elige un método de pago", "choose_a_provider": "Elige un proveedor", @@ -237,6 +238,8 @@ "color_theme": "Tema de color", "coming_soon_tag": "Próximamente", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nComisión: ${fee}", + "compatible_services": "Servicios compatibles", + "compatible_services_desc": "Usa la herramienta Escanear para leer los siguientes tipos de códigos QR compatibles con Cake Wallet:", "compatible_networks": "Redes compatibles", "compatible_services": "Servicios compatibles", "compatible_services_desc": "Usa la herramienta Escanear para leer los siguientes tipos de códigos QR compatibles con Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Este par fijo no es compatible con los servicios de swap seleccionados", "fixed_rate": "Tipo de cambio fijo", "fixed_rate_alert": "Podrás ingresar la cantidad a recibir cuando el modo de tipo de cambio fijo esté activado. ¿Quieres cambiar al modo de tipo de cambio fijo?", + "followed_assets": "Activos seguidos", "forgot_password": "Olvidé mi contraseña", "freeze": "Congelar", "frequently_asked_questions": "Preguntas frecuentes", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Todos los modelos más recientes (GPT-5.3, Claude 4.6).\nSin suscripción, paga con cripto.", "narrow": "Estrecho", "network": "Red", + "networks": "Redes", "network_address_detected": "${network} dirección detectada", "network_prefix_on": "en", "network_switch_failed": "No se pudo cambiar a la red solicitada. El pago no se aplicó, inténtelo nuevamente.", diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index 92b263e54..845556d2a 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -205,6 +205,7 @@ "change_username": "تغییر نام کاربری", "change_wallet_alert_content": "آیا می‌خواهید کیف پول فعلی را به ${wallet_name} تغییر دهید؟", "change_wallet_alert_title": "تغییر کیف پول فعلی", + "charts": "نمودارها", "check_receiver_forms": "لطفاً فرم‌های گیرنده را بررسی کنید", "choose_a_payment_method": "یک روش پرداخت را انتخاب کنید", "choose_a_provider": "یک ارائه‌دهنده را انتخاب کنید", @@ -237,6 +238,8 @@ "color_theme": "تم رنگ", "coming_soon_tag": "به‌زودی", "commit_transaction_amount_fee": "ثبت تراکنش\nمقدار: ${amount}\nکارمزد: ${fee}", + "compatible_services": "سرویس‌های سازگار", + "compatible_services_desc": "از ابزار اسکن برای خواندن انواع کدهای QR زیر که در Cake Wallet پشتیبانی می‌شوند استفاده کنید:", "compatible_networks": "شبکه های سازگار", "compatible_services": "سرویس‌های سازگار", "compatible_services_desc": "از ابزار اسکن برای خواندن انواع کدهای QR زیر که در Cake Wallet پشتیبانی می‌شوند استفاده کنید:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "این جفت‌ارز ثابت توسط سرویس‌های سواپ انتخاب‌شده پشتیبانی نمی‌شود", "fixed_rate": "نرخ ثابت", "fixed_rate_alert": "وقتی حالت نرخ ثابت فعال باشد، می‌توانید مقدار دریافتی را وارد کنید.\nآیا می‌خواهید به حالت نرخ ثابت تغییر دهید؟", + "followed_assets": "دارایی های دنبال شده", "forgot_password": "رمز عبور را فراموش کرده‌اید", "freeze": "فریز", "frequently_asked_questions": "سوالات متداول", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "همه جدیدترین مدل‌ها (GPT-5.3, Claude 4.6).\nبدون اشتراک، پرداخت با کریپتو.", "narrow": "باریک", "network": "شبکه", + "networks": "شبکه‌ها", "network_address_detected": "آدرس ${network} شناسایی شد", "network_prefix_on": "در", "network_switch_failed": "تغییر به شبکه درخواستی انجام نشد. پرداخت اعمال نشد، لطفاً دوباره امتحان کنید.", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 5e974e727..660e1213f 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -205,6 +205,7 @@ "change_username": "Changer le nom d’utilisateur", "change_wallet_alert_content": "Voulez-vous remplacer le portefeuille actuel par ${wallet_name} ?", "change_wallet_alert_title": "Changer le portefeuille actuel", + "charts": "Graphiques", "check_receiver_forms": "Veuillez vérifier les informations du destinataire", "choose_a_payment_method": "Choisissez un mode de paiement", "choose_a_provider": "Choisissez un fournisseur", @@ -237,6 +238,8 @@ "color_theme": "Thème de couleur", "coming_soon_tag": "Bientôt disponible", "commit_transaction_amount_fee": "Confirmer la transaction\nMontant : ${amount}\nFrais : ${fee}", + "compatible_services": "Services compatibles", + "compatible_services_desc": "Utilisez l'outil Scan pour lire les types de codes QR suivants pris en charge par Cake Wallet :", "compatible_networks": "Réseaux compatibles", "compatible_services": "Services compatibles", "compatible_services_desc": "Utilisez l'outil Scan pour lire les types de codes QR suivants pris en charge par Cake Wallet :", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge par les services d'échange sélectionnés", "fixed_rate": "Taux fixe", "fixed_rate_alert": "Vous pourrez saisir le montant à recevoir lorsque le mode taux fixe est activé. Souhaitez-vous passer en mode taux fixe ?", + "followed_assets": "Actifs suivis", "forgot_password": "Mot de passe oublié", "freeze": "Geler", "frequently_asked_questions": "Foire aux questions", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Tous les modèles les plus récents (GPT-5.3, Claude 4.6).\nPas d'abonnement, payez en crypto.", "narrow": "Étroit", "network": "Réseau", + "networks": "Réseaux", "network_address_detected": "Adresse ${network} détectée", "network_prefix_on": "sur", "network_switch_failed": "Échec du basculement vers le réseau demandé. Le paiement n'a pas été appliqué, veuillez réessayer.", diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 6f357d925..f82152a74 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -205,6 +205,7 @@ "change_username": "Emoambue puruhára réra", "change_wallet_alert_content": "¿Reipotápa emoambue ko'ág̃a pe billetera hag̃ua ${wallet_name}?", "change_wallet_alert_title": "Emoambue billetera ko'ágãgua", + "charts": "Gráfico-kuéra rehegua", "check_receiver_forms": "Ikatúpa, ehecha umi formulario receptor rehegua.", "choose_a_payment_method": "Eiporavo peteĩ jehepyme'ẽrã", "choose_a_provider": "Eiporavo peteĩ me'ẽha", @@ -237,6 +238,8 @@ "color_theme": "Téma sa'y", "coming_soon_tag": "Og̃uahẽ pototáma", "commit_transaction_amount_fee": "Emoneĩ ñembohasa\nMboheta: ${amount}\nHepyme’ẽ: ${fee}", + "compatible_services": "Servicio-kuéra Ojokupytýva", + "compatible_services_desc": "Eipuru pe Scan tembiporu emoñe'ẽ hag̃ua ko'ã QR código Cake Wallet oipytyvõva:", "compatible_networks": "Redes Ojogueraháva", "compatible_services": "Servicio-kuéra Ojokupytýva", "compatible_services_desc": "Eipuru pe Scan tembiporu emoñe'ẽ hag̃ua ko'ã QR código Cake Wallet oipytyvõva:", @@ -279,6 +282,7 @@ "contact_support": "Eñe’ẽ Support ndive", "contacts": "Contactos", "continue_text": "Eku'e", + "contract_address": "Contrato Kundaharape", "continue_to_swap": "Esegíkena eintercambia", "contract_address": "Contrato Kundaharape", "contract_warning": "Ko contrato dirección ojehechakuaa hag̃ua ikatuha haʼe peteĩ ñembotavy térã mba’e vai. Eñangareko porãke.", @@ -474,6 +478,7 @@ "etherscan_history": "Etherscan rembiasakue", "event": "Tembiaporã", "events": "Tembiaporã", + "evm_ecosystem_description": "Tekotevẽramo, emoambue pe red ha token reipuruséva ko jehepyme'ẽme g̃uarã.", "evm_address_detected": "EVM Dirección Ojejuhúva", "evm_ecosystem_description": "Tekotevẽramo, emoambue pe red ha token reipuruséva ko jehepyme'ẽme g̃uarã.", "exchange": "Mbojopyru", @@ -528,6 +533,7 @@ "fixed_pair_not_supported": "Ko mokõi par fijo ndojehepyme’ẽi umi swap rembiapo ojeiporavo va’ekuépe.", "fixed_rate": "Tasa fija", "fixed_rate_alert": "Ikatu hag̃aite emoinge hag̃ua pe monto rehechaukáva rehecha hag̃ua reiporavóramo pe modo tasa fija. ¿Reipota piko remoambue pe modo tasa fija-pe?", + "followed_assets": "Umi activo rapykuéri oúva", "forgot_password": "Nderesarái ñe’ẽñemi", "freeze": "Mbo’y", "frequently_asked_questions": "Porandu ha mbohovái ojeporuvéva", @@ -711,6 +717,7 @@ "nanogpt_subtitle": "Opaite umi modelo ipyahuvéva (GPT-5.3, Claude 4.6).\nNdaipóri suscripción, ehepyme’ẽ cripto rupive.", "narrow": "Poʼi", "network": "Red", + "networks": "Red-kuéra", "network_address_detected": "${network} Dirección Ojejuhúva", "network_prefix_on": "pe", "network_switch_failed": "Ndojehechái oñembohasa hag̃ua red ojejerurévape. Pe pago ndojejapóikuri, eñeha’ã jey.", @@ -1234,6 +1241,8 @@ "support_title_other_links": "Ambue juaju pytyvõ rehegua", "supported": "Ojepytyvõva", "swap": "Swap", + "supported": "Ojepytyvõva", + "swap": "Swap", "swap_from_network": "Embohasa ${network} guive.", "swap_from_network_description": "Emondo hag̃ua viru ${targetNetwork} red-pe, tekotevẽta rembohasa ne ${currentNetwork} viru. ¿Reprocedese piko?", "swap_id": "ID ñemoambue rehegua", @@ -1248,6 +1257,7 @@ "swipe_to_send": "Emoinge hag̃ua emondo hag̃ua.", "switch_camera": "Ombohasa cámara", "switch_input_currency": "Oñemoambue cripto ha fiat jeike apytépe", + "switch_wallet": "Emoambue Billetera", "switch_to_x_wallet": "Embohasa ${network} Wallet-pe", "switch_wallet": "Emoambue Billetera", "switchToETHWallet": "Ikatúpa emoambue peteĩ billetera Ethereum-pe ha eha’ãjey.", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index aba32efa6..cba6fd6e7 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -205,6 +205,7 @@ "change_username": "Canja sunan mai amfani", "change_wallet_alert_content": "Kana so ka canza walat ɗin yanzu zuwa ${wallet_name}?", "change_wallet_alert_title": "Canja walat na yanzu", + "charts": "Charts", "check_receiver_forms": "Da fatan za a duba fom ɗin mai karɓa", "choose_a_payment_method": "Zaɓi hanyar biyan kuɗi", "choose_a_provider": "Zaɓi mai ba da sabis", @@ -237,6 +238,8 @@ "color_theme": "Jigon launi", "coming_soon_tag": "Zai zo nan ba da jimawa ba", "commit_transaction_amount_fee": "Tabbatar da ma'amala\nAdadi: ${amount}\nKuɗi: ${fee}", + "compatible_services": "Sabis masu jituwa", + "compatible_services_desc": "Yi amfani da kayan aikin Scan don karanta nau'ikan QR code masu zuwa da Cake Wallet ke goyon baya:", "compatible_networks": "Hanyoyin sadarwa masu jituwa", "compatible_services": "Sabis masu jituwa", "compatible_services_desc": "Yi amfani da kayan aikin Scan don karanta nau'ikan QR code masu zuwa da Cake Wallet ke goyon baya:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Ba a tallafa wa wannan tsayayyen nau'i ba tare da sabis ɗin musayar da aka zaɓa ba", "fixed_rate": "Kafaffen farashi", "fixed_rate_alert": "Za ka iya shigar da adadin da za a karɓa idan an zaɓi yanayin ƙayyadadden ƙima. Kana so ka canza zuwa yanayin ƙayyadadden ƙima?", + "followed_assets": "Kadarori masu biyo baya", "forgot_password": "An manta kalmar wucewa", "freeze": "Daskare", "frequently_asked_questions": "Tambayoyin da ake yawan yi", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Duk sabbin samfura (GPT-5.3, Claude 4.6).\nBabu biyan kuɗin rajista, biya da crypto.", "narrow": "Kunkuntar", "network": "Hanyar sadarwa", + "networks": "Hanyoyin sadarwa", "network_address_detected": "An gano adireshin ${network}", "network_prefix_on": "kan", "network_switch_failed": "An kasa canzawa zuwa cibiyar sadarwar da ake nema. Ba a yi amfani da kuɗin ba, da fatan za a sake gwadawa.", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 505e58437..0af5fb545 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -205,6 +205,7 @@ "change_username": "यूज़रनेम बदलें", "change_wallet_alert_content": "क्या आप वर्तमान वॉलेट को ${wallet_name} में बदलना चाहते हैं?", "change_wallet_alert_title": "वर्तमान वॉलेट बदलें", + "charts": "चार्ट", "check_receiver_forms": "कृपया, प्राप्तकर्ता के फ़ॉर्म जाँचें", "choose_a_payment_method": "भुगतान विधि चुनें", "choose_a_provider": "एक प्रदाता चुनें", @@ -237,6 +238,8 @@ "color_theme": "रंग थीम", "coming_soon_tag": "जल्द आ रहा है", "commit_transaction_amount_fee": "लेन-देन कमिट करें\nराशि: ${amount}\nशुल्क: ${fee}", + "compatible_services": "संगत सेवाएँ", + "compatible_services_desc": "Cake Wallet पर समर्थित निम्न प्रकार के QR कोड पढ़ने के लिए स्कैन टूल का उपयोग करें:", "compatible_networks": "संगत नेटवर्क", "compatible_services": "संगत सेवाएँ", "compatible_services_desc": "Cake Wallet पर समर्थित निम्न प्रकार के QR कोड पढ़ने के लिए स्कैन टूल का उपयोग करें:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित स्वैप सेवाओं के साथ समर्थित नहीं है", "fixed_rate": "स्थिर दर", "fixed_rate_alert": "फिक्स्ड रेट मोड चुनने पर आप प्राप्त राशि दर्ज कर पाएंगे। क्या आप फिक्स्ड रेट मोड पर स्विच करना चाहते हैं?", + "followed_assets": "परिसंपत्तियों का अनुसरण किया गया", "forgot_password": "पासवर्ड भूल गए", "freeze": "फ्रीज़", "frequently_asked_questions": "अक्सर पूछे जाने वाले प्रश्न", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "सभी नवीनतम मॉडल (GPT-5.3, Claude 4.6)।\nकोई सब्सक्रिप्शन नहीं, क्रिप्टो से भुगतान करें।", "narrow": "संकीर्ण", "network": "नेटवर्क", + "networks": "नेटवर्क", "network_address_detected": "${network} पता मिला", "network_prefix_on": "पर", "network_switch_failed": "अनुरोधित नेटवर्क पर स्विच करने में विफल. भुगतान लागू नहीं हुआ, कृपया पुनः प्रयास करें।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 816caecf9..9b42201a4 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -205,6 +205,7 @@ "change_username": "Promijeni korisničko ime", "change_wallet_alert_content": "Želite li promijeniti trenutni novčanik na ${wallet_name}?", "change_wallet_alert_title": "Promijeni trenutni novčanik", + "charts": "Karte", "check_receiver_forms": "Molimo provjerite obrasce primatelja", "choose_a_payment_method": "Odaberite način plaćanja", "choose_a_provider": "Odaberite pružatelja usluga", @@ -237,6 +238,8 @@ "color_theme": "Tema boja", "coming_soon_tag": "Uskoro", "commit_transaction_amount_fee": "Potvrdi transakciju\nIznos: ${amount}\nNaknada: ${fee}", + "compatible_services": "Kompatibilne usluge", + "compatible_services_desc": "Upotrijebite alat Skeniraj za čitanje sljedećih vrsta QR kodova podržanih u Cake Walletu:", "compatible_networks": "Kompatibilne mreže", "compatible_services": "Kompatibilne usluge", "compatible_services_desc": "Upotrijebite alat Skeniraj za čitanje sljedećih vrsta QR kodova podržanih u Cake Walletu:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim uslugama zamjene", "fixed_rate": "Fiksni tečaj", "fixed_rate_alert": "Moći ćete unijeti iznos za primanje kada je uključen način rada fiksne stope. Želite li se prebaciti na način rada fiksne stope?", + "followed_assets": "Praćena sredstva", "forgot_password": "Zaboravili ste lozinku", "freeze": "Zamrzni", "frequently_asked_questions": "Često postavljana pitanja", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Svi najnoviji modeli (GPT-5.3, Claude 4.6).\nBez pretplate, plaćajte kriptom.", "narrow": "Usko", "network": "Mreža", + "networks": "Mreže", "network_address_detected": "Otkrivena ${network} adresa", "network_prefix_on": "na", "network_switch_failed": "Nije uspjelo prebacivanje na traženu mrežu. Plaćanje nije primijenjeno, pokušajte ponovno.", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 725e95ba4..19795bfc6 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -205,6 +205,7 @@ "change_username": "Փոխել օգտանունը", "change_wallet_alert_content": "Ցանկանո՞ւմ եք փոխել ընթացիկ դրամապանակը՝ ${wallet_name}-ի:", "change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը", + "charts": "Գծապատկերներ", "check_receiver_forms": "Խնդրում ենք ստուգել ստացողի դաշտերը", "choose_a_payment_method": "Ընտրեք վճարման եղանակը", "choose_a_provider": "Ընտրեք մատակարար", @@ -237,6 +238,8 @@ "color_theme": "Գունային թեմա", "coming_soon_tag": "Շուտով", "commit_transaction_amount_fee": "Հաստատել գործարքը\nԳումար՝ ${amount}\nՄիջնորդավճար՝ ${fee}", + "compatible_services": "Համատեղելի ծառայություններ", + "compatible_services_desc": "Օգտագործեք Սկանավորման գործիքը՝ Cake Wallet-ի կողմից աջակցվող QR կոդերի հետևյալ տեսակները կարդալու համար՝", "compatible_networks": "Համատեղելի ցանցեր", "compatible_services": "Համատեղելի ծառայություններ", "compatible_services_desc": "Օգտագործեք Սկանավորման գործիքը՝ Cake Wallet-ի կողմից աջակցվող QR կոդերի հետևյալ տեսակները կարդալու համար՝", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Այս ֆիքսված զույգը չի աջակցվում ընտրված փոխանակման ծառայությունների կողմից", "fixed_rate": "Ֆիքսված փոխարժեք", "fixed_rate_alert": "Դուք կկարողանաք մուտքագրել ստացվող գումարը, երբ ֆիքսված փոխարժեքի ռեժիմը միացված է։ Ցանկանո՞ւմ եք անցնել ֆիքսված փոխարժեքի ռեժիմին։", + "followed_assets": "Հետևվող ակտիվներ", "forgot_password": "Մոռացել եք գաղտնաբառը", "freeze": "Սառեցնել", "frequently_asked_questions": "Հաճախ տրվող հարցեր", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Բոլոր ամենանոր մոդելները (GPT-5.3, Claude 4.6).\nԱռանց բաժանորդագրության՝ վճարեք կրիպտոարժույթով։", "narrow": "Նեղ", "network": "Ցանց", + "networks": "Ցանցեր", "network_address_detected": "Հայտնաբերվել է ${network} հասցե", "network_prefix_on": "վրա", "network_switch_failed": "Չհաջողվեց անցնել պահանջվող ցանցին: Վճարումը չի կիրառվել, խնդրում ենք կրկին փորձել:", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5fc98a3f7..952afe604 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -205,6 +205,7 @@ "change_username": "Ubah Nama Pengguna", "change_wallet_alert_content": "Apakah Anda ingin mengganti dompet saat ini menjadi ${wallet_name}?", "change_wallet_alert_title": "Ubah dompet saat ini", + "charts": "Grafik", "check_receiver_forms": "Silakan periksa formulir penerima", "choose_a_payment_method": "Pilih metode pembayaran", "choose_a_provider": "Pilih penyedia", @@ -237,6 +238,8 @@ "color_theme": "Tema warna", "coming_soon_tag": "Segera hadir", "commit_transaction_amount_fee": "Konfirmasi transaksi\nJumlah: ${amount}\nBiaya: ${fee}", + "compatible_services": "Layanan yang Kompatibel", + "compatible_services_desc": "Gunakan alat Pindai untuk membaca jenis kode QR berikut yang didukung di Cake Wallet:", "compatible_networks": "Jaringan yang Kompatibel", "compatible_services": "Layanan yang Kompatibel", "compatible_services_desc": "Gunakan alat Pindai untuk membaca jenis kode QR berikut yang didukung di Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Pasangan tetap ini tidak didukung oleh layanan swap yang dipilih", "fixed_rate": "Kurs tetap", "fixed_rate_alert": "Anda akan dapat memasukkan jumlah yang akan diterima saat mode kurs tetap dicentang. Apakah Anda ingin beralih ke mode kurs tetap?", + "followed_assets": "Aset yang diikuti", "forgot_password": "Lupa Kata Sandi", "freeze": "Bekukan", "frequently_asked_questions": "Pertanyaan yang Sering Diajukan", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Semua model terbaru (GPT-5.3, Claude 4.6).\nTanpa langganan, bayar dengan kripto.", "narrow": "Sempit", "network": "Jaringan", + "networks": "Jaringan", "network_address_detected": "Alamat ${network} terdeteksi", "network_prefix_on": "pada", "network_switch_failed": "Gagal beralih ke jaringan yang diminta. Pembayaran tidak diterapkan, silakan coba lagi.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 5664405b8..554781556 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -205,6 +205,7 @@ "change_username": "Cambia nome utente", "change_wallet_alert_content": "Vuoi cambiare il portafoglio attuale in ${wallet_name}?", "change_wallet_alert_title": "Cambia portafoglio corrente", + "charts": "Grafici", "check_receiver_forms": "Per favore, controlla i campi del destinatario", "choose_a_payment_method": "Scegli un metodo di pagamento", "choose_a_provider": "Scegli un provider", @@ -237,6 +238,8 @@ "color_theme": "Tema colore", "coming_soon_tag": "Prossimamente", "commit_transaction_amount_fee": "Conferma transazione\nImporto: ${amount}\nCommissione: ${fee}", + "compatible_services": "Servizi compatibili", + "compatible_services_desc": "Usa lo strumento Scansione per leggere i seguenti tipi di codici QR supportati su Cake Wallet:", "compatible_networks": "Reti compatibili", "compatible_services": "Servizi compatibili", "compatible_services_desc": "Usa lo strumento Scansione per leggere i seguenti tipi di codici QR supportati su Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Questa coppia fissa non è supportata dai servizi di swap selezionati", "fixed_rate": "Tasso fisso", "fixed_rate_alert": "Potrai inserire l'importo da ricevere quando è selezionata la modalità a tasso fisso. Vuoi passare alla modalità a tasso fisso?", + "followed_assets": "Risorse seguite", "forgot_password": "Password dimenticata", "freeze": "Blocca", "frequently_asked_questions": "Domande frequenti", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Tutti i modelli più recenti (GPT-5.3, Claude 4.6).\nNessun abbonamento, paga con cripto.", "narrow": "Stretto", "network": "Rete", + "networks": "Reti", "network_address_detected": "Indirizzo ${network} rilevato", "network_prefix_on": "SU", "network_switch_failed": "Impossibile passare alla rete richiesta. Il pagamento non è stato applicato, riprova.", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 3cff5d011..240229ecf 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -205,6 +205,7 @@ "change_username": "ユーザー名を変更", "change_wallet_alert_content": "現在のウォレットを${wallet_name}に変更しますか?", "change_wallet_alert_title": "現在のウォレットを変更", + "charts": "チャート", "check_receiver_forms": "受取人情報の入力内容を確認してください", "choose_a_payment_method": "支払い方法を選択", "choose_a_provider": "プロバイダーを選択", @@ -237,6 +238,8 @@ "color_theme": "カラーテーマ", "coming_soon_tag": "近日公開", "commit_transaction_amount_fee": "トランザクションを確定\n金額: ${amount}\n手数料: ${fee}", + "compatible_services": "対応サービス", + "compatible_services_desc": "スキャンツールを使用して、Cake Walletで対応している以下の種類のQRコードを読み取れます:", "compatible_networks": "互換性のあるネットワーク", "compatible_services": "対応サービス", "compatible_services_desc": "スキャンツールを使用して、Cake Walletで対応している以下の種類のQRコードを読み取れます:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "この固定ペアは、選択したスワップサービスではサポートされていません", "fixed_rate": "固定レート", "fixed_rate_alert": "固定レートモードにチェックを入れると、受取額を入力できるようになります。固定レートモードに切り替えますか?", + "followed_assets": "フォローされているアセット", "forgot_password": "パスワードをお忘れですか?", "freeze": "凍結", "frequently_asked_questions": "よくある質問", @@ -710,6 +714,7 @@ "nanogpt_subtitle": "すべての最新モデル(GPT-5.3、Claude 4.6)。\nサブスクリプション不要、暗号資産で支払い。", "narrow": "狭い", "network": "ネットワーク", + "networks": "ネットワーク", "network_address_detected": "${network}アドレスが検出されました", "network_prefix_on": "の上", "network_switch_failed": "要求されたネットワークへの切り替えに失敗しました。支払いは適用されませんでした。もう一度お試しください。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index da1d21922..0dd006a6c 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -205,6 +205,7 @@ "change_username": "사용자 이름 변경", "change_wallet_alert_content": "현재 지갑을 ${wallet_name}(으)로 변경하시겠습니까?", "change_wallet_alert_title": "현재 지갑 변경", + "charts": "차트", "check_receiver_forms": "수신자 입력란을 확인해 주세요.", "choose_a_payment_method": "결제 방법을 선택하세요", "choose_a_provider": "제공업체 선택", @@ -237,6 +238,8 @@ "color_theme": "색상 테마", "coming_soon_tag": "출시 예정", "commit_transaction_amount_fee": "트랜잭션 제출\n금액: ${amount}\n수수료: ${fee}", + "compatible_services": "호환 서비스", + "compatible_services_desc": "스캔 도구를 사용하여 Cake Wallet에서 지원하는 다음 유형의 QR 코드를 읽으세요:", "compatible_networks": "호환 가능한 네트워크", "compatible_services": "호환 서비스", "compatible_services_desc": "스캔 도구를 사용하여 Cake Wallet에서 지원하는 다음 유형의 QR 코드를 읽으세요:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "이 고정 페어는 선택한 스왑 서비스에서 지원되지 않습니다", "fixed_rate": "고정 환율", "fixed_rate_alert": "고정 환율 모드를 선택하면 수령 금액을 입력할 수 있습니다. 고정 환율 모드로 전환하시겠습니까?", + "followed_assets": "팔로우하는 자산", "forgot_password": "비밀번호를 잊으셨나요?", "freeze": "동결", "frequently_asked_questions": "자주 묻는 질문", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "최신 모델 모두(GPT-5.3, Claude 4.6).\n구독 없이 암호화폐로 결제.", "narrow": "좁게", "network": "네트워크", + "networks": "네트워크", "network_address_detected": "${network} 주소가 감지되었습니다", "network_prefix_on": "~에", "network_switch_failed": "요청한 네트워크로 전환하지 못했습니다. 결제가 적용되지 않았습니다. 다시 시도해 주세요.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 1fa7acc77..67fce097f 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -205,6 +205,7 @@ "change_username": "အသုံးပြုသူအမည် ပြောင်းရန်", "change_wallet_alert_content": "လက်ရှိပိုက်ဆံအိတ်ကို ${wallet_name} သို့ ပြောင်းလိုပါသလား။", "change_wallet_alert_title": "လက်ရှိ Wallet ကို ပြောင်းရန်", + "charts": "ဇယားများ", "check_receiver_forms": "ကျေးဇူးပြု၍ လက်ခံသူဖောင်များကို စစ်ဆေးပါ။", "choose_a_payment_method": "ငွေပေးချေမှုနည်းလမ်းကို ရွေးချယ်ပါ", "choose_a_provider": "ဝန်ဆောင်မှုပေးသူကို ရွေးချယ်ပါ", @@ -237,6 +238,8 @@ "color_theme": "အရောင်အပြင်အဆင်", "coming_soon_tag": "မကြာမီလာမည်", "commit_transaction_amount_fee": "ငွေလွှဲမှုကို အတည်ပြုပါ\nပမာဏ: ${amount}\nအခကြေးငွေ: ${fee}", + "compatible_services": "တွဲဖက်အသုံးပြုနိုင်သော ဝန်ဆောင်မှုများ", + "compatible_services_desc": "Cake Wallet တွင် ပံ့ပိုးထားသော အောက်ပါ QR ကုဒ်အမျိုးအစားများကို ဖတ်ရန် Scan tool ကို အသုံးပြုပါ:", "compatible_networks": "လိုက်ဖက်သောကွန်ရက်များ", "compatible_services": "တွဲဖက်အသုံးပြုနိုင်သော ဝန်ဆောင်မှုများ", "compatible_services_desc": "Cake Wallet တွင် ပံ့ပိုးထားသော အောက်ပါ QR ကုဒ်အမျိုးအစားများကို ဖတ်ရန် Scan tool ကို အသုံးပြုပါ:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "ဤပုံသေစုံတွဲကို ရွေးချယ်ထားသော swap ဝန်ဆောင်မှုများဖြင့် မထောက်ပံ့ပါ", "fixed_rate": "ပုံသေ နှုန်းထား", "fixed_rate_alert": "ပုံသေနှုန်းထားမုဒ်ကို ရွေးထားသောအခါ လက်ခံရရှိမည့် ပမာဏကို ထည့်သွင်းနိုင်မည်ဖြစ်သည်။ ပုံသေနှုန်းထားမုဒ်သို့ ပြောင်းလိုပါသလား။", + "followed_assets": "လိုက်နာအပ်ပါတယ်။", "forgot_password": "စကားဝှက်မေ့သွားပါသလား", "freeze": "ရပ်ဆိုင်း", "frequently_asked_questions": "မေးလေ့ရှိသော မေးခွန်းများ", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "နောက်ဆုံးပေါ်မော်ဒယ်များအားလုံး (GPT-5.3, Claude 4.6)။\nစာရင်းသွင်းမှုမလို၊ crypto ဖြင့်ပေးချေပါ။", "narrow": "ကျဉ်း", "network": "ကွန်ရက်", + "networks": "ကွန်ရက်များ", "network_address_detected": "${network} လိပ်စာကို တွေ့ရှိသည်", "network_prefix_on": "on", "network_switch_failed": "တောင်းဆိုထားသောကွန်ရက်သို့ ပြောင်း၍မရပါ။ ငွေပေးချေမှုကို မသက်ရောက်ပါ၊ ထပ်စမ်းကြည့်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index f73de1c8f..a0f661000 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -205,6 +205,7 @@ "change_username": "Verander de gebruikersnaam", "change_wallet_alert_content": "Wil je de huidige wallet wijzigen in ${wallet_name}?", "change_wallet_alert_title": "Wijzig huidige wallet", + "charts": "Grafieken", "check_receiver_forms": "Controleer de formulieren van de ontvanger", "choose_a_payment_method": "Kies een betaalmethode", "choose_a_provider": "Kies een provider", @@ -237,6 +238,8 @@ "color_theme": "Kleurthema", "coming_soon_tag": "Binnenkort", "commit_transaction_amount_fee": "Bevestig transactie\nBedrag: ${amount}\nKosten: ${fee}", + "compatible_services": "Compatibele services", + "compatible_services_desc": "Gebruik de Scan-tool om de volgende soorten QR-codes te lezen die door Cake Wallet worden ondersteund:", "compatible_networks": "Compatibele netwerken", "compatible_services": "Compatibele services", "compatible_services_desc": "Gebruik de Scan-tool om de volgende soorten QR-codes te lezen die door Cake Wallet worden ondersteund:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund met de geselecteerde swap-services", "fixed_rate": "Vast tarief", "fixed_rate_alert": "Je kunt het ontvangen bedrag invoeren wanneer de modus voor vaste tarieven is aangevinkt. Wil je overschakelen naar de vaste-tariefmodus?", + "followed_assets": "Gevolgde activa", "forgot_password": "Wachtwoord vergeten", "freeze": "Bevriezen", "frequently_asked_questions": "Veelgestelde vragen", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Alle nieuwste modellen (GPT-5.3, Claude 4.6).\nGeen abonnement, betaal met crypto.", "narrow": "Smal", "network": "Netwerk", + "networks": "Netwerken", "network_address_detected": "${network}-adres gedetecteerd", "network_prefix_on": "op", "network_switch_failed": "Kan niet overschakelen naar het gevraagde netwerk. De betaling is niet toegepast. Probeer het opnieuw.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 619f1be0a..e3675bfcb 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -205,6 +205,7 @@ "change_username": "Zmień nazwę użytkownika", "change_wallet_alert_content": "Czy chcesz zmienić bieżący portfel na ${wallet_name}?", "change_wallet_alert_title": "Zmień bieżący portfel", + "charts": "Wykresy", "check_receiver_forms": "Proszę sprawdzić formularze odbiorcy", "choose_a_payment_method": "Wybierz metodę płatności", "choose_a_provider": "Wybierz dostawcę", @@ -237,6 +238,8 @@ "color_theme": "Motyw kolorystyczny", "coming_soon_tag": "Wkrótce", "commit_transaction_amount_fee": "Zatwierdź transakcję\nKwota: ${amount}\nOpłata: ${fee}", + "compatible_services": "Zgodne usługi", + "compatible_services_desc": "Użyj narzędzia Skanuj, aby odczytać następujące typy kodów QR obsługiwane w Cake Wallet:", "compatible_networks": "Kompatybilne sieci", "compatible_services": "Zgodne usługi", "compatible_services_desc": "Użyj narzędzia Skanuj, aby odczytać następujące typy kodów QR obsługiwane w Cake Wallet:", @@ -472,6 +475,7 @@ "etherscan_history": "Historia w Etherscan", "event": "Zdarzenie", "events": "Zdarzenia", + "evm_ecosystem_description": "W razie potrzeby zmień sieć i token, których chcesz użyć do tej płatności.", "evm_address_detected": "Wykryto adres EVM", "evm_ecosystem_description": "W razie potrzeby zmień sieć i token, których chcesz użyć do tej płatności.", "exchange": "Wymiana", @@ -526,6 +530,7 @@ "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana przez wybrane usługi swap.", "fixed_rate": "Stały kurs", "fixed_rate_alert": "Będziesz mógł wprowadzić kwotę do otrzymania, gdy tryb stałego kursu jest zaznaczony. Czy chcesz przełączyć się na tryb stałego kursu?", + "followed_assets": "Obserwowane aktywa", "forgot_password": "Nie pamiętasz hasła", "freeze": "Zamroź", "frequently_asked_questions": "Najczęściej zadawane pytania", @@ -709,6 +714,7 @@ "nanogpt_subtitle": "Wszystkie najnowsze modele (GPT-5.3, Claude 4.6).\nBez subskrypcji, płać kryptowalutą.", "narrow": "Wąski", "network": "Sieć", + "networks": "Sieci", "network_address_detected": "Wykryto adres ${network}", "network_prefix_on": "NA", "network_switch_failed": "Nie udało się przełączyć na żądaną sieć. Płatność nie została zastosowana. Spróbuj ponownie.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index ba74d37a4..3e01398f8 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -205,6 +205,7 @@ "change_username": "Alterar nome de utilizador", "change_wallet_alert_content": "Deseja alterar a carteira atual para ${wallet_name}?", "change_wallet_alert_title": "Alterar a carteira atual", + "charts": "Gráficos", "check_receiver_forms": "Por favor, verifique os campos do destinatário", "choose_a_payment_method": "Escolha um método de pagamento", "choose_a_provider": "Escolha um provedor", @@ -237,6 +238,8 @@ "color_theme": "Tema de cores", "coming_soon_tag": "Em breve", "commit_transaction_amount_fee": "Confirmar transação\nValor: ${amount}\nTaxa: ${fee}", + "compatible_services": "Serviços compatíveis", + "compatible_services_desc": "Use a ferramenta de digitalização para ler os seguintes tipos de códigos QR suportados na Cake Wallet:", "compatible_networks": "Redes Compatíveis", "compatible_services": "Serviços compatíveis", "compatible_services_desc": "Use a ferramenta de digitalização para ler os seguintes tipos de códigos QR suportados na Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Este par fixo não é compatível com os serviços de swap selecionados", "fixed_rate": "Taxa fixa", "fixed_rate_alert": "Você poderá inserir o valor a receber quando o modo de taxa fixa estiver selecionado. Deseja mudar para o modo de taxa fixa?", + "followed_assets": "Ativos seguidos", "forgot_password": "Esqueci minha senha", "freeze": "Congelar", "frequently_asked_questions": "Perguntas frequentes", @@ -710,6 +714,7 @@ "nanogpt_subtitle": "Todos os modelos mais recentes (GPT-5.3, Claude 4.6).\nSem assinatura, pague com cripto.", "narrow": "Estreito", "network": "Rede", + "networks": "Redes", "network_address_detected": "Endereço ${network} detectado", "network_prefix_on": "sobre", "network_switch_failed": "Falha ao mudar para a rede solicitada. O pagamento não foi aplicado, tente novamente.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 724c46823..e7c1a6116 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -205,6 +205,7 @@ "change_username": "Изменить имя пользователя", "change_wallet_alert_content": "Вы хотите сменить текущий кошелёк на ${wallet_name}?", "change_wallet_alert_title": "Сменить текущий кошелек", + "charts": "Графики", "check_receiver_forms": "Пожалуйста, проверьте формы получателя", "choose_a_payment_method": "Выберите способ оплаты", "choose_a_provider": "Выберите провайдера", @@ -237,6 +238,8 @@ "color_theme": "Цветовая тема", "coming_soon_tag": "Скоро", "commit_transaction_amount_fee": "Подтвердить транзакцию\nСумма: ${amount}\nКомиссия: ${fee}", + "compatible_services": "Совместимые сервисы", + "compatible_services_desc": "Используйте инструмент сканирования, чтобы считывать следующие типы QR-кодов, поддерживаемые в Cake Wallet:", "compatible_networks": "Совместимые сети", "compatible_services": "Совместимые сервисы", "compatible_services_desc": "Используйте инструмент сканирования, чтобы считывать следующие типы QR-кодов, поддерживаемые в Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными сервисами обмена", "fixed_rate": "Фиксированный курс", "fixed_rate_alert": "Вы сможете ввести сумму получения, когда будет включён режим фиксированного курса. Хотите переключиться в режим фиксированного курса?", + "followed_assets": "Отслеживаемые активы", "forgot_password": "Забыли пароль?", "freeze": "Заморозить", "frequently_asked_questions": "Часто задаваемые вопросы", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Все новейшие модели (GPT-5.3, Claude 4.6).\nБез подписки, оплата криптовалютой.", "narrow": "Узкий", "network": "Сеть", + "networks": "Сети", "network_address_detected": "Обнаружен адрес ${network}", "network_prefix_on": "на", "network_switch_failed": "Не удалось переключиться на запрошенную сеть. Платеж не был применен, попробуйте еще раз.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index c417c2108..c58b64043 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -205,6 +205,7 @@ "change_username": "เปลี่ยนชื่อผู้ใช้", "change_wallet_alert_content": "คุณต้องการเปลี่ยนกระเป๋าเงินปัจจุบันเป็น ${wallet_name} หรือไม่?", "change_wallet_alert_title": "เปลี่ยนกระเป๋าเงินปัจจุบัน", + "charts": "แผนภูมิ", "check_receiver_forms": "กรุณาตรวจสอบแบบฟอร์มของผู้รับ", "choose_a_payment_method": "เลือกวิธีการชำระเงิน", "choose_a_provider": "เลือกผู้ให้บริการ", @@ -237,6 +238,8 @@ "color_theme": "ธีมสี", "coming_soon_tag": "เร็ว ๆ นี้", "commit_transaction_amount_fee": "ยืนยันธุรกรรม\nจำนวน: ${amount}\nค่าธรรมเนียม: ${fee}", + "compatible_services": "บริการที่รองรับ", + "compatible_services_desc": "ใช้เครื่องมือสแกนเพื่ออ่าน QR Code ประเภทต่อไปนี้ที่ Cake Wallet รองรับ:", "compatible_networks": "เครือข่ายที่เข้ากันได้", "compatible_services": "บริการที่รองรับ", "compatible_services_desc": "ใช้เครื่องมือสแกนเพื่ออ่าน QR Code ประเภทต่อไปนี้ที่ Cake Wallet รองรับ:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "คู่แบบคงที่นี้ไม่รองรับกับบริการสวอปที่เลือก", "fixed_rate": "อัตราคงที่", "fixed_rate_alert": "คุณจะสามารถป้อนจำนวนเงินที่ต้องการรับได้เมื่อเปิดใช้งานโหมดอัตราคงที่ คุณต้องการสลับไปที่โหมดอัตราคงที่หรือไม่?", + "followed_assets": "ทรัพย์สินที่ตามมา", "forgot_password": "ลืมรหัสผ่าน", "freeze": "ระงับ", "frequently_asked_questions": "คำถามที่พบบ่อย", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "โมเดลใหม่ล่าสุดทั้งหมด (GPT-5.3, Claude 4.6)\nไม่ต้องสมัครสมาชิก จ่ายด้วยคริปโต", "narrow": "แคบ", "network": "เครือข่าย", + "networks": "เครือข่าย", "network_address_detected": "ตรวจพบที่อยู่ ${network}", "network_prefix_on": "บน", "network_switch_failed": "ไม่สามารถสลับไปยังเครือข่ายที่ร้องขอได้ การชำระเงินไม่ได้ใช้ โปรดลองอีกครั้ง", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 2c5ef7aa8..7354522d5 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -205,6 +205,7 @@ "change_username": "Palitan ang Username", "change_wallet_alert_content": "Gusto mo bang palitan ang kasalukuyang wallet ng ${wallet_name}?", "change_wallet_alert_title": "Palitan ang kasalukuyang wallet", + "charts": "Mga tsart", "check_receiver_forms": "Mangyaring suriin ang mga form ng tatanggap", "choose_a_payment_method": "Pumili ng paraan ng pagbabayad", "choose_a_provider": "Pumili ng provider", @@ -237,6 +238,8 @@ "color_theme": "Tema ng kulay", "coming_soon_tag": "Paparating na", "commit_transaction_amount_fee": "I-commit ang transaksyon\nHalaga: ${amount}\nBayad: ${fee}", + "compatible_services": "Mga Katugmang Serbisyo", + "compatible_services_desc": "Gamitin ang Scan tool upang basahin ang mga sumusunod na uri ng QR code na sinusuportahan sa Cake Wallet:", "compatible_networks": "Mga Katugmang Network", "compatible_services": "Mga Katugmang Serbisyo", "compatible_services_desc": "Gamitin ang Scan tool upang basahin ang mga sumusunod na uri ng QR code na sinusuportahan sa Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Hindi sinusuportahan ang nakapirming pares na ito sa mga napiling serbisyo ng swap", "fixed_rate": "Nakapirming rate", "fixed_rate_alert": "Makakapaglagay ka ng halagang matatanggap kapag naka-check ang fixed rate mode. Gusto mo bang lumipat sa fixed rate mode?", + "followed_assets": "Sinunod ang mga asset", "forgot_password": "Nakalimutan ang Password", "freeze": "I-freeze", "frequently_asked_questions": "Mga madalas itanong", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Lahat ng pinakabagong modelo (GPT-5.3, Claude 4.6).\nWalang subscription, magbayad gamit ang crypto.", "narrow": "Makitid", "network": "Network", + "networks": "Mga Network", "network_address_detected": "Natukoy ang ${network} address", "network_prefix_on": "sa", "network_switch_failed": "Nabigong lumipat sa hiniling na network. Hindi nailapat ang pagbabayad, pakisubukang muli.", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 23a70ebcc..95740662e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -205,6 +205,7 @@ "change_username": "Kullanıcı Adını Değiştir", "change_wallet_alert_content": "Mevcut cüzdanı ${wallet_name} ile değiştirmek istiyor musunuz?", "change_wallet_alert_title": "Mevcut cüzdanı değiştir", + "charts": "Grafikler", "check_receiver_forms": "Lütfen alıcı bilgilerini kontrol edin", "choose_a_payment_method": "Bir ödeme yöntemi seçin", "choose_a_provider": "Bir sağlayıcı seçin", @@ -237,6 +238,8 @@ "color_theme": "Renk teması", "coming_soon_tag": "Yakında", "commit_transaction_amount_fee": "İşlemi onayla\nTutar: ${amount}\nÜcret: ${fee}", + "compatible_services": "Uyumlu Hizmetler", + "compatible_services_desc": "Cake Wallet'ta desteklenen aşağıdaki QR kodu türlerini okumak için Tarama aracını kullanın:", "compatible_networks": "Uyumlu Ağlar", "compatible_services": "Uyumlu Hizmetler", "compatible_services_desc": "Cake Wallet'ta desteklenen aşağıdaki QR kodu türlerini okumak için Tarama aracını kullanın:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Bu sabit parite, seçilen takas hizmetleriyle desteklenmiyor", "fixed_rate": "Sabit kur", "fixed_rate_alert": "Sabit oran modu seçiliyken alınacak tutarı girebileceksin. Sabit oran moduna geçmek ister misin?", + "followed_assets": "Takip edilen varlıklar", "forgot_password": "Parolamı Unuttum", "freeze": "Dondur", "frequently_asked_questions": "Sıkça sorulan sorular", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "En yeni modellerin tümü (GPT-5.3, Claude 4.6).\nAbonelik yok, kripto ile ödeyin.", "narrow": "Dar", "network": "Ağ", + "networks": "Ağlar", "network_address_detected": "${network} adresi algılandı", "network_prefix_on": "Açık", "network_switch_failed": "İstenilen ağa geçiş yapılamadı. Ödeme uygulanmadı, lütfen tekrar deneyin.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index a5ab68ff7..6db2374b6 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -205,6 +205,7 @@ "change_username": "Змінити ім’я користувача", "change_wallet_alert_content": "Ви хочете змінити поточний гаманець на ${wallet_name}?", "change_wallet_alert_title": "Змінити поточний гаманець", + "charts": "Діаграми", "check_receiver_forms": "Будь ласка, перевірте форми одержувача", "choose_a_payment_method": "Виберіть спосіб оплати", "choose_a_provider": "Виберіть провайдера", @@ -237,6 +238,8 @@ "color_theme": "Колірна тема", "coming_soon_tag": "Скоро", "commit_transaction_amount_fee": "Підтвердити транзакцію\nСума: ${amount}\nКомісія: ${fee}", + "compatible_services": "Сумісні сервіси", + "compatible_services_desc": "Скористайтеся інструментом сканування, щоб зчитати такі типи QR-кодів, які підтримує Cake Wallet:", "compatible_networks": "Сумісні мережі", "compatible_services": "Сумісні сервіси", "compatible_services_desc": "Скористайтеся інструментом сканування, щоб зчитати такі типи QR-кодів, які підтримує Cake Wallet:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними сервісами обміну", "fixed_rate": "Фіксований курс", "fixed_rate_alert": "Ви зможете ввести суму отримання, коли буде увімкнено режим фіксованого курсу. Хочете перейти в режим фіксованого курсу?", + "followed_assets": "Активи, за якими ви стежите", "forgot_password": "Забули пароль", "freeze": "Заморозити", "frequently_asked_questions": "Поширені запитання", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "Усі найновіші моделі (GPT-5.3, Claude 4.6).\nБез підписки, оплата криптовалютою.", "narrow": "Вузький", "network": "Мережа", + "networks": "Мережі", "network_address_detected": "Виявлено адресу ${network}", "network_prefix_on": "на", "network_switch_failed": "Не вдалося переключитися на потрібну мережу. Платіж не застосовано, спробуйте ще раз.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2b730fdb3..49d576e9c 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -205,6 +205,7 @@ "change_username": "صارف نام تبدیل کریں", "change_wallet_alert_content": "کیا آپ موجودہ والیٹ کو ${wallet_name} میں تبدیل کرنا چاہتے ہیں؟", "change_wallet_alert_title": "موجودہ والیٹ تبدیل کریں", + "charts": "چارٹس", "check_receiver_forms": "براہِ کرم وصول کنندہ کے فارم چیک کریں", "choose_a_payment_method": "ادائیگی کا طریقہ منتخب کریں", "choose_a_provider": "ایک فراہم کنندہ منتخب کریں", @@ -237,6 +238,8 @@ "color_theme": "رنگ تھیم", "coming_soon_tag": "جلد آ رہا ہے", "commit_transaction_amount_fee": "لین دین کی تصدیق کریں\nرقم: ${amount}\nفیس: ${fee}", + "compatible_services": "موافق سروسز", + "compatible_services_desc": "Cake Wallet پر معاون درج ذیل اقسام کے QR کوڈز پڑھنے کے لیے Scan ٹول استعمال کریں:", "compatible_networks": "ہم آہنگ نیٹ ورکس", "compatible_services": "موافق سروسز", "compatible_services_desc": "Cake Wallet پر معاون درج ذیل اقسام کے QR کوڈز پڑھنے کے لیے Scan ٹول استعمال کریں:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "منتخب کردہ سواپ سروسز کے ساتھ یہ فکسڈ جوڑی سپورٹ نہیں کی جاتی", "fixed_rate": "مقررہ شرح", "fixed_rate_alert": "فکسڈ ریٹ موڈ منتخب ہونے پر آپ وصولی کی رقم درج کر سکیں گے۔ کیا آپ فکسڈ ریٹ موڈ پر سوئچ کرنا چاہتے ہیں؟", + "followed_assets": "اثاثوں کی پیروی کی۔", "forgot_password": "پاس ورڈ بھول گئے", "freeze": "منجمد کریں", "frequently_asked_questions": "اکثر پوچھے جانے والے سوالات", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "تمام جدید ترین ماڈلز (GPT-5.3، Claude 4.6)۔\nکوئی سبسکرپشن نہیں، کرپٹو کے ساتھ ادائیگی کریں۔", "narrow": "تنگ", "network": "نیٹ ورک", + "networks": "نیٹ ورکس", "network_address_detected": "${network} ایڈریس کا پتہ چلا", "network_prefix_on": "پر", "network_switch_failed": "درخواست کردہ نیٹ ورک پر سوئچ کرنے میں ناکام۔ ادائیگی لاگو نہیں ہوئی، براہ کرم دوبارہ کوشش کریں۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 88b975821..a29ca098c 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -205,6 +205,7 @@ "change_username": "Thay đổi tên người dùng", "change_wallet_alert_content": "Bạn có muốn chuyển ví hiện tại sang ${wallet_name} không?", "change_wallet_alert_title": "Thay đổi ví hiện tại", + "charts": "Biểu đồ", "check_receiver_forms": "Vui lòng kiểm tra thông tin người nhận", "choose_a_payment_method": "Chọn phương thức thanh toán", "choose_a_provider": "Chọn nhà cung cấp", @@ -237,6 +238,8 @@ "color_theme": "Chủ đề màu", "coming_soon_tag": "Sắp ra mắt", "commit_transaction_amount_fee": "Xác nhận giao dịch\nSố tiền: ${amount}\nPhí: ${fee}", + "compatible_services": "Dịch vụ tương thích", + "compatible_services_desc": "Sử dụng công cụ Quét để đọc các loại mã QR sau được hỗ trợ trên Cake Wallet:", "compatible_networks": "Mạng tương thích", "compatible_services": "Dịch vụ tương thích", "compatible_services_desc": "Sử dụng công cụ Quét để đọc các loại mã QR sau được hỗ trợ trên Cake Wallet:", @@ -527,6 +530,7 @@ "fixed_pair_not_supported": "Cặp tỷ giá cố định này không được hỗ trợ với các dịch vụ hoán đổi đã chọn", "fixed_rate": "Tỷ giá cố định", "fixed_rate_alert": "Bạn sẽ có thể nhập số lượng nhận khi chế độ tỷ giá cố định được bật. Bạn có muốn chuyển sang chế độ tỷ giá cố định không?", + "followed_assets": "Nội dung được theo dõi", "forgot_password": "Quên mật khẩu", "freeze": "Đóng băng", "frequently_asked_questions": "Các câu hỏi thường gặp", @@ -710,6 +714,7 @@ "nanogpt_subtitle": "Tất cả các mô hình mới nhất (GPT-5.3, Claude 4.6).\nKhông cần đăng ký, thanh toán bằng crypto.", "narrow": "Hẹp", "network": "Mạng", + "networks": "Mạng", "network_address_detected": "Đã phát hiện địa chỉ ${network}", "network_prefix_on": "TRÊN", "network_switch_failed": "Không thể chuyển sang mạng được yêu cầu. Thanh toán chưa được áp dụng, vui lòng thử lại.", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 663c7620e..459eccc87 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -205,6 +205,7 @@ "change_username": "Yi Orúkọ Olùmúlò Padà", "change_wallet_alert_content": "Ṣé o fẹ́ yí wálẹ́ẹ̀tì tó wà lọ́wọ́lọ́wọ́ padà sí ${wallet_name}?", "change_wallet_alert_title": "Pààrọ̀ àpamọ́wọ́ lọ́wọ́lọ́wọ́", + "charts": "Awọn aworan atọka", "check_receiver_forms": "Jọwọ, ṣayẹwo àwọn fọ́ọ̀mù olùgbà", "choose_a_payment_method": "Yan ọna ìsanwó kan", "choose_a_provider": "Yan olupese kan", @@ -237,6 +238,8 @@ "color_theme": "Àkòrí àwọ̀", "coming_soon_tag": "N bọ laipẹ", "commit_transaction_amount_fee": "Fọwọ́sí ìdúnàdúrà\nIye: ${amount}\nOwó ìdúnàdúrà: ${fee}", + "compatible_services": "Àwọn Iṣẹ́ Tó Bámu", + "compatible_services_desc": "Lo irinṣẹ́ Scan láti ka àwọn irú kóòdù QR wọ̀nyí tí a ṣe àtìlẹ́yìn fún lórí Cake Wallet:", "compatible_networks": "Awọn nẹtiwọki ibaramu", "compatible_services": "Àwọn Iṣẹ́ Tó Bámu", "compatible_services_desc": "Lo irinṣẹ́ Scan láti ka àwọn irú kóòdù QR wọ̀nyí tí a ṣe àtìlẹ́yìn fún lórí Cake Wallet:", @@ -527,6 +530,7 @@ "fixed_pair_not_supported": "Akọsopọ paṣipaarọ ti o wa titi yii ko ni atilẹyin pẹlu awọn iṣẹ swap ti o yan", "fixed_rate": "Oṣuwọn tí ó dúró ṣinṣin", "fixed_rate_alert": "Ẹ ó lè tẹ iye tí ẹ fẹ́ gba wọlé nígbà tí a bá ti yan ipo oṣuwọn títọ́. Ṣé ẹ fẹ́ yí padà sí ipo oṣuwọn títọ́?", + "followed_assets": "Awọn ohun-ini atẹle", "forgot_password": "Gbagbe Ọ̀rọ̀ aṣínà", "freeze": "Dì", "frequently_asked_questions": "Àwọn ìbéèrè tí a máa ń béèrè lọ́pọ̀ ìgbà", @@ -710,6 +714,7 @@ "nanogpt_subtitle": "Gbogbo àwọn àwòṣe tuntun jùlọ (GPT-5.3, Claude 4.6).\nKò sí ìforúkọsílẹ̀, sanwó pẹ̀lú crypto.", "narrow": "Dín", "network": "Nẹ́tíwọ́ọ̀kì", + "networks": "Àwọn Nẹ́tíwọ́ọ̀kì", "network_address_detected": "A ti rí àdírẹ́sì ${network}", "network_prefix_on": "lori", "network_switch_failed": "Kuna lati yipada si nẹtiwọki ti o beere. Owo sisan ko lo, jọwọ gbiyanju lẹẹkansi.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 7053faca6..c37b5f1b3 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -205,6 +205,7 @@ "change_username": "更改用户名", "change_wallet_alert_content": "是否要将当前钱包切换为${wallet_name}?", "change_wallet_alert_title": "更改当前钱包", + "charts": "图表", "check_receiver_forms": "请检查收款信息表单", "choose_a_payment_method": "选择支付方式", "choose_a_provider": "选择服务提供商", @@ -237,6 +238,8 @@ "color_theme": "颜色主题", "coming_soon_tag": "即将推出", "commit_transaction_amount_fee": "提交交易\n金额:${amount}\n手续费:${fee}", + "compatible_services": "兼容的服务", + "compatible_services_desc": "使用扫描工具读取 Cake Wallet 支持的以下类型二维码:", "compatible_networks": "兼容网络", "compatible_services": "兼容的服务", "compatible_services_desc": "使用扫描工具读取 Cake Wallet 支持的以下类型二维码:", @@ -526,6 +529,7 @@ "fixed_pair_not_supported": "所选的兑换服务不支持该固定交易对", "fixed_rate": "固定汇率", "fixed_rate_alert": "勾选固定汇率模式后,您将可以输入接收金额。您要切换到固定汇率模式吗?", + "followed_assets": "关注资产", "forgot_password": "忘记密码", "freeze": "冻结", "frequently_asked_questions": "常见问题", @@ -709,6 +713,7 @@ "nanogpt_subtitle": "所有最新模型(GPT-5.3、Claude 4.6)。\n无需订阅,用加密货币支付。", "narrow": "窄", "network": "网络", + "networks": "网络", "network_address_detected": "检测到 ${network} 地址", "network_prefix_on": "在", "network_switch_failed": "无法切换到请求的网络。付款未成功,请重试。",