💄 Better bottom nav, snowing animation and notification tile

💫 Animated snowing animation
This commit is contained in:
2025-12-22 23:28:38 +08:00
parent b0b227f36b
commit 09abe79f6a
3 changed files with 99 additions and 69 deletions

View File

@@ -212,6 +212,17 @@ class TabsScreen extends HookConsumerWidget {
child: MediaQuery.removePadding( child: MediaQuery.removePadding(
context: context, context: context,
removeTop: true, removeTop: true,
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: BottomAppBar( child: BottomAppBar(
@@ -227,9 +238,8 @@ class TabsScreen extends HookConsumerWidget {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: () { children: () {
final navItems = destinations.asMap().entries.map<Widget>(( final navItems = destinations.asMap().entries.map<Widget>(
entry, (entry) {
) {
int index = entry.key; int index = entry.key;
NavigationDestination dest = entry.value; NavigationDestination dest = entry.value;
return IconButton( return IconButton(
@@ -239,7 +249,8 @@ class TabsScreen extends HookConsumerWidget {
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: null, : null,
); );
}).toList(); },
).toList();
// Add mock item to leave space for FAB based on position // Add mock item to leave space for FAB based on position
final gapIndex = switch (settings.fabPosition) { final gapIndex = switch (settings.fabPosition) {
'left' => 0, 'left' => 0,
@@ -260,6 +271,7 @@ class TabsScreen extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@@ -35,7 +35,8 @@ class AppWrapper extends HookConsumerWidget {
final networkStateShowing = useState(false); final networkStateShowing = useState(false);
final wsNotifier = ref.watch(websocketStateProvider.notifier); final wsNotifier = ref.watch(websocketStateProvider.notifier);
final websocketState = ref.watch(websocketStateProvider); final websocketState = ref.watch(websocketStateProvider);
final showSnow = useState(false); final isShowSnow = useState(false);
final isSnowGone = useState(false);
// Handle network status modal // Handle network status modal
if (websocketState == WebSocketState.duplicateDevice() && if (websocketState == WebSocketState.duplicateDevice() &&
@@ -131,10 +132,18 @@ class AppWrapper extends HookConsumerWidget {
settings.festivalFeatures && settings.festivalFeatures &&
now.month == 12 && now.month == 12 &&
(now.day >= 22 && now.day <= 28); (now.day >= 22 && now.day <= 28);
useEffect(() {
final now = DateTime.now();
if (doesShowSnow) { if (doesShowSnow) {
showSnow.value = true; isShowSnow.value = true;
Future.delayed(const Duration(seconds: 10), () { Future.delayed(const Duration(seconds: 60), () {
showSnow.value = false; if (!context.mounted) return;
isShowSnow.value = false;
Future.delayed(const Duration(seconds: 3), () {
if (!context.mounted) return;
isSnowGone.value = true;
});
}); });
} }
@@ -156,18 +165,25 @@ class AppWrapper extends HookConsumerWidget {
} }
} }
return null;
}, []);
return TourTriggerWidget( return TourTriggerWidget(
key: const Key("app_tour_trigger"), key: const Key("app_tour_trigger"),
child: Stack( child: Stack(
children: [ children: [
child, child,
if (showSnow.value) if (doesShowSnow && !isSnowGone.value)
IgnorePointer( IgnorePointer(
child: AnimatedOpacity(
opacity: isShowSnow.value ? 1 : 00,
duration: const Duration(seconds: 3),
child: SnowFallAnimation( child: SnowFallAnimation(
key: const Key("app_snow_animation"), key: const Key("app_snow_animation"),
config: SnowfallConfig(numberOfSnowflakes: 50, speed: 1.0), config: SnowfallConfig(numberOfSnowflakes: 50, speed: 1.0),
), ),
), ),
),
], ],
), ),
); );

View File

@@ -81,14 +81,14 @@ class NotificationTile extends StatelessWidget {
style: compact style: compact
? Theme.of(context).textTheme.bodySmall ? Theme.of(context).textTheme.bodySmall
: Theme.of(context).textTheme.titleMedium, : Theme.of(context).textTheme.titleMedium,
maxLines: compact ? 2 : null, maxLines: compact ? 1 : null,
overflow: compact ? TextOverflow.ellipsis : null, overflow: compact ? TextOverflow.ellipsis : null,
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (notification.subtitle.isNotEmpty && !compact) if (notification.subtitle.isNotEmpty && !compact)
Text(notification.subtitle).bold(), Text(notification.subtitle, maxLines: compact ? 3 : null).bold(),
Row( Row(
spacing: 6, spacing: 6,
children: [ children: [
@@ -114,7 +114,9 @@ class NotificationTile extends StatelessWidget {
], ],
).opacity(0.75).padding(bottom: compact ? 2 : 4), ).opacity(0.75).padding(bottom: compact ? 2 : 4),
MarkdownTextContent( MarkdownTextContent(
content: notification.content, content: (compact && notification.content.length > 60)
? '${notification.content.substring(0, 60).replaceAll('\n', ' ')}...'
: notification.content,
textStyle: textStyle:
(compact (compact
? Theme.of(context).textTheme.bodySmall ? Theme.of(context).textTheme.bodySmall