feat: слегонца блюра в chat screen
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user