feat(frontend): flossy стиль. Выбор градиент/без

This commit is contained in:
Jganenok
2026-06-12 00:13:13 +07:00
parent 213c57e524
commit e4ed1c10e1
26 changed files with 1729 additions and 1153 deletions
+41 -4
View File
@@ -39,6 +39,10 @@ class CallSession {
bool _peerMuted = false;
bool _peerVideo = false;
bool _mediaConnected = false;
bool _remoteDescSet = false;
bool _ownRemoteStream = false;
final List<RTCIceCandidate> _pendingCandidates = [];
Future<void> _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<String, dynamic> msg) {
_tail = _tail.then((_) => _onNotification(msg)).catchError((_) {});
}
Future<void> _onNotification(Map<String, dynamic> msg) async {
_applyPeerMedia(msg);
switch (msg['notification']) {
@@ -195,7 +203,10 @@ class CallSession {
Future<void> _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<void> _flushCandidates() async {
final pc = _pc;
if (pc == null || _pendingCandidates.isEmpty) return;
final pending = List<RTCIceCandidate>.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();
+18
View File
@@ -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<bool> current = ValueNotifier(true);
static Future<bool> load() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(prefKey) ?? true;
}
static Future<void> save(bool value) async {
current.value = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(prefKey, value);
}
}
+26
View File
@@ -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<VisualStyle> current =
ValueNotifier(VisualStyle.materialYou);
static Future<VisualStyle> load() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(prefKey) == 'glossy'
? VisualStyle.glossy
: VisualStyle.materialYou;
}
static Future<void> save(VisualStyle value) async {
current.value = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
prefKey,
value == VisualStyle.glossy ? 'glossy' : 'materialYou',
);
}
}
+36 -20
View File
@@ -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<CallScreen>
@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<CallScreen>
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<CallScreen>
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<CallScreen>
}
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),
),
),
),
@@ -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<ChatInfoScreen> {
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<ChatInfoScreen> {
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<ChatInfoScreen> {
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<ChatInfoScreen> {
}
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,
),
),
],
),
),
);
}
@@ -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<ChatListScreen>
),
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<ChatListScreen>
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<ChatListScreen>
);
}
},
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<ChatListScreen>
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,
),
],
),
),
],
),
),
);
+310 -138
View File
@@ -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<ChatScreen> 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<ChatScreen> 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<ChatScreen> 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<String>(
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<void> _startCall() async {
if (widget.chatType != 'DIALOG') {
showCustomNotification(context, 'Звонки доступны только в диалогах');
@@ -1127,127 +1257,170 @@ class _ChatScreenState extends State<ChatScreen> 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<String>(
valueListenable: _headerStatusNotifier,
builder: (context, status, _) => Text(
status,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
fontWeight: FontWeight.w400,
),
),
),
],
],
),
ValueListenableBuilder<String>(
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<ChatScreen> 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<ChatScreen> 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<ChatScreen> with TickerProviderStateMixin {
),
],
),
),
),
),
AnimatedBuilder(
@@ -1725,23 +1897,23 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
},
child: ValueListenableBuilder<bool>(
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,
),
),
),
),
@@ -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<ContactProfileScreen> {
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<ContactProfileScreen> {
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],
],
],
],
),
),
);
}
@@ -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<AppIconScreen> {
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<AppIcon>(
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<AppIcon>(
valueListenable: AppIconConfig.current,
builder: (context, current, _) {
return Column(
children: [
for (final icon in AppIcon.values)
_IconTile(
icon: icon,
selected: current == icon,
onTap: () => _select(icon),
),
],
);
},
),
],
),
),
],
@@ -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<AppearanceScreen> {
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<AppearanceScreen> {
_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<AppearanceScreen> {
}
}
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<VisualStyle>(
valueListenable: AppVisualStyle.current,
builder: (context, current, _) {
return SegmentedButton<VisualStyle>(
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<bool>(
valueListenable: AppPillGradient.current,
builder: (context, value, _) => Switch(
value: value,
onChanged: (v) {
Haptics.selection();
AppPillGradient.save(v);
},
),
),
],
),
);
}
}
class _PreviewSection extends StatefulWidget {
final ValueNotifier<Color> color;
final ValueNotifier<bool> 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<BubbleStyle>(
valueListenable: AppBubbleShape.current,
builder: (context, current, _) {
return SegmentedButton<BubbleStyle>(
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<BubbleStyle>(
valueListenable: AppBubbleShape.current,
builder: (context, current, _) {
return SegmentedButton<BubbleStyle>(
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<BubbleBehavior>(
valueListenable: AppBubbleBehavior.current,
builder: (context, current, _) {
return SegmentedButton<BubbleBehavior>(
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<BubbleBehavior>(
valueListenable: AppBubbleBehavior.current,
builder: (context, current, _) {
return SegmentedButton<BubbleBehavior>(
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);
},
);
},
),
],
),
);
}
@@ -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: [
@@ -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),
],
),
),
),
);
}
}
@@ -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<DebugMenuScreen> {
: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
child: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
child: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
child: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
child: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
child: ValueListenableBuilder<bool>(
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<DebugMenuScreen> {
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<DebugMenuScreen> {
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,
@@ -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<DevicesScreen>
}
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<DevicesScreen>
),
],
),
),
),
);
}
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<DevicesScreen>
),
],
],
),
),
);
}
@@ -671,15 +674,16 @@ class _DevicesScreenState extends State<DevicesScreen>
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<DevicesScreen>
),
),
],
),
),
),
)
: const SizedBox(width: double.infinity, height: 0),
@@ -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<FontSettingsScreen> {
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,
),
+16 -16
View File
@@ -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<InfoScreen> {
}
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<InfoScreen> {
),
),
],
),
),
);
}
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,
@@ -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 {
),
],
),
),
);
}
}
@@ -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<NotificationsScreen> {
}
Widget _card(ColorScheme cs, List<Widget> 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),
);
}
@@ -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<PasswordEntryScreen> {
}
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<PasswordEntryScreen> {
}
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<PasswordEntryScreen> {
),
],
),
),
);
}
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<PasswordEntryScreen> {
),
],
),
),
),
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(
@@ -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<PerformanceScreen> {
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),
),
],
),
],
),
),
],
@@ -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<SecurityScreen>
}
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<SecurityScreen>
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<SecurityScreen>
}
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<SecurityScreen>
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(
@@ -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<SettingsTab> {
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];
@@ -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 {
),
],
),
),
),
);
}
+189
View File
@@ -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<Color, _GlossyParts> _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<VisualStyle>(
valueListenable: AppVisualStyle.current,
builder: (context, style, _) {
if (style == VisualStyle.materialYou) return _flat(context);
return ValueListenableBuilder<bool>(
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),
),
],
),
),
),
);
}
}
+46 -4
View File
@@ -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<VisualStyle>(
valueListenable: AppVisualStyle.current,
builder: (context, style, _) {
if (style != VisualStyle.glossy) {
return _buildNav(context, glossy: false, gradient: false);
}
return ValueListenableBuilder<bool>(
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,
+6
View File
@@ -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;