mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 03:49:22 +00:00
feat: rough "churning" for xmr/wow
This commit is contained in:
parent
a03b0ec2aa
commit
cfc5e88c2a
20 changed files with 1942 additions and 3 deletions
3
asset_sources/svg/campfire/churn.svg
Normal file
3
asset_sources/svg/campfire/churn.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.77444 13.4823C9.60687 13.4092 9.43072 13.3749 9.25027 13.3749H3.75081C2.99076 13.3749 2.37594 13.9897 2.37594 14.7497C2.37594 15.5098 2.99076 16.1246 3.75081 16.1246H5.93126L1.40279 20.6531C0.865736 21.1901 0.865736 22.0602 1.40279 22.5972C1.93985 23.1343 2.80988 23.1343 3.34694 22.5972L7.8754 18.0709V20.2492C7.8754 21.0092 8.49023 21.6241 9.25027 21.6241C10.0103 21.6241 10.6251 21.0092 10.6251 20.2492V14.7497C10.6251 14.5708 10.5887 14.3926 10.5192 14.2247C10.3802 13.8861 10.1139 13.6198 9.77444 13.4823ZM14.2256 10.5177C14.3931 10.5908 14.5693 10.6251 14.7497 10.6251H20.2492C21.0092 10.6251 21.6241 10.0103 21.6241 9.25027C21.6241 8.49023 21.0092 7.8754 20.2492 7.8754H18.0687L22.5972 3.34694C23.1343 2.80988 23.1343 1.93985 22.5972 1.40279C22.0606 0.866166 21.1905 0.865306 20.6531 1.40279L16.1246 5.93341V3.75081C16.1246 2.99076 15.5098 2.37594 14.7497 2.37594C13.9897 2.37594 13.3749 2.99076 13.3749 3.75081V9.25027C13.3749 9.42917 13.4113 9.60739 13.4807 9.7753C13.6198 10.1139 13.8861 10.3802 14.2256 10.5177ZM9.25027 2.37594C8.4898 2.37594 7.8754 2.99076 7.8754 3.75081V5.93126L3.34823 1.40387C2.81117 0.86681 1.94114 0.86681 1.40408 1.40387C0.867025 1.94092 0.867025 2.81096 1.40408 3.34801L5.93341 7.8754H3.75081C2.99076 7.8754 2.37594 8.4898 2.37594 9.25027C2.37594 10.0107 2.99076 10.6251 3.75081 10.6251H9.25027C9.42917 10.6251 9.60739 10.5887 9.7753 10.5192C10.1139 10.3802 10.3802 10.1139 10.5177 9.77444C10.5908 9.60687 10.6251 9.43072 10.6251 9.25027V3.75081C10.6251 2.99076 10.0107 2.37594 9.25027 2.37594ZM18.0709 16.1246H20.2492C21.0092 16.1246 21.6241 15.5098 21.6241 14.7497C21.6241 13.9897 21.0092 13.3749 20.2492 13.3749H14.7497C14.5708 13.3749 14.3926 13.4113 14.2247 13.4806C13.8879 13.6199 13.6198 13.8879 13.4806 14.2247C13.4092 14.3931 13.3749 14.5693 13.3749 14.7497V20.2492C13.3749 21.0092 13.9897 21.6241 14.7497 21.6241C15.5098 21.6241 16.1246 21.0092 16.1246 20.2492V18.0687L20.6531 22.5972C21.1901 23.1343 22.0602 23.1343 22.5972 22.5972C23.1338 22.0606 23.1347 21.1905 22.5972 20.6531L18.0709 16.1246Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
3
asset_sources/svg/stack_duo/churn.svg
Normal file
3
asset_sources/svg/stack_duo/churn.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.77444 13.4823C9.60687 13.4092 9.43072 13.3749 9.25027 13.3749H3.75081C2.99076 13.3749 2.37594 13.9897 2.37594 14.7497C2.37594 15.5098 2.99076 16.1246 3.75081 16.1246H5.93126L1.40279 20.6531C0.865736 21.1901 0.865736 22.0602 1.40279 22.5972C1.93985 23.1343 2.80988 23.1343 3.34694 22.5972L7.8754 18.0709V20.2492C7.8754 21.0092 8.49023 21.6241 9.25027 21.6241C10.0103 21.6241 10.6251 21.0092 10.6251 20.2492V14.7497C10.6251 14.5708 10.5887 14.3926 10.5192 14.2247C10.3802 13.8861 10.1139 13.6198 9.77444 13.4823ZM14.2256 10.5177C14.3931 10.5908 14.5693 10.6251 14.7497 10.6251H20.2492C21.0092 10.6251 21.6241 10.0103 21.6241 9.25027C21.6241 8.49023 21.0092 7.8754 20.2492 7.8754H18.0687L22.5972 3.34694C23.1343 2.80988 23.1343 1.93985 22.5972 1.40279C22.0606 0.866166 21.1905 0.865306 20.6531 1.40279L16.1246 5.93341V3.75081C16.1246 2.99076 15.5098 2.37594 14.7497 2.37594C13.9897 2.37594 13.3749 2.99076 13.3749 3.75081V9.25027C13.3749 9.42917 13.4113 9.60739 13.4807 9.7753C13.6198 10.1139 13.8861 10.3802 14.2256 10.5177ZM9.25027 2.37594C8.4898 2.37594 7.8754 2.99076 7.8754 3.75081V5.93126L3.34823 1.40387C2.81117 0.86681 1.94114 0.86681 1.40408 1.40387C0.867025 1.94092 0.867025 2.81096 1.40408 3.34801L5.93341 7.8754H3.75081C2.99076 7.8754 2.37594 8.4898 2.37594 9.25027C2.37594 10.0107 2.99076 10.6251 3.75081 10.6251H9.25027C9.42917 10.6251 9.60739 10.5887 9.7753 10.5192C10.1139 10.3802 10.3802 10.1139 10.5177 9.77444C10.5908 9.60687 10.6251 9.43072 10.6251 9.25027V3.75081C10.6251 2.99076 10.0107 2.37594 9.25027 2.37594ZM18.0709 16.1246H20.2492C21.0092 16.1246 21.6241 15.5098 21.6241 14.7497C21.6241 13.9897 21.0092 13.3749 20.2492 13.3749H14.7497C14.5708 13.3749 14.3926 13.4113 14.2247 13.4806C13.8879 13.6199 13.6198 13.8879 13.4806 14.2247C13.4092 14.3931 13.3749 14.5693 13.3749 14.7497V20.2492C13.3749 21.0092 13.9897 21.6241 14.7497 21.6241C15.5098 21.6241 16.1246 21.0092 16.1246 20.2492V18.0687L20.6531 22.5972C21.1901 23.1343 22.0602 23.1343 22.5972 22.5972C23.1338 22.0606 23.1347 21.1905 22.5972 20.6531L18.0709 16.1246Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
3
asset_sources/svg/stack_wallet/churn.svg
Normal file
3
asset_sources/svg/stack_wallet/churn.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.77444 13.4823C9.60687 13.4092 9.43072 13.3749 9.25027 13.3749H3.75081C2.99076 13.3749 2.37594 13.9897 2.37594 14.7497C2.37594 15.5098 2.99076 16.1246 3.75081 16.1246H5.93126L1.40279 20.6531C0.865736 21.1901 0.865736 22.0602 1.40279 22.5972C1.93985 23.1343 2.80988 23.1343 3.34694 22.5972L7.8754 18.0709V20.2492C7.8754 21.0092 8.49023 21.6241 9.25027 21.6241C10.0103 21.6241 10.6251 21.0092 10.6251 20.2492V14.7497C10.6251 14.5708 10.5887 14.3926 10.5192 14.2247C10.3802 13.8861 10.1139 13.6198 9.77444 13.4823ZM14.2256 10.5177C14.3931 10.5908 14.5693 10.6251 14.7497 10.6251H20.2492C21.0092 10.6251 21.6241 10.0103 21.6241 9.25027C21.6241 8.49023 21.0092 7.8754 20.2492 7.8754H18.0687L22.5972 3.34694C23.1343 2.80988 23.1343 1.93985 22.5972 1.40279C22.0606 0.866166 21.1905 0.865306 20.6531 1.40279L16.1246 5.93341V3.75081C16.1246 2.99076 15.5098 2.37594 14.7497 2.37594C13.9897 2.37594 13.3749 2.99076 13.3749 3.75081V9.25027C13.3749 9.42917 13.4113 9.60739 13.4807 9.7753C13.6198 10.1139 13.8861 10.3802 14.2256 10.5177ZM9.25027 2.37594C8.4898 2.37594 7.8754 2.99076 7.8754 3.75081V5.93126L3.34823 1.40387C2.81117 0.86681 1.94114 0.86681 1.40408 1.40387C0.867025 1.94092 0.867025 2.81096 1.40408 3.34801L5.93341 7.8754H3.75081C2.99076 7.8754 2.37594 8.4898 2.37594 9.25027C2.37594 10.0107 2.99076 10.6251 3.75081 10.6251H9.25027C9.42917 10.6251 9.60739 10.5887 9.7753 10.5192C10.1139 10.3802 10.3802 10.1139 10.5177 9.77444C10.5908 9.60687 10.6251 9.43072 10.6251 9.25027V3.75081C10.6251 2.99076 10.0107 2.37594 9.25027 2.37594ZM18.0709 16.1246H20.2492C21.0092 16.1246 21.6241 15.5098 21.6241 14.7497C21.6241 13.9897 21.0092 13.3749 20.2492 13.3749H14.7497C14.5708 13.3749 14.3926 13.4113 14.2247 13.4806C13.8879 13.6199 13.6198 13.8879 13.4806 14.2247C13.4092 14.3931 13.3749 14.5693 13.3749 14.7497V20.2492C13.3749 21.0092 13.9897 21.6241 14.7497 21.6241C15.5098 21.6241 16.1246 21.0092 16.1246 20.2492V18.0687L20.6531 22.5972C21.1901 23.1343 22.0602 23.1343 22.5972 22.5972C23.1338 22.0606 23.1347 21.1905 22.5972 20.6531L18.0709 16.1246Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
127
lib/pages/churning/churn_error_dialog.dart
Normal file
127
lib/pages/churning/churn_error_dialog.dart
Normal file
|
@ -0,0 +1,127 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../providers/churning/churning_service_provider.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import '../../utilities/util.dart';
|
||||
import '../../widgets/conditional_parent.dart';
|
||||
import '../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../widgets/desktop/primary_button.dart';
|
||||
import '../../widgets/desktop/secondary_button.dart';
|
||||
import '../../widgets/stack_dialog.dart';
|
||||
|
||||
class ChurnErrorDialog extends ConsumerWidget {
|
||||
const ChurnErrorDialog({
|
||||
super.key,
|
||||
required this.error,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
final String error;
|
||||
final String walletId;
|
||||
|
||||
static const errorTitle = "An error occurred";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopDialog(
|
||||
maxHeight: double.infinity,
|
||||
child: child,
|
||||
),
|
||||
child: ConditionalParent(
|
||||
condition: !Util.isDesktop,
|
||||
builder: (child) => StackDialogBase(
|
||||
child: child,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Util.isDesktop
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32, top: 32),
|
||||
child: Text(
|
||||
errorTitle,
|
||||
style: STextStyles.desktopH2(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
errorTitle,
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Padding(
|
||||
padding: Util.isDesktop
|
||||
? const EdgeInsets.all(32)
|
||||
: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
error.startsWith("Exception:")
|
||||
? error.substring(10).trim()
|
||||
: error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Padding(
|
||||
padding: Util.isDesktop
|
||||
? const EdgeInsets.all(32)
|
||||
: const EdgeInsets.all(20),
|
||||
child: Text(
|
||||
"Stop churning or try and continue?",
|
||||
style: Util.isDesktop
|
||||
? STextStyles.w600_14(context)
|
||||
: STextStyles.w600_14(context),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: Util.isDesktop ? 32 : 20,
|
||||
bottom: Util.isDesktop ? 32 : 20,
|
||||
right: Util.isDesktop ? 32 : 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Stop",
|
||||
onPressed: () {
|
||||
ref.read(pChurningService(walletId)).stopChurning();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Util.isDesktop ? 20 : 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Continue",
|
||||
onPressed: () {
|
||||
ref.read(pChurningService(walletId)).unpause();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
256
lib/pages/churning/churning_progress_view.dart
Normal file
256
lib/pages/churning/churning_progress_view.dart
Normal file
|
@ -0,0 +1,256 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../providers/churning/churning_service_provider.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import '../../widgets/background.dart';
|
||||
import '../../widgets/churning/churn_progress_item.dart';
|
||||
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../widgets/desktop/primary_button.dart';
|
||||
import '../../widgets/desktop/secondary_button.dart';
|
||||
import '../../widgets/rounded_container.dart';
|
||||
import '../../widgets/stack_dialog.dart';
|
||||
import 'churn_error_dialog.dart';
|
||||
|
||||
class ChurningProgressView extends ConsumerStatefulWidget {
|
||||
const ChurningProgressView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const routeName = "/churningProgressView";
|
||||
|
||||
final String walletId;
|
||||
@override
|
||||
ConsumerState<ChurningProgressView> createState() =>
|
||||
_ChurningProgressViewState();
|
||||
}
|
||||
|
||||
class _ChurningProgressViewState extends ConsumerState<ChurningProgressView> {
|
||||
Future<bool> _requestAndProcessCancel() async {
|
||||
final shouldCancel = await showDialog<bool?>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => StackDialog(
|
||||
title: "Cancel churning?",
|
||||
leftButton: SecondaryButton(
|
||||
label: "No",
|
||||
buttonHeight: null,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
rightButton: PrimaryButton(
|
||||
label: "Yes",
|
||||
buttonHeight: null,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldCancel == true && mounted) {
|
||||
ref.read(pChurningService(widget.walletId)).stopChurning();
|
||||
|
||||
await WakelockPlus.disable();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) ref.read(pChurningService(widget.walletId)).churn();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WakelockPlus.disable();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool _succeeded = ref.watch(
|
||||
pChurningService(widget.walletId).select((s) => s.done),
|
||||
);
|
||||
|
||||
final int _roundsCompleted = ref.watch(
|
||||
pChurningService(widget.walletId).select((s) => s.roundsCompleted),
|
||||
);
|
||||
|
||||
WakelockPlus.enable();
|
||||
|
||||
ref.listen(
|
||||
pChurningService(widget.walletId).select((s) => s.lastSeenError),
|
||||
(p, n) {
|
||||
if (!ref.read(pChurningService(widget.walletId)).ignoreErrors &&
|
||||
n != null) {
|
||||
if (context.mounted) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ChurnErrorDialog(
|
||||
error: n.toString(),
|
||||
walletId: widget.walletId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return await _requestAndProcessCancel();
|
||||
},
|
||||
child: Background(
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
if (await _requestAndProcessCancel()) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Churning progress",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (builderContext, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_roundsCompleted == 0)
|
||||
RoundedContainer(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.snackBarBackError,
|
||||
child: Text(
|
||||
"Do not close this window. If you exit, "
|
||||
"the process will be canceled.",
|
||||
style:
|
||||
STextStyles.smallMed14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.snackBarTextError,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (_roundsCompleted > 0)
|
||||
RoundedContainer(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.snackBarBackInfo,
|
||||
child: Text(
|
||||
"Churning rounds completed: $_roundsCompleted",
|
||||
style: STextStyles.w500_14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.snackBarTextInfo,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
ProgressItem(
|
||||
iconAsset: Assets.svg.peers,
|
||||
label: "Waiting for balance to unlock",
|
||||
status: ref.watch(
|
||||
pChurningService(widget.walletId)
|
||||
.select((s) => s.waitingForUnlockedBalance),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ProgressItem(
|
||||
iconAsset: Assets.svg.fusing,
|
||||
label: "Creating churn transaction",
|
||||
status: ref.watch(
|
||||
pChurningService(widget.walletId)
|
||||
.select((s) => s.makingChurnTransaction),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ProgressItem(
|
||||
iconAsset: Assets.svg.checkCircle,
|
||||
label: "Complete",
|
||||
status: ref.watch(
|
||||
pChurningService(widget.walletId)
|
||||
.select((s) => s.completedStatus),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (_succeeded)
|
||||
PrimaryButton(
|
||||
label: "Churn again",
|
||||
onPressed: ref
|
||||
.read(pChurningService(widget.walletId))
|
||||
.churn,
|
||||
),
|
||||
if (_succeeded)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Cancel",
|
||||
onPressed: () async {
|
||||
if (await _requestAndProcessCancel()) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
159
lib/pages/churning/churning_rounds_selection_sheet.dart
Normal file
159
lib/pages/churning/churning_rounds_selection_sheet.dart
Normal file
|
@ -0,0 +1,159 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/constants.dart';
|
||||
import '../../utilities/extensions/extensions.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
|
||||
enum ChurnOption {
|
||||
continuous,
|
||||
custom;
|
||||
}
|
||||
|
||||
class ChurnRoundCountSelectSheet extends HookWidget {
|
||||
const ChurnRoundCountSelectSheet({
|
||||
super.key,
|
||||
required this.currentOption,
|
||||
});
|
||||
|
||||
final ChurnOption currentOption;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final option = useState(currentOption);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
Navigator.of(context).pop(option.value);
|
||||
return false;
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 10,
|
||||
bottom: 0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
width: 60,
|
||||
height: 4,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 36,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Rounds of churn",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
for (int i = 0; i < ChurnOption.values.length; i++)
|
||||
Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
option.value = ChurnOption.values[i];
|
||||
Navigator.of(context).pop(option.value);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.start,
|
||||
// children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Radio(
|
||||
activeColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.radioButtonIconEnabled,
|
||||
value: ChurnOption.values[i],
|
||||
groupValue: option.value,
|
||||
onChanged: (_) {
|
||||
option.value = ChurnOption.values[i];
|
||||
Navigator.of(context).pop(option.value);
|
||||
},
|
||||
),
|
||||
),
|
||||
// ],
|
||||
// ),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
ChurnOption.values[i].name.capitalize(),
|
||||
style: STextStyles.titleBold12(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
Text(
|
||||
ChurnOption.values[i] ==
|
||||
ChurnOption.continuous
|
||||
? "Keep churning until manually stopped"
|
||||
: "Stop after a set number of churns",
|
||||
style: STextStyles.itemSubtitle12(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark3,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
275
lib/pages/churning/churning_view.dart
Normal file
275
lib/pages/churning/churning_view.dart
Normal file
|
@ -0,0 +1,275 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../providers/churning/churning_service_provider.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/constants.dart';
|
||||
import '../../utilities/extensions/extensions.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import '../../widgets/background.dart';
|
||||
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../widgets/custom_buttons/checkbox_text_button.dart';
|
||||
import '../../widgets/desktop/primary_button.dart';
|
||||
import '../../widgets/rounded_container.dart';
|
||||
import '../../widgets/rounded_white_container.dart';
|
||||
import '../../widgets/stack_text_field.dart';
|
||||
import 'churning_progress_view.dart';
|
||||
import 'churning_rounds_selection_sheet.dart';
|
||||
|
||||
class ChurningView extends ConsumerStatefulWidget {
|
||||
const ChurningView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const routeName = "/churnView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<ChurningView> createState() => _ChurnViewState();
|
||||
}
|
||||
|
||||
class _ChurnViewState extends ConsumerState<ChurningView> {
|
||||
late final TextEditingController churningRoundController;
|
||||
late final FocusNode churningRoundFocusNode;
|
||||
|
||||
bool _enableStartButton = false;
|
||||
|
||||
ChurnOption _option = ChurnOption.continuous;
|
||||
|
||||
Future<void> _startChurn() async {
|
||||
final churningService = ref.read(pChurningService(widget.walletId));
|
||||
|
||||
final int rounds = _option == ChurnOption.continuous
|
||||
? 0
|
||||
: int.parse(churningRoundController.text);
|
||||
|
||||
churningService.rounds = rounds;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
ChurningProgressView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
churningRoundController = TextEditingController();
|
||||
|
||||
churningRoundFocusNode = FocusNode();
|
||||
|
||||
final rounds = ref.read(pChurningService(widget.walletId)).rounds;
|
||||
|
||||
_option = rounds == 0 ? ChurnOption.continuous : ChurnOption.custom;
|
||||
churningRoundController.text = rounds.toString();
|
||||
|
||||
_enableStartButton = churningRoundController.text.isNotEmpty;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
churningRoundController.dispose();
|
||||
churningRoundFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Background(
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: const AppBarBackButton(),
|
||||
title: Text(
|
||||
"Churn",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
actions: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: AppBarIconButton(
|
||||
size: 36,
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.circleQuestion,
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.topNavIconPrimary,
|
||||
),
|
||||
onPressed: () async {
|
||||
//' TODO show about?
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (builderContext, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Churning helps anonymize your coins by mixing them.",
|
||||
style: STextStyles.w500_12(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"Configuration",
|
||||
style: STextStyles.w500_14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark3,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
RoundedContainer(
|
||||
onPressed: () async {
|
||||
final option =
|
||||
await showModalBottomSheet<ChurnOption?>(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
builder: (_) {
|
||||
return ChurnRoundCountSelectSheet(
|
||||
currentOption: _option,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (option != null) {
|
||||
setState(() {
|
||||
_option = option;
|
||||
});
|
||||
}
|
||||
},
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveBG,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_option.name.capitalize(),
|
||||
style: STextStyles.w500_12(context),
|
||||
),
|
||||
SvgPicture.asset(
|
||||
Assets.svg.chevronDown,
|
||||
width: 12,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_option == ChurnOption.custom)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (_option == ChurnOption.custom)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: churningRoundController,
|
||||
focusNode: churningRoundFocusNode,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_enableStartButton = value.isNotEmpty;
|
||||
});
|
||||
},
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Number of churns",
|
||||
churningRoundFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
labelText: "Enter number of churns..",
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
CheckboxTextButton(
|
||||
label: "Pause on errors",
|
||||
initialValue: !ref
|
||||
.read(pChurningService(widget.walletId))
|
||||
.ignoreErrors,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(pChurningService(widget.walletId))
|
||||
.ignoreErrors = !value;
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const Spacer(),
|
||||
PrimaryButton(
|
||||
label: "Start",
|
||||
enabled: _enableStartButton,
|
||||
onPressed: _startChurn,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ import '../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
|||
import '../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import '../../wallets/wallet/impl/firo_wallet.dart';
|
||||
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
||||
import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
||||
import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
|
||||
import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart';
|
||||
|
@ -66,6 +67,7 @@ import '../../widgets/loading_indicator.dart';
|
|||
import '../../widgets/small_tor_icon.dart';
|
||||
import '../../widgets/stack_dialog.dart';
|
||||
import '../../widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart';
|
||||
import '../../widgets/wallet_navigation_bar/components/icons/churn_nav_icon.dart';
|
||||
import '../../widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart';
|
||||
import '../../widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart';
|
||||
import '../../widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart';
|
||||
|
@ -78,6 +80,7 @@ import '../../widgets/wallet_navigation_bar/components/wallet_navigation_bar_ite
|
|||
import '../../widgets/wallet_navigation_bar/wallet_navigation_bar.dart';
|
||||
import '../buy_view/buy_in_wallet_view.dart';
|
||||
import '../cashfusion/cashfusion_view.dart';
|
||||
import '../churning/churning_view.dart';
|
||||
import '../coin_control/coin_control_view.dart';
|
||||
import '../exchange_view/wallet_initiated_exchange_view.dart';
|
||||
import '../monkey/monkey_view.dart';
|
||||
|
@ -1226,6 +1229,22 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
if (ref.watch(
|
||||
pWallets.select(
|
||||
(value) =>
|
||||
value.getWallet(widget.walletId) is LibMoneroWallet,
|
||||
),
|
||||
))
|
||||
WalletNavigationBarItemData(
|
||||
label: "Churn",
|
||||
icon: const ChurnNavIcon(),
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
ChurningView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
411
lib/pages_desktop_specific/churning/desktop_churning_view.dart
Normal file
411
lib/pages_desktop_specific/churning/desktop_churning_view.dart
Normal file
|
@ -0,0 +1,411 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../pages/churning/churning_rounds_selection_sheet.dart';
|
||||
import '../../providers/churning/churning_service_provider.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/constants.dart';
|
||||
import '../../utilities/extensions/extensions.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../widgets/custom_buttons/checkbox_text_button.dart';
|
||||
import '../../widgets/desktop/desktop_app_bar.dart';
|
||||
import '../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../widgets/desktop/desktop_scaffold.dart';
|
||||
import '../../widgets/desktop/primary_button.dart';
|
||||
import '../../widgets/rounded_white_container.dart';
|
||||
import '../../widgets/stack_text_field.dart';
|
||||
import 'sub_widgets/churning_dialog.dart';
|
||||
|
||||
class DesktopChurningView extends ConsumerStatefulWidget {
|
||||
const DesktopChurningView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/desktopChurningView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<DesktopChurningView> createState() => _DesktopChurning();
|
||||
}
|
||||
|
||||
class _DesktopChurning extends ConsumerState<DesktopChurningView> {
|
||||
late final TextEditingController churningRoundController;
|
||||
late final FocusNode churningRoundFocusNode;
|
||||
|
||||
bool _enableStartButton = false;
|
||||
|
||||
ChurnOption _option = ChurnOption.continuous;
|
||||
|
||||
Future<void> _startChurn() async {
|
||||
final churningService = ref.read(pChurningService(widget.walletId));
|
||||
|
||||
final int rounds = _option == ChurnOption.continuous
|
||||
? 0
|
||||
: int.parse(churningRoundController.text);
|
||||
|
||||
churningService.rounds = rounds;
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return ChurnDialogView(
|
||||
walletId: widget.walletId,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
churningRoundController = TextEditingController();
|
||||
|
||||
churningRoundFocusNode = FocusNode();
|
||||
|
||||
final rounds = ref.read(pChurningService(widget.walletId)).rounds;
|
||||
|
||||
_option = rounds == 0 ? ChurnOption.continuous : ChurnOption.custom;
|
||||
churningRoundController.text = rounds.toString();
|
||||
|
||||
_enableStartButton = churningRoundController.text.isNotEmpty;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
churningRoundController.dispose();
|
||||
churningRoundFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
return DesktopScaffold(
|
||||
appBar: DesktopAppBar(
|
||||
background: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
isCompactHeight: true,
|
||||
useSpacers: false,
|
||||
leading: Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// const SizedBox(
|
||||
// width: 32,
|
||||
// ),
|
||||
AppBarIconButton(
|
||||
size: 32,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
shadows: const [],
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.arrowLeft,
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.topNavIconPrimary,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 15,
|
||||
),
|
||||
SvgPicture.asset(
|
||||
Assets.svg.churn,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(
|
||||
"Churning",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.circleQuestion,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.radioButtonIconBorder,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: "What is churning?",
|
||||
style: STextStyles.richLink(context).copyWith(
|
||||
fontSize: 16,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 580,
|
||||
maxHeight: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
left: 20,
|
||||
bottom: 20,
|
||||
right: 10,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"What is churning?",
|
||||
style: STextStyles.desktopH2(
|
||||
context,
|
||||
),
|
||||
),
|
||||
DesktopDialogCloseButton(
|
||||
onPressedOverride: () =>
|
||||
Navigator.of(context)
|
||||
.pop(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"Churning info text",
|
||||
style:
|
||||
STextStyles.desktopTextMedium(
|
||||
context,
|
||||
).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 460,
|
||||
child: RoundedWhiteContainer(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Churning helps anonymize your coins by mixing them.",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
SizedBox(
|
||||
width: 460,
|
||||
child: RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Configuration",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<ChurnOption>(
|
||||
value: _option,
|
||||
items: [
|
||||
...ChurnOption.values.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(
|
||||
e.name.capitalize(),
|
||||
style: STextStyles.smallMed14(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value is ChurnOption) {
|
||||
setState(() {
|
||||
_option = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
isExpanded: true,
|
||||
iconStyleData: IconStyleData(
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.chevronDown,
|
||||
width: 12,
|
||||
height: 6,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight,
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
offset: const Offset(0, -10),
|
||||
elevation: 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_option == ChurnOption.custom)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (_option == ChurnOption.custom)
|
||||
SizedBox(
|
||||
width: 460,
|
||||
child: RoundedWhiteContainer(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: churningRoundController,
|
||||
focusNode: churningRoundFocusNode,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_enableStartButton = value.isNotEmpty;
|
||||
});
|
||||
},
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Number of churns",
|
||||
churningRoundFocusNode,
|
||||
context,
|
||||
desktopMed: true,
|
||||
).copyWith(
|
||||
labelText: "Enter number of churns..",
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CheckboxTextButton(
|
||||
label: "Pause on errors",
|
||||
initialValue: !ref
|
||||
.read(pChurningService(widget.walletId))
|
||||
.ignoreErrors,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(pChurningService(widget.walletId))
|
||||
.ignoreErrors = !value;
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Start",
|
||||
enabled: _enableStartButton,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: _startChurn,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../../pages/churning/churn_error_dialog.dart';
|
||||
import '../../../providers/churning/churning_service_provider.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/assets.dart';
|
||||
import '../../../utilities/text_styles.dart';
|
||||
import '../../../widgets/churning/churn_progress_item.dart';
|
||||
import '../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../widgets/desktop/secondary_button.dart';
|
||||
import '../../../widgets/rounded_container.dart';
|
||||
import '../../../widgets/rounded_white_container.dart';
|
||||
|
||||
class ChurnDialogView extends ConsumerStatefulWidget {
|
||||
const ChurnDialogView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<ChurnDialogView> createState() => _ChurnDialogViewState();
|
||||
}
|
||||
|
||||
class _ChurnDialogViewState extends ConsumerState<ChurnDialogView> {
|
||||
Future<bool> _requestAndProcessCancel() async {
|
||||
final bool? shouldCancel = await showDialog<bool?>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => DesktopDialog(
|
||||
maxWidth: 580,
|
||||
maxHeight: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 32,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Cancel churning?",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
DesktopDialogCloseButton(
|
||||
onPressedOverride: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 0,
|
||||
right: 32,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Do you really want to cancel the churning process?",
|
||||
style: STextStyles.smallMed14(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "No",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Yes",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldCancel == true && mounted) {
|
||||
ref.read(pChurningService(widget.walletId)).stopChurning();
|
||||
|
||||
await WakelockPlus.disable();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) ref.read(pChurningService(widget.walletId)).churn();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
WakelockPlus.disable();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool _succeeded = ref.watch(
|
||||
pChurningService(widget.walletId).select((s) => s.done),
|
||||
);
|
||||
|
||||
final int _roundsCompleted = ref.watch(
|
||||
pChurningService(widget.walletId).select((s) => s.roundsCompleted),
|
||||
);
|
||||
|
||||
if (!Platform.isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
ref.listen(
|
||||
pChurningService(widget.walletId).select((s) => s.lastSeenError),
|
||||
(p, n) {
|
||||
if (!ref.read(pChurningService(widget.walletId)).ignoreErrors &&
|
||||
n != null) {
|
||||
if (context.mounted) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ChurnErrorDialog(
|
||||
error: n.toString(),
|
||||
walletId: widget.walletId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return DesktopDialog(
|
||||
maxHeight: 600,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Text(
|
||||
"Churn progress",
|
||||
style: STextStyles.desktopH2(context),
|
||||
),
|
||||
),
|
||||
DesktopDialogCloseButton(
|
||||
onPressedOverride: () async {
|
||||
if (_succeeded) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
if (await _requestAndProcessCancel()) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 32,
|
||||
right: 32,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_roundsCompleted > 0
|
||||
? RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Churn rounds completed: $_roundsCompleted",
|
||||
style: STextStyles.w500_14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: RoundedContainer(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.snackBarBackError,
|
||||
child: Text(
|
||||
"Do not close this window. If you exit, "
|
||||
"the process will be canceled.",
|
||||
style: STextStyles.smallMed14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.snackBarTextError,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
ProgressItem(
|
||||
iconAsset: Assets.svg.peers,
|
||||
label: "Waiting for balance to unlock",
|
||||
status: ref.watch(
|
||||
pChurningService(widget.walletId)
|
||||
.select((s) => s.waitingForUnlockedBalance),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ProgressItem(
|
||||
iconAsset: Assets.svg.fusing,
|
||||
label: "Creating churn transaction",
|
||||
status: ref.watch(
|
||||
pChurningService(widget.walletId)
|
||||
.select((s) => s.makingChurnTransaction),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ProgressItem(
|
||||
iconAsset: Assets.svg.checkCircle,
|
||||
label: "Complete",
|
||||
status: ref.watch(
|
||||
pChurningService(widget.walletId)
|
||||
.select((s) => s.completedStatus),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (_succeeded)
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
buttonHeight: ButtonHeight.m,
|
||||
label: "Churn again",
|
||||
onPressed: ref
|
||||
.read(pChurningService(widget.walletId))
|
||||
.churn,
|
||||
),
|
||||
),
|
||||
if (_succeeded)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
if (!_succeeded) const Spacer(),
|
||||
if (!_succeeded)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
buttonHeight: ButtonHeight.m,
|
||||
enabled: true,
|
||||
label: _succeeded ? "Done" : "Cancel",
|
||||
onPressed: () async {
|
||||
if (_succeeded) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
if (await _requestAndProcessCancel()) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ import '../../../../widgets/desktop/primary_button.dart';
|
|||
import '../../../../widgets/desktop/secondary_button.dart';
|
||||
import '../../../../widgets/loading_indicator.dart';
|
||||
import '../../../cashfusion/desktop_cashfusion_view.dart';
|
||||
import '../../../churning/desktop_churning_view.dart';
|
||||
import '../../../coin_control/desktop_coin_control_view.dart';
|
||||
import '../../../desktop_menu.dart';
|
||||
import '../../../ordinals/desktop_ordinals_view.dart';
|
||||
|
@ -92,6 +93,7 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
|
|||
onOrdinalsPressed: _onOrdinalsPressed,
|
||||
onMonkeyPressed: _onMonkeyPressed,
|
||||
onFusionPressed: _onFusionPressed,
|
||||
onChurnPressed: _onChurnPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -348,6 +350,15 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
|
|||
);
|
||||
}
|
||||
|
||||
void _onChurnPressed() {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
|
||||
Navigator.of(context).pushNamed(
|
||||
DesktopChurningView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
|
||||
|
|
|
@ -23,6 +23,7 @@ import '../../../../../utilities/text_styles.dart';
|
|||
import '../../../../../wallets/crypto_currency/crypto_currency.dart';
|
||||
import '../../../../../wallets/isar/models/wallet_info.dart';
|
||||
import '../../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
||||
import '../../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
||||
import '../../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
|
||||
import '../../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart';
|
||||
|
@ -48,6 +49,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget {
|
|||
required this.onOrdinalsPressed,
|
||||
required this.onMonkeyPressed,
|
||||
required this.onFusionPressed,
|
||||
required this.onChurnPressed,
|
||||
});
|
||||
|
||||
final String walletId;
|
||||
|
@ -58,6 +60,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget {
|
|||
final VoidCallback? onOrdinalsPressed;
|
||||
final VoidCallback? onMonkeyPressed;
|
||||
final VoidCallback? onFusionPressed;
|
||||
final VoidCallback? onChurnPressed;
|
||||
|
||||
@override
|
||||
ConsumerState<MoreFeaturesDialog> createState() => _MoreFeaturesDialogState();
|
||||
|
@ -304,6 +307,13 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
|||
iconAsset: Assets.svg.cashFusion,
|
||||
onPressed: () async => widget.onFusionPressed?.call(),
|
||||
),
|
||||
if (wallet is LibMoneroWallet)
|
||||
_MoreFeaturesItem(
|
||||
label: "Churn",
|
||||
detail: "Churning",
|
||||
iconAsset: Assets.svg.churn,
|
||||
onPressed: () async => widget.onChurnPressed?.call(),
|
||||
),
|
||||
if (wallet is SparkInterface)
|
||||
_MoreFeaturesClearSparkCacheItem(
|
||||
cryptoCurrency: wallet.cryptoCurrency,
|
||||
|
|
12
lib/providers/churning/churning_service_provider.dart
Normal file
12
lib/providers/churning/churning_service_provider.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../services/churning_service.dart';
|
||||
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
||||
import '../global/wallets_provider.dart';
|
||||
|
||||
final pChurningService = ChangeNotifierProvider.family<ChurningService, String>(
|
||||
(ref, walletId) {
|
||||
final wallet = ref.watch(pWallets.select((s) => s.getWallet(walletId)));
|
||||
return ChurningService(wallet: wallet as LibMoneroWallet);
|
||||
},
|
||||
);
|
|
@ -52,6 +52,8 @@ import 'pages/buy_view/buy_quote_preview.dart';
|
|||
import 'pages/buy_view/buy_view.dart';
|
||||
import 'pages/cashfusion/cashfusion_view.dart';
|
||||
import 'pages/cashfusion/fusion_progress_view.dart';
|
||||
import 'pages/churning/churning_progress_view.dart';
|
||||
import 'pages/churning/churning_view.dart';
|
||||
import 'pages/coin_control/coin_control_view.dart';
|
||||
import 'pages/coin_control/utxo_details_view.dart';
|
||||
import 'pages/exchange_view/choose_from_stack_view.dart';
|
||||
|
@ -155,6 +157,7 @@ import 'pages/wallets_view/wallets_view.dart';
|
|||
import 'pages_desktop_specific/address_book_view/desktop_address_book.dart';
|
||||
import 'pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart';
|
||||
import 'pages_desktop_specific/cashfusion/desktop_cashfusion_view.dart';
|
||||
import 'pages_desktop_specific/churning/desktop_churning_view.dart';
|
||||
import 'pages_desktop_specific/coin_control/desktop_coin_control_view.dart';
|
||||
// import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_buys_view.dart';
|
||||
import 'pages_desktop_specific/desktop_buy/desktop_buy_view.dart';
|
||||
|
@ -779,6 +782,34 @@ class RouteGenerator {
|
|||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case ChurningView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => ChurningView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case ChurningProgressView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => ChurningProgressView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case DesktopCashFusionView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
|
@ -793,6 +824,20 @@ class RouteGenerator {
|
|||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case DesktopChurningView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => DesktopChurningView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case GlobalSettingsView.routeName:
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
|
|
154
lib/services/churning_service.dart
Normal file
154
lib/services/churning_service.dart
Normal file
|
@ -0,0 +1,154 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cs_monero/cs_monero.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
||||
import '../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
||||
|
||||
enum ChurnStatus {
|
||||
waiting,
|
||||
running,
|
||||
failed,
|
||||
success;
|
||||
}
|
||||
|
||||
class ChurningService extends ChangeNotifier {
|
||||
// stack only uses account 0 at this point in time
|
||||
static const kAccount = 0;
|
||||
|
||||
ChurningService({required this.wallet});
|
||||
|
||||
final LibMoneroWallet wallet;
|
||||
Wallet get csWallet => wallet.libMoneroWallet!;
|
||||
|
||||
int rounds = 1; // default
|
||||
bool ignoreErrors = false; // default
|
||||
|
||||
bool _running = false;
|
||||
|
||||
ChurnStatus waitingForUnlockedBalance = ChurnStatus.waiting;
|
||||
ChurnStatus makingChurnTransaction = ChurnStatus.waiting;
|
||||
ChurnStatus completedStatus = ChurnStatus.waiting;
|
||||
int roundsCompleted = 0;
|
||||
bool done = false;
|
||||
Object? lastSeenError;
|
||||
|
||||
bool _canChurn() {
|
||||
if (csWallet.getUnlockedBalance(accountIndex: kAccount) > BigInt.zero) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final _pause = Mutex();
|
||||
bool get isPaused => _pause.isLocked;
|
||||
void unpause() {
|
||||
if (_pause.isLocked) _pause.release();
|
||||
}
|
||||
|
||||
Future<void> churn() async {
|
||||
if (rounds < 0 || _running) {
|
||||
// TODO: error?
|
||||
return;
|
||||
}
|
||||
|
||||
_running = true;
|
||||
waitingForUnlockedBalance = ChurnStatus.running;
|
||||
makingChurnTransaction = ChurnStatus.waiting;
|
||||
completedStatus = ChurnStatus.waiting;
|
||||
roundsCompleted = 0;
|
||||
done = false;
|
||||
lastSeenError = null;
|
||||
notifyListeners();
|
||||
|
||||
final roundsToDo = rounds;
|
||||
final continuous = rounds == 0;
|
||||
|
||||
bool complete() => !continuous && roundsCompleted >= roundsToDo;
|
||||
|
||||
while (!complete() && _running) {
|
||||
if (_canChurn()) {
|
||||
waitingForUnlockedBalance = ChurnStatus.success;
|
||||
makingChurnTransaction = ChurnStatus.running;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
Logging.log?.i("Doing churn #${roundsCompleted + 1}");
|
||||
await _churnTxSimple();
|
||||
waitingForUnlockedBalance = ChurnStatus.success;
|
||||
makingChurnTransaction = ChurnStatus.success;
|
||||
roundsCompleted++;
|
||||
notifyListeners();
|
||||
} catch (e, s) {
|
||||
Logging.log?.e(
|
||||
"Churning round #${roundsCompleted + 1} failed",
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
lastSeenError = e;
|
||||
makingChurnTransaction = ChurnStatus.failed;
|
||||
notifyListeners();
|
||||
if (!ignoreErrors) {
|
||||
await _pause.acquire();
|
||||
await _pause.protect(() async {});
|
||||
|
||||
if (!_running) {
|
||||
completedStatus = ChurnStatus.failed;
|
||||
// exit if stop option chosen on error
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logging.log?.i("Can't churn yet, waiting...");
|
||||
}
|
||||
|
||||
if (!complete() && _running) {
|
||||
waitingForUnlockedBalance = ChurnStatus.running;
|
||||
makingChurnTransaction = ChurnStatus.waiting;
|
||||
completedStatus = ChurnStatus.waiting;
|
||||
notifyListeners();
|
||||
// sleep
|
||||
await Future<void>.delayed(const Duration(seconds: 30));
|
||||
}
|
||||
}
|
||||
|
||||
waitingForUnlockedBalance = ChurnStatus.success;
|
||||
makingChurnTransaction = ChurnStatus.success;
|
||||
completedStatus = ChurnStatus.success;
|
||||
done = true;
|
||||
_running = false;
|
||||
notifyListeners();
|
||||
Logging.log?.i("Churning complete");
|
||||
}
|
||||
|
||||
void stopChurning() {
|
||||
done = true;
|
||||
_running = false;
|
||||
notifyListeners();
|
||||
unpause();
|
||||
}
|
||||
|
||||
Future<void> _churnTxSimple({
|
||||
final TransactionPriority priority = TransactionPriority.normal,
|
||||
}) async {
|
||||
final address = csWallet.getAddress(
|
||||
accountIndex: kAccount,
|
||||
addressIndex: 0,
|
||||
);
|
||||
|
||||
final pending = await csWallet.createTx(
|
||||
output: Recipient(
|
||||
address: address.value,
|
||||
amount: BigInt.zero, // Doesn't matter if `sweep` is true
|
||||
),
|
||||
priority: priority,
|
||||
accountIndex: kAccount,
|
||||
sweep: true,
|
||||
);
|
||||
|
||||
await csWallet.commitTx(pending);
|
||||
}
|
||||
}
|
|
@ -205,6 +205,7 @@ class _SVG {
|
|||
String get robotHead => "assets/svg/robot-head.svg";
|
||||
String get whirlPool => "assets/svg/whirlpool.svg";
|
||||
String get cashFusion => "assets/svg/cashfusion-icon.svg";
|
||||
String get churn => "assets/svg/churn.svg";
|
||||
String get fingerprint => "assets/svg/fingerprint.svg";
|
||||
String get faceId => "assets/svg/faceid.svg";
|
||||
String get tokens => "assets/svg/tokens.svg";
|
||||
|
|
98
lib/widgets/churning/churn_progress_item.dart
Normal file
98
lib/widgets/churning/churn_progress_item.dart
Normal file
|
@ -0,0 +1,98 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../../pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_item_card.dart';
|
||||
import '../../services/churning_service.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import '../../utilities/util.dart';
|
||||
import '../conditional_parent.dart';
|
||||
import '../rounded_container.dart';
|
||||
|
||||
class ProgressItem extends StatelessWidget {
|
||||
const ProgressItem({
|
||||
super.key,
|
||||
required this.iconAsset,
|
||||
required this.label,
|
||||
required this.status,
|
||||
this.error,
|
||||
});
|
||||
|
||||
final String iconAsset;
|
||||
final String label;
|
||||
final ChurnStatus status;
|
||||
final Object? error;
|
||||
|
||||
Widget _getIconForState(ChurnStatus status, BuildContext context) {
|
||||
switch (status) {
|
||||
case ChurnStatus.waiting:
|
||||
return SvgPicture.asset(
|
||||
Assets.svg.loader,
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
|
||||
);
|
||||
case ChurnStatus.running:
|
||||
return SvgPicture.asset(
|
||||
Assets.svg.loader,
|
||||
color: Theme.of(context).extension<StackColors>()!.accentColorGreen,
|
||||
);
|
||||
case ChurnStatus.success:
|
||||
return SvgPicture.asset(
|
||||
Assets.svg.checkCircle,
|
||||
color: Theme.of(context).extension<StackColors>()!.accentColorGreen,
|
||||
);
|
||||
case ChurnStatus.failed:
|
||||
return SvgPicture.asset(
|
||||
Assets.svg.circleAlert,
|
||||
color: Theme.of(context).extension<StackColors>()!.textError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => RoundedContainer(
|
||||
padding: EdgeInsets.zero,
|
||||
color: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
borderColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
child: child,
|
||||
),
|
||||
child: RestoringItemCard(
|
||||
left: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: RoundedContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
iconAsset,
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: Theme.of(context).extension<StackColors>()!.textDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
right: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: _getIconForState(status, context),
|
||||
),
|
||||
title: label,
|
||||
subTitle: error != null
|
||||
? Text(
|
||||
error!.toString(),
|
||||
style: STextStyles.w500_12(context).copyWith(
|
||||
color: Theme.of(context).extension<StackColors>()!.textError,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,18 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../utilities/text_styles.dart';
|
||||
|
||||
class CheckboxTextButton extends StatefulWidget {
|
||||
const CheckboxTextButton({super.key, required this.label, this.onChanged});
|
||||
const CheckboxTextButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onChanged,
|
||||
this.initialValue = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final void Function(bool)? onChanged;
|
||||
final bool initialValue;
|
||||
|
||||
@override
|
||||
State<CheckboxTextButton> createState() => _CheckboxTextButtonState();
|
||||
}
|
||||
|
||||
class _CheckboxTextButtonState extends State<CheckboxTextButton> {
|
||||
bool _value = false;
|
||||
late bool _value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_value = widget.initialValue;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
|
||||
class ChurnNavIcon extends StatelessWidget {
|
||||
const ChurnNavIcon({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SvgPicture.asset(
|
||||
Assets.svg.churn,
|
||||
height: 20,
|
||||
width: 20,
|
||||
color: Theme.of(context).extension<StackColors>()!.bottomNavIconIcon,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
``#include <cs_monero_flutter_libs_linux/cs_monero_flutter_libs_linux_plugin.h>
|
||||
#include <cs_monero_flutter_libs_linux/cs_monero_flutter_libs_linux_plugin.h>
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <devicelocale/devicelocale_plugin.h>
|
||||
#include <flutter_libepiccash/flutter_libepiccash_plugin.h>
|
||||
|
|
Loading…
Reference in a new issue