A better saving indicator for auto save fields
This commit is contained in:
parent
863f3151b3
commit
f8b8723d26
|
|
@ -44,7 +44,8 @@ class TaskDetailScreen extends ConsumerStatefulWidget {
|
||||||
ConsumerState<TaskDetailScreen> createState() => _TaskDetailScreenState();
|
ConsumerState<TaskDetailScreen> createState() => _TaskDetailScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
final _messageController = TextEditingController();
|
final _messageController = TextEditingController();
|
||||||
// Controllers for editable signatories
|
// Controllers for editable signatories
|
||||||
final _requestedController = TextEditingController();
|
final _requestedController = TextEditingController();
|
||||||
|
|
@ -65,6 +66,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
bool _typeSaved = false;
|
bool _typeSaved = false;
|
||||||
bool _categorySaving = false;
|
bool _categorySaving = false;
|
||||||
bool _categorySaved = false;
|
bool _categorySaved = false;
|
||||||
|
late final AnimationController _saveAnimController;
|
||||||
|
late final Animation<double> _savePulse;
|
||||||
static const List<String> _statusOptions = [
|
static const List<String> _statusOptions = [
|
||||||
'queued',
|
'queued',
|
||||||
'in_progress',
|
'in_progress',
|
||||||
|
|
@ -82,6 +85,13 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
.read(notificationsControllerProvider)
|
.read(notificationsControllerProvider)
|
||||||
.markReadForTask(widget.taskId),
|
.markReadForTask(widget.taskId),
|
||||||
);
|
);
|
||||||
|
_saveAnimController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 700),
|
||||||
|
);
|
||||||
|
_savePulse = Tween(begin: 1.0, end: 0.78).animate(
|
||||||
|
CurvedAnimation(parent: _saveAnimController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -93,9 +103,30 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
_requestedDebounce?.cancel();
|
_requestedDebounce?.cancel();
|
||||||
_notedDebounce?.cancel();
|
_notedDebounce?.cancel();
|
||||||
_receivedDebounce?.cancel();
|
_receivedDebounce?.cancel();
|
||||||
|
_saveAnimController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _anySaving =>
|
||||||
|
_requestedSaving ||
|
||||||
|
_notedSaving ||
|
||||||
|
_receivedSaving ||
|
||||||
|
_typeSaving ||
|
||||||
|
_categorySaving;
|
||||||
|
|
||||||
|
void _updateSaveAnim() {
|
||||||
|
if (_anySaving) {
|
||||||
|
if (!_saveAnimController.isAnimating) {
|
||||||
|
_saveAnimController.repeat(reverse: true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_saveAnimController.isAnimating) {
|
||||||
|
_saveAnimController.stop();
|
||||||
|
_saveAnimController.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tasksAsync = ref.watch(tasksProvider);
|
final tasksAsync = ref.watch(tasksProvider);
|
||||||
|
|
@ -145,6 +176,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
ticketId == null ? null : ref.watch(ticketMessagesProvider(ticketId)),
|
ticketId == null ? null : ref.watch(ticketMessagesProvider(ticketId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateSaveAnim());
|
||||||
|
|
||||||
return ResponsiveBody(
|
return ResponsiveBody(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
|
@ -264,19 +297,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
initialValue: task.requestType,
|
initialValue: task.requestType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
suffixIcon: _typeSaving
|
suffixIcon: _typeSaving
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
width: 12,
|
width: 16,
|
||||||
height: 12,
|
height: 16,
|
||||||
child:
|
child: ScaleTransition(
|
||||||
CircularProgressIndicator(
|
scale: _savePulse,
|
||||||
strokeWidth: 1.0,
|
child: const Icon(
|
||||||
|
Icons.save,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _typeSaved
|
: _typeSaved
|
||||||
? const Icon(
|
? SizedBox(
|
||||||
Icons.check,
|
width: 16,
|
||||||
color: Colors.green,
|
height: 16,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(
|
||||||
|
Icons.save,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
@ -332,19 +386,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Details',
|
hintText: 'Details',
|
||||||
suffixIcon: _typeSaving
|
suffixIcon: _typeSaving
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
width: 12,
|
width: 16,
|
||||||
height: 12,
|
height: 16,
|
||||||
child:
|
child: ScaleTransition(
|
||||||
CircularProgressIndicator(
|
scale: _savePulse,
|
||||||
strokeWidth: 1.0,
|
child: const Icon(
|
||||||
|
Icons.save,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _typeSaved
|
: _typeSaved
|
||||||
? const Icon(
|
? SizedBox(
|
||||||
Icons.check,
|
width: 16,
|
||||||
color: Colors.green,
|
height: 16,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(
|
||||||
|
Icons.save,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
@ -392,19 +467,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
initialValue: task.requestCategory,
|
initialValue: task.requestCategory,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
suffixIcon: _categorySaving
|
suffixIcon: _categorySaving
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
width: 12,
|
width: 16,
|
||||||
height: 12,
|
height: 16,
|
||||||
child:
|
child: ScaleTransition(
|
||||||
CircularProgressIndicator(
|
scale: _savePulse,
|
||||||
strokeWidth: 1.0,
|
child: const Icon(
|
||||||
|
Icons.save,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _categorySaved
|
: _categorySaved
|
||||||
? const Icon(
|
? SizedBox(
|
||||||
Icons.check,
|
width: 16,
|
||||||
color: Colors.green,
|
height: 16,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(
|
||||||
|
Icons.save,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
@ -483,19 +579,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Requester name or id',
|
hintText: 'Requester name or id',
|
||||||
suffixIcon: _requestedSaving
|
suffixIcon: _requestedSaving
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
width: 12,
|
width: 16,
|
||||||
height: 12,
|
height: 16,
|
||||||
child:
|
child: ScaleTransition(
|
||||||
CircularProgressIndicator(
|
scale: _savePulse,
|
||||||
strokeWidth: 1.0,
|
child: const Icon(
|
||||||
|
Icons.save,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _requestedSaved
|
: _requestedSaved
|
||||||
? const Icon(
|
? SizedBox(
|
||||||
Icons.check,
|
width: 16,
|
||||||
color: Colors.green,
|
height: 16,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(
|
||||||
|
Icons.save,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
@ -661,19 +778,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Supervisor/Senior',
|
hintText: 'Supervisor/Senior',
|
||||||
suffixIcon: _notedSaving
|
suffixIcon: _notedSaving
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
width: 12,
|
width: 16,
|
||||||
height: 12,
|
height: 16,
|
||||||
child:
|
child: ScaleTransition(
|
||||||
CircularProgressIndicator(
|
scale: _savePulse,
|
||||||
strokeWidth: 1.0,
|
child: const Icon(
|
||||||
|
Icons.save,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _notedSaved
|
: _notedSaved
|
||||||
? const Icon(
|
? SizedBox(
|
||||||
Icons.check,
|
width: 16,
|
||||||
color: Colors.green,
|
height: 16,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(
|
||||||
|
Icons.save,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
@ -837,19 +975,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Receiver name or id',
|
hintText: 'Receiver name or id',
|
||||||
suffixIcon: _receivedSaving
|
suffixIcon: _receivedSaving
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
width: 12,
|
width: 16,
|
||||||
height: 12,
|
height: 16,
|
||||||
child:
|
child: ScaleTransition(
|
||||||
CircularProgressIndicator(
|
scale: _savePulse,
|
||||||
strokeWidth: 1.0,
|
child: const Icon(
|
||||||
|
Icons.save,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _receivedSaved
|
: _receivedSaved
|
||||||
? const Icon(
|
? SizedBox(
|
||||||
Icons.check,
|
width: 16,
|
||||||
color: Colors.green,
|
height: 16,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(
|
||||||
|
Icons.save,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
@ -859,6 +1018,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
const Duration(milliseconds: 700),
|
const Duration(milliseconds: 700),
|
||||||
() async {
|
() async {
|
||||||
final name = v.trim();
|
final name = v.trim();
|
||||||
|
setState(() {
|
||||||
|
_receivedSaving = true;
|
||||||
|
_receivedSaved = false;
|
||||||
|
});
|
||||||
|
try {
|
||||||
await ref
|
await ref
|
||||||
.read(tasksControllerProvider)
|
.read(tasksControllerProvider)
|
||||||
.updateTask(
|
.updateTask(
|
||||||
|
|
@ -877,6 +1041,30 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||||
.upsert({'name': name});
|
.upsert({'name': name});
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
_receivedSaved =
|
||||||
|
name.isNotEmpty;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_receivedSaving = false;
|
||||||
|
});
|
||||||
|
if (_receivedSaved) {
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(
|
||||||
|
() => _receivedSaved =
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user