💄 Better bottom nav, snowing animation and notification tile
💫 Animated snowing animation
This commit is contained in:
@@ -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<Widget>((
|
||||
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<Widget>(
|
||||
(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;
|
||||
}(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user