From 09abe79f6ae4d106861536f56340e3191ba44569 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 22 Dec 2025 23:28:38 +0800 Subject: [PATCH] :lipstick: Better bottom nav, snowing animation and notification tile :dizzy: Animated snowing animation --- lib/screens/tabs.dart | 92 +++++++++++++++++------------- lib/widgets/app_wrapper.dart | 68 +++++++++++++--------- lib/widgets/notification_tile.dart | 8 ++- 3 files changed, 99 insertions(+), 69 deletions(-) diff --git a/lib/screens/tabs.dart b/lib/screens/tabs.dart index 9cc8b461..140b5ae3 100644 --- a/lib/screens/tabs.dart +++ b/lib/screens/tabs.dart @@ -212,48 +212,60 @@ class TabsScreen extends HookConsumerWidget { child: MediaQuery.removePadding( context: context, removeTop: true, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: BottomAppBar( - height: 56, - padding: EdgeInsets.symmetric(horizontal: 24), - shape: AutomaticNotchedShape( - RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), ), - ), - color: Theme.of(context).colorScheme.surface.withOpacity(0.8), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: () { - final navItems = destinations.asMap().entries.map(( - entry, - ) { - int index = entry.key; - NavigationDestination dest = entry.value; - return IconButton( - icon: dest.icon, - onPressed: () => onDestinationSelected(index), - color: index == currentIndex - ? Theme.of(context).colorScheme.primary - : null, + ], + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: BottomAppBar( + height: 56, + padding: EdgeInsets.symmetric(horizontal: 24), + shape: AutomaticNotchedShape( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + color: Theme.of(context).colorScheme.surface.withOpacity(0.8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: () { + final navItems = destinations.asMap().entries.map( + (entry) { + int index = entry.key; + NavigationDestination dest = entry.value; + return IconButton( + icon: dest.icon, + onPressed: () => onDestinationSelected(index), + color: index == currentIndex + ? Theme.of(context).colorScheme.primary + : null, + ); + }, + ).toList(); + // Add mock item to leave space for FAB based on position + final gapIndex = switch (settings.fabPosition) { + 'left' => 0, + 'right' => navItems.length, + _ => navItems.length ~/ 2, // center + }; + navItems.insert( + gapIndex, + SizedBox( + width: settings.fabPosition == 'center' ? 72 : 48, + ), ); - }).toList(); - // Add mock item to leave space for FAB based on position - final gapIndex = switch (settings.fabPosition) { - 'left' => 0, - 'right' => navItems.length, - _ => navItems.length ~/ 2, // center - }; - navItems.insert( - gapIndex, - SizedBox( - width: settings.fabPosition == 'center' ? 72 : 48, - ), - ); - return navItems; - }(), + return navItems; + }(), + ), ), ), ), diff --git a/lib/widgets/app_wrapper.dart b/lib/widgets/app_wrapper.dart index 5016a9f6..d787c7eb 100644 --- a/lib/widgets/app_wrapper.dart +++ b/lib/widgets/app_wrapper.dart @@ -35,7 +35,8 @@ class AppWrapper extends HookConsumerWidget { final networkStateShowing = useState(false); final wsNotifier = ref.watch(websocketStateProvider.notifier); final websocketState = ref.watch(websocketStateProvider); - final showSnow = useState(false); + final isShowSnow = useState(false); + final isSnowGone = useState(false); // Handle network status modal if (websocketState == WebSocketState.duplicateDevice() && @@ -131,41 +132,56 @@ class AppWrapper extends HookConsumerWidget { settings.festivalFeatures && now.month == 12 && (now.day >= 22 && now.day <= 28); - if (doesShowSnow) { - showSnow.value = true; - Future.delayed(const Duration(seconds: 10), () { - showSnow.value = false; - }); - } - if (settings.firstLaunchAt == null) { - settingsNotifier.setFirstLaunchAt(now.toIso8601String()); - } else if (!settings.askedReview) { - final launchAt = DateTime.parse(settings.firstLaunchAt!); - final daysSinceFirstLaunch = now.difference(launchAt).inDays; - if (daysSinceFirstLaunch >= 3 && - !kIsWeb && - (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { - final InAppReview inAppReview = InAppReview.instance; - Future(() async { - if (await inAppReview.isAvailable()) { - inAppReview.requestReview(); - } + useEffect(() { + final now = DateTime.now(); + if (doesShowSnow) { + isShowSnow.value = true; + Future.delayed(const Duration(seconds: 60), () { + if (!context.mounted) return; + isShowSnow.value = false; + Future.delayed(const Duration(seconds: 3), () { + if (!context.mounted) return; + isSnowGone.value = true; + }); }); - settingsNotifier.setAskedReview(true); } - } + + if (settings.firstLaunchAt == null) { + settingsNotifier.setFirstLaunchAt(now.toIso8601String()); + } else if (!settings.askedReview) { + final launchAt = DateTime.parse(settings.firstLaunchAt!); + final daysSinceFirstLaunch = now.difference(launchAt).inDays; + if (daysSinceFirstLaunch >= 3 && + !kIsWeb && + (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { + final InAppReview inAppReview = InAppReview.instance; + Future(() async { + if (await inAppReview.isAvailable()) { + inAppReview.requestReview(); + } + }); + settingsNotifier.setAskedReview(true); + } + } + + return null; + }, []); return TourTriggerWidget( key: const Key("app_tour_trigger"), child: Stack( children: [ child, - if (showSnow.value) + if (doesShowSnow && !isSnowGone.value) IgnorePointer( - child: SnowFallAnimation( - key: const Key("app_snow_animation"), - config: SnowfallConfig(numberOfSnowflakes: 50, speed: 1.0), + child: AnimatedOpacity( + opacity: isShowSnow.value ? 1 : 00, + duration: const Duration(seconds: 3), + child: SnowFallAnimation( + key: const Key("app_snow_animation"), + config: SnowfallConfig(numberOfSnowflakes: 50, speed: 1.0), + ), ), ), ], diff --git a/lib/widgets/notification_tile.dart b/lib/widgets/notification_tile.dart index b4231350..d6e4b390 100644 --- a/lib/widgets/notification_tile.dart +++ b/lib/widgets/notification_tile.dart @@ -81,14 +81,14 @@ class NotificationTile extends StatelessWidget { style: compact ? Theme.of(context).textTheme.bodySmall : Theme.of(context).textTheme.titleMedium, - maxLines: compact ? 2 : null, + maxLines: compact ? 1 : null, overflow: compact ? TextOverflow.ellipsis : null, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (notification.subtitle.isNotEmpty && !compact) - Text(notification.subtitle).bold(), + Text(notification.subtitle, maxLines: compact ? 3 : null).bold(), Row( spacing: 6, children: [ @@ -114,7 +114,9 @@ class NotificationTile extends StatelessWidget { ], ).opacity(0.75).padding(bottom: compact ? 2 : 4), MarkdownTextContent( - content: notification.content, + content: (compact && notification.content.length > 60) + ? '${notification.content.substring(0, 60).replaceAll('\n', ' ')}...' + : notification.content, textStyle: (compact ? Theme.of(context).textTheme.bodySmall