From 532fae4b39cc7003ae36faa0338bb838eabcf489 Mon Sep 17 00:00:00 2001 From: Jganenok Date: Fri, 12 Jun 2026 00:12:12 +0700 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20flossy=20=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C.=20=D0=92=D1=8B=D0=B1=D0=BE=D1=80=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=B4=D0=B8=D0=B5=D0=BD=D1=82/=D0=B1=D0=B5=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/calls/call_session.dart | 45 +- lib/core/config/app_pill_gradient.dart | 18 + lib/core/config/app_visual_style.dart | 26 + lib/frontend/screens/calls/call_screen.dart | 56 +- .../screens/chats/chat_info_screen.dart | 179 ++--- .../screens/chats/chat_list_screen.dart | 164 ++-- lib/frontend/screens/chats/chat_screen.dart | 448 +++++++---- .../contacts/contact_profile_screen.dart | 43 +- .../screens/profile/app_icon_screen.dart | 82 +- .../screens/profile/appearance_screen.dart | 344 +++++--- .../screens/profile/cloud_storage_screen.dart | 27 +- .../screens/profile/customization_screen.dart | 21 +- .../screens/profile/debug_menu_screen.dart | 741 +++++++++--------- .../screens/profile/devices_screen.dart | 44 +- .../screens/profile/font_settings_screen.dart | 91 +-- lib/frontend/screens/profile/info_screen.dart | 32 +- .../profile/message_actions_screen.dart | 10 +- .../screens/profile/notifications_screen.dart | 10 +- .../profile/password_entry_screen.dart | 49 +- .../screens/profile/performance_screen.dart | 92 +-- .../screens/profile/security_screen.dart | 37 +- .../screens/profile/settings_tab.dart | 10 +- .../profile/theme_settings_screen.dart | 68 +- lib/frontend/widgets/glossy_pill.dart | 189 +++++ lib/frontend/widgets/sliding_pill_nav.dart | 50 +- lib/main.dart | 6 + 26 files changed, 1729 insertions(+), 1153 deletions(-) create mode 100644 lib/core/config/app_pill_gradient.dart create mode 100644 lib/core/config/app_visual_style.dart create mode 100644 lib/frontend/widgets/glossy_pill.dart diff --git a/lib/core/calls/call_session.dart b/lib/core/calls/call_session.dart index 0c17a5e..a3f788d 100644 --- a/lib/core/calls/call_session.dart +++ b/lib/core/calls/call_session.dart @@ -39,6 +39,10 @@ class CallSession { bool _peerMuted = false; bool _peerVideo = false; bool _mediaConnected = false; + bool _remoteDescSet = false; + bool _ownRemoteStream = false; + final List _pendingCandidates = []; + Future _tail = Future.value(); final CallInfo info = CallInfo(); @@ -81,11 +85,15 @@ class CallSession { info.region = ws2Config.uri.host; final signaling = Ws2Signaling(ws2Config); _signaling = signaling; - signaling.notifications.listen(_onNotification, onError: (_) => _end()); + signaling.notifications.listen(_enqueue, onError: (_) => _end()); signaling.done.then((_) => _end()); await signaling.connect(); } + void _enqueue(Map msg) { + _tail = _tail.then((_) => _onNotification(msg)).catchError((_) {}); + } + Future _onNotification(Map msg) async { _applyPeerMedia(msg); switch (msg['notification']) { @@ -195,7 +203,10 @@ class CallSession { Future _pushRemoteTrack(MediaStreamTrack track) async { var stream = _remoteStreamRef; - stream ??= await createLocalMediaStream('komet_remote'); + if (stream == null) { + stream = await createLocalMediaStream('komet_remote'); + _ownRemoteStream = true; + } _remoteStreamRef = stream; if (!stream.getTracks().any((t) => t.id == track.id)) { try { @@ -322,6 +333,8 @@ class CallSession { } await pc.setRemoteDescription(RTCSessionDescription(desc, type)); + _remoteDescSet = true; + await _flushCandidates(); if (type == 'offer') { final answer = await pc.createAnswer({}); @@ -348,11 +361,30 @@ class CallSession { final candidate = data['candidate']; if (candidate is Map) { _applyRemoteCandidate(candidate['candidate']); - await pc.addCandidate(RTCIceCandidate( + final ice = RTCIceCandidate( candidate['candidate'] as String?, candidate['sdpMid'] as String?, candidate['sdpMLineIndex'] as int?, - )); + ); + if (_remoteDescSet) { + try { + await pc.addCandidate(ice); + } catch (_) {} + } else { + _pendingCandidates.add(ice); + } + } + } + + Future _flushCandidates() async { + final pc = _pc; + if (pc == null || _pendingCandidates.isEmpty) return; + final pending = List.from(_pendingCandidates); + _pendingCandidates.clear(); + for (final c in pending) { + try { + await pc.addCandidate(c); + } catch (_) {} } } @@ -411,6 +443,11 @@ class CallSession { } await _localStream?.dispose(); await _pc?.close(); + if (_ownRemoteStream) { + try { + await _remoteStreamRef?.dispose(); + } catch (_) {} + } await _signaling?.close(); if (!_state.isClosed) await _state.close(); if (!_remoteStream.isClosed) await _remoteStream.close(); diff --git a/lib/core/config/app_pill_gradient.dart b/lib/core/config/app_pill_gradient.dart new file mode 100644 index 0000000..f31a1f5 --- /dev/null +++ b/lib/core/config/app_pill_gradient.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppPillGradient { + static const prefKey = 'app_pill_gradient'; + static final ValueNotifier current = ValueNotifier(true); + + static Future load() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(prefKey) ?? true; + } + + static Future save(bool value) async { + current.value = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(prefKey, value); + } +} diff --git a/lib/core/config/app_visual_style.dart b/lib/core/config/app_visual_style.dart new file mode 100644 index 0000000..c82ff8a --- /dev/null +++ b/lib/core/config/app_visual_style.dart @@ -0,0 +1,26 @@ +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +enum VisualStyle { materialYou, glossy } + +class AppVisualStyle { + static const prefKey = 'app_visual_style'; + static final ValueNotifier current = + ValueNotifier(VisualStyle.materialYou); + + static Future load() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(prefKey) == 'glossy' + ? VisualStyle.glossy + : VisualStyle.materialYou; + } + + static Future save(VisualStyle value) async { + current.value = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString( + prefKey, + value == VisualStyle.glossy ? 'glossy' : 'materialYou', + ); + } +} diff --git a/lib/frontend/screens/calls/call_screen.dart b/lib/frontend/screens/calls/call_screen.dart index 525f52a..2682d3a 100644 --- a/lib/frontend/screens/calls/call_screen.dart +++ b/lib/frontend/screens/calls/call_screen.dart @@ -20,6 +20,7 @@ import '../../../core/calls/call_controller.dart'; import '../../../core/calls/call_info.dart'; import '../../../core/calls/call_session.dart'; import '../../../core/utils/format.dart'; +import '../../widgets/glossy_pill.dart'; const Color _kEndRed = Color(0xFFE5484D); const Color _kAcceptGreen = Color(0xFF2EC36B); @@ -283,6 +284,11 @@ class _CallScreenState extends State @override Widget build(BuildContext context) { final cs = _darkScheme(context); + final avatar = _buildAvatar(cs); + final name = _buildName(cs); + final status = _buildStatus(cs); + final peerBar = _peerStateBar(cs); + final controls = _buildControls(cs); return Theme( data: Theme.of(context).copyWith(colorScheme: cs), @@ -296,16 +302,29 @@ class _CallScreenState extends State backgroundColor: cs.surface, body: AnimatedBuilder( animation: _videoController, - builder: (context, _) => _buildBody(cs), + builder: (context, _) => _buildBody( + cs, + avatar: avatar, + name: name, + status: status, + peerBar: peerBar, + controls: controls, + ), ), ), ), ); } - Widget _buildBody(ColorScheme cs) { + Widget _buildBody( + ColorScheme cs, { + required Widget avatar, + required Widget name, + required Widget status, + required Widget? peerBar, + required Widget controls, + }) { final t = Curves.easeInOut.transform(_videoController.value); - final peerBar = _peerStateBar(cs); final showVideo = t > 0.001 && _remoteRenderer.srcObject != null; return Stack( @@ -351,17 +370,17 @@ class _CallScreenState extends State children: [ _buildTopBar(cs, t), const Spacer(flex: 2), - _collapse(t, _buildAvatar(cs)), + _collapse(t, avatar), SizedBox(height: 36 * (1 - t)), - _collapse(t, _buildName(cs)), + _collapse(t, name), SizedBox(height: 12 * (1 - t)), - _collapse(t, _buildStatus(cs)), + _collapse(t, status), if (peerBar != null) ...[ SizedBox(height: 14 * (1 - t)), _collapse(t, peerBar), ], const Spacer(flex: 5), - _buildControls(cs), + controls, const SizedBox(height: 24), ], ), @@ -479,12 +498,11 @@ class _CallScreenState extends State } Widget _statePill(ColorScheme cs, IconData icon, String label) { - return Container( + return GlossyPill( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(100), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(100), - ), + depth: 5, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -763,15 +781,13 @@ class _CallButton extends StatelessWidget { SizedBox( width: 62, height: 62, - child: Material( + child: GlossyPill( color: background, - shape: const CircleBorder(), - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: onTap, - child: Center( - child: Icon(icon, color: foreground, size: 26, fill: 1), - ), + borderRadius: BorderRadius.circular(31), + onTap: onTap, + depth: 9, + child: Center( + child: Icon(icon, color: foreground, size: 26, fill: 1), ), ), ), diff --git a/lib/frontend/screens/chats/chat_info_screen.dart b/lib/frontend/screens/chats/chat_info_screen.dart index 360a519..aedece3 100644 --- a/lib/frontend/screens/chats/chat_info_screen.dart +++ b/lib/frontend/screens/chats/chat_info_screen.dart @@ -6,6 +6,7 @@ import '../../../backend/modules/messages.dart' show ContactCache; import '../../../core/cache/info_cache.dart'; import '../../../core/storage/app_database.dart'; import '../../../core/utils/format.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/komet_avatar.dart'; class _MemberInfo { @@ -331,12 +332,11 @@ class _ChatInfoScreenState extends State { Widget _actionBtn(ColorScheme cs, IconData icon, String label) { return Expanded( - child: Container( + child: GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), + depth: 6, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -400,41 +400,41 @@ class _ChatInfoScreenState extends State { String value, { bool isLink = false, }) { - return Container( - width: double.infinity, + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.fromLTRB(16, 12, 16, 14), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), - ), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - color: isLink ? const Color(0xFF007AFF) : cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, + depth: 6, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), ), - ), - ], + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + color: isLink ? const Color(0xFF007AFF) : cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), ); } Widget _linkCard(ColorScheme cs, String link) { - return Container( + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.fromLTRB(16, 12, 8, 14), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), + depth: 6, child: Row( children: [ Expanded( @@ -473,38 +473,41 @@ class _ChatInfoScreenState extends State { const int collapsedLines = 3; final isLong = desc.length > 120; - return Container( - width: double.infinity, + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Описание', - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), - ), - const SizedBox(height: 4), - Text( - desc, - style: TextStyle(color: cs.onSurface, fontSize: 15, height: 1.4), - maxLines: (_descExpanded || !isLong) ? null : collapsedLines, - overflow: (_descExpanded || !isLong) ? null : TextOverflow.ellipsis, - ), - if (isLong) ...[ - const SizedBox(height: 6), - GestureDetector( - onTap: () => setState(() => _descExpanded = !_descExpanded), - child: Text( - _descExpanded ? 'Свернуть' : 'Ещё', - style: const TextStyle(color: Color(0xFF007AFF), fontSize: 13), - ), + depth: 6, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Описание', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), ), + const SizedBox(height: 4), + Text( + desc, + style: TextStyle(color: cs.onSurface, fontSize: 15, height: 1.4), + maxLines: (_descExpanded || !isLong) ? null : collapsedLines, + overflow: + (_descExpanded || !isLong) ? null : TextOverflow.ellipsis, + ), + if (isLong) ...[ + const SizedBox(height: 6), + GestureDetector( + onTap: () => setState(() => _descExpanded = !_descExpanded), + child: Text( + _descExpanded ? 'Свернуть' : 'Ещё', + style: + const TextStyle(color: Color(0xFF007AFF), fontSize: 13), + ), + ), + ], ], - ], + ), ), ); } @@ -668,41 +671,41 @@ class _ChatInfoScreenState extends State { } Widget _buildInfoRowsCard(ColorScheme cs) { - return Container( + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.fromLTRB(16, 12, 16, 14), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), + depth: 6, child: _buildAllInfoRows(cs), ); } Widget _infoCard(ColorScheme cs, String label, String value) { - return Container( - width: double.infinity, + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.fromLTRB(16, 12, 16, 14), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), - ), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, + depth: 6, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), ), - ), - ], + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), ); } diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index b52bf1c..0f2ad61 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -10,6 +10,7 @@ import 'chat_screen.dart'; import 'create_group_flow.dart'; import '../../widgets/adaptive_shell.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/sheet_helpers.dart'; import '../../widgets/swipe_route.dart'; import '../../widgets/sliding_pill_nav.dart'; @@ -1228,43 +1229,44 @@ class _ChatListScreenState extends State ), Padding( padding: const EdgeInsets.fromLTRB(20, 3, 20, 8), - child: Container( - height: 44, - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(50), - ), + child: GlossyPill( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), padding: const EdgeInsets.symmetric( horizontal: 16, ), - child: Row( - children: [ - Icon( - Symbols.search, - color: cs.outline, - size: 20, - weight: 400, - ), - const SizedBox(width: 10), - Expanded( - child: TextField( - style: TextStyle( - color: cs.onSurface, - fontSize: 15, - ), - decoration: InputDecoration( - hintText: 'Поиск', - hintStyle: TextStyle( - color: cs.outline, + depth: 6, + child: SizedBox( + height: 44, + child: Row( + children: [ + Icon( + Symbols.search, + color: cs.outline, + size: 20, + weight: 400, + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + style: TextStyle( + color: cs.onSurface, fontSize: 15, ), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle( + color: cs.outline, + fontSize: 15, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), ), ), - ), - ], + ], + ), ), ), ), @@ -1805,18 +1807,25 @@ class _ChatListScreenState extends State Positioned( right: 20, bottom: bottomInset + 90, - child: FloatingActionButton( - onPressed: _toggleFab, - backgroundColor: cs.primaryContainer, - elevation: 4, - shape: const CircleBorder(), - child: Transform.rotate( - angle: val * (pi / 4), - child: Icon( - Symbols.add, - color: cs.onPrimaryContainer, - size: 28, - weight: 400, + child: GlossyPill( + onTap: _toggleFab, + color: cs.primaryContainer, + borderRadius: BorderRadius.circular(28), + elevated: true, + depth: 12, + child: SizedBox( + width: 56, + height: 56, + child: Center( + child: Transform.rotate( + angle: val * (pi / 4), + child: Icon( + Symbols.add, + color: cs.onPrimaryContainer, + size: 28, + weight: 400, + ), + ), ), ), ), @@ -2025,20 +2034,20 @@ class _ChatListScreenState extends State ); } }, - child: Container( - alignment: Alignment.center, + child: GlossyPill( + color: isSelected ? cs.primaryContainer : cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(50), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: isSelected ? cs.primaryContainer : cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(50), - ), - child: Text( - title, - textAlign: TextAlign.center, - style: TextStyle( - color: isSelected ? cs.onPrimaryContainer : cs.primary, - fontSize: 13, - fontWeight: FontWeight.w500, + depth: 4, + child: Center( + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected ? cs.onPrimaryContainer : cs.primary, + fontSize: 13, + fontWeight: FontWeight.w500, + ), ), ), ), @@ -2372,38 +2381,27 @@ class _ChatListScreenState extends State Widget _buildFabMenuItem(IconData icon, String title, {VoidCallback? onTap}) { final cs = Theme.of(context).colorScheme; - return Container( + return SizedBox( width: 220, - decoration: BoxDecoration( + child: GlossyPill( + onTap: onTap, color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(100), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(100), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - Icon(icon, color: cs.onSurface, size: 22), - const SizedBox(width: 12), - Text( - title, - style: TextStyle( - color: cs.onSurface, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + elevated: true, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(icon, color: cs.onSurface, size: 22), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, ), - ], - ), + ), + ], ), ), ); diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 559e2c0..ebf09ec 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -32,7 +32,9 @@ import '../../../core/config/app_cache_extent.dart'; import '../../../core/config/app_message_actions_style.dart'; import '../../../core/config/app_swipe_back_desktop.dart'; import '../../../core/config/app_pranks.dart'; +import '../../../core/config/app_visual_style.dart'; import '../../../models/attachment.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/message_bubble.dart'; import '../../widgets/theme_reveal.dart'; import '../../widgets/message_actions_overlay.dart'; @@ -172,6 +174,7 @@ class _ChatScreenState extends State with TickerProviderStateMixin { super.initState(); _messageController.addListener(_onTextChanged); _scrollController.addListener(_onScrollForDate); + AppVisualStyle.current.addListener(_onVisualStyleChanged); _shimmerController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), @@ -430,6 +433,7 @@ class _ChatScreenState extends State with TickerProviderStateMixin { void dispose() { _messageController.removeListener(_onTextChanged); _scrollController.removeListener(_onScrollForDate); + AppVisualStyle.current.removeListener(_onVisualStyleChanged); _floatingDateTimer?.cancel(); _floatingDateCurved.dispose(); _floatingDateAnimController.dispose(); @@ -660,6 +664,132 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } catch (_) {} } + void _onVisualStyleChanged() { + if (mounted) setState(() {}); + } + + PreferredSizeWidget _materialAppBar(ColorScheme cs) { + return PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: InkWell( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatInfoScreen( + chatId: widget.chatId, + name: widget.name, + imageUrl: widget.imageUrl, + chatType: widget.chatType, + ), + ), + ), + child: AppBar( + backgroundColor: cs.surfaceContainerHigh, + foregroundColor: cs.onSurface, + elevation: 0, + surfaceTintColor: Colors.transparent, + iconTheme: IconThemeData(color: cs.onSurface), + leading: IconButton( + icon: Icon( + widget.embedded ? Symbols.close : Symbols.arrow_back, + weight: 400, + ), + onPressed: () { + if (widget.embedded) { + widget.onClose?.call(); + } else { + Navigator.pop(context); + } + }, + ), + titleSpacing: 0, + title: Row( + children: [ + if (widget.imageUrl.isNotEmpty) + CircleAvatar( + radius: 18, + backgroundImage: CachedNetworkImageProvider( + widget.imageUrl, + maxWidth: 144, + maxHeight: 144, + ), + ) + else + CircleAvatar( + radius: 18, + backgroundColor: cs.primaryContainer, + child: Text( + widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 12, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + widget.name, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (chat?.isOfficial ?? false) ...[ + const SizedBox(width: 4), + Icon( + Symbols.verified, + color: cs.primary, + size: 16, + weight: 600, + fill: 1, + ), + ], + ], + ), + ValueListenableBuilder( + valueListenable: _headerStatusNotifier, + builder: (context, status, _) => Text( + status, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Symbols.call, weight: 400), + onPressed: _startCall, + ), + IconButton( + icon: const Icon(Symbols.more_vert, weight: 400), + onPressed: () {}, + ), + ], + ), + ), + ); + } + Future _startCall() async { if (widget.chatType != 'DIALOG') { showCustomNotification(context, 'Звонки доступны только в диалогах'); @@ -1127,127 +1257,170 @@ class _ChatScreenState extends State with TickerProviderStateMixin { ), child: Scaffold( backgroundColor: cs.surface, - appBar: PreferredSize( - preferredSize: Size.fromHeight(kToolbarHeight), - child: InkWell( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChatInfoScreen( - chatId: widget.chatId, - name: widget.name, - imageUrl: widget.imageUrl, - chatType: widget.chatType, - ), - ), - ), - child: AppBar( - backgroundColor: cs.surfaceContainerHigh, - foregroundColor: cs.onSurface, - elevation: 0, - surfaceTintColor: Colors.transparent, - iconTheme: IconThemeData(color: cs.onSurface), - leading: IconButton( - icon: Icon( - widget.embedded ? Symbols.close : Symbols.arrow_back, - weight: 400, - ), - onPressed: () { - if (widget.embedded) { - widget.onClose?.call(); - } else { - Navigator.pop(context); - } - }, - ), - titleSpacing: 0, - title: Row( - children: [ - if (widget.imageUrl.isNotEmpty) - CircleAvatar( - radius: 18, - backgroundImage: CachedNetworkImageProvider( - widget.imageUrl, - maxWidth: 144, - maxHeight: 144, + appBar: AppVisualStyle.current.value == VisualStyle.glossy + ? AppBar( + backgroundColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + elevation: 0, + toolbarHeight: 76, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: const EdgeInsets.fromLTRB(10, 4, 10, 8), + child: Row( + children: [ + SizedBox( + width: 56, + height: 56, + child: GlossyPill( + onTap: () { + if (widget.embedded) { + widget.onClose?.call(); + } else { + Navigator.pop(context); + } + }, + child: Center( + child: Icon( + widget.embedded + ? Symbols.close + : Symbols.arrow_back, + color: cs.onSurface, + weight: 500, + size: 24, ), - ) - else - CircleAvatar( - radius: 18, - backgroundColor: cs.primaryContainer, - child: Text( - widget.name.isNotEmpty - ? widget.name[0].toUpperCase() - : '?', - style: TextStyle( - color: cs.onPrimaryContainer, - fontSize: 12, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: GlossyPill( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatInfoScreen( + chatId: widget.chatId, + name: widget.name, + imageUrl: widget.imageUrl, + chatType: widget.chatType, ), ), ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + padding: const EdgeInsets.fromLTRB(6, 6, 16, 6), + child: Row( children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - widget.name, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + if (widget.imageUrl.isNotEmpty) + CircleAvatar( + radius: 22, + backgroundImage: CachedNetworkImageProvider( + widget.imageUrl, + maxWidth: 144, + maxHeight: 144, + ), + ) + else + CircleAvatar( + radius: 22, + backgroundColor: cs.primaryContainer, + child: Text( + widget.name.isNotEmpty + ? widget.name[0].toUpperCase() + : '?', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', ), ), - if (chat?.isOfficial ?? false) ...[ - const SizedBox(width: 4), - Icon( - Symbols.verified, - color: cs.primary, - size: 16, - weight: 600, - fill: 1, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + widget.name, + style: TextStyle( + color: cs.onSurface, + fontSize: 17, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (chat?.isOfficial ?? false) ...[ + const SizedBox(width: 4), + Icon( + Symbols.verified, + color: cs.primary, + size: 16, + weight: 600, + fill: 1, + ), + ], + ], + ), + ValueListenableBuilder( + valueListenable: _headerStatusNotifier, + builder: (context, status, _) => Text( + status, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), ), ], - ], - ), - ValueListenableBuilder( - valueListenable: _headerStatusNotifier, - builder: (context, status, _) => Text( - status, - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 12, - fontWeight: FontWeight.w400, - ), ), ), ], ), ), - ], - ), - actions: [ - IconButton( - icon: const Icon(Symbols.call, weight: 400), - onPressed: _startCall, ), - IconButton( - icon: const Icon(Symbols.more_vert, weight: 400), - onPressed: () {}, + const SizedBox(width: 8), + GlossyPill( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: SizedBox( + height: 56, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + Symbols.call, + weight: 500, + color: cs.onSurface, + ), + onPressed: _startCall, + ), + IconButton( + icon: Icon( + Symbols.more_vert, + weight: 500, + color: cs.onSurface, + ), + onPressed: () {}, + ), + ], + ), + ), ), ], ), ), - ), + ) + : _materialAppBar(cs), body: Column( children: [ Expanded( @@ -1531,22 +1704,21 @@ class _ChatScreenState extends State with TickerProviderStateMixin { return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: GestureDetector( + child: GlossyPill( onTap: () {}, - child: Container( + color: Color.alphaBlend( + cs.surfaceContainerHighest.withValues(alpha: 0.92), + cs.surface, + ), + borderRadius: BorderRadius.circular(28), + padding: const EdgeInsets.symmetric(vertical: 16), + depth: 8, + borderSide: BorderSide( + color: cs.outlineVariant.withValues(alpha: 0.5), + width: 0.5, + ), + child: SizedBox( width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - color: Color.alphaBlend( - cs.surfaceContainerHighest.withValues(alpha: 0.92), - cs.surface, - ), - borderRadius: BorderRadius.circular(28), - border: Border.all( - color: cs.outlineVariant.withValues(alpha: 0.5), - width: 0.5, - ), - ), child: Center( child: Text( 'Отключить уведомления', @@ -1576,19 +1748,18 @@ class _ChatScreenState extends State with TickerProviderStateMixin { minHeight: 54, maxHeight: 180, ), - decoration: BoxDecoration( + child: GlossyPill( color: Color.alphaBlend( cs.surfaceContainerHighest.withValues(alpha: 0.92), cs.surface, ), borderRadius: BorderRadius.circular(28), - border: Border.all( + depth: 8, + borderSide: BorderSide( color: cs.outlineVariant.withValues(alpha: 0.5), width: 0.5, ), - ), - clipBehavior: Clip.hardEdge, - child: Stack( + child: Stack( alignment: Alignment.center, children: [ AnimatedBuilder( @@ -1693,6 +1864,7 @@ class _ChatScreenState extends State with TickerProviderStateMixin { ), ], ), + ), ), ), AnimatedBuilder( @@ -1725,23 +1897,23 @@ class _ChatScreenState extends State with TickerProviderStateMixin { }, child: ValueListenableBuilder( valueListenable: _hasText, - builder: (context, hasText, _) => Container( - width: 54, - height: 54, - alignment: Alignment.center, - decoration: BoxDecoration( - color: hasText - ? cs.primary - : cs.surfaceContainerHighest, - shape: BoxShape.circle, - ), - child: GestureDetector( - onTap: hasText ? _sendMessage : null, - child: Icon( - hasText ? Symbols.send : Symbols.mic, - color: hasText ? cs.onPrimary : cs.onSurface, - size: 24, - weight: 400, + builder: (context, hasText, _) => GlossyPill( + color: hasText + ? cs.primary + : cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(27), + onTap: hasText ? _sendMessage : null, + depth: 8, + child: SizedBox( + width: 54, + height: 54, + child: Center( + child: Icon( + hasText ? Symbols.send : Symbols.mic, + color: hasText ? cs.onPrimary : cs.onSurface, + size: 24, + weight: 400, + ), ), ), ), diff --git a/lib/frontend/screens/contacts/contact_profile_screen.dart b/lib/frontend/screens/contacts/contact_profile_screen.dart index 6a89c0b..952ca49 100644 --- a/lib/frontend/screens/contacts/contact_profile_screen.dart +++ b/lib/frontend/screens/contacts/contact_profile_screen.dart @@ -6,6 +6,7 @@ import '../../../core/storage/app_database.dart'; import '../../../core/storage/token_storage.dart'; import '../../../core/utils/format.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/komet_avatar.dart'; import '../../widgets/swipe_route.dart'; import '../chats/chat_screen.dart'; @@ -215,12 +216,11 @@ class _ContactProfileScreenState extends State { Expanded( child: GestureDetector( onTap: actions[i].onTap, - child: Container( + child: GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), + depth: 6, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -324,24 +324,25 @@ class _ContactProfileScreenState extends State { if (rows.isEmpty) return const SizedBox.shrink(); - return Container( - width: double.infinity, - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - children: [ - for (var i = 0; i < rows.length; i++) ...[ - if (i > 0) - Divider( - height: 1, - color: cs.outlineVariant.withValues(alpha: 0.3), - ), - rows[i], + depth: 6, + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + for (var i = 0; i < rows.length; i++) ...[ + if (i > 0) + Divider( + height: 1, + color: cs.outlineVariant.withValues(alpha: 0.3), + ), + rows[i], + ], ], - ], + ), ), ); } diff --git a/lib/frontend/screens/profile/app_icon_screen.dart b/lib/frontend/screens/profile/app_icon_screen.dart index 4534725..4faa96c 100644 --- a/lib/frontend/screens/profile/app_icon_screen.dart +++ b/lib/frontend/screens/profile/app_icon_screen.dart @@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../../core/config/app_icon.dart'; import '../../../core/utils/haptics.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; class AppIconScreen extends StatefulWidget { const AppIconScreen({super.key}); @@ -55,51 +56,50 @@ class _AppIconScreenState extends State { physics: const BouncingScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), children: [ - Material( + GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Внешний вид иконки', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w700, - ), + padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), + depth: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Внешний вид иконки', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w700, ), - const SizedBox(height: 4), - Text( - AppIconConfig.isSupported - ? 'На Android приложение закроется — лаунчер подхватит новую иконку. На iOS — мгновенно с системным диалогом.' - : 'Доступно только на Android и iOS', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - height: 1.35, - ), + ), + const SizedBox(height: 4), + Text( + AppIconConfig.isSupported + ? 'На Android приложение закроется — лаунчер подхватит новую иконку. На iOS — мгновенно с системным диалогом.' + : 'Доступно только на Android и iOS', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + height: 1.35, ), - const SizedBox(height: 12), - ValueListenableBuilder( - valueListenable: AppIconConfig.current, - builder: (context, current, _) { - return Column( - children: [ - for (final icon in AppIcon.values) - _IconTile( - icon: icon, - selected: current == icon, - onTap: () => _select(icon), - ), - ], - ); - }, - ), - ], - ), + ), + const SizedBox(height: 12), + ValueListenableBuilder( + valueListenable: AppIconConfig.current, + builder: (context, current, _) { + return Column( + children: [ + for (final icon in AppIcon.values) + _IconTile( + icon: icon, + selected: current == icon, + onTap: () => _select(icon), + ), + ], + ); + }, + ), + ], ), ), ], diff --git a/lib/frontend/screens/profile/appearance_screen.dart b/lib/frontend/screens/profile/appearance_screen.dart index 09c96fd..e5c1d95 100644 --- a/lib/frontend/screens/profile/appearance_screen.dart +++ b/lib/frontend/screens/profile/appearance_screen.dart @@ -6,9 +6,12 @@ import 'package:material_symbols_icons/symbols.dart'; 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/utils/bubble_radius.dart'; import '../../../core/utils/haptics.dart'; import '../../../main.dart'; +import '../../widgets/glossy_pill.dart'; class AppearanceScreen extends StatefulWidget { const AppearanceScreen({super.key}); @@ -83,10 +86,7 @@ class _AppearanceScreenState extends State { return Scaffold( backgroundColor: cs.surface, - appBar: AppBarM3E( - titleText: 'Внешний вид', - backgroundColor: cs.surface, - ), + appBar: AppBarM3E(titleText: 'Внешний вид', backgroundColor: cs.surface), body: SafeArea( top: false, child: ListView( @@ -107,6 +107,10 @@ class _AppearanceScreenState extends State { _BubbleShapeCard(onChanged: _onStyleChanged), const SizedBox(height: 12), _BubbleBehaviorCard(onChanged: _onBehaviorChanged), + const SizedBox(height: 12), + const _VisualStyleCard(), + const SizedBox(height: 12), + const _GradientToggleCard(), ], ), ), @@ -114,6 +118,115 @@ class _AppearanceScreenState extends State { } } +class _VisualStyleCard extends StatelessWidget { + const _VisualStyleCard(); + + @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( + 'Material You или объёмные Glossy-капсулы', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: AppVisualStyle.current, + builder: (context, current, _) { + return SegmentedButton( + segments: const [ + ButtonSegment( + value: VisualStyle.materialYou, + label: Text('Material You'), + ), + ButtonSegment( + value: VisualStyle.glossy, + label: Text('Glossy'), + ), + ], + selected: {current}, + onSelectionChanged: (set) { + if (set.isNotEmpty) { + Haptics.selection(); + AppVisualStyle.save(set.first); + } + }, + ); + }, + ), + ], + ), + ); + } +} + +class _GradientToggleCard extends StatelessWidget { + const _GradientToggleCard(); + + @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, 14, 12, 14), + depth: 6, + child: Row( + children: [ + Icon(Symbols.blur_on, color: cs.onSurface, size: 24, weight: 500), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Градиент', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + 'Объём и блики в Glossy-капсулах', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), + ), + ], + ), + ), + ValueListenableBuilder( + valueListenable: AppPillGradient.current, + builder: (context, value, _) => Switch( + value: value, + onChanged: (v) { + Haptics.selection(); + AppPillGradient.save(v); + }, + ), + ), + ], + ), + ); + } +} + class _PreviewSection extends StatefulWidget { final ValueNotifier color; final ValueNotifier isSystem; @@ -162,9 +275,9 @@ class _PreviewSectionState extends State<_PreviewSection> { valueListenable: widget.color, builder: (context, color, _) { return Theme( - data: Theme.of(context).copyWith( - colorScheme: _schemeFor(color, brightness), - ), + data: Theme.of( + context, + ).copyWith(colorScheme: _schemeFor(color, brightness)), child: const _ChatPreview(), ); }, @@ -204,25 +317,23 @@ class _ChatPreview extends StatelessWidget { final cs = Theme.of(context).colorScheme; return ListenableBuilder( - listenable: Listenable.merge( - [AppBubbleShape.current, AppBubbleBehavior.current], - ), + listenable: Listenable.merge([ + AppBubbleShape.current, + AppBubbleBehavior.current, + ]), builder: (context, _) { final style = AppBubbleShape.current.value; final behavior = AppBubbleBehavior.current.value; - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerLow, - borderRadius: BorderRadius.circular(28), - border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)), - ), + return GlossyPill( + color: cs.surfaceContainerLow, + borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + depth: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < _messages.length; i++) ...[ - if (i > 0) - SizedBox(height: _messages[i].isTop ? 8 : 2), + if (i > 0) SizedBox(height: _messages[i].isTop ? 8 : 2), _PreviewBubble( text: _messages[i].text, isMe: _messages[i].isMe, @@ -313,10 +424,10 @@ class _ColorPickerCard extends StatelessWidget { Widget _buildBody(ColorScheme cs, Color col, bool sys) { final swatchColor = sys ? cs.primary : col; - return Material( + return GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - clipBehavior: Clip.antiAlias, + depth: 6, child: Column( children: [ InkWell( @@ -385,10 +496,7 @@ class _ColorPickerCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _HueStripPicker( - color: col, - onChanged: onColorChanged, - ), + _HueStripPicker(color: col, onChanged: onColorChanged), const SizedBox(height: 20), SizedBox( width: double.infinity, @@ -402,11 +510,17 @@ class _ColorPickerCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Symbols.auto_awesome, size: 18, weight: 500), + Icon( + Symbols.auto_awesome, + size: 18, + weight: 500, + ), const SizedBox(width: 8), - Text(sys - ? 'Системный цвет активен' - : 'Сбросить на системный'), + Text( + sys + ? 'Системный цвет активен' + : 'Сбросить на системный', + ), ], ), ), @@ -431,53 +545,52 @@ class _BubbleShapeCard extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Material( + return GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Форма сообщения', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w700, - ), + 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( - valueListenable: AppBubbleShape.current, - builder: (context, current, _) { - return SegmentedButton( - segments: const [ - ButtonSegment( - value: BubbleStyle.mobile, - label: Text('TG Mobile'), - icon: Icon(Symbols.smartphone), - ), - ButtonSegment( - value: BubbleStyle.desktop, - label: Text('TG Desktop'), - icon: Icon(Symbols.desktop_windows), - ), - ], - selected: {current}, - onSelectionChanged: (set) { - if (set.isNotEmpty) onChanged(set.first); - }, - ); - }, - ), - ], - ), + ), + const SizedBox(height: 4), + Text( + 'Скругление углов пузырей', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: AppBubbleShape.current, + builder: (context, current, _) { + return SegmentedButton( + segments: const [ + ButtonSegment( + value: BubbleStyle.mobile, + label: Text('TG Mobile'), + icon: Icon(Symbols.smartphone), + ), + ButtonSegment( + value: BubbleStyle.desktop, + label: Text('TG Desktop'), + icon: Icon(Symbols.desktop_windows), + ), + ], + selected: {current}, + onSelectionChanged: (set) { + if (set.isNotEmpty) onChanged(set.first); + }, + ); + }, + ), + ], ), ); } @@ -492,53 +605,52 @@ class _BubbleBehaviorCard extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Material( + return GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Поведение сообщения', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w700, - ), + 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( - valueListenable: AppBubbleBehavior.current, - builder: (context, current, _) { - return SegmentedButton( - segments: const [ - ButtonSegment( - value: BubbleBehavior.mutable, - label: Text('Изменяемая'), - icon: Icon(Symbols.auto_fix), - ), - ButtonSegment( - value: BubbleBehavior.immutable, - label: Text('Неизменяемая'), - icon: Icon(Symbols.lock), - ), - ], - selected: {current}, - onSelectionChanged: (set) { - if (set.isNotEmpty) onChanged(set.first); - }, - ); - }, - ), - ], - ), + ), + const SizedBox(height: 4), + Text( + 'Меняется ли форма пузыря по соседям в группе', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: AppBubbleBehavior.current, + builder: (context, current, _) { + return SegmentedButton( + segments: const [ + ButtonSegment( + value: BubbleBehavior.mutable, + label: Text('Изменяемая'), + icon: Icon(Symbols.auto_fix), + ), + ButtonSegment( + value: BubbleBehavior.immutable, + label: Text('Неизменяемая'), + icon: Icon(Symbols.lock), + ), + ], + selected: {current}, + onSelectionChanged: (set) { + if (set.isNotEmpty) onChanged(set.first); + }, + ); + }, + ), + ], ), ); } diff --git a/lib/frontend/screens/profile/cloud_storage_screen.dart b/lib/frontend/screens/profile/cloud_storage_screen.dart index 851db05..aef2967 100644 --- a/lib/frontend/screens/profile/cloud_storage_screen.dart +++ b/lib/frontend/screens/profile/cloud_storage_screen.dart @@ -14,6 +14,7 @@ import '../../../core/storage/app_database.dart'; import '../../../core/utils/format.dart'; import '../../../main.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/sheet_helpers.dart'; enum _EnvState { loading, notConfigured, ready } @@ -656,16 +657,11 @@ class _CornerAction extends StatelessWidget { child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), - child: Container( + child: GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: cs.outlineVariant.withValues(alpha: 0.5), - width: 0.5, - ), - ), + depth: 6, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -855,15 +851,10 @@ class _CloudFileCard extends StatelessWidget { onTap: onTap, child: AspectRatio( aspectRatio: 1.0, - child: Container( - decoration: BoxDecoration( - color: cs.surfaceContainerLow, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: cs.outlineVariant.withValues(alpha: 0.5), - width: 0.5, - ), - ), + child: GlossyPill( + color: cs.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + depth: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/frontend/screens/profile/customization_screen.dart b/lib/frontend/screens/profile/customization_screen.dart index f2d2122..7f5bc2d 100644 --- a/lib/frontend/screens/profile/customization_screen.dart +++ b/lib/frontend/screens/profile/customization_screen.dart @@ -3,6 +3,7 @@ import 'package:m3e_collection/m3e_collection.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../core/utils/haptics.dart'; +import '../../widgets/glossy_pill.dart'; import 'app_icon_screen.dart'; import 'appearance_screen.dart'; import 'font_settings_screen.dart'; @@ -118,18 +119,16 @@ class _CategoryCard extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Material( + return GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18), - child: Row( - children: [ - Container( - width: 48, + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18), + depth: 6, + onTap: onTap, + child: Row( + children: [ + Container( + width: 48, height: 48, decoration: BoxDecoration( color: cs.primaryContainer, @@ -172,8 +171,6 @@ class _CategoryCard extends StatelessWidget { Icon(Symbols.chevron_right, color: cs.outline, size: 22), ], ), - ), - ), ); } } diff --git a/lib/frontend/screens/profile/debug_menu_screen.dart b/lib/frontend/screens/profile/debug_menu_screen.dart index c24d8eb..ecef538 100644 --- a/lib/frontend/screens/profile/debug_menu_screen.dart +++ b/lib/frontend/screens/profile/debug_menu_screen.dart @@ -17,6 +17,7 @@ import '../../../core/utils/logger.dart'; import '../../../core/utils/media_cache.dart'; import '../../../main.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/sheet_helpers.dart'; import '../../widgets/login_success_screen.dart'; import '../calls/call_screen.dart'; @@ -249,57 +250,54 @@ class _DebugMenuScreenState extends State { : ValueListenableBuilder( valueListenable: appState.fpsOverlayEnabled, builder: (context, fpsOn, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.speed, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Оверлей FPS', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.speed, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Оверлей FPS', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 2), - Text( - 'Показ текущего фреймрейта поверх интерфейса', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), + ), + const SizedBox(height: 2), + Text( + 'Показ текущего фреймрейта поверх интерфейса', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, ), - ], - ), + ), + ], ), - Switch( - value: fpsOn, - onChanged: (v) { - appState.setFpsOverlayEnabled(v); - }, - ), - ], - ), + ), + Switch( + value: fpsOn, + onChanged: (v) { + appState.setFpsOverlayEnabled(v); + }, + ), + ], ), ); }, @@ -314,59 +312,56 @@ class _DebugMenuScreenState extends State { : ValueListenableBuilder( valueListenable: appState.vpnBypassEnabled, builder: (context, bypassOn, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.vpn_key_off, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Обход VPN', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.vpn_key_off, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Обход VPN', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 2), - Text( - 'Если обнаружен VPN (tun-интерфейс), ' - 'подключаться напрямую через Wi-Fi или ' - 'моб. сеть в обход туннеля. Только Android', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), + ), + const SizedBox(height: 2), + Text( + 'Если обнаружен VPN (tun-интерфейс), ' + 'подключаться напрямую через Wi-Fi или ' + 'моб. сеть в обход туннеля. Только Android', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, ), - ], - ), + ), + ], ), - Switch( - value: bypassOn, - onChanged: (v) { - appState.setVpnBypassEnabled(v); - }, - ), - ], - ), + ), + Switch( + value: bypassOn, + onChanged: (v) { + appState.setVpnBypassEnabled(v); + }, + ), + ], ), ); }, @@ -381,60 +376,57 @@ class _DebugMenuScreenState extends State { : ValueListenableBuilder( valueListenable: appState.tlsInsecureEnabled, builder: (context, insecureOn, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.gpp_bad, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Отключить проверку TLS', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.gpp_bad, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Отключить проверку TLS', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 2), - Text( - 'Принимать любой сертификат сервера. ' - 'Только для отладки через MitM-прокси — ' - 'соединение становится уязвимым к ' - 'перехвату трафика', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), + ), + const SizedBox(height: 2), + Text( + 'Принимать любой сертификат сервера. ' + 'Только для отладки через MitM-прокси — ' + 'соединение становится уязвимым к ' + 'перехвату трафика', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, ), - ], - ), + ), + ], ), - Switch( - value: insecureOn, - onChanged: (v) { - appState.setTlsInsecureEnabled(v); - }, - ), - ], - ), + ), + Switch( + value: insecureOn, + onChanged: (v) { + appState.setTlsInsecureEnabled(v); + }, + ), + ], ), ); }, @@ -447,58 +439,55 @@ class _DebugMenuScreenState extends State { child: ValueListenableBuilder( valueListenable: AppSwipeBackDesktop.current, builder: (context, swipeOn, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.swipe_right, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Свайп-назад в десктоп-режиме', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.swipe_right, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Свайп-назад в десктоп-режиме', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 2), - Text( - 'Включает жест «провести от левого края, чтобы ' - 'закрыть» внутри встроенной панели чата на ' - 'десктопе — для тестирования курсором', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), + ), + const SizedBox(height: 2), + Text( + 'Включает жест «провести от левого края, чтобы ' + 'закрыть» внутри встроенной панели чата на ' + 'десктопе — для тестирования курсором', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, ), - ], - ), + ), + ], ), - Switch( - value: swipeOn, - onChanged: (v) { - AppSwipeBackDesktop.save(v); - }, - ), - ], - ), + ), + Switch( + value: swipeOn, + onChanged: (v) { + AppSwipeBackDesktop.save(v); + }, + ), + ], ), ); }, @@ -511,48 +500,45 @@ class _DebugMenuScreenState extends State { child: ValueListenableBuilder( valueListenable: AppPranks.current, builder: (context, pranksOn, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.auto_awesome, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Приколь4ики', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.auto_awesome, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Приколь4ики', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - ], - ), + ), + ], ), - Switch( - value: pranksOn, - onChanged: (v) { - AppPranks.save(v); - }, - ), - ], - ), + ), + Switch( + value: pranksOn, + onChanged: (v) { + AppPranks.save(v); + }, + ), + ], ), ); }, @@ -565,58 +551,55 @@ class _DebugMenuScreenState extends State { child: ValueListenableBuilder( valueListenable: AppDigitalIdNative.current, builder: (context, native, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.badge, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Нативный Цифровой ID', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.badge, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Нативный Цифровой ID', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 2), - Text( - native - ? 'Нативный экран (REST ext-api.max.ru)' - : 'Оригинальная страница в WebView', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), + ), + const SizedBox(height: 2), + Text( + native + ? 'Нативный экран (REST ext-api.max.ru)' + : 'Оригинальная страница в WebView', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, ), - ], - ), + ), + ], ), - Switch( - value: native, - onChanged: (v) { - AppDigitalIdNative.save(v); - }, - ), - ], - ), + ), + Switch( + value: native, + onChanged: (v) { + AppDigitalIdNative.save(v); + }, + ), + ], ), ); }, @@ -689,56 +672,53 @@ class _DebugMenuScreenState extends State { child: ValueListenableBuilder( valueListenable: AppStories.current, builder: (context, storiesOn, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.amp_stories, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Истории', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.amp_stories, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Истории', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 2), - Text( - 'Отображение ленты историй в списке чатов', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), + ), + const SizedBox(height: 2), + Text( + 'Отображение ленты историй в списке чатов', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, ), - ], - ), + ), + ], ), - Switch( - value: storiesOn, - onChanged: (v) { - AppStories.save(v); - }, - ), - ], - ), + ), + Switch( + value: storiesOn, + onChanged: (v) { + AppStories.save(v); + }, + ), + ], ), ); }, @@ -751,56 +731,53 @@ class _DebugMenuScreenState extends State { child: ValueListenableBuilder( valueListenable: AppLinkPreview.current, builder: (context, linkPreviewOn, _) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 17, - ), - child: Row( - children: [ - Icon( - Symbols.link, - color: cs.onSurfaceVariant, - size: 22, - weight: 400, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Предпросмотр ссылок', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + children: [ + Icon( + Symbols.link, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Предпросмотр ссылок', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 2), - Text( - 'Карточки с превью для ссылок в сообщениях', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), + ), + const SizedBox(height: 2), + Text( + 'Карточки с превью для ссылок в сообщениях', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, ), - ], - ), + ), + ], ), - Switch( - value: linkPreviewOn, - onChanged: (v) { - AppLinkPreview.save(v); - }, - ), - ], - ), + ), + Switch( + value: linkPreviewOn, + onChanged: (v) { + AppLinkPreview.save(v); + }, + ), + ], ), ); }, @@ -1007,11 +984,10 @@ class _DebugMenuScreenState extends State { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - child: Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + child: GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1084,11 +1060,10 @@ class _DebugMenuScreenState extends State { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - child: Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + child: GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/frontend/screens/profile/devices_screen.dart b/lib/frontend/screens/profile/devices_screen.dart index 067b6c7..1363d21 100644 --- a/lib/frontend/screens/profile/devices_screen.dart +++ b/lib/frontend/screens/profile/devices_screen.dart @@ -10,6 +10,7 @@ import '../../../core/utils/format.dart'; import '../../../main.dart' show accountModule; import '../../../backend/modules/account.dart' show SessionInfo; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/sheet_helpers.dart'; import 'web_qr_scan_screen.dart'; @@ -352,14 +353,14 @@ class _DevicesScreenState extends State } Widget _buildPromoCard(BuildContext context, ColorScheme cs) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), - decoration: BoxDecoration( + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(24), - ), - child: Center( + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + depth: 6, + child: Center( child: Column( children: [ Container( @@ -414,19 +415,20 @@ class _DevicesScreenState extends State ), ], ), + ), ), ); } Widget _buildDevicesList(BuildContext context, ColorScheme cs) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(24), - ), - child: Column( + padding: const EdgeInsets.symmetric(vertical: 8), + depth: 6, + child: Column( children: [ if (_isLoading) ...List.generate(5, (index) => _buildShimmerItem(cs)) @@ -475,6 +477,7 @@ class _DevicesScreenState extends State ), ], ], + ), ), ); } @@ -671,15 +674,16 @@ class _DevicesScreenState extends State duration: const Duration(milliseconds: 300), curve: Curves.easeOutQuart, child: isExpanded && details != null - ? Container( - width: double.infinity, - margin: const EdgeInsets.only(top: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( + ? Padding( + padding: const EdgeInsets.only(top: 12), + child: GlossyPill( color: cs.onSurface.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(12), - ), - child: Stack( + padding: const EdgeInsets.all(12), + depth: 6, + child: SizedBox( + width: double.infinity, + child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -740,6 +744,8 @@ class _DevicesScreenState extends State ), ), ], + ), + ), ), ) : const SizedBox(width: double.infinity, height: 0), diff --git a/lib/frontend/screens/profile/font_settings_screen.dart b/lib/frontend/screens/profile/font_settings_screen.dart index 7436b6a..8c3f878 100644 --- a/lib/frontend/screens/profile/font_settings_screen.dart +++ b/lib/frontend/screens/profile/font_settings_screen.dart @@ -6,6 +6,7 @@ import '../../../core/config/app_fonts.dart'; import '../../../core/utils/haptics.dart'; import '../../../main.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; class FontSettingsScreen extends StatefulWidget { const FontSettingsScreen({super.key}); @@ -138,10 +139,7 @@ class _FontSettingsScreenState extends State { return Scaffold( backgroundColor: cs.surface, - appBar: AppBarM3E( - titleText: 'Шрифты', - backgroundColor: cs.surface, - ), + appBar: AppBarM3E(titleText: 'Шрифты', backgroundColor: cs.surface), body: SafeArea( top: false, child: ListView( @@ -217,42 +215,44 @@ class _PreviewCard extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Container( - width: double.infinity, + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'ПРЕДПРОСМОТР', - style: TextStyle( - color: cs.primary, - fontSize: 12, - fontWeight: FontWeight.w700, - letterSpacing: 1.2, + depth: 6, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'ПРЕДПРОСМОТР', + style: TextStyle( + color: cs.primary, + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + ), ), - ), - const SizedBox(height: 16), - Text( - 'Съешь ещё этих мягких булок', - style: AppFonts.sample(fontId, fontSize: 22).copyWith( - color: cs.onSurface, - fontWeight: FontWeight.w600, - height: 1.25, + const SizedBox(height: 16), + Text( + 'Съешь ещё этих мягких булок', + style: AppFonts.sample(fontId, fontSize: 22).copyWith( + color: cs.onSurface, + fontWeight: FontWeight.w600, + height: 1.25, + ), ), - ), - const SizedBox(height: 10), - Text( - 'The quick brown fox 0123', - style: AppFonts.sample(fontId, fontSize: 15).copyWith( - color: cs.onSurfaceVariant, + const SizedBox(height: 10), + Text( + 'The quick brown fox 0123', + style: AppFonts.sample( + fontId, + fontSize: 15, + ).copyWith(color: cs.onSurfaceVariant), ), - ), - ], + ], + ), ), ); } @@ -316,10 +316,7 @@ class _FontOption extends StatelessWidget { : (font.isSystem ? Symbols.smartphone : Symbols.font_download), fill: selected ? 1 : 0, ), - label: Text( - font.label, - style: AppFonts.sample(font.id, fontSize: 16), - ), + label: Text(font.label, style: AppFonts.sample(font.id, fontSize: 16)), ); if (onDelete == null) { @@ -357,12 +354,11 @@ class _FontSizeControl extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDefault = (scale - AppFonts.defaultScale).abs() < 0.001; - return Container( + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.fromLTRB(20, 16, 12, 16), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), - ), + depth: 6, child: Column( children: [ Row( @@ -378,9 +374,8 @@ class _FontSizeControl extends StatelessWidget { value: AppFonts.clampScale(scale), min: AppFonts.minScale, max: AppFonts.maxScale, - divisions: - ((AppFonts.maxScale - AppFonts.minScale) / 0.05) - .round(), + divisions: ((AppFonts.maxScale - AppFonts.minScale) / 0.05) + .round(), onChanged: onChanged, onChangeEnd: onChangeEnd, ), diff --git a/lib/frontend/screens/profile/info_screen.dart b/lib/frontend/screens/profile/info_screen.dart index 0a93bfb..405ec8b 100644 --- a/lib/frontend/screens/profile/info_screen.dart +++ b/lib/frontend/screens/profile/info_screen.dart @@ -5,6 +5,7 @@ import '../../../core/storage/app_database.dart'; import '../../../core/storage/token_storage.dart'; import '../../../l10n/app_localizations.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/section_header.dart'; class InfoScreen extends StatefulWidget { @@ -170,14 +171,14 @@ class _InfoScreenState extends State { } Widget _buildRow(String key, String label, String value, ColorScheme cs) { - return Container( - margin: const EdgeInsets.only(bottom: 1), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), - decoration: BoxDecoration( + return Padding( + padding: const EdgeInsets.only(bottom: 1), + child: GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), - ), - child: Row( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), + depth: 6, + child: Row( children: [ Expanded( flex: 2, @@ -204,27 +205,26 @@ class _InfoScreenState extends State { ), ), ], + ), ), ); } Widget _buildListRow(List? items, ColorScheme cs) { if (items == null || items.isEmpty) { - return Container( + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(12), - ), + depth: 6, child: Text('-', style: TextStyle(color: cs.onSurfaceVariant)), ); } - return Container( + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(12), - ), + depth: 6, child: Wrap( spacing: 8, runSpacing: 4, diff --git a/lib/frontend/screens/profile/message_actions_screen.dart b/lib/frontend/screens/profile/message_actions_screen.dart index c0abf82..843ab9a 100644 --- a/lib/frontend/screens/profile/message_actions_screen.dart +++ b/lib/frontend/screens/profile/message_actions_screen.dart @@ -4,6 +4,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../../core/config/app_message_actions_style.dart'; import '../../../core/utils/haptics.dart'; +import '../../widgets/glossy_pill.dart'; class MessageActionsScreen extends StatelessWidget { const MessageActionsScreen({super.key}); @@ -52,12 +53,12 @@ class _StyleCard extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Material( + return GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), - child: Column( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), + depth: 6, + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -97,7 +98,6 @@ class _StyleCard extends StatelessWidget { ), ], ), - ), ); } } diff --git a/lib/frontend/screens/profile/notifications_screen.dart b/lib/frontend/screens/profile/notifications_screen.dart index 30ae97d..81e0dbb 100644 --- a/lib/frontend/screens/profile/notifications_screen.dart +++ b/lib/frontend/screens/profile/notifications_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:m3e_collection/m3e_collection.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/section_header.dart'; import '../../widgets/sheet_helpers.dart'; @@ -164,11 +165,10 @@ class _NotificationsScreenState extends State { } Widget _card(ColorScheme cs, List children) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Column(children: children), ); } diff --git a/lib/frontend/screens/profile/password_entry_screen.dart b/lib/frontend/screens/profile/password_entry_screen.dart index 0cca827..362f682 100644 --- a/lib/frontend/screens/profile/password_entry_screen.dart +++ b/lib/frontend/screens/profile/password_entry_screen.dart @@ -4,6 +4,7 @@ import '../../../main.dart' show accountModule; import '../../../backend/modules/account.dart' show TwoFactorDetails; import '../../../core/storage/app_database.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; class PasswordEntryScreen extends StatefulWidget { const PasswordEntryScreen({super.key}); @@ -185,11 +186,10 @@ class _PasswordEntryScreenState extends State { } Widget _buildSetupSection(ColorScheme cs) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Column( children: [ _buildHeaderTile( @@ -217,14 +217,14 @@ class _PasswordEntryScreenState extends State { } Widget _buildPasswordGate(ColorScheme cs) { - return Container( - width: double.infinity, + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), - child: Column( + depth: 6, + child: SizedBox( + width: double.infinity, + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( @@ -296,20 +296,21 @@ class _PasswordEntryScreenState extends State { ), ], ), + ), ); } Widget _buildManageSection(ColorScheme cs) { return Column( children: [ - Container( - width: double.infinity, + GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), - child: Row( + depth: 6, + child: SizedBox( + width: double.infinity, + child: Row( children: [ Container( width: 48, @@ -360,13 +361,13 @@ class _PasswordEntryScreenState extends State { ), ], ), + ), ), const SizedBox(height: 16), - Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Column( children: [ _buildActionRow( diff --git a/lib/frontend/screens/profile/performance_screen.dart b/lib/frontend/screens/profile/performance_screen.dart index 6d146f3..f34a387 100644 --- a/lib/frontend/screens/profile/performance_screen.dart +++ b/lib/frontend/screens/profile/performance_screen.dart @@ -4,6 +4,7 @@ import 'package:m3e_collection/m3e_collection.dart'; import '../../../core/config/app_cache_extent.dart'; import '../../../core/utils/haptics.dart'; import '../../widgets/confirm_dialog.dart'; +import '../../widgets/glossy_pill.dart'; class PerformanceScreen extends StatefulWidget { const PerformanceScreen({super.key}); @@ -100,55 +101,54 @@ class _PerformanceScreenState extends State { physics: const BouncingScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), children: [ - Material( + GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Кеш сообщений', - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w700, + 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: hint, fontSize: 13, height: 1.3), + ), + const SizedBox(height: 18), + Text( + 'Текущий cacheExtent: ${_value.round()}', + style: TextStyle(color: hint, fontSize: 12), + ), + const SizedBox(height: 4), + Slider( + value: _value, + min: AppCacheExtent.min, + max: AppCacheExtent.max, + onChanged: _onChanged, + onChangeEnd: _onChangeEnd, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Меньше потребление', + style: TextStyle(color: hint, fontSize: 11), ), - ), - const SizedBox(height: 4), - Text( - 'Сколько пикселей сообщений держать построенными за пределами видимой области.', - style: TextStyle(color: hint, fontSize: 13, height: 1.3), - ), - const SizedBox(height: 18), - Text( - 'Текущий cacheExtent: ${_value.round()}', - style: TextStyle(color: hint, fontSize: 12), - ), - const SizedBox(height: 4), - Slider( - value: _value, - min: AppCacheExtent.min, - max: AppCacheExtent.max, - onChanged: _onChanged, - onChangeEnd: _onChangeEnd, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Меньше потребление', - style: TextStyle(color: hint, fontSize: 11), - ), - Text( - 'Больше FPS', - style: TextStyle(color: hint, fontSize: 11), - ), - ], - ), - ], - ), + Text( + 'Больше FPS', + style: TextStyle(color: hint, fontSize: 11), + ), + ], + ), + ], ), ), ], diff --git a/lib/frontend/screens/profile/security_screen.dart b/lib/frontend/screens/profile/security_screen.dart index a4bf972..db4c779 100644 --- a/lib/frontend/screens/profile/security_screen.dart +++ b/lib/frontend/screens/profile/security_screen.dart @@ -7,6 +7,7 @@ import '../../../backend/modules/account.dart' import '../../../core/storage/app_database.dart'; import '../../widgets/confirm_dialog.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/sheet_helpers.dart'; import 'password_entry_screen.dart'; @@ -238,11 +239,10 @@ class _SecurityScreenState extends State } Widget _buildTopSection(ColorScheme cs) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Column( children: [ _buildPasswordRow(cs), @@ -336,11 +336,10 @@ class _SecurityScreenState extends State Widget _buildPrivacySettings(ColorScheme cs) { final isSafeMode = _privacyConfig?.safeMode ?? false; - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Column( children: [ Padding( @@ -725,11 +724,10 @@ class _SecurityScreenState extends State } Widget _buildConfidentialSection(ColorScheme cs) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Column( children: [ _buildSwitchRow( @@ -771,11 +769,10 @@ class _SecurityScreenState extends State Widget _buildBlacklistSection(ColorScheme cs) { final count = _blockedContacts.length; - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Material( color: Colors.transparent, child: InkWell( diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 45c8420..968b869 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -10,6 +10,7 @@ import '../../../core/storage/token_storage.dart'; import '../../../core/utils/haptics.dart'; import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; +import '../../widgets/glossy_pill.dart'; import '../../widgets/info_action_sheet.dart'; import '../../widgets/komet_avatar.dart'; import '../../widgets/sheet_helpers.dart'; @@ -631,11 +632,10 @@ class _SettingsTabState extends State { ColorScheme cs, { required List<_SettingsItem> items, }) { - return Container( - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - ), + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, child: Column( children: List.generate(items.length, (index) { final item = items[index]; diff --git a/lib/frontend/screens/profile/theme_settings_screen.dart b/lib/frontend/screens/profile/theme_settings_screen.dart index 2332fe5..bdc2d62 100644 --- a/lib/frontend/screens/profile/theme_settings_screen.dart +++ b/lib/frontend/screens/profile/theme_settings_screen.dart @@ -7,6 +7,7 @@ import '../../../core/config/app_theme_mode.dart'; import '../../../core/config/app_theme_schedule.dart'; import '../../../core/utils/haptics.dart'; import '../../../main.dart'; +import '../../widgets/glossy_pill.dart'; class ThemeSettingsScreen extends StatelessWidget { const ThemeSettingsScreen({super.key}); @@ -49,16 +50,16 @@ class _ThemeModeCard extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Material( + return GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Режим темы', + padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), + depth: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Режим темы', style: TextStyle( color: cs.onSurface, fontSize: 16, @@ -94,7 +95,6 @@ class _ThemeModeCard extends StatelessWidget { ), ], ), - ), ); } } @@ -176,15 +176,15 @@ class _AmoledCardState extends State<_AmoledCard> { return Listener( behavior: HitTestBehavior.translucent, onPointerDown: (e) => _lastPointerPosition = e.position, - child: Material( + child: GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 14, 12, 14), - child: Row( - children: [ - Icon( - Symbols.contrast, + padding: const EdgeInsets.fromLTRB(20, 14, 12, 14), + depth: 6, + child: Row( + children: [ + Icon( + Symbols.contrast, color: cs.onSurface, size: 24, weight: 500, @@ -231,7 +231,6 @@ class _AmoledCardState extends State<_AmoledCard> { ], ), ), - ), ); } } @@ -249,16 +248,16 @@ class _ScheduleCard extends StatelessWidget { return AnimatedOpacity( opacity: enabled ? 1 : 0.5, duration: const Duration(milliseconds: 200), - child: Material( + child: GlossyPill( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Расписание', + padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), + depth: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Расписание', style: TextStyle( color: cs.onSurface, fontSize: 16, @@ -313,7 +312,6 @@ class _ScheduleCard extends StatelessWidget { ), ], ), - ), ), ); }, @@ -339,17 +337,15 @@ class _TimeRow extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Material( + return GlossyPill( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(16), - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: enabled ? () => _pick(context) : null, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), - child: Row( - children: [ - Icon(icon, color: cs.onSurface, size: 22, weight: 500), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + depth: 6, + onTap: enabled ? () => _pick(context) : null, + child: Row( + children: [ + Icon(icon, color: cs.onSurface, size: 22, weight: 500), const SizedBox(width: 12), Expanded( child: Text( @@ -372,8 +368,6 @@ class _TimeRow extends StatelessWidget { ), ], ), - ), - ), ); } diff --git a/lib/frontend/widgets/glossy_pill.dart b/lib/frontend/widgets/glossy_pill.dart new file mode 100644 index 0000000..f5cf94c --- /dev/null +++ b/lib/frontend/widgets/glossy_pill.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; + +import '../../core/config/app_pill_gradient.dart'; +import '../../core/config/app_visual_style.dart'; + +class _GlossyParts { + final bool dark; + final Gradient fill; + final Border rim; + final Gradient topSheen; + final Gradient bottomShade; + + const _GlossyParts({ + required this.dark, + required this.fill, + required this.rim, + required this.topSheen, + required this.bottomShade, + }); +} + +class GlossyDecor { + static final Map _cache = {}; + + static _GlossyParts _parts(Color base) { + final cached = _cache[base]; + if (cached != null) return cached; + if (_cache.length > 64) _cache.clear(); + final hsl = HSLColor.fromColor(base); + final dark = hsl.lightness < 0.5; + Color shift(double d) => + hsl.withLightness((hsl.lightness + d).clamp(0.0, 1.0)).toColor(); + final parts = _GlossyParts( + dark: dark, + fill: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [shift(dark ? 0.06 : 0.05), base, shift(dark ? -0.05 : -0.07)], + stops: const [0.0, 0.5, 1.0], + ), + rim: Border.all( + color: Colors.white.withValues(alpha: dark ? 0.08 : 0.6), + width: 0.8, + ), + topSheen: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.center, + colors: [ + Colors.white.withValues(alpha: dark ? 0.11 : 0.45), + Colors.white.withValues(alpha: 0.0), + ], + ), + bottomShade: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.center, + colors: [ + Colors.black.withValues(alpha: dark ? 0.2 : 0.07), + Colors.black.withValues(alpha: 0.0), + ], + ), + ); + _cache[base] = parts; + return parts; + } + + static bool isDark(Color base) => _parts(base).dark; + static Gradient fillGradient(Color base) => _parts(base).fill; + static Border rimBorder(Color base) => _parts(base).rim; + static Gradient topSheen(Color base) => _parts(base).topSheen; + static Gradient bottomShade(Color base) => _parts(base).bottomShade; + + static BoxShadow dropShadow(Color base, double depth) { + final dark = _parts(base).dark; + return BoxShadow( + color: Colors.black.withValues(alpha: dark ? 0.5 : 0.22), + blurRadius: depth * 1.6, + spreadRadius: -depth * 0.3, + offset: Offset(0, depth * 0.6), + ); + } +} + +class GlossyPill extends StatelessWidget { + final Widget child; + final EdgeInsetsGeometry padding; + final BorderRadius borderRadius; + final Color? color; + final VoidCallback? onTap; + final double depth; + final bool elevated; + final BorderSide? borderSide; + + const GlossyPill({ + super.key, + required this.child, + this.padding = EdgeInsets.zero, + BorderRadius? borderRadius, + this.color, + this.onTap, + this.depth = 10, + this.elevated = false, + this.borderSide, + }) : borderRadius = + borderRadius ?? const BorderRadius.all(Radius.circular(100)); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: AppVisualStyle.current, + builder: (context, style, _) { + if (style == VisualStyle.materialYou) return _flat(context); + return ValueListenableBuilder( + valueListenable: AppPillGradient.current, + builder: (context, gradient, _) => _glossy(context, gradient), + ); + }, + ); + } + + Widget _flat(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final base = color ?? cs.surfaceContainerHigh; + final content = Padding(padding: padding, child: child); + return Material( + color: base, + elevation: elevated ? 3 : 0, + shadowColor: Colors.black.withValues(alpha: 0.4), + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + side: borderSide ?? BorderSide.none, + ), + clipBehavior: Clip.antiAlias, + child: onTap == null ? content : InkWell(onTap: onTap, child: content), + ); + } + + Widget _glossy(BuildContext context, bool gradient) { + final cs = Theme.of(context).colorScheme; + final base = color ?? cs.surfaceContainerHigh; + final content = Padding(padding: padding, child: child); + + return RepaintBoundary( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: gradient ? null : base, + gradient: gradient ? GlossyDecor.fillGradient(base) : null, + border: GlossyDecor.rimBorder(base), + boxShadow: [GlossyDecor.dropShadow(base, depth)], + ), + child: ClipRRect( + borderRadius: borderRadius, + child: Stack( + fit: StackFit.passthrough, + children: [ + if (gradient) ...[ + Positioned.fill( + child: IgnorePointer( + child: DecoratedBox( + decoration: + BoxDecoration(gradient: GlossyDecor.topSheen(base)), + ), + ), + ), + Positioned.fill( + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: GlossyDecor.bottomShade(base), + ), + ), + ), + ), + ], + if (onTap == null) + content + else + Material( + type: MaterialType.transparency, + child: InkWell(onTap: onTap, child: content), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/frontend/widgets/sliding_pill_nav.dart b/lib/frontend/widgets/sliding_pill_nav.dart index f642e6f..29169fb 100644 --- a/lib/frontend/widgets/sliding_pill_nav.dart +++ b/lib/frontend/widgets/sliding_pill_nav.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; +import '../../core/config/app_pill_gradient.dart'; +import '../../core/config/app_visual_style.dart'; +import 'glossy_pill.dart'; + class PillNavItem { final IconData icon; final String label; @@ -74,17 +78,43 @@ class SlidingPillNav extends StatelessWidget { @override Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: AppVisualStyle.current, + builder: (context, style, _) { + if (style != VisualStyle.glossy) { + return _buildNav(context, glossy: false, gradient: false); + } + return ValueListenableBuilder( + valueListenable: AppPillGradient.current, + builder: (context, gradient, _) => + _buildNav(context, glossy: true, gradient: gradient), + ); + }, + ); + } + + Widget _buildNav( + BuildContext context, { + required bool glossy, + required bool gradient, + }) { final cs = Theme.of(context).colorScheme; final visualSel = position.round().clamp(0, items.length - 1); + final base = backgroundColor ?? cs.surfaceContainerHigh; + final useGradient = glossy && gradient; + return Container( height: height, padding: const EdgeInsets.symmetric(horizontal: 2), decoration: BoxDecoration( - color: backgroundColor ?? cs.surfaceContainerHigh, + color: useGradient ? null : base, + gradient: useGradient ? GlossyDecor.fillGradient(base) : null, borderRadius: BorderRadius.circular(34), - border: borderColor != null - ? Border.all(color: borderColor!, width: 0.5) - : null, + border: glossy + ? GlossyDecor.rimBorder(base) + : (borderColor != null + ? Border.all(color: borderColor!, width: 0.5) + : null), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.5), @@ -96,6 +126,18 @@ class SlidingPillNav extends StatelessWidget { child: Stack( clipBehavior: Clip.hardEdge, children: [ + if (useGradient) + Positioned.fill( + child: IgnorePointer( + child: ClipRRect( + borderRadius: BorderRadius.circular(34), + child: DecoratedBox( + decoration: + BoxDecoration(gradient: GlossyDecor.topSheen(base)), + ), + ), + ), + ), AnimatedPositioned( duration: animationDuration, curve: Curves.easeOutCubic, diff --git a/lib/main.dart b/lib/main.dart index 5d4d5ab..343f7e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,8 @@ import 'core/config/app_pranks.dart'; import 'core/config/app_stories.dart'; 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_theme_mode.dart'; import 'core/config/app_theme_schedule.dart'; import 'core/config/app_digital_id_mode.dart'; @@ -94,6 +96,8 @@ void main() async { final cacheExtentFuture = AppCacheExtent.load(); final themeModeFuture = AppThemeModeConfig.load(); final amoledFuture = AppAmoled.load(); + final pillGradientFuture = AppPillGradient.load(); + final visualStyleFuture = AppVisualStyle.load(); final themeScheduleFuture = AppThemeSchedule.load(); final messageActionsFuture = AppMessageActionsStyle.load(); final swipeBackFuture = AppSwipeBackDesktop.load(); @@ -126,6 +130,8 @@ void main() async { AppCacheExtent.current.value = await cacheExtentFuture; AppThemeModeConfig.current.value = await themeModeFuture; AppAmoled.current.value = await amoledFuture; + AppPillGradient.current.value = await pillGradientFuture; + AppVisualStyle.current.value = await visualStyleFuture; AppThemeSchedule.current.value = await themeScheduleFuture; AppMessageActionsStyle.current.value = await messageActionsFuture; AppSwipeBackDesktop.current.value = await swipeBackFuture;