utxo details ui and edit functionality

This commit is contained in:
julian 2023-03-07 11:11:57 -06:00
parent 6e547d6f34
commit e40cb3d66d
8 changed files with 670 additions and 45 deletions

View file

@ -173,7 +173,14 @@ class MainDB {
if (storedUtxo != null) {
// update
set.remove(utxo);
set.add(storedUtxo);
set.add(
storedUtxo.copyWith(
value: utxo.value,
address: utxo.address,
blockTime: utxo.blockTime,
blockHeight: utxo.blockHeight,
),
);
}
}

View file

@ -67,6 +67,38 @@ class UTXO {
return confirmations >= minimumConfirms;
}
UTXO copyWith({
Id? id,
String? walletId,
String? txid,
int? vout,
int? value,
String? name,
bool? isBlocked,
String? blockedReason,
bool? isCoinbase,
String? blockHash,
int? blockHeight,
int? blockTime,
String? address,
String? otherData,
}) =>
UTXO(
walletId: walletId ?? this.walletId,
txid: txid ?? this.txid,
vout: vout ?? this.vout,
value: value ?? this.value,
name: name ?? this.name,
isBlocked: isBlocked ?? this.isBlocked,
blockedReason: blockedReason ?? this.blockedReason,
isCoinbase: isCoinbase ?? this.isCoinbase,
blockHash: blockHash ?? this.blockHash,
blockHeight: blockHeight ?? this.blockHeight,
blockTime: blockTime ?? this.blockTime,
address: address ?? this.address,
otherData: otherData ?? this.otherData,
)..id = id ?? this.id;
@override
String toString() => "{ "
"id: $id, "

View file

@ -42,8 +42,6 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
.idProperty()
.findAllSync();
print(ids);
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -120,14 +118,17 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
key: Key("${utxo.walletId}_${utxo.id}"),
walletId: widget.walletId,
utxo: utxo,
onPressed: () {
Navigator.of(context).pushNamed(
onPressed: () async {
final result = await Navigator.of(context).pushNamed(
UtxoDetailsView.routeName,
arguments: Tuple2(
utxo.id,
widget.walletId,
),
);
if (mounted && result == "refresh") {
setState(() {});
}
},
);
},

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
@ -6,9 +8,15 @@ import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class UtxoDetailsView extends ConsumerStatefulWidget {
@ -28,10 +36,18 @@ class UtxoDetailsView extends ConsumerStatefulWidget {
}
class _UtxoDetailsViewState extends ConsumerState<UtxoDetailsView> {
late Stream<UTXO?> stream;
static const double _spacing = 12;
late Stream<UTXO?> stream;
UTXO? utxo;
bool _popWithRefresh = false;
Future<void> _toggleFreeze() async {
_popWithRefresh = true;
await MainDB.instance.putUTXO(utxo!.copyWith(isBlocked: !utxo!.isBlocked));
}
@override
void initState() {
utxo = MainDB.instance.isar.utxos
@ -46,53 +62,294 @@ class _UtxoDetailsViewState extends ConsumerState<UtxoDetailsView> {
@override
Widget build(BuildContext context) {
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).coin));
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
final coin = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value.getManager(widget.walletId).coin,
),
);
final currentHeight = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value.getManager(widget.walletId).currentHeight,
),
);
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop(_popWithRefresh ? "refresh" : null);
},
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
),
);
},
),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: StreamBuilder<UTXO?>(
stream: stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
utxo = snapshot.data!;
}
),
child: StreamBuilder<UTXO?>(
stream: stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
utxo = snapshot.data!;
}
return Column(
children: [
const SizedBox(
height: 10,
),
RoundedWhiteContainer(
child: Row(
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 10,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${Format.satoshisToAmount(
utxo!.value,
coin: coin,
).toStringAsFixed(
coin.decimals,
)} ${coin.ticker}",
style: STextStyles.pageTitleH2(context),
),
Text(
utxo!.isBlocked ? "Frozen" : "Available",
style: STextStyles.w500_14(context).copyWith(
color: utxo!.isBlocked
? Theme.of(context)
.extension<StackColors>()!
.accentColorBlue
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${Format.satoshisToAmount(
utxo!.value,
coin: coin,
).toStringAsFixed(
coin.decimals,
)} ${coin.ticker}",
"Address",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleCopyButton(
data: utxo!.address!,
),
],
),
)
],
);
},
),
),
const SizedBox(
height: 4,
),
Text(
utxo!.address!,
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Transaction ID",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleCopyButton(
data: utxo!.txid,
),
],
),
const SizedBox(
height: 4,
),
Text(
utxo!.txid,
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Confirmations",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
const SizedBox(
height: 4,
),
Text(
"${currentHeight - utxo!.blockHeight!}",
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Note",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleEditButton(
editValue: utxo!.name,
editLabel: "note",
onValueChanged: (newName) {
MainDB.instance.putUTXO(
utxo!.copyWith(
name: newName,
),
);
},
),
],
),
const SizedBox(
height: 4,
),
Text(
utxo!.name,
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
if (utxo!.isBlocked)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Freeze reason",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleEditButton(
editValue: utxo!.blockedReason ?? "",
editLabel: "freeze reason",
onValueChanged: (newReason) {
MainDB.instance.putUTXO(
utxo!.copyWith(
blockedReason: newReason,
),
);
},
),
],
),
const SizedBox(
height: 4,
),
Text(
utxo!.blockedReason ?? "",
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
],
),
const Spacer(),
SecondaryButton(
label: utxo!.isBlocked ? "Unfreeze" : "Freeze",
onPressed: _toggleFreeze,
),
const SizedBox(
height: 16,
),
],
);
},
),
);
}

