chore: add sync indicator and better album state management (#20004)

* album list rerendering

* sync indicator

* sync indicator

* fix: lint
This commit is contained in:
Alex 2025-07-18 08:39:28 -05:00 committed by GitHub
parent 137f0d48c0
commit 2e63b9d951
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 226 additions and 158 deletions

View file

@ -9,6 +9,7 @@ import 'package:immich_mobile/models/server_info/server_info.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@ -59,13 +60,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
centerTitle: false,
title: title ?? const _ImmichLogoWithText(),
actions: [
if (actions != null)
...actions!.map(
(action) => Padding(
padding: const EdgeInsets.only(right: 16),
child: action,
),
),
if (isCasting)
Padding(
padding: const EdgeInsets.only(right: 12),
@ -81,6 +75,14 @@ class ImmichSliverAppBar extends ConsumerWidget {
),
),
),
const _SyncStatusIndicator(),
if (actions != null)
...actions!.map(
(action) => Padding(
padding: const EdgeInsets.only(right: 16),
child: action,
),
),
if (showUploadButton)
const Padding(
padding: EdgeInsets.only(right: 20),
@ -273,3 +275,100 @@ class _BackupIndicator extends ConsumerWidget {
return null;
}
}
class _SyncStatusIndicator extends ConsumerStatefulWidget {
const _SyncStatusIndicator();
@override
ConsumerState<_SyncStatusIndicator> createState() =>
_SyncStatusIndicatorState();
}
class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator>
with TickerProviderStateMixin {
late AnimationController _rotationController;
late AnimationController _dismissalController;
late Animation<double> _rotationAnimation;
late Animation<double> _dismissalAnimation;
@override
void initState() {
super.initState();
_rotationController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_dismissalController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(_rotationController);
_dismissalAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _dismissalController,
curve: Curves.easeOutQuart,
),
);
}
@override
void dispose() {
_rotationController.dispose();
_dismissalController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final syncStatus = ref.watch(syncStatusProvider);
final isSyncing = syncStatus.isRemoteSyncing;
// Control animations based on sync status
if (isSyncing) {
if (!_rotationController.isAnimating) {
_rotationController.repeat();
}
_dismissalController.reset();
} else {
_rotationController.stop();
if (_dismissalController.status == AnimationStatus.dismissed) {
_dismissalController.forward();
}
}
// Don't show anything if not syncing and dismissal animation is complete
if (!isSyncing &&
_dismissalController.status == AnimationStatus.completed) {
return const SizedBox.shrink();
}
return AnimatedBuilder(
animation: Listenable.merge([_rotationAnimation, _dismissalAnimation]),
builder: (context, child) {
return Padding(
padding: EdgeInsets.only(right: isSyncing ? 16 : 0),
child: Transform.scale(
scale: isSyncing ? 1.0 : _dismissalAnimation.value,
child: Opacity(
opacity: isSyncing ? 1.0 : _dismissalAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159,
child: Icon(
Icons.sync,
size: 24,
color: context.primaryColor,
),
),
),
),
);
},
);
}
}