toggle_switch_widget.dart 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import "dart:io";
  2. import "package:flutter/cupertino.dart";
  3. import 'package:flutter/material.dart';
  4. import 'package:photos/ente_theme_data.dart';
  5. import 'package:photos/models/execution_states.dart';
  6. import 'package:photos/models/typedefs.dart';
  7. import 'package:photos/ui/common/loading_widget.dart';
  8. import 'package:photos/utils/debouncer.dart';
  9. class ToggleSwitchWidget extends StatefulWidget {
  10. final BoolCallBack value;
  11. final FutureVoidCallback onChanged;
  12. const ToggleSwitchWidget({
  13. required this.value,
  14. required this.onChanged,
  15. Key? key,
  16. }) : super(key: key);
  17. @override
  18. State<ToggleSwitchWidget> createState() => _ToggleSwitchWidgetState();
  19. }
  20. class _ToggleSwitchWidgetState extends State<ToggleSwitchWidget> {
  21. bool? toggleValue;
  22. ExecutionState executionState = ExecutionState.idle;
  23. final _debouncer = Debouncer(const Duration(milliseconds: 300));
  24. @override
  25. void initState() {
  26. super.initState();
  27. toggleValue = widget.value.call();
  28. }
  29. @override
  30. Widget build(BuildContext context) {
  31. final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
  32. final Widget stateIcon = _stateIcon(enteColorScheme);
  33. return Row(
  34. children: [
  35. Padding(
  36. padding: const EdgeInsets.only(right: 2),
  37. child: AnimatedSwitcher(
  38. duration: const Duration(milliseconds: 175),
  39. switchInCurve: Curves.easeInExpo,
  40. switchOutCurve: Curves.easeOutExpo,
  41. child: stateIcon,
  42. ),
  43. ),
  44. SizedBox(
  45. height: 31,
  46. child: FittedBox(
  47. fit: BoxFit.contain,
  48. child: Platform.isAndroid
  49. ? Switch(
  50. inactiveTrackColor: Colors.transparent,
  51. activeTrackColor: enteColorScheme.primary500,
  52. activeColor: Colors.white,
  53. inactiveThumbColor: enteColorScheme.primary500,
  54. trackOutlineColor: MaterialStateColor.resolveWith(
  55. (states) => enteColorScheme.primary500,
  56. ),
  57. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  58. value: toggleValue ?? false,
  59. onChanged: _onChanged,
  60. )
  61. : CupertinoSwitch(
  62. value: toggleValue ?? false,
  63. onChanged: _onChanged,
  64. ),
  65. ),
  66. ),
  67. ],
  68. );
  69. }
  70. Widget _stateIcon(enteColorScheme) {
  71. if (executionState == ExecutionState.idle) {
  72. return const SizedBox(width: 24);
  73. } else if (executionState == ExecutionState.inProgress) {
  74. return EnteLoadingWidget(
  75. color: enteColorScheme.strokeMuted,
  76. );
  77. } else if (executionState == ExecutionState.successful) {
  78. return Padding(
  79. padding: const EdgeInsets.symmetric(horizontal: 1),
  80. child: Icon(
  81. Icons.check_outlined,
  82. size: 22,
  83. color: enteColorScheme.primary500,
  84. ),
  85. );
  86. } else {
  87. return const SizedBox(width: 24);
  88. }
  89. }
  90. Future<void> _feedbackOnUnsuccessfulToggle(Stopwatch stopwatch) async {
  91. final timeElapsed = stopwatch.elapsedMilliseconds;
  92. if (timeElapsed < 200) {
  93. await Future.delayed(
  94. Duration(milliseconds: 200 - timeElapsed),
  95. );
  96. }
  97. }
  98. Future<void> _onChanged(bool negationOfToggleValue) async {
  99. setState(() {
  100. toggleValue = negationOfToggleValue;
  101. //start showing inProgress statu icons if toggle takes more than debounce time
  102. _debouncer.run(
  103. () => Future(
  104. () {
  105. setState(() {
  106. executionState = ExecutionState.inProgress;
  107. });
  108. },
  109. ),
  110. );
  111. });
  112. final Stopwatch stopwatch = Stopwatch()..start();
  113. await widget.onChanged.call().onError(
  114. (error, stackTrace) => _debouncer.cancelDebounce(),
  115. );
  116. //for toggle feedback on short unsuccessful onChanged
  117. await _feedbackOnUnsuccessfulToggle(stopwatch);
  118. //debouncer gets canceled if onChanged takes less than debounce time
  119. _debouncer.cancelDebounce();
  120. final newValue = widget.value.call();
  121. setState(() {
  122. if (toggleValue == newValue) {
  123. if (executionState == ExecutionState.inProgress) {
  124. executionState = ExecutionState.successful;
  125. Future.delayed(const Duration(seconds: 2), () {
  126. if (mounted) {
  127. setState(() {
  128. executionState = ExecutionState.idle;
  129. });
  130. }
  131. });
  132. }
  133. } else {
  134. toggleValue = !toggleValue!;
  135. executionState = ExecutionState.idle;
  136. }
  137. });
  138. }
  139. }