feat: liquid glass, пока-что скрыт.
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../frontend/widgets/liquid_glass.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 {
|
||||
static const prefKey = 'app_chat_chrome';
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../frontend/widgets/liquid_glass.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 {
|
||||
static const prefKey = 'app_composer_background';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../frontend/widgets/liquid_glass.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 {
|
||||
static const prefKey = 'app_nav_pill_style';
|
||||
|
||||
@@ -2,7 +2,11 @@ import 'package:flutter/foundation.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 {
|
||||
static const prefKey = 'app_visual_style';
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:komet/frontend/widgets/online_dot.dart';
|
||||
class ChatHeaderRow extends StatelessWidget {
|
||||
final bool glossy;
|
||||
final bool frosted;
|
||||
final bool liquid;
|
||||
final BackdropKey? backdropKey;
|
||||
final ColorScheme cs;
|
||||
final bool embedded;
|
||||
@@ -32,6 +33,7 @@ class ChatHeaderRow extends StatelessWidget {
|
||||
super.key,
|
||||
required this.glossy,
|
||||
required this.frosted,
|
||||
this.liquid = false,
|
||||
this.backdropKey,
|
||||
required this.cs,
|
||||
required this.embedded,
|
||||
@@ -56,9 +58,9 @@ class ChatHeaderRow extends StatelessWidget {
|
||||
Widget build(BuildContext 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) {
|
||||
return Padding(
|
||||
@@ -73,6 +75,7 @@ class ChatHeaderRow extends StatelessWidget {
|
||||
child: GlossyPill(
|
||||
color: _pillColor,
|
||||
blurSigma: _pillBlur,
|
||||
liquid: liquid,
|
||||
backdropKey: backdropKey,
|
||||
onTap: () {
|
||||
if (embedded) {
|
||||
@@ -97,6 +100,7 @@ class ChatHeaderRow extends StatelessWidget {
|
||||
child: GlossyPill(
|
||||
color: _pillColor,
|
||||
blurSigma: _pillBlur,
|
||||
liquid: liquid,
|
||||
backdropKey: backdropKey,
|
||||
onTap: onOpenInfo,
|
||||
padding: const EdgeInsets.fromLTRB(6, 6, 16, 6),
|
||||
@@ -185,6 +189,7 @@ class ChatHeaderRow extends StatelessWidget {
|
||||
GlossyPill(
|
||||
color: _pillColor,
|
||||
blurSigma: _pillBlur,
|
||||
liquid: liquid,
|
||||
backdropKey: backdropKey,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
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/voice_record_controller.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';
|
||||
|
||||
class ComposerInputBar extends StatelessWidget {
|
||||
@@ -418,10 +419,14 @@ class ComposerInputBar extends StatelessWidget {
|
||||
|
||||
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) {
|
||||
if (!_flat || background == ComposerBackground.frostBlur) return child;
|
||||
if (!_flat || _translucent) return child;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
@@ -434,13 +439,14 @@ class ComposerInputBar extends StatelessWidget {
|
||||
Widget _fieldSurface(ColorScheme cs, Widget child) {
|
||||
if (_flat) return child;
|
||||
return GlossyPill(
|
||||
color: _frost
|
||||
color: _translucent
|
||||
? AppFrost.inputTint(cs)
|
||||
: Color.alphaBlend(
|
||||
cs.surfaceContainerHighest.withValues(alpha: 0.92),
|
||||
cs.surface,
|
||||
),
|
||||
blurSigma: _frost ? AppFrost.sigma : null,
|
||||
liquid: _liquid,
|
||||
backdropKey: backdropKey,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
depth: 8,
|
||||
@@ -471,6 +477,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
return GlossyPill(
|
||||
color: color,
|
||||
blurSigma: _frost ? AppFrost.sigma : null,
|
||||
liquid: _liquid,
|
||||
backdropKey: backdropKey,
|
||||
borderRadius: BorderRadius.circular(27),
|
||||
onTap: onTap,
|
||||
@@ -538,23 +545,14 @@ class ComposerInputBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
);
|
||||
if (_flat && _frost) return row;
|
||||
if (!_frost && chrome != ChatChromeStyle.transparent) return row;
|
||||
return ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: AppFrost.sigma,
|
||||
sigmaY: AppFrost.sigma,
|
||||
),
|
||||
backdropGroupKey: backdropKey,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: AppFrost.panelTint(cs),
|
||||
border: Border(top: AppFrost.hairline(cs)),
|
||||
),
|
||||
child: row,
|
||||
),
|
||||
),
|
||||
if (_flat && _translucent) return row;
|
||||
if (!_translucent && chrome != ChatChromeStyle.transparent) return row;
|
||||
return GlassSurface(
|
||||
liquid: _liquid,
|
||||
frostTint: AppFrost.panelTint(cs),
|
||||
border: Border(top: AppFrost.hairline(cs)),
|
||||
backdropKey: backdropKey,
|
||||
child: row,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2033,17 +2033,21 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
ValueListenableBuilder<NavPillStyle>(
|
||||
valueListenable: AppNavPillStyle.current,
|
||||
builder: (context, navStyle, child) {
|
||||
final liquid =
|
||||
style.glossyChrome &&
|
||||
NavPillMaterial.isLiquid(navStyle);
|
||||
final frost =
|
||||
style == VisualStyle.glossy &&
|
||||
navStyle == NavPillStyle.frostBlur;
|
||||
style.glossyChrome &&
|
||||
NavPillMaterial.isFrost(navStyle);
|
||||
return GlossyPill(
|
||||
onTap: _toggleFab,
|
||||
color: frost
|
||||
color: frost || liquid
|
||||
? AppFrost.fabTint(cs)
|
||||
: cs.primaryContainer,
|
||||
blurSigma: frost
|
||||
? AppFrost.sigma
|
||||
: null,
|
||||
liquid: liquid,
|
||||
backdropKey: _frostBackdrop,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
elevated: true,
|
||||
|
||||
@@ -93,6 +93,7 @@ import '../../widgets/schedule_time_picker.dart';
|
||||
import '../../widgets/chat_wallpaper_sheet.dart';
|
||||
import '../../widgets/chat_wallpaper_view.dart';
|
||||
import '../../widgets/glossy_pill.dart';
|
||||
import '../../widgets/liquid_glass.dart';
|
||||
import 'scheduled_messages_screen.dart';
|
||||
import 'chat_wallpaper_preview_screen.dart';
|
||||
|
||||
@@ -129,15 +130,12 @@ class _FrostedPanel extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
|
||||
backdropGroupKey: backdropKey,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: tint, border: border),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
return GlassSurface(
|
||||
frostTint: tint,
|
||||
frostSigma: sigma,
|
||||
border: border,
|
||||
backdropKey: backdropKey,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -474,13 +472,18 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
ChatWallpaper? _wallpaper;
|
||||
|
||||
bool get _composerFrosted =>
|
||||
AppComposerBackground.current.value == ComposerBackground.frostBlur;
|
||||
AppComposerBackground.current.value != ComposerBackground.standard;
|
||||
|
||||
bool get _composerUnderlap =>
|
||||
AppChatChrome.current.value != ChatChromeStyle.color || _composerFrosted;
|
||||
|
||||
bool get _liquidChrome =>
|
||||
AppVisualStyle.current.value.glossyChrome &&
|
||||
ChatChromeMaterial.isLiquid(AppChatChrome.current.value);
|
||||
|
||||
ChatChromeStyle get _effectiveChrome {
|
||||
final chrome = AppChatChrome.current.value;
|
||||
if (chrome == ChatChromeStyle.liquidGlass) return ChatChromeStyle.transparent;
|
||||
if (_wallpaper != null && chrome == ChatChromeStyle.none) {
|
||||
return ChatChromeStyle.blur;
|
||||
}
|
||||
@@ -2565,7 +2568,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
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 height = glossy
|
||||
? ui.lerpDouble(_glossyHeaderHeight, _glossySearchHeight, searchT)!
|
||||
@@ -2644,6 +2647,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
glossy: glossy,
|
||||
frosted:
|
||||
glossy && chrome == ChatChromeStyle.transparent,
|
||||
liquid: _liquidChrome,
|
||||
backdropKey: _pillBackdrop,
|
||||
cs: cs,
|
||||
embedded: widget.embedded,
|
||||
@@ -4381,6 +4385,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
isPreview: pinned.pinnedMsgIsPreview,
|
||||
floating: floating,
|
||||
frosted: _effectiveChrome == ChatChromeStyle.transparent,
|
||||
liquid: _liquidChrome,
|
||||
backdropKey: _pillBackdrop,
|
||||
onTap: _jumpToPinnedMessage,
|
||||
onUnpin: pinned.canPinMessages(_myId)
|
||||
@@ -4437,7 +4442,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
double _pinnedBannerTop() {
|
||||
final glossy = AppVisualStyle.current.value == VisualStyle.glossy;
|
||||
final glossy = AppVisualStyle.current.value.glossyChrome;
|
||||
return MediaQuery.paddingOf(context).top +
|
||||
(glossy ? _glossyHeaderHeight : kToolbarHeight) -
|
||||
_pinnedBannerLift;
|
||||
@@ -4536,7 +4541,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (height != null) {
|
||||
resolved = height;
|
||||
} else {
|
||||
final glossy = AppVisualStyle.current.value == VisualStyle.glossy;
|
||||
final glossy = AppVisualStyle.current.value.glossyChrome;
|
||||
resolved =
|
||||
MediaQuery.paddingOf(context).top +
|
||||
(glossy ? _glossyHeaderHeight : kToolbarHeight);
|
||||
@@ -4591,7 +4596,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
|
||||
double _floatingDateTop(double pinnedHeight) {
|
||||
if (AppChatChrome.current.value == ChatChromeStyle.color) {
|
||||
final glossy = AppVisualStyle.current.value == VisualStyle.glossy;
|
||||
final glossy = AppVisualStyle.current.value.glossyChrome;
|
||||
return glossy ? 2 : 4;
|
||||
}
|
||||
if (chat?.hasPinnedMessage == true && pinnedHeight > 0) {
|
||||
@@ -4919,8 +4924,9 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
width: 46,
|
||||
height: 46,
|
||||
child: GlossyPill(
|
||||
color: frosted ? AppFrost.pillTint(cs) : null,
|
||||
blurSigma: frosted ? AppFrost.sigma : null,
|
||||
color: frosted || _liquidChrome ? AppFrost.pillTint(cs) : null,
|
||||
blurSigma: frosted && !_liquidChrome ? AppFrost.sigma : null,
|
||||
liquid: _liquidChrome,
|
||||
backdropKey: _pillBackdrop,
|
||||
elevated: true,
|
||||
onTap: _onScrollDownTap,
|
||||
@@ -5980,6 +5986,7 @@ class _PinnedMessageBanner extends StatelessWidget {
|
||||
final VoidCallback? onUnpin;
|
||||
final bool floating;
|
||||
final bool frosted;
|
||||
final bool liquid;
|
||||
final BackdropKey? backdropKey;
|
||||
|
||||
const _PinnedMessageBanner({
|
||||
@@ -5989,6 +5996,7 @@ class _PinnedMessageBanner extends StatelessWidget {
|
||||
this.onUnpin,
|
||||
this.floating = false,
|
||||
this.frosted = false,
|
||||
this.liquid = false,
|
||||
this.backdropKey,
|
||||
});
|
||||
|
||||
@@ -6058,19 +6066,15 @@ class _PinnedMessageBanner extends StatelessWidget {
|
||||
final bottomBorder = Border(bottom: AppFrost.hairline(cs));
|
||||
|
||||
if (frosted) {
|
||||
return ClipRRect(
|
||||
borderRadius: floating ? BorderRadius.circular(16) : BorderRadius.zero,
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: AppFrost.sigma,
|
||||
sigmaY: AppFrost.sigma,
|
||||
),
|
||||
backdropGroupKey: backdropKey,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(border: floating ? null : bottomBorder),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
return GlassSurface(
|
||||
liquid: liquid,
|
||||
borderRadius: floating
|
||||
? BorderRadius.circular(16)
|
||||
: BorderRadius.zero,
|
||||
frostTint: Colors.transparent,
|
||||
border: floating ? null : bottomBorder,
|
||||
backdropKey: backdropKey,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../../core/utils/haptics.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/glossy_pill.dart';
|
||||
import '../../widgets/liquid_glass.dart';
|
||||
|
||||
class AppearanceScreen extends StatefulWidget {
|
||||
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 {
|
||||
const _VisualStyleCard();
|
||||
|
||||
@@ -166,7 +186,12 @@ class _VisualStyleCard extends StatelessWidget {
|
||||
ValueListenableBuilder<VisualStyle>(
|
||||
valueListenable: AppVisualStyle.current,
|
||||
builder: (context, current, _) {
|
||||
final selectable =
|
||||
current == VisualStyle.liquidGlass && !LiquidGlass.isSupported
|
||||
? VisualStyle.glossy
|
||||
: current;
|
||||
return SegmentedButton<VisualStyle>(
|
||||
showSelectedIcon: false,
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: VisualStyle.materialYou,
|
||||
@@ -176,12 +201,17 @@ class _VisualStyleCard extends StatelessWidget {
|
||||
value: VisualStyle.glossy,
|
||||
label: Text(l10n.appearanceVisualStyleGlossy),
|
||||
),
|
||||
if (LiquidGlass.isSupported)
|
||||
ButtonSegment(
|
||||
value: VisualStyle.liquidGlass,
|
||||
label: Text(l10n.appearanceVisualStyleLiquidGlass),
|
||||
),
|
||||
],
|
||||
selected: {current},
|
||||
selected: {selectable},
|
||||
onSelectionChanged: (set) {
|
||||
if (set.isNotEmpty) {
|
||||
Haptics.selection();
|
||||
AppVisualStyle.save(set.first);
|
||||
_applyVisualStyle(set.first);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -225,32 +255,46 @@ class _ChatChromeCard extends StatelessWidget {
|
||||
ValueListenableBuilder<ChatChromeStyle>(
|
||||
valueListenable: AppChatChrome.current,
|
||||
builder: (context, current, _) {
|
||||
return SegmentedButton<ChatChromeStyle>(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: ChatChromeStyle.color,
|
||||
label: Text(l10n.appearanceChatChromeColor),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ChatChromeStyle.blur,
|
||||
label: Text(l10n.appearanceChatChromeBlur),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ChatChromeStyle.none,
|
||||
label: Text(l10n.appearanceChatChromeNone),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ChatChromeStyle.transparent,
|
||||
label: Text(l10n.appearanceChatChromeTransparent),
|
||||
),
|
||||
],
|
||||
selected: {current},
|
||||
onSelectionChanged: (set) {
|
||||
if (set.isNotEmpty) {
|
||||
Haptics.selection();
|
||||
AppChatChrome.save(set.first);
|
||||
}
|
||||
},
|
||||
final selectable =
|
||||
current == ChatChromeStyle.liquidGlass &&
|
||||
!LiquidGlass.isSupported
|
||||
? ChatChromeStyle.transparent
|
||||
: current;
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SegmentedButton<ChatChromeStyle>(
|
||||
showSelectedIcon: false,
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: ChatChromeStyle.color,
|
||||
label: Text(l10n.appearanceChatChromeColor),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ChatChromeStyle.blur,
|
||||
label: Text(l10n.appearanceChatChromeBlur),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ChatChromeStyle.none,
|
||||
label: Text(l10n.appearanceChatChromeNone),
|
||||
),
|
||||
ButtonSegment(
|
||||
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>(
|
||||
valueListenable: AppComposerBackground.current,
|
||||
builder: (context, current, _) {
|
||||
final selectable =
|
||||
current == ComposerBackground.liquidGlass &&
|
||||
!LiquidGlass.isSupported
|
||||
? ComposerBackground.frostBlur
|
||||
: current;
|
||||
return SegmentedButton<ComposerBackground>(
|
||||
showSelectedIcon: false,
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: ComposerBackground.standard,
|
||||
@@ -327,8 +377,13 @@ class _ComposerBarCard extends StatelessWidget {
|
||||
value: ComposerBackground.frostBlur,
|
||||
label: Text(l10n.appearanceComposerBackgroundFrost),
|
||||
),
|
||||
if (LiquidGlass.isSupported)
|
||||
ButtonSegment(
|
||||
value: ComposerBackground.liquidGlass,
|
||||
label: Text(l10n.appearanceGlassMaterial),
|
||||
),
|
||||
],
|
||||
selected: {current},
|
||||
selected: {selectable},
|
||||
onSelectionChanged: (set) {
|
||||
if (set.isNotEmpty) {
|
||||
Haptics.selection();
|
||||
@@ -376,7 +431,13 @@ class _NavPillStyleCard extends StatelessWidget {
|
||||
ValueListenableBuilder<NavPillStyle>(
|
||||
valueListenable: AppNavPillStyle.current,
|
||||
builder: (context, current, _) {
|
||||
final selectable =
|
||||
current == NavPillStyle.liquidGlass &&
|
||||
!LiquidGlass.isSupported
|
||||
? NavPillStyle.frostBlur
|
||||
: current;
|
||||
return SegmentedButton<NavPillStyle>(
|
||||
showSelectedIcon: false,
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: NavPillStyle.glossy,
|
||||
@@ -386,8 +447,13 @@ class _NavPillStyleCard extends StatelessWidget {
|
||||
value: NavPillStyle.frostBlur,
|
||||
label: Text(l10n.appearanceNavPillFrost),
|
||||
),
|
||||
if (LiquidGlass.isSupported)
|
||||
ButtonSegment(
|
||||
value: NavPillStyle.liquidGlass,
|
||||
label: Text(l10n.appearanceGlassMaterial),
|
||||
),
|
||||
],
|
||||
selected: {current},
|
||||
selected: {selectable},
|
||||
onSelectionChanged: (set) {
|
||||
if (set.isNotEmpty) {
|
||||
Haptics.selection();
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../../main.dart' show storiesModule;
|
||||
import '../../../models/story.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
import '../../widgets/komet_avatar.dart';
|
||||
import '../../widgets/liquid_glass.dart';
|
||||
import 'story_owner_info.dart';
|
||||
|
||||
const _quickReactions = ['❤️', '🔥', '😍', '👏', '😂', '😮'];
|
||||
@@ -627,33 +628,26 @@ class _StoryViewerScreenState extends State<StoryViewerScreen>
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
child: GlassSurface(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 14, sigmaY: 14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.18),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final emoji in _quickReactions)
|
||||
_ReactionButton(
|
||||
emoji: emoji,
|
||||
selected: current == emoji,
|
||||
onTap: () => _toggleReaction(emoji),
|
||||
),
|
||||
],
|
||||
),
|
||||
frostTint: Colors.white.withValues(alpha: 0.12),
|
||||
frostSigma: 14,
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.18)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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,
|
||||
builder: (context, navStyle, _) {
|
||||
final frost =
|
||||
style == VisualStyle.glossy &&
|
||||
navStyle == NavPillStyle.frostBlur;
|
||||
style.glossyChrome &&
|
||||
NavPillMaterial.isFrost(navStyle);
|
||||
final liquid =
|
||||
style.glossyChrome &&
|
||||
NavPillMaterial.isLiquid(navStyle);
|
||||
return SlidingPillNav(
|
||||
items: navItems,
|
||||
position: _currentPageT(),
|
||||
geometry: geometry,
|
||||
onTap: _onSectionTap,
|
||||
backgroundColor: frost
|
||||
? AppFrost.navPillTint(cs)
|
||||
: _composerColor(cs),
|
||||
backgroundColor: liquid
|
||||
? null
|
||||
: (frost
|
||||
? AppFrost.navPillTint(cs)
|
||||
: _composerColor(cs)),
|
||||
borderColor: _composerBorderColor(cs),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/config/app_pill_gradient.dart';
|
||||
import '../../core/config/app_visual_style.dart';
|
||||
import 'liquid_glass.dart';
|
||||
|
||||
class _GlossyParts {
|
||||
final bool dark;
|
||||
@@ -93,6 +94,7 @@ class GlossyPill extends StatelessWidget {
|
||||
final bool elevated;
|
||||
final BorderSide? borderSide;
|
||||
final double? blurSigma;
|
||||
final bool liquid;
|
||||
final BackdropKey? backdropKey;
|
||||
|
||||
const GlossyPill({
|
||||
@@ -107,6 +109,7 @@ class GlossyPill extends StatelessWidget {
|
||||
this.elevated = false,
|
||||
this.borderSide,
|
||||
this.blurSigma,
|
||||
this.liquid = false,
|
||||
this.backdropKey,
|
||||
}) : borderRadius =
|
||||
borderRadius ?? const BorderRadius.all(Radius.circular(100));
|
||||
@@ -120,6 +123,7 @@ class GlossyPill extends StatelessWidget {
|
||||
valueListenable: AppVisualStyle.current,
|
||||
builder: (context, style, _) {
|
||||
if (style == VisualStyle.materialYou) return _flat(context);
|
||||
if (liquid && LiquidGlass.isSupported) return _liquid(context);
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: AppPillGradient.current,
|
||||
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) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final base = color ?? cs.surfaceContainerHigh;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -13,6 +12,7 @@ import '../../backend/modules/messages.dart';
|
||||
import '../../backend/modules/shared_content.dart';
|
||||
import '../../core/cache/info_cache.dart';
|
||||
import '../../core/config/app_frost.dart';
|
||||
import 'liquid_glass.dart';
|
||||
import '../../core/utils/format.dart';
|
||||
import '../../core/utils/media_cache.dart';
|
||||
import '../../core/utils/media_saver.dart';
|
||||
@@ -578,32 +578,25 @@ class _PhotoViewerScreenState extends State<PhotoViewerScreen> {
|
||||
}
|
||||
|
||||
Widget _buildCaption(String caption) {
|
||||
return ClipRRect(
|
||||
return GlassSurface(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: AppFrost.panelSigma,
|
||||
sigmaY: AppFrost.panelSigma,
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 120),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.28),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
width: 0.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
caption,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
height: 1.3,
|
||||
),
|
||||
frostTint: Colors.black.withValues(alpha: 0.28),
|
||||
frostSigma: AppFrost.panelSigma,
|
||||
liquidTint: Colors.black.withValues(alpha: 0.28),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
width: 0.5,
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 120),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
caption,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,11 +3,13 @@ import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.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_pill_gradient.dart';
|
||||
import '../../core/config/app_visual_style.dart';
|
||||
import 'animated_lottie_icon.dart';
|
||||
import 'glossy_pill.dart';
|
||||
import 'liquid_glass.dart';
|
||||
|
||||
class PillNavItem {
|
||||
final IconData icon;
|
||||
@@ -90,12 +92,13 @@ class SlidingPillNav extends StatelessWidget {
|
||||
return ValueListenableBuilder<VisualStyle>(
|
||||
valueListenable: AppVisualStyle.current,
|
||||
builder: (context, style, _) {
|
||||
if (style != VisualStyle.glossy) {
|
||||
if (style == VisualStyle.materialYou) {
|
||||
return _buildNav(
|
||||
context,
|
||||
glossy: false,
|
||||
gradient: false,
|
||||
frost: false,
|
||||
liquid: false,
|
||||
);
|
||||
}
|
||||
return ValueListenableBuilder<bool>(
|
||||
@@ -107,7 +110,8 @@ class SlidingPillNav extends StatelessWidget {
|
||||
context,
|
||||
glossy: true,
|
||||
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 gradient,
|
||||
required bool frost,
|
||||
required bool liquid,
|
||||
}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final visualSel = position.round().clamp(0, items.length - 1);
|
||||
final base =
|
||||
backgroundColor ??
|
||||
(frost ? AppFrost.navPillTint(cs) : cs.surfaceContainerHigh);
|
||||
final useGradient = glossy && gradient;
|
||||
final frosted = frost && base.a < 1;
|
||||
final translucent = backgroundColor != null && backgroundColor!.a < 1;
|
||||
final base = liquid
|
||||
? (translucent ? backgroundColor! : AppLiquidGlass.navTint(cs))
|
||||
: (backgroundColor ??
|
||||
(frost ? AppFrost.navPillTint(cs) : cs.surfaceContainerHigh));
|
||||
final useGradient = glossy && gradient && !liquid;
|
||||
final frosted = frost && !liquid && base.a < 1;
|
||||
|
||||
return Container(
|
||||
height: height,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: useGradient ? null : base,
|
||||
color: useGradient || liquid ? null : base,
|
||||
gradient: useGradient ? GlossyDecor.fillGradient(base) : null,
|
||||
borderRadius: BorderRadius.circular(34),
|
||||
border: glossy
|
||||
@@ -145,8 +152,8 @@ class SlidingPillNav extends StatelessWidget {
|
||||
? null
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
color: Colors.black.withValues(alpha: liquid ? 0.28 : 0.5),
|
||||
blurRadius: liquid ? 26 : 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
@@ -154,6 +161,15 @@ class SlidingPillNav extends StatelessWidget {
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
if (liquid)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: LiquidGlassSurface(
|
||||
borderRadius: BorderRadius.circular(34),
|
||||
tint: base,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (frosted)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -17,6 +16,8 @@ import 'segmented_pill_toggle.dart';
|
||||
import 'small_spinner.dart';
|
||||
import 'lottie_image.dart';
|
||||
import 'sticker_peek.dart';
|
||||
import '../../core/config/app_frost.dart';
|
||||
import 'liquid_glass.dart';
|
||||
|
||||
class _DragScrollBehavior extends MaterialScrollBehavior {
|
||||
const _DragScrollBehavior();
|
||||
@@ -248,30 +249,19 @@ class _StickerPanelState extends State<StickerPanel>
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 34, sigmaY: 34),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface.withValues(alpha: 0.38),
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: cs.outlineVariant.withValues(alpha: 0.4),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: GlassSurface(
|
||||
liquid: false,
|
||||
frostTint: AppFrost.panelTint(cs),
|
||||
border: Border(top: AppFrost.hairline(cs)),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _mode == _modeEmoji && widget.onEmojiTap != null
|
||||
? EmojiPanel(onEmojiTap: widget.onEmojiTap!)
|
||||
: _buildStickerBody(cs),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _mode == _modeEmoji && widget.onEmojiTap != null
|
||||
? EmojiPanel(onEmojiTap: widget.onEmojiTap!)
|
||||
: _buildStickerBody(cs),
|
||||
),
|
||||
if (widget.onEmojiTap != null) _buildToggleBar(cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.onEmojiTap != null) _buildToggleBar(cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -290,6 +290,8 @@
|
||||
"appearanceVisualStyleSubtitle": "Material You or dimensional Glossy capsules",
|
||||
"appearanceVisualStyleMaterialYou": "Material You",
|
||||
"appearanceVisualStyleGlossy": "Glossy",
|
||||
"appearanceVisualStyleLiquidGlass": "Liquid Glass",
|
||||
"appearanceGlassMaterial": "Glass",
|
||||
"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",
|
||||
"appearanceChatChromeColor": "Color",
|
||||
|
||||
@@ -1526,6 +1526,18 @@ abstract class AppLocalizations {
|
||||
/// **'Glossy'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -758,6 +758,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appearanceVisualStyleGlossy => 'Glossy';
|
||||
|
||||
@override
|
||||
String get appearanceVisualStyleLiquidGlass => 'Liquid Glass';
|
||||
|
||||
@override
|
||||
String get appearanceGlassMaterial => 'Glass';
|
||||
|
||||
@override
|
||||
String get appearanceChatChromeTitle => 'Chat screen elements';
|
||||
|
||||
|
||||
@@ -761,6 +761,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appearanceVisualStyleGlossy => 'Glossy';
|
||||
|
||||
@override
|
||||
String get appearanceVisualStyleLiquidGlass => 'Liquid Glass';
|
||||
|
||||
@override
|
||||
String get appearanceGlassMaterial => 'Стекло';
|
||||
|
||||
@override
|
||||
String get appearanceChatChromeTitle => 'Элементы экрана чата';
|
||||
|
||||
|
||||
@@ -255,6 +255,8 @@
|
||||
"appearanceVisualStyleSubtitle": "Material You или объёмные Glossy-капсулы",
|
||||
"appearanceVisualStyleMaterialYou": "Material You",
|
||||
"appearanceVisualStyleGlossy": "Glossy",
|
||||
"appearanceVisualStyleLiquidGlass": "Liquid Glass",
|
||||
"appearanceGlassMaterial": "Стекло",
|
||||
"appearanceChatChromeTitle": "Элементы экрана чата",
|
||||
"appearanceChatChromeSubtitle": "Фон панелей сверху и снизу: цвет, размытие или прозрачно. При размытии и прозрачности сообщения заходят под панели",
|
||||
"appearanceChatChromeColor": "Цвет",
|
||||
|
||||
@@ -79,6 +79,7 @@ import 'frontend/debug/fps_overlay_layer.dart';
|
||||
import 'frontend/screens/auth/login_screen.dart';
|
||||
import 'frontend/widgets/adaptive_shell.dart';
|
||||
import 'frontend/widgets/custom_notification.dart';
|
||||
import 'frontend/widgets/liquid_glass.dart';
|
||||
import 'frontend/widgets/theme_reveal.dart';
|
||||
|
||||
final api = Api();
|
||||
@@ -194,6 +195,7 @@ void main(List<String> args) async {
|
||||
final amoledFuture = AppAmoled.load();
|
||||
final pillGradientFuture = AppPillGradient.load();
|
||||
final visualStyleFuture = AppVisualStyle.load();
|
||||
final liquidGlassFuture = LiquidGlass.load();
|
||||
final chatChromeFuture = AppChatChrome.load();
|
||||
final composerStyleFuture = AppComposerStyle.load();
|
||||
final composerBackgroundFuture = AppComposerBackground.load();
|
||||
@@ -248,6 +250,7 @@ void main(List<String> args) async {
|
||||
amoledFuture,
|
||||
pillGradientFuture,
|
||||
visualStyleFuture,
|
||||
liquidGlassFuture,
|
||||
chatChromeFuture,
|
||||
composerStyleFuture,
|
||||
composerBackgroundFuture,
|
||||
|
||||
@@ -125,6 +125,9 @@ flutter_launcher_icons:
|
||||
flutter:
|
||||
generate: true
|
||||
|
||||
shaders:
|
||||
- shaders/liquid_glass.frag
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user