feat: liquid glass, пока-что скрыт.
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 '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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 => 'Элементы экрана чата';
|
||||||
|
|
||||||
|
|||||||
@@ -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": "Цвет",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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