feat: немного анимированных иконок

This commit is contained in:
Jganenokk
2026-07-04 13:48:12 +07:00
parent 18a5df27a2
commit f9de0f331c
13 changed files with 236 additions and 16 deletions
@@ -34,6 +34,7 @@ import '../../../backend/api.dart';
import '../../../core/protocol/opcode_map.dart';
import '../../../core/protocol/packet.dart';
import '../../../core/utils/haptics.dart';
import '../../../core/config/app_animations.dart';
import '../../../core/config/app_stories.dart';
import '../../../backend/models/chat_folder.dart';
import '../../../backend/modules/account.dart';
@@ -138,6 +139,7 @@ class _ChatListScreenState extends State<ChatListScreen>
icon: Symbols.settings,
label: 'Настройки',
longPressable: true,
animationAsset: AppAnimations.settings,
),
];
+33 -4
View File
@@ -25,6 +25,7 @@ import 'package:komet/frontend/screens/chats/chat_list_screen.dart';
import 'package:komet/frontend/screens/chats/poll_create_screen.dart';
import 'package:komet/frontend/widgets/custom_notification.dart';
import 'package:komet/frontend/widgets/chat_menu_overlay.dart';
import 'package:komet/frontend/widgets/animated_lottie_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../main.dart';
import '../../../backend/api.dart';
@@ -42,6 +43,7 @@ import '../../../core/storage/draft_store.dart';
import '../../../core/cache/info_cache.dart';
import '../../../core/cache/message_session_cache.dart';
import '../../../core/utils/haptics.dart';
import '../../../core/config/app_animations.dart';
import '../../../core/config/app_cache_extent.dart';
import '../../../core/config/app_message_actions_style.dart';
import '../../../core/config/app_swipe_back_desktop.dart';
@@ -2268,7 +2270,25 @@ class _ChatScreenState extends State<ChatScreen>
),
child: const SizedBox.expand(),
)
: null,
: (chrome == ChatChromeStyle.none && !glossy)
? IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
cs.surface,
cs.surface,
cs.surface.withValues(alpha: 0.0),
],
stops: const [0.0, 0.72, 1.0],
),
),
child: const SizedBox.expand(),
),
)
: null,
foregroundColor: cs.onSurface,
surfaceTintColor: Colors.transparent,
iconTheme: IconThemeData(color: cs.onSurface),
@@ -2368,8 +2388,13 @@ class _ChatScreenState extends State<ChatScreen>
onPressed: _closeSearch,
);
final searchBtn = IconButton(
icon: Icon(Symbols.search, weight: glossy ? 500 : 400,
color: cs.onSurface),
icon: AnimatedLottieIcon(
asset: AppAnimations.search,
color: cs.onSurface,
size: 24,
active: true,
animateOnMount: true,
),
onPressed: () {
_searchDebounce?.cancel();
_runSearch(_searchController.text);
@@ -4380,6 +4405,7 @@ class _ChatScreenState extends State<ChatScreen>
children: [
Expanded(
child: Stack(
fit: StackFit.expand,
children: [
if (_wallpaper != null)
Positioned.fill(
@@ -4509,7 +4535,10 @@ class _ChatScreenState extends State<ChatScreen>
double _floatingDateTop(BuildContext context) {
if (AppChatChrome.current.value == ChatChromeStyle.color) return 8;
return MediaQuery.paddingOf(context).top + 8;
final glossy = AppVisualStyle.current.value == VisualStyle.glossy;
return MediaQuery.paddingOf(context).top +
(glossy ? _glossyHeaderHeight : kToolbarHeight) +
8;
}
Widget _buildLoadMoreIndicator() {
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
class AnimatedIconButton extends StatefulWidget {
final String asset;
final VoidCallback? onPressed;
final double size;
final String? tooltip;
const AnimatedIconButton({
super.key,
required this.asset,
this.onPressed,
this.size = 28,
this.tooltip,
});
@override
State<AnimatedIconButton> createState() => _AnimatedIconButtonState();
}
class _AnimatedIconButtonState extends State<AnimatedIconButton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _enter() => _controller.forward();
void _exit() => _controller.reverse();
void _tapDown() => _controller.forward(from: 0);
@override
Widget build(BuildContext context) {
final Widget button = MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => _enter(),
onExit: (_) => _exit(),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.onPressed,
onTapDown: (_) => _tapDown(),
child: SizedBox.square(
dimension: widget.size,
child: Lottie.asset(
widget.asset,
controller: _controller,
fit: BoxFit.contain,
onLoaded: (composition) {
_controller.duration = composition.duration;
},
),
),
),
);
final tooltip = widget.tooltip;
if (tooltip != null) {
return Tooltip(message: tooltip, child: button);
}
return button;
}
}
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
class AnimatedLottieIcon extends StatefulWidget {
final String asset;
final Color color;
final double size;
final bool active;
final bool animateOnMount;
const AnimatedLottieIcon({
super.key,
required this.asset,
required this.color,
required this.size,
this.active = false,
this.animateOnMount = false,
});
@override
State<AnimatedLottieIcon> createState() => _AnimatedLottieIconState();
}
class _AnimatedLottieIconState extends State<AnimatedLottieIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
if (widget.active && widget.animateOnMount) {
_controller.forward(from: 0);
} else {
_controller.value = widget.active ? 1.0 : 0.0;
}
}
@override
void didUpdateWidget(AnimatedLottieIcon oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.active && !oldWidget.active) {
_controller.forward(from: 0);
} else if (!widget.active && oldWidget.active) {
_controller.value = 0;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: widget.size,
child: Lottie.asset(
widget.asset,
controller: _controller,
fit: BoxFit.contain,
delegates: LottieDelegates(
values: [
ValueDelegate.color(const ['**'], value: widget.color),
ValueDelegate.strokeColor(const ['**'], value: widget.color),
],
),
onLoaded: (composition) {
_controller.duration = composition.duration;
},
),
);
}
}
+19 -12
View File
@@ -2,17 +2,20 @@ import 'package:flutter/material.dart';
import '../../core/config/app_pill_gradient.dart';
import '../../core/config/app_visual_style.dart';
import 'animated_lottie_icon.dart';
import 'glossy_pill.dart';
class PillNavItem {
final IconData icon;
final String label;
final bool longPressable;
final String? animationAsset;
const PillNavItem({
required this.icon,
required this.label,
this.longPressable = false,
this.animationAsset,
});
}
@@ -210,6 +213,20 @@ class _PillNavCell extends StatelessWidget {
required this.onLongPress,
});
Widget _buildIcon() {
final color = selected ? cs.onPrimary : cs.onSurface;
final asset = item.animationAsset;
if (asset != null) {
return AnimatedLottieIcon(
asset: asset,
color: color,
size: iconSize,
active: selected,
);
}
return Icon(item.icon, color: color, size: iconSize, fill: 1);
}
@override
Widget build(BuildContext context) {
final opacityDuration = animationDuration == Duration.zero
@@ -223,24 +240,14 @@ class _PillNavCell extends StatelessWidget {
behavior: HitTestBehavior.opaque,
child: Center(
child: iconsOnly
? Icon(
item.icon,
color: selected ? cs.onPrimary : cs.onSurface,
size: iconSize,
fill: 1,
)
? _buildIcon()
: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
item.icon,
color: selected ? cs.onPrimary : cs.onSurface,
size: iconSize,
fill: 1,
),
_buildIcon(),
AnimatedContainer(
duration: animationDuration,
curve: Curves.easeOutCubic,