feat: немного анимированных иконок
This commit is contained in:
@@ -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,
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user