mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
chore: add sync indicator and better album state management (#20004)
* album list rerendering * sync indicator * sync indicator * fix: lint
This commit is contained in:
parent
137f0d48c0
commit
2e63b9d951
7 changed files with 226 additions and 158 deletions
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue