feat: liquid glass, пока-что скрыт.

This commit is contained in:
Jganenokk
2026-07-14 21:04:23 +07:00
parent f7103da880
commit d65b46f7da
25 changed files with 883 additions and 179 deletions
+7 -1
View File
@@ -1,8 +1,14 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../frontend/widgets/liquid_glass.dart';
import 'persisted_setting.dart'; import 'persisted_setting.dart';
enum ChatChromeStyle { color, blur, none, transparent } enum ChatChromeStyle { color, blur, none, transparent, liquidGlass }
class ChatChromeMaterial {
static bool isLiquid(ChatChromeStyle style) =>
style == ChatChromeStyle.liquidGlass && LiquidGlass.isSupported;
}
class AppChatChrome { class AppChatChrome {
static const prefKey = 'app_chat_chrome'; static const prefKey = 'app_chat_chrome';
+11 -1
View File
@@ -1,8 +1,18 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../frontend/widgets/liquid_glass.dart';
import 'persisted_setting.dart'; import 'persisted_setting.dart';
enum ComposerBackground { standard, frostBlur } enum ComposerBackground { standard, frostBlur, liquidGlass }
class ComposerMaterial {
static bool isLiquid(ComposerBackground value) =>
value == ComposerBackground.liquidGlass && LiquidGlass.isSupported;
static bool isFrost(ComposerBackground value) =>
value == ComposerBackground.frostBlur ||
(value == ComposerBackground.liquidGlass && !LiquidGlass.isSupported);
}
class AppComposerBackground { class AppComposerBackground {
static const prefKey = 'app_composer_background'; static const prefKey = 'app_composer_background';
+19
View File
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class AppLiquidGlass {
static const bool enabled = false;
static const double blurSigma = 0;
static const double spread = 1;
static const double refraction = 34;
static const double chroma = 0;
static const double specular = 0.45;
static const double rimWidth = 8;
static const Offset light = Offset(-0.4, -1);
static const double tintFeather = 44;
static Color navTint(ColorScheme cs) =>
cs.surfaceContainerHigh.withValues(alpha: 0);
static Color panelTint(ColorScheme cs) => cs.surface.withValues(alpha: 0.24);
}
+11 -1
View File
@@ -1,8 +1,18 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../frontend/widgets/liquid_glass.dart';
import 'persisted_setting.dart'; import 'persisted_setting.dart';
enum NavPillStyle { glossy, frostBlur } enum NavPillStyle { glossy, frostBlur, liquidGlass }
class NavPillMaterial {
static bool isLiquid(NavPillStyle style) =>
style == NavPillStyle.liquidGlass && LiquidGlass.isSupported;
static bool isFrost(NavPillStyle style) =>
style == NavPillStyle.frostBlur ||
(style == NavPillStyle.liquidGlass && !LiquidGlass.isSupported);
}
class AppNavPillStyle { class AppNavPillStyle {
static const prefKey = 'app_nav_pill_style'; static const prefKey = 'app_nav_pill_style';
+5 -1
View File
@@ -2,7 +2,11 @@ import 'package:flutter/foundation.dart';
import 'persisted_setting.dart'; import 'persisted_setting.dart';
enum VisualStyle { materialYou, glossy } enum VisualStyle { materialYou, glossy, liquidGlass }
extension VisualStyleChrome on VisualStyle {
bool get glossyChrome => this != VisualStyle.materialYou;
}
class AppVisualStyle { class AppVisualStyle {
static const prefKey = 'app_visual_style'; static const prefKey = 'app_visual_style';
@@ -9,6 +9,7 @@ import 'package:komet/frontend/widgets/online_dot.dart';
class ChatHeaderRow extends StatelessWidget { class ChatHeaderRow extends StatelessWidget {
final bool glossy; final bool glossy;
final bool frosted; final bool frosted;
final bool liquid;
final BackdropKey? backdropKey; final BackdropKey? backdropKey;
final ColorScheme cs; final ColorScheme cs;
final bool embedded; final bool embedded;
@@ -32,6 +33,7 @@ class ChatHeaderRow extends StatelessWidget {
super.key, super.key,
required this.glossy, required this.glossy,
required this.frosted, required this.frosted,
this.liquid = false,
this.backdropKey, this.backdropKey,
required this.cs, required this.cs,
required this.embedded, required this.embedded,
@@ -56,9 +58,9 @@ class ChatHeaderRow extends StatelessWidget {
Widget build(BuildContext context) => Widget build(BuildContext context) =>
glossy ? _glossyRow(context) : _materialRow(context); glossy ? _glossyRow(context) : _materialRow(context);
Color? get _pillColor => frosted ? AppFrost.pillTint(cs) : null; Color? get _pillColor => frosted || liquid ? AppFrost.pillTint(cs) : null;
double? get _pillBlur => frosted ? AppFrost.sigma : null; double? get _pillBlur => frosted && !liquid ? AppFrost.sigma : null;
Widget _glossyRow(BuildContext context) { Widget _glossyRow(BuildContext context) {
return Padding( return Padding(
@@ -73,6 +75,7 @@ class ChatHeaderRow extends StatelessWidget {
child: GlossyPill( child: GlossyPill(
color: _pillColor, color: _pillColor,
blurSigma: _pillBlur, blurSigma: _pillBlur,
liquid: liquid,
backdropKey: backdropKey, backdropKey: backdropKey,
onTap: () { onTap: () {
if (embedded) { if (embedded) {
@@ -97,6 +100,7 @@ class ChatHeaderRow extends StatelessWidget {
child: GlossyPill( child: GlossyPill(
color: _pillColor, color: _pillColor,
blurSigma: _pillBlur, blurSigma: _pillBlur,
liquid: liquid,
backdropKey: backdropKey, backdropKey: backdropKey,
onTap: onOpenInfo, onTap: onOpenInfo,
padding: const EdgeInsets.fromLTRB(6, 6, 16, 6), padding: const EdgeInsets.fromLTRB(6, 6, 16, 6),
@@ -185,6 +189,7 @@ class ChatHeaderRow extends StatelessWidget {
GlossyPill( GlossyPill(
color: _pillColor, color: _pillColor,
blurSigma: _pillBlur, blurSigma: _pillBlur,
liquid: liquid,
backdropKey: backdropKey, backdropKey: backdropKey,
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
child: SizedBox( child: SizedBox(
@@ -15,6 +15,7 @@ import 'package:komet/frontend/screens/chats/chat/upload_status.dart';
import 'package:komet/frontend/screens/chats/chat/video_note_controller.dart'; import 'package:komet/frontend/screens/chats/chat/video_note_controller.dart';
import 'package:komet/frontend/screens/chats/chat/voice_record_controller.dart'; import 'package:komet/frontend/screens/chats/chat/voice_record_controller.dart';
import 'package:komet/frontend/widgets/glossy_pill.dart'; import 'package:komet/frontend/widgets/glossy_pill.dart';
import 'package:komet/frontend/widgets/liquid_glass.dart';
import 'package:komet/frontend/widgets/rich_message_controller.dart'; import 'package:komet/frontend/widgets/rich_message_controller.dart';
class ComposerInputBar extends StatelessWidget { class ComposerInputBar extends StatelessWidget {
@@ -418,10 +419,14 @@ class ComposerInputBar extends StatelessWidget {
bool get _flat => style == ComposerStyle.materialYou; bool get _flat => style == ComposerStyle.materialYou;
bool get _frost => background == ComposerBackground.frostBlur; bool get _frost => ComposerMaterial.isFrost(background);
bool get _liquid => ComposerMaterial.isLiquid(background);
bool get _translucent => _frost || _liquid;
Widget _barSurface(ColorScheme cs, Widget child) { Widget _barSurface(ColorScheme cs, Widget child) {
if (!_flat || background == ComposerBackground.frostBlur) return child; if (!_flat || _translucent) return child;
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.surface, color: cs.surface,
@@ -434,13 +439,14 @@ class ComposerInputBar extends StatelessWidget {
Widget _fieldSurface(ColorScheme cs, Widget child) { Widget _fieldSurface(ColorScheme cs, Widget child) {
if (_flat) return child; if (_flat) return child;
return GlossyPill( return GlossyPill(
color: _frost color: _translucent
? AppFrost.inputTint(cs) ? AppFrost.inputTint(cs)
: Color.alphaBlend( : Color.alphaBlend(
cs.surfaceContainerHighest.withValues(alpha: 0.92), cs.surfaceContainerHighest.withValues(alpha: 0.92),
cs.surface, cs.surface,
), ),
blurSigma: _frost ? AppFrost.sigma : null, blurSigma: _frost ? AppFrost.sigma : null,
liquid: _liquid,
backdropKey: backdropKey, backdropKey: backdropKey,
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
depth: 8, depth: 8,
@@ -471,6 +477,7 @@ class ComposerInputBar extends StatelessWidget {
return GlossyPill( return GlossyPill(
color: color, color: color,
blurSigma: _frost ? AppFrost.sigma : null, blurSigma: _frost ? AppFrost.sigma : null,
liquid: _liquid,
backdropKey: backdropKey, backdropKey: backdropKey,
borderRadius: BorderRadius.circular(27), borderRadius: BorderRadius.circular(27),
onTap: onTap, onTap: onTap,
@@ -538,23 +545,14 @@ class ComposerInputBar extends StatelessWidget {
], ],
), ),
); );
if (_flat && _frost) return row; if (_flat && _translucent) return row;
if (!_frost && chrome != ChatChromeStyle.transparent) return row; if (!_translucent && chrome != ChatChromeStyle.transparent) return row;
return ClipRect( return GlassSurface(
child: BackdropFilter( liquid: _liquid,
filter: ui.ImageFilter.blur( frostTint: AppFrost.panelTint(cs),
sigmaX: AppFrost.sigma, border: Border(top: AppFrost.hairline(cs)),
sigmaY: AppFrost.sigma, backdropKey: backdropKey,
), child: row,
backdropGroupKey: backdropKey,
child: DecoratedBox(
decoration: BoxDecoration(
color: AppFrost.panelTint(cs),
border: Border(top: AppFrost.hairline(cs)),
),
child: row,
),
),
); );
}, },
); );
@@ -2033,17 +2033,21 @@ class _ChatListScreenState extends State<ChatListScreen>
ValueListenableBuilder<NavPillStyle>( ValueListenableBuilder<NavPillStyle>(
valueListenable: AppNavPillStyle.current, valueListenable: AppNavPillStyle.current,
builder: (context, navStyle, child) { builder: (context, navStyle, child) {
final liquid =
style.glossyChrome &&
NavPillMaterial.isLiquid(navStyle);
final frost = final frost =
style == VisualStyle.glossy && style.glossyChrome &&
navStyle == NavPillStyle.frostBlur; NavPillMaterial.isFrost(navStyle);
return GlossyPill( return GlossyPill(
onTap: _toggleFab, onTap: _toggleFab,
color: frost color: frost || liquid
? AppFrost.fabTint(cs) ? AppFrost.fabTint(cs)
: cs.primaryContainer, : cs.primaryContainer,
blurSigma: frost blurSigma: frost
? AppFrost.sigma ? AppFrost.sigma
: null, : null,
liquid: liquid,
backdropKey: _frostBackdrop, backdropKey: _frostBackdrop,
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
elevated: true, elevated: true,
+33 -29
View File
@@ -93,6 +93,7 @@ import '../../widgets/schedule_time_picker.dart';
import '../../widgets/chat_wallpaper_sheet.dart'; import '../../widgets/chat_wallpaper_sheet.dart';
import '../../widgets/chat_wallpaper_view.dart'; import '../../widgets/chat_wallpaper_view.dart';
import '../../widgets/glossy_pill.dart'; import '../../widgets/glossy_pill.dart';
import '../../widgets/liquid_glass.dart';
import 'scheduled_messages_screen.dart'; import 'scheduled_messages_screen.dart';
import 'chat_wallpaper_preview_screen.dart'; import 'chat_wallpaper_preview_screen.dart';
@@ -129,15 +130,12 @@ class _FrostedPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRect( return GlassSurface(
child: BackdropFilter( frostTint: tint,
filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma), frostSigma: sigma,
backdropGroupKey: backdropKey, border: border,
child: DecoratedBox( backdropKey: backdropKey,
decoration: BoxDecoration(color: tint, border: border), child: child,
child: child,
),
),
); );
} }
} }
@@ -474,13 +472,18 @@ class _ChatScreenState extends State<ChatScreen>
ChatWallpaper? _wallpaper; ChatWallpaper? _wallpaper;
bool get _composerFrosted => bool get _composerFrosted =>
AppComposerBackground.current.value == ComposerBackground.frostBlur; AppComposerBackground.current.value != ComposerBackground.standard;
bool get _composerUnderlap => bool get _composerUnderlap =>
AppChatChrome.current.value != ChatChromeStyle.color || _composerFrosted; AppChatChrome.current.value != ChatChromeStyle.color || _composerFrosted;
bool get _liquidChrome =>
AppVisualStyle.current.value.glossyChrome &&
ChatChromeMaterial.isLiquid(AppChatChrome.current.value);
ChatChromeStyle get _effectiveChrome { ChatChromeStyle get _effectiveChrome {
final chrome = AppChatChrome.current.value; final chrome = AppChatChrome.current.value;
if (chrome == ChatChromeStyle.liquidGlass) return ChatChromeStyle.transparent;
if (_wallpaper != null && chrome == ChatChromeStyle.none) { if (_wallpaper != null && chrome == ChatChromeStyle.none) {
return ChatChromeStyle.blur; return ChatChromeStyle.blur;
} }
@@ -2565,7 +2568,7 @@ class _ChatScreenState extends State<ChatScreen>
} }
PreferredSizeWidget _buildAppBar(ColorScheme cs) { PreferredSizeWidget _buildAppBar(ColorScheme cs) {
final glossy = AppVisualStyle.current.value == VisualStyle.glossy; final glossy = AppVisualStyle.current.value.glossyChrome;
final searchT = Curves.easeOut.transform(_searchAnim.value.clamp(0.0, 1.0)); final searchT = Curves.easeOut.transform(_searchAnim.value.clamp(0.0, 1.0));
final height = glossy final height = glossy
? ui.lerpDouble(_glossyHeaderHeight, _glossySearchHeight, searchT)! ? ui.lerpDouble(_glossyHeaderHeight, _glossySearchHeight, searchT)!
@@ -2644,6 +2647,7 @@ class _ChatScreenState extends State<ChatScreen>
glossy: glossy, glossy: glossy,
frosted: frosted:
glossy && chrome == ChatChromeStyle.transparent, glossy && chrome == ChatChromeStyle.transparent,
liquid: _liquidChrome,
backdropKey: _pillBackdrop, backdropKey: _pillBackdrop,
cs: cs, cs: cs,
embedded: widget.embedded, embedded: widget.embedded,
@@ -4381,6 +4385,7 @@ class _ChatScreenState extends State<ChatScreen>
isPreview: pinned.pinnedMsgIsPreview, isPreview: pinned.pinnedMsgIsPreview,
floating: floating, floating: floating,
frosted: _effectiveChrome == ChatChromeStyle.transparent, frosted: _effectiveChrome == ChatChromeStyle.transparent,
liquid: _liquidChrome,
backdropKey: _pillBackdrop, backdropKey: _pillBackdrop,
onTap: _jumpToPinnedMessage, onTap: _jumpToPinnedMessage,
onUnpin: pinned.canPinMessages(_myId) onUnpin: pinned.canPinMessages(_myId)
@@ -4437,7 +4442,7 @@ class _ChatScreenState extends State<ChatScreen>
} }
double _pinnedBannerTop() { double _pinnedBannerTop() {
final glossy = AppVisualStyle.current.value == VisualStyle.glossy; final glossy = AppVisualStyle.current.value.glossyChrome;
return MediaQuery.paddingOf(context).top + return MediaQuery.paddingOf(context).top +
(glossy ? _glossyHeaderHeight : kToolbarHeight) - (glossy ? _glossyHeaderHeight : kToolbarHeight) -
_pinnedBannerLift; _pinnedBannerLift;
@@ -4536,7 +4541,7 @@ class _ChatScreenState extends State<ChatScreen>
if (height != null) { if (height != null) {
resolved = height; resolved = height;
} else { } else {
final glossy = AppVisualStyle.current.value == VisualStyle.glossy; final glossy = AppVisualStyle.current.value.glossyChrome;
resolved = resolved =
MediaQuery.paddingOf(context).top + MediaQuery.paddingOf(context).top +
(glossy ? _glossyHeaderHeight : kToolbarHeight); (glossy ? _glossyHeaderHeight : kToolbarHeight);
@@ -4591,7 +4596,7 @@ class _ChatScreenState extends State<ChatScreen>
double _floatingDateTop(double pinnedHeight) { double _floatingDateTop(double pinnedHeight) {
if (AppChatChrome.current.value == ChatChromeStyle.color) { if (AppChatChrome.current.value == ChatChromeStyle.color) {
final glossy = AppVisualStyle.current.value == VisualStyle.glossy; final glossy = AppVisualStyle.current.value.glossyChrome;
return glossy ? 2 : 4; return glossy ? 2 : 4;
} }
if (chat?.hasPinnedMessage == true && pinnedHeight > 0) { if (chat?.hasPinnedMessage == true && pinnedHeight > 0) {
@@ -4919,8 +4924,9 @@ class _ChatScreenState extends State<ChatScreen>
width: 46, width: 46,
height: 46, height: 46,
child: GlossyPill( child: GlossyPill(
color: frosted ? AppFrost.pillTint(cs) : null, color: frosted || _liquidChrome ? AppFrost.pillTint(cs) : null,
blurSigma: frosted ? AppFrost.sigma : null, blurSigma: frosted && !_liquidChrome ? AppFrost.sigma : null,
liquid: _liquidChrome,
backdropKey: _pillBackdrop, backdropKey: _pillBackdrop,
elevated: true, elevated: true,
onTap: _onScrollDownTap, onTap: _onScrollDownTap,
@@ -5980,6 +5986,7 @@ class _PinnedMessageBanner extends StatelessWidget {
final VoidCallback? onUnpin; final VoidCallback? onUnpin;
final bool floating; final bool floating;
final bool frosted; final bool frosted;
final bool liquid;
final BackdropKey? backdropKey; final BackdropKey? backdropKey;
const _PinnedMessageBanner({ const _PinnedMessageBanner({
@@ -5989,6 +5996,7 @@ class _PinnedMessageBanner extends StatelessWidget {
this.onUnpin, this.onUnpin,
this.floating = false, this.floating = false,
this.frosted = false, this.frosted = false,
this.liquid = false,
this.backdropKey, this.backdropKey,
}); });
@@ -6058,19 +6066,15 @@ class _PinnedMessageBanner extends StatelessWidget {
final bottomBorder = Border(bottom: AppFrost.hairline(cs)); final bottomBorder = Border(bottom: AppFrost.hairline(cs));
if (frosted) { if (frosted) {
return ClipRRect( return GlassSurface(
borderRadius: floating ? BorderRadius.circular(16) : BorderRadius.zero, liquid: liquid,
child: BackdropFilter( borderRadius: floating
filter: ui.ImageFilter.blur( ? BorderRadius.circular(16)
sigmaX: AppFrost.sigma, : BorderRadius.zero,
sigmaY: AppFrost.sigma, frostTint: Colors.transparent,
), border: floating ? null : bottomBorder,
backdropGroupKey: backdropKey, backdropKey: backdropKey,
child: DecoratedBox( child: content,
decoration: BoxDecoration(border: floating ? null : bottomBorder),
child: content,
),
),
); );
} }
@@ -19,6 +19,7 @@ import '../../../core/utils/haptics.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../widgets/glossy_pill.dart'; import '../../widgets/glossy_pill.dart';
import '../../widgets/liquid_glass.dart';
class AppearanceScreen extends StatefulWidget { class AppearanceScreen extends StatefulWidget {
const AppearanceScreen({super.key}); const AppearanceScreen({super.key});
@@ -134,6 +135,25 @@ class _AppearanceScreenState extends State<AppearanceScreen> {
} }
} }
void _applyVisualStyle(VisualStyle style) {
AppVisualStyle.save(style);
if (style == VisualStyle.liquidGlass) {
AppNavPillStyle.save(NavPillStyle.liquidGlass);
AppComposerBackground.save(ComposerBackground.liquidGlass);
AppChatChrome.save(ChatChromeStyle.liquidGlass);
return;
}
if (AppNavPillStyle.current.value == NavPillStyle.liquidGlass) {
AppNavPillStyle.save(NavPillStyle.frostBlur);
}
if (AppComposerBackground.current.value == ComposerBackground.liquidGlass) {
AppComposerBackground.save(ComposerBackground.frostBlur);
}
if (AppChatChrome.current.value == ChatChromeStyle.liquidGlass) {
AppChatChrome.save(ChatChromeStyle.transparent);
}
}
class _VisualStyleCard extends StatelessWidget { class _VisualStyleCard extends StatelessWidget {
const _VisualStyleCard(); const _VisualStyleCard();
@@ -166,7 +186,12 @@ class _VisualStyleCard extends StatelessWidget {
ValueListenableBuilder<VisualStyle>( ValueListenableBuilder<VisualStyle>(
valueListenable: AppVisualStyle.current, valueListenable: AppVisualStyle.current,
builder: (context, current, _) { builder: (context, current, _) {
final selectable =
current == VisualStyle.liquidGlass && !LiquidGlass.isSupported
? VisualStyle.glossy
: current;
return SegmentedButton<VisualStyle>( return SegmentedButton<VisualStyle>(
showSelectedIcon: false,
segments: [ segments: [
ButtonSegment( ButtonSegment(
value: VisualStyle.materialYou, value: VisualStyle.materialYou,
@@ -176,12 +201,17 @@ class _VisualStyleCard extends StatelessWidget {
value: VisualStyle.glossy, value: VisualStyle.glossy,
label: Text(l10n.appearanceVisualStyleGlossy), label: Text(l10n.appearanceVisualStyleGlossy),
), ),
if (LiquidGlass.isSupported)
ButtonSegment(
value: VisualStyle.liquidGlass,
label: Text(l10n.appearanceVisualStyleLiquidGlass),
),
], ],
selected: {current}, selected: {selectable},
onSelectionChanged: (set) { onSelectionChanged: (set) {
if (set.isNotEmpty) { if (set.isNotEmpty) {
Haptics.selection(); Haptics.selection();
AppVisualStyle.save(set.first); _applyVisualStyle(set.first);
} }
}, },
); );
@@ -225,32 +255,46 @@ class _ChatChromeCard extends StatelessWidget {
ValueListenableBuilder<ChatChromeStyle>( ValueListenableBuilder<ChatChromeStyle>(
valueListenable: AppChatChrome.current, valueListenable: AppChatChrome.current,
builder: (context, current, _) { builder: (context, current, _) {
return SegmentedButton<ChatChromeStyle>( final selectable =
segments: [ current == ChatChromeStyle.liquidGlass &&
ButtonSegment( !LiquidGlass.isSupported
value: ChatChromeStyle.color, ? ChatChromeStyle.transparent
label: Text(l10n.appearanceChatChromeColor), : current;
), return SingleChildScrollView(
ButtonSegment( scrollDirection: Axis.horizontal,
value: ChatChromeStyle.blur, child: SegmentedButton<ChatChromeStyle>(
label: Text(l10n.appearanceChatChromeBlur), showSelectedIcon: false,
), segments: [
ButtonSegment( ButtonSegment(
value: ChatChromeStyle.none, value: ChatChromeStyle.color,
label: Text(l10n.appearanceChatChromeNone), label: Text(l10n.appearanceChatChromeColor),
), ),
ButtonSegment( ButtonSegment(
value: ChatChromeStyle.transparent, value: ChatChromeStyle.blur,
label: Text(l10n.appearanceChatChromeTransparent), label: Text(l10n.appearanceChatChromeBlur),
), ),
], ButtonSegment(
selected: {current}, value: ChatChromeStyle.none,
onSelectionChanged: (set) { label: Text(l10n.appearanceChatChromeNone),
if (set.isNotEmpty) { ),
Haptics.selection(); ButtonSegment(
AppChatChrome.save(set.first); value: ChatChromeStyle.transparent,
} label: Text(l10n.appearanceChatChromeTransparent),
}, ),
if (LiquidGlass.isSupported)
ButtonSegment(
value: ChatChromeStyle.liquidGlass,
label: Text(l10n.appearanceGlassMaterial),
),
],
selected: {selectable},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppChatChrome.save(set.first);
}
},
),
); );
}, },
), ),
@@ -317,7 +361,13 @@ class _ComposerBarCard extends StatelessWidget {
ValueListenableBuilder<ComposerBackground>( ValueListenableBuilder<ComposerBackground>(
valueListenable: AppComposerBackground.current, valueListenable: AppComposerBackground.current,
builder: (context, current, _) { builder: (context, current, _) {
final selectable =
current == ComposerBackground.liquidGlass &&
!LiquidGlass.isSupported
? ComposerBackground.frostBlur
: current;
return SegmentedButton<ComposerBackground>( return SegmentedButton<ComposerBackground>(
showSelectedIcon: false,
segments: [ segments: [
ButtonSegment( ButtonSegment(
value: ComposerBackground.standard, value: ComposerBackground.standard,
@@ -327,8 +377,13 @@ class _ComposerBarCard extends StatelessWidget {
value: ComposerBackground.frostBlur, value: ComposerBackground.frostBlur,
label: Text(l10n.appearanceComposerBackgroundFrost), label: Text(l10n.appearanceComposerBackgroundFrost),
), ),
if (LiquidGlass.isSupported)
ButtonSegment(
value: ComposerBackground.liquidGlass,
label: Text(l10n.appearanceGlassMaterial),
),
], ],
selected: {current}, selected: {selectable},
onSelectionChanged: (set) { onSelectionChanged: (set) {
if (set.isNotEmpty) { if (set.isNotEmpty) {
Haptics.selection(); Haptics.selection();
@@ -376,7 +431,13 @@ class _NavPillStyleCard extends StatelessWidget {
ValueListenableBuilder<NavPillStyle>( ValueListenableBuilder<NavPillStyle>(
valueListenable: AppNavPillStyle.current, valueListenable: AppNavPillStyle.current,
builder: (context, current, _) { builder: (context, current, _) {
final selectable =
current == NavPillStyle.liquidGlass &&
!LiquidGlass.isSupported
? NavPillStyle.frostBlur
: current;
return SegmentedButton<NavPillStyle>( return SegmentedButton<NavPillStyle>(
showSelectedIcon: false,
segments: [ segments: [
ButtonSegment( ButtonSegment(
value: NavPillStyle.glossy, value: NavPillStyle.glossy,
@@ -386,8 +447,13 @@ class _NavPillStyleCard extends StatelessWidget {
value: NavPillStyle.frostBlur, value: NavPillStyle.frostBlur,
label: Text(l10n.appearanceNavPillFrost), label: Text(l10n.appearanceNavPillFrost),
), ),
if (LiquidGlass.isSupported)
ButtonSegment(
value: NavPillStyle.liquidGlass,
label: Text(l10n.appearanceGlassMaterial),
),
], ],
selected: {current}, selected: {selectable},
onSelectionChanged: (set) { onSelectionChanged: (set) {
if (set.isNotEmpty) { if (set.isNotEmpty) {
Haptics.selection(); Haptics.selection();
@@ -13,6 +13,7 @@ import '../../../main.dart' show storiesModule;
import '../../../models/story.dart'; import '../../../models/story.dart';
import '../../widgets/custom_notification.dart'; import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.dart'; import '../../widgets/komet_avatar.dart';
import '../../widgets/liquid_glass.dart';
import 'story_owner_info.dart'; import 'story_owner_info.dart';
const _quickReactions = ['❤️', '🔥', '😍', '👏', '😂', '😮']; const _quickReactions = ['❤️', '🔥', '😍', '👏', '😂', '😮'];
@@ -627,33 +628,26 @@ class _StoryViewerScreenState extends State<StoryViewerScreen>
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Center( child: Center(
child: ClipRRect( child: GlassSurface(
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
child: BackdropFilter( frostTint: Colors.white.withValues(alpha: 0.12),
filter: ui.ImageFilter.blur(sigmaX: 14, sigmaY: 14), frostSigma: 14,
child: Container( border: Border.all(color: Colors.white.withValues(alpha: 0.18)),
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 10, padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 10,
), vertical: 8,
decoration: BoxDecoration( ),
color: Colors.white.withValues(alpha: 0.12), child: Row(
borderRadius: BorderRadius.circular(30), mainAxisSize: MainAxisSize.min,
border: Border.all( children: [
color: Colors.white.withValues(alpha: 0.18), for (final emoji in _quickReactions)
), _ReactionButton(
), emoji: emoji,
child: Row( selected: current == emoji,
mainAxisSize: MainAxisSize.min, onTap: () => _toggleReaction(emoji),
children: [ ),
for (final emoji in _quickReactions) ],
_ReactionButton(
emoji: emoji,
selected: current == emoji,
onTap: () => _toggleReaction(emoji),
),
],
),
), ),
), ),
), ),
@@ -749,16 +749,21 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
valueListenable: AppNavPillStyle.current, valueListenable: AppNavPillStyle.current,
builder: (context, navStyle, _) { builder: (context, navStyle, _) {
final frost = final frost =
style == VisualStyle.glossy && style.glossyChrome &&
navStyle == NavPillStyle.frostBlur; NavPillMaterial.isFrost(navStyle);
final liquid =
style.glossyChrome &&
NavPillMaterial.isLiquid(navStyle);
return SlidingPillNav( return SlidingPillNav(
items: navItems, items: navItems,
position: _currentPageT(), position: _currentPageT(),
geometry: geometry, geometry: geometry,
onTap: _onSectionTap, onTap: _onSectionTap,
backgroundColor: frost backgroundColor: liquid
? AppFrost.navPillTint(cs) ? null
: _composerColor(cs), : (frost
? AppFrost.navPillTint(cs)
: _composerColor(cs)),
borderColor: _composerBorderColor(cs), borderColor: _composerBorderColor(cs),
); );
}, },
+34
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../../core/config/app_pill_gradient.dart'; import '../../core/config/app_pill_gradient.dart';
import '../../core/config/app_visual_style.dart'; import '../../core/config/app_visual_style.dart';
import 'liquid_glass.dart';
class _GlossyParts { class _GlossyParts {
final bool dark; final bool dark;
@@ -93,6 +94,7 @@ class GlossyPill extends StatelessWidget {
final bool elevated; final bool elevated;
final BorderSide? borderSide; final BorderSide? borderSide;
final double? blurSigma; final double? blurSigma;
final bool liquid;
final BackdropKey? backdropKey; final BackdropKey? backdropKey;
const GlossyPill({ const GlossyPill({
@@ -107,6 +109,7 @@ class GlossyPill extends StatelessWidget {
this.elevated = false, this.elevated = false,
this.borderSide, this.borderSide,
this.blurSigma, this.blurSigma,
this.liquid = false,
this.backdropKey, this.backdropKey,
}) : borderRadius = }) : borderRadius =
borderRadius ?? const BorderRadius.all(Radius.circular(100)); borderRadius ?? const BorderRadius.all(Radius.circular(100));
@@ -120,6 +123,7 @@ class GlossyPill extends StatelessWidget {
valueListenable: AppVisualStyle.current, valueListenable: AppVisualStyle.current,
builder: (context, style, _) { builder: (context, style, _) {
if (style == VisualStyle.materialYou) return _flat(context); if (style == VisualStyle.materialYou) return _flat(context);
if (liquid && LiquidGlass.isSupported) return _liquid(context);
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
valueListenable: AppPillGradient.current, valueListenable: AppPillGradient.current,
builder: (context, gradient, _) => _glossy(context, gradient), builder: (context, gradient, _) => _glossy(context, gradient),
@@ -128,6 +132,36 @@ class GlossyPill extends StatelessWidget {
); );
} }
Widget _liquid(BuildContext context) {
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,
border: GlossyDecor.rimBorder(base),
boxShadow: [GlossyDecor.dropShadow(base, depth)],
),
child: LiquidGlassSurface(
borderRadius: borderRadius,
tint: Colors.transparent,
child: onTap == null && onLongPress == null
? content
: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: content,
),
),
),
),
);
}
Widget _flat(BuildContext context) { Widget _flat(BuildContext context) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final base = color ?? cs.surfaceContainerHigh; final base = color ?? cs.surfaceContainerHigh;
+408
View File
@@ -0,0 +1,408 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../../core/config/app_frost.dart';
import '../../core/config/app_liquid_glass.dart';
import '../../core/config/app_visual_style.dart';
class LiquidGlass {
static const String _asset = 'shaders/liquid_glass.frag';
static ui.FragmentProgram? _program;
static bool _loadAttempted = false;
static bool get isSupported => _program != null;
static bool get active =>
isSupported && AppVisualStyle.current.value == VisualStyle.liquidGlass;
static Future<void> load() async {
if (_loadAttempted) return;
_loadAttempted = true;
if (!AppLiquidGlass.enabled) return;
if (!ui.ImageFilter.isShaderFilterSupported) return;
try {
_program = await ui.FragmentProgram.fromAsset(_asset);
} catch (_) {
_program = null;
}
}
}
class GlassSurface extends StatelessWidget {
final bool liquid;
final BorderRadius borderRadius;
final Color frostTint;
final double frostSigma;
final Color liquidTint;
final BoxBorder? border;
final BackdropKey? backdropKey;
final Widget child;
const GlassSurface({
super.key,
this.liquid = false,
this.borderRadius = BorderRadius.zero,
required this.frostTint,
this.frostSigma = AppFrost.sigma,
this.liquidTint = Colors.transparent,
this.border,
this.backdropKey,
required this.child,
});
@override
Widget build(BuildContext context) {
final glass = liquid && LiquidGlass.isSupported;
final decorated = DecoratedBox(
decoration: BoxDecoration(
color: glass ? null : frostTint,
border: border,
),
child: child,
);
if (glass) {
return LiquidGlassSurface(
borderRadius: borderRadius,
tint: liquidTint,
child: decorated,
);
}
return ClipRRect(
borderRadius: borderRadius,
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: frostSigma, sigmaY: frostSigma),
backdropGroupKey: backdropKey,
child: decorated,
),
);
}
}
class LiquidGlassSurface extends StatelessWidget {
final BorderRadius borderRadius;
final Color tint;
final double blurSigma;
final double spread;
final double refraction;
final double chroma;
final double specular;
final Offset light;
final double tintFeather;
final double rimWidth;
final Widget child;
const LiquidGlassSurface({
super.key,
required this.borderRadius,
required this.tint,
this.blurSigma = AppLiquidGlass.blurSigma,
this.spread = AppLiquidGlass.spread,
this.refraction = AppLiquidGlass.refraction,
this.chroma = AppLiquidGlass.chroma,
this.specular = AppLiquidGlass.specular,
this.light = AppLiquidGlass.light,
this.tintFeather = AppLiquidGlass.tintFeather,
this.rimWidth = AppLiquidGlass.rimWidth,
this.child = const SizedBox.expand(),
});
@override
Widget build(BuildContext context) {
if (!LiquidGlass.isSupported) return child;
return _LiquidGlassBackdrop(
borderRadius: borderRadius,
tint: tint,
blurSigma: blurSigma,
spread: spread,
refraction: refraction,
chroma: chroma,
specular: specular,
light: light,
tintFeather: tintFeather,
rimWidth: rimWidth,
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
child: child,
);
}
}
class _LiquidGlassBackdrop extends SingleChildRenderObjectWidget {
final BorderRadius borderRadius;
final Color tint;
final double blurSigma;
final double spread;
final double refraction;
final double chroma;
final double specular;
final Offset light;
final double tintFeather;
final double rimWidth;
final double devicePixelRatio;
const _LiquidGlassBackdrop({
required this.borderRadius,
required this.tint,
required this.blurSigma,
required this.spread,
required this.refraction,
required this.chroma,
required this.specular,
required this.light,
required this.tintFeather,
required this.rimWidth,
required this.devicePixelRatio,
required super.child,
});
@override
_RenderLiquidGlass createRenderObject(BuildContext context) {
return _RenderLiquidGlass(
borderRadius: borderRadius,
tint: tint,
blurSigma: blurSigma,
spread: spread,
refraction: refraction,
chroma: chroma,
specular: specular,
light: light,
tintFeather: tintFeather,
rimWidth: rimWidth,
devicePixelRatio: devicePixelRatio,
);
}
@override
void updateRenderObject(
BuildContext context,
_RenderLiquidGlass renderObject,
) {
renderObject
..borderRadius = borderRadius
..tint = tint
..blurSigma = blurSigma
..spread = spread
..refraction = refraction
..chroma = chroma
..specular = specular
..light = light
..tintFeather = tintFeather
..rimWidth = rimWidth
..devicePixelRatio = devicePixelRatio;
}
}
class _RenderLiquidGlass extends RenderProxyBox {
_RenderLiquidGlass({
required BorderRadius borderRadius,
required Color tint,
required double blurSigma,
required double spread,
required double refraction,
required double chroma,
required double specular,
required Offset light,
required double tintFeather,
required double rimWidth,
required double devicePixelRatio,
}) : _borderRadius = borderRadius,
_tint = tint,
_blurSigma = blurSigma,
_spread = spread,
_refraction = refraction,
_chroma = chroma,
_specular = specular,
_light = light,
_tintFeather = tintFeather,
_rimWidth = rimWidth,
_devicePixelRatio = devicePixelRatio;
final LayerHandle<ClipRRectLayer> _blurClipHandle =
LayerHandle<ClipRRectLayer>();
final LayerHandle<BackdropFilterLayer> _blurHandle =
LayerHandle<BackdropFilterLayer>();
final LayerHandle<ClipRRectLayer> _clipHandle = LayerHandle<ClipRRectLayer>();
final LayerHandle<BackdropFilterLayer> _backdropHandle =
LayerHandle<BackdropFilterLayer>();
ui.FragmentShader? _shader;
BorderRadius _borderRadius;
set borderRadius(BorderRadius value) {
if (_borderRadius == value) return;
_borderRadius = value;
markNeedsPaint();
}
Color _tint;
set tint(Color value) {
if (_tint == value) return;
_tint = value;
markNeedsPaint();
}
double _blurSigma;
set blurSigma(double value) {
if (_blurSigma == value) return;
_blurSigma = value;
markNeedsPaint();
}
double _spread;
set spread(double value) {
if (_spread == value) return;
_spread = value;
markNeedsPaint();
}
double _refraction;
set refraction(double value) {
if (_refraction == value) return;
_refraction = value;
markNeedsPaint();
}
double _chroma;
set chroma(double value) {
if (_chroma == value) return;
_chroma = value;
markNeedsPaint();
}
double _specular;
set specular(double value) {
if (_specular == value) return;
_specular = value;
markNeedsPaint();
}
Offset _light;
set light(Offset value) {
if (_light == value) return;
_light = value;
markNeedsPaint();
}
double _tintFeather;
set tintFeather(double value) {
if (_tintFeather == value) return;
_tintFeather = value;
markNeedsPaint();
}
double _rimWidth;
set rimWidth(double value) {
if (_rimWidth == value) return;
_rimWidth = value;
markNeedsPaint();
}
double _devicePixelRatio;
set devicePixelRatio(double value) {
if (_devicePixelRatio == value) return;
_devicePixelRatio = value;
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => true;
@override
void dispose() {
_blurClipHandle.layer = null;
_blurHandle.layer = null;
_clipHandle.layer = null;
_backdropHandle.layer = null;
_shader?.dispose();
_shader = null;
super.dispose();
}
ui.ImageFilter? _buildFilter() {
final program = LiquidGlass._program;
if (program == null) return null;
final shader = _shader ??= program.fragmentShader();
final dpr = _devicePixelRatio;
final topLeft = localToGlobal(Offset.zero);
final left = (topLeft.dx * dpr).roundToDouble();
final top = (topLeft.dy * dpr).roundToDouble();
final width = (size.width * dpr).roundToDouble();
final height = (size.height * dpr).roundToDouble();
final radius = _borderRadius.topLeft.x * dpr;
shader
..setFloat(2, left)
..setFloat(3, top)
..setFloat(4, width)
..setFloat(5, height)
..setFloat(6, radius)
..setFloat(7, _spread)
..setFloat(8, _refraction * dpr)
..setFloat(9, _chroma)
..setFloat(10, _specular)
..setFloat(11, _tint.r)
..setFloat(12, _tint.g)
..setFloat(13, _tint.b)
..setFloat(14, _tint.a)
..setFloat(15, _light.dx)
..setFloat(16, _light.dy)
..setFloat(17, _tintFeather * dpr)
..setFloat(18, _rimWidth * dpr);
return ui.ImageFilter.shader(shader);
}
@override
void paint(PaintingContext context, Offset offset) {
final filter = size.isEmpty ? null : _buildFilter();
if (filter == null) {
_blurClipHandle.layer = null;
_blurHandle.layer = null;
_clipHandle.layer = null;
_backdropHandle.layer = null;
super.paint(context, offset);
return;
}
final bounds = Offset.zero & size;
final shape = _borderRadius.toRRect(bounds);
if (_blurSigma > 0) {
_blurClipHandle.layer = context.pushClipRRect(
needsCompositing,
offset,
bounds,
shape,
(PaintingContext innerContext, Offset innerOffset) {
final blur = _blurHandle.layer ??= BackdropFilterLayer();
blur.filter = ui.ImageFilter.blur(
sigmaX: _blurSigma,
sigmaY: _blurSigma,
tileMode: TileMode.mirror,
);
innerContext.pushLayer(blur, (_, _) {}, innerOffset);
},
oldLayer: _blurClipHandle.layer,
);
} else {
_blurClipHandle.layer = null;
_blurHandle.layer = null;
}
_clipHandle.layer = context.pushClipRRect(
needsCompositing,
offset,
bounds,
shape,
(PaintingContext innerContext, Offset innerOffset) {
final backdrop = _backdropHandle.layer ??= BackdropFilterLayer();
backdrop.filter = filter;
innerContext.pushLayer(backdrop, super.paint, innerOffset);
},
oldLayer: _clipHandle.layer,
);
}
}
+19 -26
View File
@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@@ -13,6 +12,7 @@ import '../../backend/modules/messages.dart';
import '../../backend/modules/shared_content.dart'; import '../../backend/modules/shared_content.dart';
import '../../core/cache/info_cache.dart'; import '../../core/cache/info_cache.dart';
import '../../core/config/app_frost.dart'; import '../../core/config/app_frost.dart';
import 'liquid_glass.dart';
import '../../core/utils/format.dart'; import '../../core/utils/format.dart';
import '../../core/utils/media_cache.dart'; import '../../core/utils/media_cache.dart';
import '../../core/utils/media_saver.dart'; import '../../core/utils/media_saver.dart';
@@ -578,32 +578,25 @@ class _PhotoViewerScreenState extends State<PhotoViewerScreen> {
} }
Widget _buildCaption(String caption) { Widget _buildCaption(String caption) {
return ClipRRect( return GlassSurface(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: BackdropFilter( frostTint: Colors.black.withValues(alpha: 0.28),
filter: ui.ImageFilter.blur( frostSigma: AppFrost.panelSigma,
sigmaX: AppFrost.panelSigma, liquidTint: Colors.black.withValues(alpha: 0.28),
sigmaY: AppFrost.panelSigma, border: Border.all(
), color: Colors.white.withValues(alpha: 0.12),
child: Container( width: 0.5,
constraints: const BoxConstraints(maxHeight: 120), ),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), child: Container(
decoration: BoxDecoration( constraints: const BoxConstraints(maxHeight: 120),
color: Colors.black.withValues(alpha: 0.28), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
border: Border.all( child: SingleChildScrollView(
color: Colors.white.withValues(alpha: 0.12), child: Text(
width: 0.5, caption,
), style: const TextStyle(
borderRadius: BorderRadius.circular(12), color: Colors.white,
), fontSize: 15,
child: SingleChildScrollView( height: 1.3,
child: Text(
caption,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
height: 1.3,
),
), ),
), ),
), ),
+26 -10
View File
@@ -3,11 +3,13 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/config/app_frost.dart'; import '../../core/config/app_frost.dart';
import '../../core/config/app_liquid_glass.dart';
import '../../core/config/app_nav_pill_style.dart'; import '../../core/config/app_nav_pill_style.dart';
import '../../core/config/app_pill_gradient.dart'; import '../../core/config/app_pill_gradient.dart';
import '../../core/config/app_visual_style.dart'; import '../../core/config/app_visual_style.dart';
import 'animated_lottie_icon.dart'; import 'animated_lottie_icon.dart';
import 'glossy_pill.dart'; import 'glossy_pill.dart';
import 'liquid_glass.dart';
class PillNavItem { class PillNavItem {
final IconData icon; final IconData icon;
@@ -90,12 +92,13 @@ class SlidingPillNav extends StatelessWidget {
return ValueListenableBuilder<VisualStyle>( return ValueListenableBuilder<VisualStyle>(
valueListenable: AppVisualStyle.current, valueListenable: AppVisualStyle.current,
builder: (context, style, _) { builder: (context, style, _) {
if (style != VisualStyle.glossy) { if (style == VisualStyle.materialYou) {
return _buildNav( return _buildNav(
context, context,
glossy: false, glossy: false,
gradient: false, gradient: false,
frost: false, frost: false,
liquid: false,
); );
} }
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
@@ -107,7 +110,8 @@ class SlidingPillNav extends StatelessWidget {
context, context,
glossy: true, glossy: true,
gradient: gradient, gradient: gradient,
frost: navStyle == NavPillStyle.frostBlur, frost: NavPillMaterial.isFrost(navStyle),
liquid: NavPillMaterial.isLiquid(navStyle),
), ),
), ),
); );
@@ -120,20 +124,23 @@ class SlidingPillNav extends StatelessWidget {
required bool glossy, required bool glossy,
required bool gradient, required bool gradient,
required bool frost, required bool frost,
required bool liquid,
}) { }) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final visualSel = position.round().clamp(0, items.length - 1); final visualSel = position.round().clamp(0, items.length - 1);
final base = final translucent = backgroundColor != null && backgroundColor!.a < 1;
backgroundColor ?? final base = liquid
(frost ? AppFrost.navPillTint(cs) : cs.surfaceContainerHigh); ? (translucent ? backgroundColor! : AppLiquidGlass.navTint(cs))
final useGradient = glossy && gradient; : (backgroundColor ??
final frosted = frost && base.a < 1; (frost ? AppFrost.navPillTint(cs) : cs.surfaceContainerHigh));
final useGradient = glossy && gradient && !liquid;
final frosted = frost && !liquid && base.a < 1;
return Container( return Container(
height: height, height: height,
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: useGradient ? null : base, color: useGradient || liquid ? null : base,
gradient: useGradient ? GlossyDecor.fillGradient(base) : null, gradient: useGradient ? GlossyDecor.fillGradient(base) : null,
borderRadius: BorderRadius.circular(34), borderRadius: BorderRadius.circular(34),
border: glossy border: glossy
@@ -145,8 +152,8 @@ class SlidingPillNav extends StatelessWidget {
? null ? null
: [ : [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.5), color: Colors.black.withValues(alpha: liquid ? 0.28 : 0.5),
blurRadius: 20, blurRadius: liquid ? 26 : 20,
offset: const Offset(0, 10), offset: const Offset(0, 10),
), ),
], ],
@@ -154,6 +161,15 @@ class SlidingPillNav extends StatelessWidget {
child: Stack( child: Stack(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
children: [ children: [
if (liquid)
Positioned.fill(
child: IgnorePointer(
child: LiquidGlassSurface(
borderRadius: BorderRadius.circular(34),
tint: base,
),
),
),
if (frosted) if (frosted)
Positioned.fill( Positioned.fill(
child: IgnorePointer( child: IgnorePointer(
+14 -24
View File
@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -17,6 +16,8 @@ import 'segmented_pill_toggle.dart';
import 'small_spinner.dart'; import 'small_spinner.dart';
import 'lottie_image.dart'; import 'lottie_image.dart';
import 'sticker_peek.dart'; import 'sticker_peek.dart';
import '../../core/config/app_frost.dart';
import 'liquid_glass.dart';
class _DragScrollBehavior extends MaterialScrollBehavior { class _DragScrollBehavior extends MaterialScrollBehavior {
const _DragScrollBehavior(); const _DragScrollBehavior();
@@ -248,30 +249,19 @@ class _StickerPanelState extends State<StickerPanel>
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
return SizedBox( return SizedBox(
height: widget.height, height: widget.height,
child: ClipRect( child: GlassSurface(
child: BackdropFilter( liquid: false,
filter: ui.ImageFilter.blur(sigmaX: 34, sigmaY: 34), frostTint: AppFrost.panelTint(cs),
child: DecoratedBox( border: Border(top: AppFrost.hairline(cs)),
decoration: BoxDecoration( child: Column(
color: cs.surface.withValues(alpha: 0.38), children: [
border: Border( Expanded(
top: BorderSide( child: _mode == _modeEmoji && widget.onEmojiTap != null
color: cs.outlineVariant.withValues(alpha: 0.4), ? EmojiPanel(onEmojiTap: widget.onEmojiTap!)
width: 0.5, : _buildStickerBody(cs),
),
),
), ),
child: Column( if (widget.onEmojiTap != null) _buildToggleBar(cs),
children: [ ],
Expanded(
child: _mode == _modeEmoji && widget.onEmojiTap != null
? EmojiPanel(onEmojiTap: widget.onEmojiTap!)
: _buildStickerBody(cs),
),
if (widget.onEmojiTap != null) _buildToggleBar(cs),
],
),
),
), ),
), ),
); );
+2
View File
@@ -290,6 +290,8 @@
"appearanceVisualStyleSubtitle": "Material You or dimensional Glossy capsules", "appearanceVisualStyleSubtitle": "Material You or dimensional Glossy capsules",
"appearanceVisualStyleMaterialYou": "Material You", "appearanceVisualStyleMaterialYou": "Material You",
"appearanceVisualStyleGlossy": "Glossy", "appearanceVisualStyleGlossy": "Glossy",
"appearanceVisualStyleLiquidGlass": "Liquid Glass",
"appearanceGlassMaterial": "Glass",
"appearanceChatChromeTitle": "Chat screen elements", "appearanceChatChromeTitle": "Chat screen elements",
"appearanceChatChromeSubtitle": "Background of the top and bottom panels: color, blur, or transparent. With blur or transparency, messages scroll under the panels", "appearanceChatChromeSubtitle": "Background of the top and bottom panels: color, blur, or transparent. With blur or transparency, messages scroll under the panels",
"appearanceChatChromeColor": "Color", "appearanceChatChromeColor": "Color",
+12
View File
@@ -1526,6 +1526,18 @@ abstract class AppLocalizations {
/// **'Glossy'** /// **'Glossy'**
String get appearanceVisualStyleGlossy; String get appearanceVisualStyleGlossy;
/// No description provided for @appearanceVisualStyleLiquidGlass.
///
/// In en, this message translates to:
/// **'Liquid Glass'**
String get appearanceVisualStyleLiquidGlass;
/// No description provided for @appearanceGlassMaterial.
///
/// In en, this message translates to:
/// **'Glass'**
String get appearanceGlassMaterial;
/// No description provided for @appearanceChatChromeTitle. /// No description provided for @appearanceChatChromeTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+6
View File
@@ -758,6 +758,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get appearanceVisualStyleGlossy => 'Glossy'; String get appearanceVisualStyleGlossy => 'Glossy';
@override
String get appearanceVisualStyleLiquidGlass => 'Liquid Glass';
@override
String get appearanceGlassMaterial => 'Glass';
@override @override
String get appearanceChatChromeTitle => 'Chat screen elements'; String get appearanceChatChromeTitle => 'Chat screen elements';
+6
View File
@@ -761,6 +761,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get appearanceVisualStyleGlossy => 'Glossy'; String get appearanceVisualStyleGlossy => 'Glossy';
@override
String get appearanceVisualStyleLiquidGlass => 'Liquid Glass';
@override
String get appearanceGlassMaterial => 'Стекло';
@override @override
String get appearanceChatChromeTitle => 'Элементы экрана чата'; String get appearanceChatChromeTitle => 'Элементы экрана чата';
+2
View File
@@ -255,6 +255,8 @@
"appearanceVisualStyleSubtitle": "Material You или объёмные Glossy-капсулы", "appearanceVisualStyleSubtitle": "Material You или объёмные Glossy-капсулы",
"appearanceVisualStyleMaterialYou": "Material You", "appearanceVisualStyleMaterialYou": "Material You",
"appearanceVisualStyleGlossy": "Glossy", "appearanceVisualStyleGlossy": "Glossy",
"appearanceVisualStyleLiquidGlass": "Liquid Glass",
"appearanceGlassMaterial": "Стекло",
"appearanceChatChromeTitle": "Элементы экрана чата", "appearanceChatChromeTitle": "Элементы экрана чата",
"appearanceChatChromeSubtitle": "Фон панелей сверху и снизу: цвет, размытие или прозрачно. При размытии и прозрачности сообщения заходят под панели", "appearanceChatChromeSubtitle": "Фон панелей сверху и снизу: цвет, размытие или прозрачно. При размытии и прозрачности сообщения заходят под панели",
"appearanceChatChromeColor": "Цвет", "appearanceChatChromeColor": "Цвет",
+3
View File
@@ -79,6 +79,7 @@ import 'frontend/debug/fps_overlay_layer.dart';
import 'frontend/screens/auth/login_screen.dart'; import 'frontend/screens/auth/login_screen.dart';
import 'frontend/widgets/adaptive_shell.dart'; import 'frontend/widgets/adaptive_shell.dart';
import 'frontend/widgets/custom_notification.dart'; import 'frontend/widgets/custom_notification.dart';
import 'frontend/widgets/liquid_glass.dart';
import 'frontend/widgets/theme_reveal.dart'; import 'frontend/widgets/theme_reveal.dart';
final api = Api(); final api = Api();
@@ -194,6 +195,7 @@ void main(List<String> args) async {
final amoledFuture = AppAmoled.load(); final amoledFuture = AppAmoled.load();
final pillGradientFuture = AppPillGradient.load(); final pillGradientFuture = AppPillGradient.load();
final visualStyleFuture = AppVisualStyle.load(); final visualStyleFuture = AppVisualStyle.load();
final liquidGlassFuture = LiquidGlass.load();
final chatChromeFuture = AppChatChrome.load(); final chatChromeFuture = AppChatChrome.load();
final composerStyleFuture = AppComposerStyle.load(); final composerStyleFuture = AppComposerStyle.load();
final composerBackgroundFuture = AppComposerBackground.load(); final composerBackgroundFuture = AppComposerBackground.load();
@@ -248,6 +250,7 @@ void main(List<String> args) async {
amoledFuture, amoledFuture,
pillGradientFuture, pillGradientFuture,
visualStyleFuture, visualStyleFuture,
liquidGlassFuture,
chatChromeFuture, chatChromeFuture,
composerStyleFuture, composerStyleFuture,
composerBackgroundFuture, composerBackgroundFuture,
+3
View File
@@ -125,6 +125,9 @@ flutter_launcher_icons:
flutter: flutter:
generate: true generate: true
shaders:
- shaders/liquid_glass.frag
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.
+104
View File
@@ -0,0 +1,104 @@
#version 460 core
#include <flutter/runtime_effect.glsl>
precision highp float;
uniform vec2 uSize;
uniform vec2 uRectOrigin;
uniform vec2 uRectSize;
uniform float uRadius;
uniform float uSpread;
uniform float uRefraction;
uniform float uChroma;
uniform float uSpecular;
uniform vec4 uTint;
uniform vec2 uLight;
uniform float uTintFeather;
uniform float uRimWidth;
uniform sampler2D uBackdrop;
out vec4 fragColor;
float roundedBoxSdf(vec2 p, vec2 halfSize, float radius) {
vec2 q = abs(p) - halfSize + radius;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - radius;
}
vec2 surfaceNormal(vec2 p, vec2 halfSize, float radius) {
vec2 unit = vec2(1.0, 0.0);
float dx = roundedBoxSdf(p + unit.xy, halfSize, radius) -
roundedBoxSdf(p - unit.xy, halfSize, radius);
float dy = roundedBoxSdf(p + unit.yx, halfSize, radius) -
roundedBoxSdf(p - unit.yx, halfSize, radius);
return normalize(vec2(dx, dy) + vec2(1e-6));
}
const int TAPS = 5;
float displacement(float distance, float reach) {
float bevel = 1.0 - clamp(-distance / reach, 0.0, 1.0);
return uRefraction * bevel * bevel * (1.0 + bevel);
}
vec3 sampleBackdrop(vec2 coord) {
vec2 uv = coord / uSize;
#ifdef IMPELLER_TARGET_OPENGLES
uv.y = 1.0 - uv.y;
#endif
return texture(uBackdrop, clamp(uv, vec2(0.0), vec2(1.0))).rgb;
}
void main() {
vec2 fragCoord = FlutterFragCoord().xy;
vec2 halfSize = uRectSize * 0.5;
vec2 center = uRectOrigin + halfSize;
vec2 p = fragCoord - center;
float radius = min(uRadius, min(halfSize.x, halfSize.y));
float sd = roundedBoxSdf(p, halfSize, radius);
if (sd > 0.0) {
fragColor = vec4(sampleBackdrop(fragCoord), 1.0);
return;
}
vec2 normal = surfaceNormal(p, halfSize, radius);
float reach = max(uSpread * min(halfSize.x, halfSize.y), 1.0);
float shift = displacement(sd, reach);
float lens = shift / max(uRefraction, 1e-6);
float slope = displacement(sd + 1.0, reach) - displacement(sd - 1.0, reach);
float footprint = clamp(abs(1.0 + slope * 0.5), 1.0, 24.0);
float aberration = uChroma * lens;
vec3 refracted = vec3(0.0);
for (int i = 0; i < TAPS; i++) {
float offset = (float(i) / float(TAPS - 1) - 0.5) * footprint;
vec2 base = fragCoord + normal * (shift + offset);
if (aberration > 0.0) {
refracted.r += sampleBackdrop(base + normal * shift * aberration).r;
refracted.g += sampleBackdrop(base).g;
refracted.b += sampleBackdrop(base - normal * shift * aberration).b;
} else {
refracted += sampleBackdrop(base);
}
}
refracted /= float(TAPS);
float veil = smoothstep(0.0, 1.0, clamp(-sd / max(uTintFeather, 1.0), 0.0, 1.0));
vec3 color = mix(refracted, uTint.rgb, uTint.a * veil);
vec2 light = normalize(uLight + vec2(1e-6));
float facing = dot(normal, light);
float rim = 1.0 - clamp(-sd / max(uRimWidth, 1.0), 0.0, 1.0);
rim = rim * rim;
float highlight = pow(max(facing, 0.0), 5.0) * rim * uSpecular;
float shade = pow(max(-facing, 0.0), 4.0) * rim * uSpecular * 0.35;
color += vec3(highlight);
color = mix(color, color * 0.72, shade);
fragColor = vec4(clamp(color, vec3(0.0), vec3(1.0)), 1.0);
}