feat: слегонца блюра в chat screen

This commit is contained in:
Jganenokk
2026-07-03 19:07:43 +07:00
parent 473889186b
commit 53125b60a3
4 changed files with 303 additions and 33 deletions
+43
View File
@@ -0,0 +1,43 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum ChatChromeStyle { color, blur, none }
class AppChatChrome {
static const prefKey = 'app_chat_chrome';
static final ValueNotifier<ChatChromeStyle> current =
ValueNotifier(ChatChromeStyle.blur);
static ChatChromeStyle _parse(String? value) {
switch (value) {
case 'color':
return ChatChromeStyle.color;
case 'none':
return ChatChromeStyle.none;
default:
return ChatChromeStyle.blur;
}
}
static String _encode(ChatChromeStyle value) {
switch (value) {
case ChatChromeStyle.color:
return 'color';
case ChatChromeStyle.blur:
return 'blur';
case ChatChromeStyle.none:
return 'none';
}
}
static Future<ChatChromeStyle> load() async {
final prefs = await SharedPreferences.getInstance();
return _parse(prefs.getString(prefKey));
}
static Future<void> save(ChatChromeStyle value) async {
current.value = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(prefKey, _encode(value));
}
}
+191 -33
View File
@@ -48,6 +48,7 @@ import '../../../core/config/app_swipe_back_desktop.dart';
import '../../../core/config/app_pranks.dart';
import '../../../core/config/app_commands.dart';
import '../../../core/config/app_visual_style.dart';
import '../../../core/config/app_chat_chrome.dart';
import '../../../core/config/komet_settings.dart';
import '../../../models/attachment.dart';
import '../../../models/sticker.dart';
@@ -183,6 +184,70 @@ class _ButtonClipper extends CustomClipper<Rect> {
bool shouldReclip(_ButtonClipper old) => old.t != t;
}
class _FrostedPanel extends StatelessWidget {
final Color tint;
final Border? border;
final Widget child;
const _FrostedPanel({required this.tint, this.border, required this.child});
@override
Widget build(BuildContext context) {
return ClipRect(
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 24, sigmaY: 24),
child: DecoratedBox(
decoration: BoxDecoration(color: tint, border: border),
child: child,
),
),
);
}
}
class _MeasureSize extends StatefulWidget {
final Widget child;
final ValueChanged<double> onHeight;
const _MeasureSize({required this.onHeight, required this.child});
@override
State<_MeasureSize> createState() => _MeasureSizeState();
}
class _MeasureSizeState extends State<_MeasureSize> {
final GlobalKey _key = GlobalKey();
double _last = -1;
void _report() {
if (!mounted) return;
final height = _key.currentContext?.size?.height;
if (height == null) return;
if ((height - _last).abs() > 0.5) {
_last = height;
widget.onHeight(height);
}
}
void _scheduleReport() {
WidgetsBinding.instance.addPostFrameCallback((_) => _report());
}
@override
Widget build(BuildContext context) {
_scheduleReport();
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (_) {
_scheduleReport();
return true;
},
child: SizeChangedLayoutNotifier(
child: SizedBox(key: _key, child: widget.child),
),
);
}
}
class ChatScreen extends StatefulWidget {
final int chatId;
final String name;
@@ -330,6 +395,7 @@ class _ChatScreenState extends State<ChatScreen>
int _myId = 0;
CachedChat? chat;
ChatWallpaper? _wallpaper;
final ValueNotifier<double> _composerHeight = ValueNotifier(96);
final ValueNotifier<DateTime?> _floatingDate = ValueNotifier(null);
Timer? _floatingDateTimer;
@@ -357,6 +423,7 @@ class _ChatScreenState extends State<ChatScreen>
_scrollController.addListener(_onScrollForDate);
_scrollController.addListener(_maybeLoadMoreHistory);
AppVisualStyle.current.addListener(_onVisualStyleChanged);
AppChatChrome.current.addListener(_onVisualStyleChanged);
_shimmerController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
@@ -875,6 +942,8 @@ class _ChatScreenState extends State<ChatScreen>
_scrollController.removeListener(_onScrollForDate);
_scrollController.removeListener(_maybeLoadMoreHistory);
AppVisualStyle.current.removeListener(_onVisualStyleChanged);
AppChatChrome.current.removeListener(_onVisualStyleChanged);
_composerHeight.dispose();
_floatingDateTimer?.cancel();
_floatingDateCurved.dispose();
_floatingDateAnimController.dispose();
@@ -1367,7 +1436,7 @@ class _ChatScreenState extends State<ChatScreen>
Widget _buildComposerArea(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
final content = Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
@@ -1449,6 +1518,17 @@ class _ChatScreenState extends State<ChatScreen>
),
],
);
if (AppChatChrome.current.value != ChatChromeStyle.blur) return content;
return _FrostedPanel(
tint: cs.surfaceContainerHigh.withValues(alpha: 0.55),
border: Border(
top: BorderSide(
color: cs.outlineVariant.withValues(alpha: 0.4),
width: 0.5,
),
),
child: content,
);
}
Widget _buildSelectionBottomBar(ColorScheme cs, Set<String> selected) {
@@ -1917,8 +1997,23 @@ class _ChatScreenState extends State<ChatScreen>
PreferredSizeWidget _buildAppBar(ColorScheme cs) {
final glossy = AppVisualStyle.current.value == VisualStyle.glossy;
final height = glossy ? 76.0 : kToolbarHeight;
final chrome = AppChatChrome.current.value;
return AppBar(
backgroundColor: glossy ? Colors.transparent : cs.surfaceContainerHigh,
backgroundColor: chrome == ChatChromeStyle.color
? (glossy ? Colors.transparent : cs.surfaceContainerHigh)
: Colors.transparent,
flexibleSpace: chrome == ChatChromeStyle.blur
? _FrostedPanel(
tint: cs.surfaceContainerHigh.withValues(alpha: 0.55),
border: Border(
bottom: BorderSide(
color: cs.outlineVariant.withValues(alpha: 0.4),
width: 0.5,
),
),
child: const SizedBox.expand(),
)
: null,
foregroundColor: cs.onSurface,
surfaceTintColor: Colors.transparent,
iconTheme: IconThemeData(color: cs.onSurface),
@@ -3511,6 +3606,7 @@ class _ChatScreenState extends State<ChatScreen>
? _prankPinkTheme(Theme.of(context))
: Theme.of(context);
final cs = theme.colorScheme;
final underlap = AppChatChrome.current.value != ChatChromeStyle.color;
// TODO: Локализация
// TODO: Cклонения
@@ -3544,33 +3640,9 @@ class _ChatScreenState extends State<ChatScreen>
),
child: Scaffold(
backgroundColor: cs.surface,
extendBodyBehindAppBar: underlap,
appBar: _buildAppBar(cs),
body: Column(
children: [
Expanded(
child: Stack(
children: [
if (_wallpaper != null)
Positioned.fill(
child: ChatWallpaperView(wallpaper: _wallpaper!),
),
Positioned.fill(
child: _isLoading && _messages.isEmpty
? _buildShimmerLoading()
: _buildMessagesList(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildCommandPanel(),
),
],
),
),
_buildComposerArea(context),
],
),
body: underlap ? _buildUnderlapBody() : _buildColorBody(),
),
),
),
@@ -3579,6 +3651,76 @@ class _ChatScreenState extends State<ChatScreen>
);
}
Widget _buildColorBody() {
return Column(
children: [
Expanded(
child: Stack(
children: [
if (_wallpaper != null)
Positioned.fill(
child: ChatWallpaperView(wallpaper: _wallpaper!),
),
Positioned.fill(
child: _isLoading && _messages.isEmpty
? _buildShimmerLoading()
: _buildMessagesList(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildCommandPanel(),
),
],
),
),
_buildComposerArea(context),
],
);
}
Widget _buildUnderlapBody() {
return Stack(
fit: StackFit.expand,
children: [
if (_wallpaper != null)
Positioned.fill(
child: ChatWallpaperView(wallpaper: _wallpaper!),
),
Positioned.fill(
child: _isLoading && _messages.isEmpty
? _buildShimmerLoading()
: _buildMessagesList(),
),
ValueListenableBuilder<double>(
valueListenable: _composerHeight,
builder: (context, height, _) => Positioned(
left: 0,
right: 0,
bottom: height,
child: _buildCommandPanel(),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Builder(
builder: (context) => MediaQuery.removePadding(
context: context,
removeTop: true,
child: _MeasureSize(
onHeight: (value) => _composerHeight.value = value,
child: _buildComposerArea(context),
),
),
),
),
],
);
}
Widget _buildMessagesList() {
return ValueListenableBuilder<int>(
valueListenable: _messagesRev,
@@ -3586,6 +3728,22 @@ class _ChatScreenState extends State<ChatScreen>
);
}
EdgeInsets _messagesListPadding(BuildContext context) {
if (AppChatChrome.current.value == ChatChromeStyle.color) {
return const EdgeInsets.symmetric(vertical: 8);
}
final topInset = MediaQuery.paddingOf(context).top;
return EdgeInsets.only(
top: topInset + 8,
bottom: _composerHeight.value + 8,
);
}
double _floatingDateTop(BuildContext context) {
if (AppChatChrome.current.value == ChatChromeStyle.color) return 8;
return MediaQuery.paddingOf(context).top + 8;
}
Widget _buildLoadMoreIndicator() {
final cs = Theme.of(context).colorScheme;
return Padding(
@@ -3620,14 +3778,14 @@ class _ChatScreenState extends State<ChatScreen>
return Stack(
key: _listKey,
children: [
ValueListenableBuilder<int>(
valueListenable: _otherReadTime,
builder: (context, _, _) => ValueListenableBuilder<double>(
ListenableBuilder(
listenable: Listenable.merge([_otherReadTime, _composerHeight]),
builder: (context, _) => ValueListenableBuilder<double>(
valueListenable: AppCacheExtent.current,
builder: (context, cacheExtent, _) => ListView.builder(
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.symmetric(vertical: 8),
padding: _messagesListPadding(context),
cacheExtent: cacheExtent,
itemCount: items.length + (_isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
@@ -3767,7 +3925,7 @@ class _ChatScreenState extends State<ChatScreen>
),
),
Positioned(
top: 8,
top: _floatingDateTop(context),
left: 0,
right: 0,
child: IgnorePointer(
@@ -10,6 +10,7 @@ import '../../../core/config/app_bubble_behavior.dart';
import '../../../core/config/app_bubble_shape.dart';
import '../../../core/config/app_pill_gradient.dart';
import '../../../core/config/app_visual_style.dart';
import '../../../core/config/app_chat_chrome.dart';
import '../../../core/utils/bubble_radius.dart';
import '../../../core/utils/haptics.dart';
import '../../../main.dart';
@@ -115,6 +116,8 @@ class _AppearanceScreenState extends State<AppearanceScreen> {
const SizedBox(height: 12),
const _VisualStyleCard(),
const SizedBox(height: 12),
const _ChatChromeCard(),
const SizedBox(height: 12),
const _GradientToggleCard(),
],
),
@@ -181,6 +184,69 @@ class _VisualStyleCard extends StatelessWidget {
}
}
class _ChatChromeCard extends StatelessWidget {
const _ChatChromeCard();
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Элементы экрана чата',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'Фон панелей сверху и снизу: цвет, размытие или прозрачно. '
'При размытии и прозрачности сообщения заходят под панели',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 16),
ValueListenableBuilder<ChatChromeStyle>(
valueListenable: AppChatChrome.current,
builder: (context, current, _) {
return SegmentedButton<ChatChromeStyle>(
segments: const [
ButtonSegment(
value: ChatChromeStyle.color,
label: Text('Цвет'),
),
ButtonSegment(
value: ChatChromeStyle.blur,
label: Text('Блюр'),
),
ButtonSegment(
value: ChatChromeStyle.none,
label: Text('Нет'),
),
],
selected: {current},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppChatChrome.save(set.first);
}
},
);
},
),
],
),
);
}
}
class _GradientToggleCard extends StatelessWidget {
const _GradientToggleCard();
+3
View File
@@ -33,6 +33,7 @@ import 'core/config/app_link_preview.dart';
import 'core/config/app_media_cache.dart';
import 'core/config/app_pill_gradient.dart';
import 'core/config/app_visual_style.dart';
import 'core/config/app_chat_chrome.dart';
import 'core/config/app_theme_mode.dart';
import 'core/config/app_theme_schedule.dart';
import 'core/config/app_digital_id_mode.dart';
@@ -123,6 +124,7 @@ void main() async {
final amoledFuture = AppAmoled.load();
final pillGradientFuture = AppPillGradient.load();
final visualStyleFuture = AppVisualStyle.load();
final chatChromeFuture = AppChatChrome.load();
final themeScheduleFuture = AppThemeSchedule.load();
final messageActionsFuture = AppMessageActionsStyle.load();
final swipeBackFuture = AppSwipeBackDesktop.load();
@@ -170,6 +172,7 @@ void main() async {
AppAmoled.current.value = await amoledFuture;
AppPillGradient.current.value = await pillGradientFuture;
AppVisualStyle.current.value = await visualStyleFuture;
AppChatChrome.current.value = await chatChromeFuture;
AppThemeSchedule.current.value = await themeScheduleFuture;
AppMessageActionsStyle.current.value = await messageActionsFuture;
AppSwipeBackDesktop.current.value = await swipeBackFuture;