CW-512 wallet sort (#1195)

* add shared preferences key and make list re-orderable

* macos sonoma build fix

* everything (almost) works

* add translations

* more translations

* everything works

* fixes

* translation merge file fix

* fix for mobile platforms

* fix filter icon being invisible on bright theme

* first pass of adding ascending/descending to filter widget

* small fix

* update ascending switch

* fixes

* fix

* reverse creation sort

* review fixes part 1

* refactor into function rather than tuple + color changes

* more fixes

* remove unrelated file

* updated wording

* cleaner callback

* undo sonoma fix

---------

Co-authored-by: fossephate <fosse@book.local>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Matthew Fosse
2023-12-13 23:58:43 +02:00
committed by GitHub
co-authored by fossephate Omar Hatem
parent 049e4def27
commit 2ff81df9e7
37 changed files with 624 additions and 36 deletions
+34
View File
@@ -0,0 +1,34 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/enumerable_item.dart';
class ListOrderMode extends EnumerableItem<int> with Serializable<int> {
const ListOrderMode({required String title, required int raw}) : super(title: title, raw: raw);
static const all = [ListOrderMode.ascending, ListOrderMode.descending];
static const ascending = ListOrderMode(raw: 0, title: 'Ascending');
static const descending = ListOrderMode(raw: 1, title: 'Descending');
static ListOrderMode deserialize({required int raw}) {
switch (raw) {
case 0:
return ascending;
case 1:
return descending;
default:
throw Exception('Unexpected token: $raw for ListOrderMode deserialize');
}
}
@override
String toString() {
switch (this) {
case ListOrderMode.ascending:
return S.current.ascending;
case ListOrderMode.descending:
return S.current.descending;
default:
return '';
}
}
}
+2
View File
@@ -20,6 +20,8 @@ class PreferencesKey {
static const disableBuyKey = 'disable_buy';
static const disableSellKey = 'disable_sell';
static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order';
static const walletListAscending = 'wallet_list_ascending';
static const currentFiatApiModeKey = 'current_fiat_api_mode';
static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication';
static const useTOTP2FA = 'use_totp_2fa';
+22
View File
@@ -0,0 +1,22 @@
import 'package:cake_wallet/generated/i18n.dart';
enum WalletListOrderType {
CreationDate,
Alphabetical,
GroupByType,
Custom;
@override
String toString() {
switch (this) {
case WalletListOrderType.CreationDate:
return S.current.creation_date;
case WalletListOrderType.Alphabetical:
return S.current.alphabetical;
case WalletListOrderType.GroupByType:
return S.current.group_by_type;
case WalletListOrderType.Custom:
return S.current.custom_drag;
}
}
}
@@ -0,0 +1,158 @@
import 'package:cake_wallet/entities/list_order_mode.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/src/widgets/section_divider.dart';
import 'package:cake_wallet/themes/extensions/menu_theme.dart';
import 'package:cake_wallet/view_model/settings/choices_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
class FilterListWidget extends StatefulWidget {
FilterListWidget({
required this.initalType,
required this.initalAscending,
required this.onClose,
});
final WalletListOrderType? initalType;
final bool initalAscending;
final Function(bool, WalletListOrderType) onClose;
@override
FilterListWidgetState createState() => FilterListWidgetState();
}
class FilterListWidgetState extends State<FilterListWidget> {
late bool ascending;
late WalletListOrderType? type;
@override
void initState() {
super.initState();
ascending = widget.initalAscending;
type = widget.initalType;
}
void setSelectedOrderType(WalletListOrderType? orderType) {
setState(() {
type = orderType;
});
}
@override
Widget build(BuildContext context) {
const sectionDivider = const HorizontalSectionDivider();
return PickerWrapperWidget(
onClose: () {
widget.onClose(ascending, type!);
Navigator.of(context).pop();
},
children: [
Padding(
padding: EdgeInsets.only(left: 24, right: 24, top: 24),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(24)),
child: Container(
color: Theme.of(context).extension<CakeMenuTheme>()!.backgroundColor,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Padding(
padding: EdgeInsets.all(24.0),
child: Text(
S.of(context).order_by,
style: TextStyle(
color:
Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
fontSize: 16,
fontFamily: 'Lato',
decoration: TextDecoration.none,
),
),
),
if (type != WalletListOrderType.Custom) ...[
sectionDivider,
SettingsChoicesCell(
ChoicesListItem<ListOrderMode>(
title: "",
items: ListOrderMode.all,
selectedItem: ascending ? ListOrderMode.ascending : ListOrderMode.descending,
onItemSelected: (ListOrderMode listOrderMode) {
setState(() {
ascending = listOrderMode == ListOrderMode.ascending;
});
},
),
),
],
sectionDivider,
RadioListTile(
value: WalletListOrderType.CreationDate,
groupValue: type,
title: Text(
WalletListOrderType.CreationDate.toString(),
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.bold,
decoration: TextDecoration.none),
),
onChanged: setSelectedOrderType,
activeColor: Theme.of(context).primaryColor,
),
RadioListTile(
value: WalletListOrderType.Alphabetical,
groupValue: type,
title: Text(
WalletListOrderType.Alphabetical.toString(),
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.bold,
decoration: TextDecoration.none),
),
onChanged: setSelectedOrderType,
activeColor: Theme.of(context).primaryColor,
),
RadioListTile(
value: WalletListOrderType.GroupByType,
groupValue: type,
title: Text(
WalletListOrderType.GroupByType.toString(),
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.bold,
decoration: TextDecoration.none),
),
onChanged: setSelectedOrderType,
activeColor: Theme.of(context).primaryColor,
),
RadioListTile(
value: WalletListOrderType.Custom,
groupValue: type,
title: Text(
WalletListOrderType.Custom.toString(),
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.bold,
decoration: TextDecoration.none),
),
onChanged: setSelectedOrderType,
activeColor: Theme.of(context).primaryColor,
),
]),
),
),
)
],
);
}
}
@@ -17,19 +17,21 @@ class SettingsChoicesCell extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
choicesListItem.title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
if (choicesListItem.title.isNotEmpty) ...[
Row(
children: [
Text(
choicesListItem.title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
],
),
const SizedBox(height: 24),
],
),
const SizedBox(height: 24),
],
Center(
child: Container(
decoration: BoxDecoration(
@@ -49,9 +51,7 @@ class SettingsChoicesCell extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: isSelected
? Theme.of(context).primaryColor
: null,
color: isSelected ? Theme.of(context).primaryColor : null,
),
child: Center(
child: Text(
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class FilteredList extends StatefulWidget {
FilteredList({
required this.list,
required this.itemBuilder,
required this.updateFunction,
});
final ObservableList<dynamic> list;
final Widget Function(BuildContext, int) itemBuilder;
final Function updateFunction;
@override
FilteredListState createState() => FilteredListState();
}
class FilteredListState extends State<FilteredList> {
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) => ReorderableListView.builder(
physics: const BouncingScrollPhysics(),
itemBuilder: widget.itemBuilder,
itemCount: widget.list.length,
onReorder: (int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final dynamic item = widget.list.removeAt(oldIndex);
widget.list.insert(newIndex, item);
widget.updateFunction();
},
),
);
}
}
@@ -1,8 +1,15 @@
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart';
import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/themes/extensions/filter_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:another_flushbar/flushbar.dart';
import 'package:flutter/material.dart';
@@ -28,6 +35,53 @@ class WalletListPage extends BasePage {
@override
Widget body(BuildContext context) =>
WalletListBody(walletListViewModel: walletListViewModel, authService: authService);
@override
Widget trailing(BuildContext context) {
final filterIcon = Image.asset('assets/images/filter_icon.png',
color: Theme.of(context).extension<FilterTheme>()!.iconColor);
return MergeSemantics(
child: SizedBox(
height: 37,
width: 37,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
container: true,
child: GestureDetector(
onTap: () async {
await showPopUp<void>(
context: context,
builder: (context) => FilterListWidget(
initalType: walletListViewModel.orderType,
initalAscending: walletListViewModel.ascending,
onClose: (bool ascending, WalletListOrderType type) async {
walletListViewModel.setAscending(ascending);
await walletListViewModel.setOrderType(type);
},
),
);
},
child: Semantics(
label: 'Transaction Filter',
button: true,
enabled: true,
child: Container(
height: 36,
width: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).extension<FilterTheme>()!.buttonColor,
),
child: filterIcon,
),
),
),
),
),
),
);
}
}
class WalletListBody extends StatefulWidget {
@@ -70,11 +124,9 @@ class WalletListBodyState extends State<WalletListBody> {
Expanded(
child: Container(
child: Observer(
builder: (_) => ListView.separated(
physics: const BouncingScrollPhysics(),
separatorBuilder: (_, index) =>
Divider(color: Theme.of(context).colorScheme.background, height: 32),
itemCount: widget.walletListViewModel.wallets.length,
builder: (_) => FilteredList(
list: widget.walletListViewModel.wallets,
updateFunction: widget.walletListViewModel.reorderAccordingToWalletList,
itemBuilder: (__, index) {
final wallet = widget.walletListViewModel.wallets[index];
final currentColor = wallet.isCurrent
@@ -83,6 +135,7 @@ class WalletListBodyState extends State<WalletListBody> {
.createNewWalletButtonBackgroundColor
: Theme.of(context).colorScheme.background;
final row = GestureDetector(
key: ValueKey(wallet.name),
onTap: () => wallet.isCurrent ? null : _loadWallet(wallet),
child: Container(
height: tileHeight,
@@ -117,7 +170,7 @@ class WalletListBodyState extends State<WalletListBody> {
maxLines: null,
softWrap: true,
style: TextStyle(
fontSize: 22,
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<CakeTextTheme>()!
@@ -137,13 +190,15 @@ class WalletListBodyState extends State<WalletListBody> {
return wallet.isCurrent
? row
: Row(
key: ValueKey(wallet.name),
children: [
Expanded(child: row),
GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit,
arguments: [widget.walletListViewModel, wallet]),
child: Container(
padding: EdgeInsets.only(right: 20),
padding: EdgeInsets.only(
right: DeviceInfo.instance.isMobile ? 20 : 40),
child: Center(
child: Container(
height: 40,
+3 -2
View File
@@ -4,10 +4,11 @@ import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/alert_close_button.dart';
class PickerWrapperWidget extends StatelessWidget {
PickerWrapperWidget({required this.children, this.hasTitle = false});
PickerWrapperWidget({required this.children, this.hasTitle = false, this.onClose});
final List<Widget> children;
final bool hasTitle;
final Function()? onClose;
@override
Widget build(BuildContext context) {
@@ -45,7 +46,7 @@ class PickerWrapperWidget extends StatelessWidget {
children: children,
),
SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight),
AlertCloseButton(bottom: closeButtonBottom),
AlertCloseButton(bottom: closeButtonBottom, onTap: onClose),
],
),
),
+30 -4
View File
@@ -12,6 +12,7 @@ import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/seed_phrase_length.dart';
import 'package:cake_wallet/entities/seed_type.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
@@ -53,7 +54,8 @@ abstract class SettingsStoreBase with Store {
required bool initialAppSecure,
required bool initialDisableBuy,
required bool initialDisableSell,
required BuyProviderType initialDefaultBuyProvider,
required WalletListOrderType initialWalletListOrder,
required bool initialWalletListAscending,
required FiatApiMode initialFiatMode,
required bool initialAllowBiometricalAuthentication,
required String initialTotpSecretKey,
@@ -122,6 +124,8 @@ abstract class SettingsStoreBase with Store {
isAppSecure = initialAppSecure,
disableBuy = initialDisableBuy,
disableSell = initialDisableSell,
walletListOrder = initialWalletListOrder,
walletListAscending = initialWalletListAscending,
shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
exchangeStatus = initialExchangeStatus,
currentTheme = initialTheme,
@@ -257,6 +261,16 @@ abstract class SettingsStoreBase with Store {
}
);
reaction(
(_) => walletListOrder,
(WalletListOrderType walletListOrder) =>
sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index));
reaction(
(_) => walletListAscending,
(bool walletListAscending) =>
sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending));
reaction(
(_) => autoGenerateSubaddressStatus,
(AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt(
@@ -504,6 +518,12 @@ abstract class SettingsStoreBase with Store {
@observable
bool disableSell;
@observable
WalletListOrderType walletListOrder;
@observable
bool walletListAscending;
@observable
bool allowBiometricalAuthentication;
@@ -717,8 +737,10 @@ abstract class SettingsStoreBase with Store {
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false;
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
final defaultBuyProvider =
BuyProviderType.values[sharedPreferences.getInt(PreferencesKey.defaultBuyProvider) ?? 0];
final walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
final walletListAscending =
sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
final currentFiatApiMode = FiatApiMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ??
FiatApiMode.enabled.raw);
@@ -897,7 +919,8 @@ abstract class SettingsStoreBase with Store {
initialAppSecure: isAppSecure,
initialDisableBuy: disableBuy,
initialDisableSell: disableSell,
initialDefaultBuyProvider: defaultBuyProvider,
initialWalletListOrder: walletListOrder,
initialWalletListAscending: walletListAscending,
initialFiatMode: currentFiatApiMode,
initialAllowBiometricalAuthentication: allowBiometricalAuthentication,
initialCake2FAPresetOptions: selectedCake2FAPreset,
@@ -1013,6 +1036,9 @@ abstract class SettingsStoreBase with Store {
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
allowBiometricalAuthentication =
sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ??
allowBiometricalAuthentication;
+1 -1
View File
@@ -78,7 +78,7 @@ class BrightTheme extends LightTheme {
FilterTheme get filterTheme => super.filterTheme.copyWith(
checkboxSecondGradientColor: Palette.pinkFlamingo,
checkboxBackgroundColor: Colors.white,
buttonColor: Colors.white.withOpacity(0.2),
buttonColor: Palette.darkGray.withOpacity(0.2),
iconColor: Colors.white);
@override
@@ -1,5 +1,6 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.dart';
@@ -19,7 +20,7 @@ abstract class WalletListViewModelBase with Store {
this._walletLoadingService,
this._authService,
) : wallets = ObservableList<WalletListItem>() {
updateList();
setOrderType(_appStore.settingsStore.walletListOrder);
reaction((_) => _appStore.wallet, (_) => updateList());
}
@@ -43,11 +44,14 @@ abstract class WalletListViewModelBase with Store {
@action
Future<void> loadWallet(WalletListItem walletItem) async {
final wallet =
await _walletLoadingService.load(walletItem.type, walletItem.name);
final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name);
_appStore.changeCurrentWallet(wallet);
}
WalletListOrderType? get orderType => _appStore.settingsStore.walletListOrder;
bool get ascending => _appStore.settingsStore.walletListAscending;
@action
void updateList() {
wallets.clear();
@@ -57,14 +61,105 @@ abstract class WalletListViewModelBase with Store {
name: info.name,
type: info.type,
key: info.key,
isCurrent: info.name == _appStore.wallet?.name &&
info.type == _appStore.wallet?.type,
isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type,
isEnabled: availableWalletTypes.contains(info.type),
),
),
);
}
Future<void> reorderAccordingToWalletList() async {
if (wallets.isEmpty) {
updateList();
return;
}
_appStore.settingsStore.walletListOrder = WalletListOrderType.Custom;
// make a copy of the walletInfoSource:
List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
// delete all wallets from walletInfoSource:
await _walletInfoSource.clear();
// add wallets from wallets list in order of wallets list, by name:
for (WalletListItem wallet in wallets) {
for (int i = 0; i < walletInfoSourceCopy.length; i++) {
if (walletInfoSourceCopy[i].name == wallet.name) {
await _walletInfoSource.add(walletInfoSourceCopy[i]);
walletInfoSourceCopy.removeAt(i);
break;
}
}
}
updateList();
}
Future<void> sortGroupByType() async {
// sort the wallets by type:
List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
await _walletInfoSource.clear();
if (ascending) {
walletInfoSourceCopy.sort((a, b) => a.type.toString().compareTo(b.type.toString()));
} else {
walletInfoSourceCopy.sort((a, b) => b.type.toString().compareTo(a.type.toString()));
}
await _walletInfoSource.addAll(walletInfoSourceCopy);
updateList();
}
Future<void> sortAlphabetically() async {
// sort the wallets alphabetically:
List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
await _walletInfoSource.clear();
if (ascending) {
walletInfoSourceCopy.sort((a, b) => a.name.compareTo(b.name));
} else {
walletInfoSourceCopy.sort((a, b) => b.name.compareTo(a.name));
}
await _walletInfoSource.addAll(walletInfoSourceCopy);
updateList();
}
Future<void> sortByCreationDate() async {
// sort the wallets by creation date:
List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
await _walletInfoSource.clear();
if (ascending) {
walletInfoSourceCopy.sort((a, b) => a.date.compareTo(b.date));
} else {
walletInfoSourceCopy.sort((a, b) => b.date.compareTo(a.date));
}
await _walletInfoSource.addAll(walletInfoSourceCopy);
updateList();
}
void setAscending(bool ascending) {
_appStore.settingsStore.walletListAscending = ascending;
}
Future<void> setOrderType(WalletListOrderType? type) async {
if (type == null) return;
_appStore.settingsStore.walletListOrder = type;
switch (type) {
case WalletListOrderType.CreationDate:
await sortByCreationDate();
break;
case WalletListOrderType.Alphabetical:
await sortAlphabetically();
break;
case WalletListOrderType.GroupByType:
await sortGroupByType();
break;
case WalletListOrderType.Custom:
default:
await reorderAccordingToWalletList();
break;
}
}
bool checkIfAuthRequired() {
return _authService.requireAuth();
}