ordinals filter view

This commit is contained in:
julian 2023-07-18 11:48:15 -06:00
parent 8b71fa70c9
commit 2af1a8db88
3 changed files with 901 additions and 4 deletions

View file

@ -0,0 +1,890 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class OrdinalFilter {
final bool isMoonbird;
final bool isPunk;
final DateTime? from;
final DateTime? to;
final String? inscription;
final String keyword;
OrdinalFilter({
required this.isMoonbird,
required this.isPunk,
required this.from,
required this.to,
required this.inscription,
required this.keyword,
});
OrdinalFilter copyWith({
bool? isMoonbird,
bool? isPunk,
DateTime? from,
DateTime? to,
String? inscription,
String? keyword,
}) {
return OrdinalFilter(
isMoonbird: isMoonbird ?? this.isMoonbird,
isPunk: isPunk ?? this.isPunk,
from: from ?? this.from,
to: to ?? this.to,
inscription: inscription ?? this.inscription,
keyword: keyword ?? this.keyword,
);
}
}
final ordinalFilterProvider = StateProvider<OrdinalFilter?>((_) => null);
class OrdinalsFilterView extends ConsumerStatefulWidget {
const OrdinalsFilterView({
Key? key,
}) : super(key: key);
static const String routeName = "/ordinalsFilterView";
@override
ConsumerState<OrdinalsFilterView> createState() => _OrdinalsFilterViewState();
}
class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
final _inscriptionTextEditingController = TextEditingController();
final _keywordTextEditingController = TextEditingController();
bool _isPunk = false;
bool _isMoonbird = false;
String _fromDateString = "";
String _toDateString = "";
final keywordTextFieldFocusNode = FocusNode();
final inscriptionTextFieldFocusNode = FocusNode();
late Color baseColor;
@override
initState() {
baseColor = ref.read(themeProvider.state).state.textSubtitle2;
final filterState = ref.read(ordinalFilterProvider.state).state;
if (filterState != null) {
_isMoonbird = filterState.isMoonbird;
_isPunk = filterState.isPunk;
_selectedToDate = filterState.to;
_selectedFromDate = filterState.from;
_keywordTextEditingController.text = filterState.keyword;
_inscriptionTextEditingController.text = filterState.inscription ?? "";
}
super.initState();
}
@override
dispose() {
_inscriptionTextEditingController.dispose();
_keywordTextEditingController.dispose();
keywordTextFieldFocusNode.dispose();
inscriptionTextFieldFocusNode.dispose();
super.dispose();
}
// The following two getters are not required if the
// date fields are to remain unclearable.
Widget get _dateFromText {
final isDateSelected = _fromDateString.isEmpty;
return Text(
isDateSelected ? "From..." : _fromDateString,
style: STextStyles.fieldLabel(context).copyWith(
color: isDateSelected
? Theme.of(context).extension<StackColors>()!.textSubtitle2
: Theme.of(context).extension<StackColors>()!.accentColorDark),
);
}
Widget get _dateToText {
final isDateSelected = _toDateString.isEmpty;
return Text(
isDateSelected ? "To..." : _toDateString,
style: STextStyles.fieldLabel(context).copyWith(
color: isDateSelected
? Theme.of(context).extension<StackColors>()!.textSubtitle2
: Theme.of(context).extension<StackColors>()!.accentColorDark),
);
}
DateTime? _selectedFromDate = DateTime(2007);
DateTime? _selectedToDate = DateTime.now();
MaterialRoundedDatePickerStyle _buildDatePickerStyle() {
return MaterialRoundedDatePickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
// backgroundHeader: Theme.of(context).extension<StackColors>()!.textSubtitle2,
paddingMonthHeader: const EdgeInsets.only(top: 11),
colorArrowNext: Theme.of(context).extension<StackColors>()!.textSubtitle1,
colorArrowPrevious:
Theme.of(context).extension<StackColors>()!.textSubtitle1,
textStyleButtonNegative: STextStyles.datePicker600(context).copyWith(
color: baseColor,
),
textStyleButtonPositive: STextStyles.datePicker600(context).copyWith(
color: baseColor,
),
textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context),
textStyleDayHeader: STextStyles.datePicker600(context),
textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith(
color: baseColor,
),
textStyleDayOnCalendarDisabled:
STextStyles.datePicker400(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle3,
),
textStyleDayOnCalendarSelected:
STextStyles.datePicker400(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
textStyleYearButton: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
// textStyleButtonAction: GoogleFonts.inter(),
);
}
MaterialRoundedYearPickerStyle _buildYearPickerStyle() {
return MaterialRoundedYearPickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
textStyleYear: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle2,
fontSize: 16,
),
textStyleYearSelected: STextStyles.datePicker600(context).copyWith(
fontSize: 18,
),
);
}
Widget _buildDateRangePicker() {
const middleSeparatorPadding = 2.0;
const middleSeparatorWidth = 12.0;
final isDesktop = Util.isDesktop;
final width = isDesktop
? null
: (MediaQuery.of(context).size.width -
(middleSeparatorWidth +
(2 * middleSeparatorPadding) +
(2 * Constants.size.standardPadding))) /
2;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: GestureDetector(
key: const Key("OrdinalsViewFromDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 125));
}
if (mounted) {
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
initialDate: DateTime.now(),
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
if (date != null) {
_selectedFromDate = date;
// flag to adjust date so from date is always before to date
final flag = _selectedToDate != null &&
!_selectedFromDate!.isBefore(_selectedToDate!);
if (flag) {
_selectedToDate = DateTime.fromMillisecondsSinceEpoch(
_selectedFromDate!.millisecondsSinceEpoch);
}
setState(() {
if (flag) {
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
}
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
});
}
}
},
child: Container(
width: width,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius:
BorderRadius.circular(Constants.size.circularBorderRadius),
border: Border.all(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
width: 1,
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: isDesktop ? 17 : 12,
),
child: Row(
children: [
SvgPicture.asset(
Assets.svg.calendar,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
const SizedBox(
width: 10,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: _dateFromText,
),
)
],
),
),
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: middleSeparatorPadding),
child: Container(
width: middleSeparatorWidth,
// height: 1,
// color: CFColors.smoke,
),
),
Expanded(
child: GestureDetector(
key: const Key("OrdinalsViewToDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 125));
}
if (mounted) {
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
initialDate: DateTime.now(),
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
if (date != null) {
_selectedToDate = date;
// flag to adjust date so from date is always before to date
final flag = _selectedFromDate != null &&
!_selectedToDate!.isAfter(_selectedFromDate!);
if (flag) {
_selectedFromDate = DateTime.fromMillisecondsSinceEpoch(
_selectedToDate!.millisecondsSinceEpoch);
}
setState(() {
if (flag) {
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
}
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
});
}
}
},
child: Container(
width: width,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius:
BorderRadius.circular(Constants.size.circularBorderRadius),
border: Border.all(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
width: 1,
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: isDesktop ? 17 : 12,
),
child: Row(
children: [
SvgPicture.asset(
Assets.svg.calendar,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
const SizedBox(
width: 10,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: _dateToText,
),
)
],
),
),
),
),
),
if (isDesktop)
const SizedBox(
width: 24,
),
],
);
}
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return DesktopDialog(
maxWidth: 576,
maxHeight: double.infinity,
child: Padding(
padding: const EdgeInsets.only(
left: 32,
bottom: 32,
),
child: _buildContent(context),
),
);
} else {
return Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Ordinals filter",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: EdgeInsets.symmetric(
horizontal: Constants.size.standardPadding,
),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: _buildContent(context),
),
),
);
},
),
),
),
);
}
}
Widget _buildContent(BuildContext context) {
final isDesktop = Util.isDesktop;
return Column(
children: [
if (isDesktop)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Ordinals filter",
style: STextStyles.desktopH3(context),
textAlign: TextAlign.center,
),
const DesktopDialogCloseButton(),
],
),
SizedBox(
height: isDesktop ? 14 : 10,
),
if (!isDesktop)
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Text(
"Collection",
style: STextStyles.smallMed12(context),
),
),
),
if (!isDesktop)
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
padding: EdgeInsets.all(isDesktop ? 0 : 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
GestureDetector(
onTap: () {
setState(() {
_isPunk = !_isPunk;
});
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
SizedBox(
height: 20,
width: 20,
child: Checkbox(
key: const Key("OrdinalsPunkCheckboxKey"),
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
value: _isPunk,
onChanged: (newValue) {
setState(() {
_isPunk = newValue!;
});
},
),
),
const SizedBox(
width: 14,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Column(
children: [
Text(
"Punks",
style: isDesktop
? STextStyles.desktopTextSmall(context)
: STextStyles.itemSubtitle12(context),
),
if (isDesktop)
const SizedBox(
height: 4,
),
],
),
),
)
],
),
),
),
],
),
SizedBox(
height: isDesktop ? 4 : 10,
),
Row(
children: [
GestureDetector(
onTap: () {
setState(() {
_isMoonbird = !_isMoonbird;
});
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
SizedBox(
height: 20,
width: 20,
child: Checkbox(
key: const Key(
"OrdinalsFilterMoonbirdCheckboxKey",
),
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
value: _isMoonbird,
onChanged: (newValue) {
setState(() {
_isMoonbird = newValue!;
});
},
),
),
const SizedBox(
width: 14,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Column(
children: [
Text(
"Moonbirds",
style: isDesktop
? STextStyles.desktopTextSmall(context)
: STextStyles.itemSubtitle12(context),
),
if (isDesktop)
const SizedBox(
height: 4,
),
],
),
),
)
],
),
),
),
],
),
],
),
),
SizedBox(
height: isDesktop ? 32 : 24,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Text(
"Date",
style: isDesktop
? STextStyles.labelExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
),
),
SizedBox(
height: isDesktop ? 10 : 8,
),
_buildDateRangePicker(),
SizedBox(
height: isDesktop ? 32 : 24,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Text(
"Inscription",
style: isDesktop
? STextStyles.labelExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
),
),
SizedBox(
height: isDesktop ? 10 : 8,
),
Padding(
padding: EdgeInsets.only(right: isDesktop ? 32 : 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("OrdinalsInscriptionFieldKey"),
controller: _inscriptionTextEditingController,
focusNode: inscriptionTextFieldFocusNode,
onChanged: (_) => setState(() {}),
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark,
height: 1.8,
)
: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter inscription number...",
keywordTextFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
)
: null,
suffixIcon: _inscriptionTextEditingController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_inscriptionTextEditingController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
SizedBox(
height: isDesktop ? 32 : 24,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Text(
"Keyword",
style: isDesktop
? STextStyles.labelExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
),
),
SizedBox(
height: isDesktop ? 10 : 8,
),
Padding(
padding: EdgeInsets.only(right: isDesktop ? 32 : 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("OrdinalsViewKeywordFieldKey"),
controller: _keywordTextEditingController,
focusNode: keywordTextFieldFocusNode,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark,
height: 1.8,
)
: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type keyword...",
keywordTextFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
)
: null,
suffixIcon: _keywordTextEditingController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_keywordTextEditingController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
if (!isDesktop) const Spacer(),
SizedBox(
height: isDesktop ? 32 : 20,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: isDesktop ? ButtonHeight.l : null,
onPressed: () async {
if (!isDesktop) {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75,
),
);
}
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: isDesktop ? ButtonHeight.l : null,
onPressed: () async {
await _onApplyPressed();
},
label: "Save",
),
),
if (isDesktop)
const SizedBox(
width: 32,
),
],
),
if (!isDesktop)
const SizedBox(
height: 20,
),
],
);
}
Future<void> _onApplyPressed() async {
final filter = OrdinalFilter(
isPunk: _isPunk,
isMoonbird: _isMoonbird,
from: _selectedFromDate,
to: _selectedToDate,
inscription: _inscriptionTextEditingController.text,
keyword: _keywordTextEditingController.text,
);
ref.read(ordinalFilterProvider.state).state = filter;
Navigator.of(context).pop();
}
}

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/ordinal.dart';
import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart';
import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
@ -104,7 +105,9 @@ class _OrdinalsViewState extends ConsumerState<OrdinalsView> {
.topNavIconPrimary,
),
onPressed: () {
// todo filter view
Navigator.of(context).pushNamed(
OrdinalsFilterView.routeName,
);
},
),
),

View file

@ -59,6 +59,7 @@ import 'package:stackwallet/pages/intro_view.dart';
import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart';
import 'package:stackwallet/pages/notification_views/notifications_view.dart';
import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart';
import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart';
import 'package:stackwallet/pages/ordinals/ordinals_view.dart';
import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
@ -422,9 +423,6 @@ class RouteGenerator {
return _routeError("${settings.name} invalid args: ${args.toString()}");
case OrdinalDetailsView.routeName:
print(args.runtimeType);
if (args is ({Ordinal ordinal, String walletId})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@ -439,6 +437,12 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case OrdinalsFilterView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const OrdinalsFilterView(),
settings: RouteSettings(name: settings.name));
case UtxoDetailsView.routeName:
if (args is Tuple2<Id, String>) {
return getRoute(