View file

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/cli_commands.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class SingleFieldEditView extends StatefulWidget {
const SingleFieldEditView({
Key? key,
required this.initialValue,
required this.label,
}) : super(key: key);
static const String routeName = "/singleFieldEdit";
final String initialValue;
final String label;
@override
State<SingleFieldEditView> createState() => _SingleFieldEditViewState();
}
class _SingleFieldEditViewState extends State<SingleFieldEditView> {
late final TextEditingController _textController;
final _textFocusNode = FocusNode();
late final bool isDesktop;
@override
void initState() {
isDesktop = Util.isDesktop;
_textController = TextEditingController()..text = widget.initialValue;
super.initState();
}
@override
void dispose() {
_textController.dispose();
_textFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !isDesktop,
builder: (child) => 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(
"Edit ${widget.label}",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!isDesktop)
const SizedBox(
height: 10,
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
left: 32,
bottom: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Edit ${widget.label}",
style: STextStyles.desktopH3(context),
),
const DesktopDialogCloseButton(),
],
),
),
Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
horizontal: 32,
)
: const EdgeInsets.all(0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _textController,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
focusNode: _textFocusNode,
decoration: standardInputDecoration(
widget.label.capitalize(),
_textFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: _textController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_textController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
// if (!isDesktop)
const Spacer(),
ConditionalParent(
condition: isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.all(32),
child: child,
),
child: PrimaryButton(
label: "Save",
onPressed: () {
if (mounted) {
Navigator.of(context).pop(_textController.text);
}
},
),
)
],
),
);
}
}

View file

@ -39,6 +39,7 @@ import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view.
import 'package:stackwallet/pages/exchange_view/send_from_view.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart';
import 'package:stackwallet/pages/generic/single_field_edit_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/intro_view.dart';
import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart';
@ -201,6 +202,21 @@ class RouteGenerator {
builder: (_) => const AddWalletView(),
settings: RouteSettings(name: settings.name));
case SingleFieldEditView.routeName:
if (args is Tuple2<String, String>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => SingleFieldEditView(
initialValue: args.item1,
label: args.item2,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case CoinControlView.routeName:
if (args is String) {
return getRoute(

View file

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class SimpleCopyButton extends StatelessWidget {
const SimpleCopyButton({
Key? key,
required this.data,
}) : super(key: key);
final String data;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
await Clipboard.setData(ClipboardData(text: data));
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
},
child: Row(
children: [
SvgPicture.asset(
Assets.svg.copy,
width: 10,
height: 10,
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
),
const SizedBox(
width: 4,
),
Text(
"Copy",
style: STextStyles.link2(context),
),
],
),
);
}
}

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/generic/single_field_edit_view.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:tuple/tuple.dart';
class SimpleEditButton extends StatelessWidget {
const SimpleEditButton({
Key? key,
required this.editValue,
required this.editLabel,
required this.onValueChanged,
}) : super(key: key);
final String editValue;
final String editLabel;
final void Function(String) onValueChanged;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
final result = await Navigator.of(context).pushNamed(
SingleFieldEditView.routeName,
arguments: Tuple2(
editValue,
editLabel,
),
);
if (result is String && result != editValue) {
onValueChanged(result);
}
},
child: Row(
children: [
SvgPicture.asset(
Assets.svg.pencil,
width: 10,
height: 10,
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
),
const SizedBox(
width: 4,
),
Text(
"Edit",
style: STextStyles.link2(context),
),
],
),
);
}
}