feat: немного анимированных иконок
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
<application
|
||||
android:label="Komet"
|
||||
android:name="${applicationName}"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
class AppAnimations {
|
||||
const AppAnimations._();
|
||||
|
||||
static const String _dir = 'assets/lottie';
|
||||
|
||||
static const String clock = '$_dir/ic_clock.json';
|
||||
static const String search = '$_dir/ic_search.json';
|
||||
static const String settings = '$_dir/ic_settings.json';
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/cupertino.dart' show CupertinoPageTransitionsBuilder;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:video_player_media_kit/video_player_media_kit.dart';
|
||||
@@ -80,6 +81,16 @@ final RouteObserver<PageRoute<dynamic>> appRouteObserver =
|
||||
|
||||
bool isOnemeFlavor = false;
|
||||
|
||||
const PageTransitionsTheme _appPageTransitions = PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
);
|
||||
|
||||
Future<Locale> _loadInitialLocale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final code = prefs.getString('app_locale');
|
||||
@@ -711,6 +722,7 @@ class KometAppState extends State<KometApp>
|
||||
ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: light,
|
||||
pageTransitionsTheme: _appPageTransitions,
|
||||
textTheme: AppFonts.textTheme(
|
||||
_fontId,
|
||||
ThemeData(brightness: Brightness.light).textTheme,
|
||||
@@ -721,6 +733,7 @@ class KometAppState extends State<KometApp>
|
||||
ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: dark,
|
||||
pageTransitionsTheme: _appPageTransitions,
|
||||
textTheme: AppFonts.textTheme(
|
||||
_fontId,
|
||||
ThemeData(brightness: Brightness.dark).textTheme,
|
||||
|
||||
@@ -125,6 +125,7 @@ flutter:
|
||||
- assets/komet.png
|
||||
- assets/komet_icon.png
|
||||
- assets/meteor_icon.png
|
||||
- assets/lottie/
|
||||
|
||||
fonts:
|
||||
- family: Inter
|
||||
|
||||
Reference in New Issue
Block a user