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();
|
||||
}
|
||||
|
||||
class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
||||
class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _messageController = TextEditingController();
|
||||
// Controllers for editable signatories
|
||||
final _requestedController = TextEditingController();
|
||||
|
|
@ -65,6 +66,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
bool _typeSaved = false;
|
||||
bool _categorySaving = false;
|
||||
bool _categorySaved = false;
|
||||
late final AnimationController _saveAnimController;
|
||||
late final Animation<double> _savePulse;
|
||||
static const List<String> _statusOptions = [
|
||||
'queued',
|
||||
'in_progress',
|
||||
|
|
@ -82,6 +85,13 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
.read(notificationsControllerProvider)
|
||||
.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
|
||||
|
|
@ -93,9 +103,30 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
_requestedDebounce?.cancel();
|
||||
_notedDebounce?.cancel();
|
||||
_receivedDebounce?.cancel();
|
||||
_saveAnimController.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
|
||||
Widget build(BuildContext context) {
|
||||
final tasksAsync = ref.watch(tasksProvider);
|
||||
|
|
@ -145,6 +176,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
ticketId == null ? null : ref.watch(ticketMessagesProvider(ticketId)),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateSaveAnim());
|
||||
|
||||
return ResponsiveBody(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
|
@ -264,19 +297,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
initialValue: task.requestType,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: _typeSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: ScaleTransition(
|
||||
scale: _savePulse,
|
||||
child: const Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _typeSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -332,19 +386,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
decoration: InputDecoration(
|
||||
hintText: 'Details',
|
||||
suffixIcon: _typeSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: ScaleTransition(
|
||||
scale: _savePulse,
|
||||
child: const Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _typeSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -392,19 +467,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
initialValue: task.requestCategory,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: _categorySaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: ScaleTransition(
|
||||
scale: _savePulse,
|
||||
child: const Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _categorySaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -483,19 +579,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
decoration: InputDecoration(
|
||||
hintText: 'Requester name or id',
|
||||
suffixIcon: _requestedSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: ScaleTransition(
|
||||
scale: _savePulse,
|
||||
child: const Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _requestedSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -661,19 +778,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
decoration: InputDecoration(
|
||||
hintText: 'Supervisor/Senior',
|
||||
suffixIcon: _notedSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: ScaleTransition(
|
||||
scale: _savePulse,
|
||||
child: const Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _notedSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -837,19 +975,40 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
decoration: InputDecoration(
|
||||
hintText: 'Receiver name or id',
|
||||
suffixIcon: _receivedSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: ScaleTransition(
|
||||
scale: _savePulse,
|
||||
child: const Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _receivedSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
),
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -859,23 +1018,52 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
const Duration(milliseconds: 700),
|
||||
() async {
|
||||
final name = v.trim();
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
receivedBy: name.isEmpty
|
||||
? null
|
||||
: name,
|
||||
setState(() {
|
||||
_receivedSaving = true;
|
||||
_receivedSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
receivedBy: name.isEmpty
|
||||
? null
|
||||
: name,
|
||||
);
|
||||
if (name.isNotEmpty) {
|
||||
try {
|
||||
await ref
|
||||
.read(
|
||||
supabaseClientProvider,
|
||||
)
|
||||
.from('clients')
|
||||
.upsert({'name': name});
|
||||
} catch (_) {}
|
||||
}
|
||||
setState(() {
|
||||
_receivedSaved =
|
||||
name.isNotEmpty;
|
||||
});
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} finally {
|
||||
setState(() {
|
||||
_receivedSaving = false;
|
||||
});
|
||||
if (_receivedSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _receivedSaved =
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (name.isNotEmpty) {
|
||||
try {
|
||||
await ref
|
||||
.read(
|
||||
supabaseClientProvider,
|
||||
)
|
||||
.from('clients')
|
||||
.upsert({'name': name});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user