feat: больше frost'а)

This commit is contained in:
Jganenokk
2026-07-14 13:42:40 +07:00
parent 38a010c486
commit d0fc7a8ec9
13 changed files with 293 additions and 44 deletions
+5
View File
@@ -12,6 +12,11 @@ class AppFrost {
static Color pillTint(ColorScheme cs) =>
cs.surfaceContainerHigh.withValues(alpha: 0.45);
static Color navPillTint(ColorScheme cs) =>
cs.surfaceContainerHigh.withValues(alpha: 0.28);
static Color fabTint(ColorScheme cs) => navPillTint(cs);
static Color inputTint(ColorScheme cs) =>
cs.surfaceContainerHighest.withValues(alpha: 0.45);
+25
View File
@@ -0,0 +1,25 @@
import 'package:flutter/foundation.dart';
import 'persisted_setting.dart';
enum NavPillStyle { glossy, frostBlur }
class AppNavPillStyle {
static const prefKey = 'app_nav_pill_style';
static final _setting = PersistedEnum<NavPillStyle>(
prefKey: prefKey,
defaultValue: NavPillStyle.glossy,
encode: (value) => value.name,
decode: _parse,
);
static ValueNotifier<NavPillStyle> get current => _setting.current;
static Future<NavPillStyle> load() => _setting.load();
static Future<void> save(NavPillStyle value) => _setting.save(value);
static NavPillStyle _parse(String? val) =>
enumFromName(NavPillStyle.values, val, NavPillStyle.glossy);
}
@@ -34,6 +34,9 @@ import '../../../core/protocol/opcode_map.dart';
import '../../../core/protocol/packet.dart';
import '../../../core/utils/haptics.dart';
import '../../../core/config/app_animations.dart';
import '../../../core/config/app_frost.dart';
import '../../../core/config/app_nav_pill_style.dart';
import '../../../core/config/app_visual_style.dart';
import '../../../core/config/app_stories.dart';
import '../../../core/config/app_colors.dart';
import '../../../core/config/komet_settings.dart';
@@ -186,6 +189,7 @@ class _ChatListScreenState extends State<ChatListScreen>
late AnimationController _navPageAnimController;
late AnimationController _fabController;
final BackdropKey _frostBackdrop = BackdropKey();
late PageController _folderPageController;
late AnimationController _storiesRevealController;
@@ -657,7 +661,9 @@ class _ChatListScreenState extends State<ChatListScreen>
final me = _profile?.id;
final self = _selfOwnerInfo();
if (me == null || self == null) return const {};
return {me: StoryOwnerInfo(name: 'Ваша история', avatarUrl: self.avatarUrl)};
return {
me: StoryOwnerInfo(name: 'Ваша история', avatarUrl: self.avatarUrl),
};
}
void _openStories(int index, [Offset? origin]) {
@@ -757,9 +763,9 @@ class _ChatListScreenState extends State<ChatListScreen>
}
var folders = await FoldersModule.loadFolders(p.id);
final foldersKnown = await FoldersModule.hasReceivedFoldersList(p.id);
final contactIds = (await ContactsModule.getContacts(p.id))
.map((c) => c.id)
.toSet();
final contactIds = (await ContactsModule.getContacts(
p.id,
)).map((c) => c.id).toSet();
final allChatsFolder = ChatFolder(
id: 'all.chat.folder',
@@ -1859,6 +1865,7 @@ class _ChatListScreenState extends State<ChatListScreen>
geometry: geometry,
iconSize: 20,
labelGap: 4,
backdropKey: _frostBackdrop,
onTap: _onNavTabSelected,
onItemLongPress: (index, pos) {
if (index == 3) _openAccountSwitcher(pos);
@@ -2020,12 +2027,32 @@ class _ChatListScreenState extends State<ChatListScreen>
Positioned(
right: 20,
bottom: bottomInset + 90,
child: GlossyPill(
onTap: _toggleFab,
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(28),
elevated: true,
depth: 12,
child: ValueListenableBuilder<VisualStyle>(
valueListenable: AppVisualStyle.current,
builder: (context, style, child) =>
ValueListenableBuilder<NavPillStyle>(
valueListenable: AppNavPillStyle.current,
builder: (context, navStyle, child) {
final frost =
style == VisualStyle.glossy &&
navStyle == NavPillStyle.frostBlur;
return GlossyPill(
onTap: _toggleFab,
color: frost
? AppFrost.fabTint(cs)
: cs.primaryContainer,
blurSigma: frost
? AppFrost.sigma
: null,
backdropKey: _frostBackdrop,
borderRadius: BorderRadius.circular(28),
elevated: true,
depth: 12,
child: child!,
);
},
child: child,
),
child: SizedBox(
width: 56,
height: 56,
@@ -2119,10 +2146,7 @@ class _ChatListScreenState extends State<ChatListScreen>
return InkWell(
onTap: () {
if (_isSelectionMode) return;
pushSwipeable(
context,
(_) => const ChatListScreen(archiveMode: true),
);
pushSwipeable(context, (_) => const ChatListScreen(archiveMode: true));
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
@@ -2151,10 +2175,7 @@ class _ChatListScreenState extends State<ChatListScreen>
if (_archivedUnread > 0)
Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(
horizontal: 7,
vertical: 2,
),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(12),
@@ -2310,7 +2331,9 @@ class _ChatListScreenState extends State<ChatListScreen>
picked.item.localFile ??
await picked.item.originFile();
if (file == null) {
if (mounted) showCustomNotification(context, 'Не удалось открыть фото');
if (mounted) {
showCustomNotification(context, 'Не удалось открыть фото');
}
return;
}
if (!mounted) return;
@@ -2915,7 +2938,6 @@ class _ChatListScreenState extends State<ChatListScreen>
),
);
}
}
class _StoriesUi extends ChangeNotifier {
+8 -1
View File
@@ -498,6 +498,7 @@ class _ChatScreenState extends State<ChatScreen>
bool _scrollDownVisible = false;
int _listEpoch = 0;
final List<({String id, double pixels, double alignment})> _returnStack = [];
bool _returningToAnchor = false;
final Map<int, GlobalKey> _separatorKeys = {};
String? _lastSentId;
final ValueNotifier<int> _otherUnread = ValueNotifier(0);
@@ -3654,6 +3655,7 @@ class _ChatScreenState extends State<ChatScreen>
}
void _onScrollDownTap() {
if (_returningToAnchor || _navigatingToTarget) return;
if (!_scrollController.hasClients) {
_scrollToBottom();
return;
@@ -3662,7 +3664,12 @@ class _ChatScreenState extends State<ChatScreen>
while (_returnStack.isNotEmpty) {
final anchor = _returnStack.removeLast();
if (anchor.pixels < pixels && _messages.any((m) => m.id == anchor.id)) {
unawaited(_returnToAnchor(anchor));
_returningToAnchor = true;
unawaited(
_returnToAnchor(
anchor,
).whenComplete(() => _returningToAnchor = false),
);
return;
}
}
@@ -12,6 +12,7 @@ import '../../../core/config/app_visual_style.dart';
import '../../../core/config/app_chat_chrome.dart';
import '../../../core/config/app_composer_background.dart';
import '../../../core/config/app_composer_style.dart';
import '../../../core/config/app_nav_pill_style.dart';
import '../../../core/utils/bubble_radius.dart';
import '../../../core/utils/debouncer.dart';
import '../../../core/utils/haptics.dart';
@@ -123,6 +124,8 @@ class _AppearanceScreenState extends State<AppearanceScreen> {
const SizedBox(height: 12),
const _ComposerBarCard(),
const SizedBox(height: 12),
const _NavPillStyleCard(),
const SizedBox(height: 12),
const _GradientToggleCard(),
],
),
@@ -341,6 +344,65 @@ class _ComposerBarCard extends StatelessWidget {
}
}
class _NavPillStyleCard extends StatelessWidget {
const _NavPillStyleCard();
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.appearanceNavPillTitle,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
l10n.appearanceNavPillSubtitle,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 16),
ValueListenableBuilder<NavPillStyle>(
valueListenable: AppNavPillStyle.current,
builder: (context, current, _) {
return SegmentedButton<NavPillStyle>(
segments: [
ButtonSegment(
value: NavPillStyle.glossy,
label: Text(l10n.appearanceNavPillGlossy),
),
ButtonSegment(
value: NavPillStyle.frostBlur,
label: Text(l10n.appearanceNavPillFrost),
),
],
selected: {current},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppNavPillStyle.save(set.first);
}
},
);
},
),
],
),
);
}
}
class _GradientToggleCard extends StatelessWidget {
const _GradientToggleCard();
@@ -590,7 +652,12 @@ class _ColorPickerCard extends StatelessWidget {
);
}
Widget _buildBody(ColorScheme cs, AppLocalizations l10n, Color col, bool sys) {
Widget _buildBody(
ColorScheme cs,
AppLocalizations l10n,
Color col,
bool sys,
) {
final swatchColor = sys ? cs.primary : col;
return GlossyPill(
@@ -4,6 +4,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/core/config/app_frost.dart';
import 'package:komet/core/config/app_nav_pill_style.dart';
import 'package:komet/core/config/app_visual_style.dart';
import 'package:komet/core/media/gallery_source.dart';
import 'package:komet/core/utils/format.dart';
import 'package:komet/frontend/widgets/attachment/media_preview_screen.dart';
@@ -739,13 +742,27 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
animation: _pageController,
builder: (context, _) {
final cs = Theme.of(context).colorScheme;
return SlidingPillNav(
items: navItems,
position: _currentPageT(),
geometry: geometry,
onTap: _onSectionTap,
backgroundColor: _composerColor(cs),
borderColor: _composerBorderColor(cs),
return ValueListenableBuilder<VisualStyle>(
valueListenable: AppVisualStyle.current,
builder: (context, style, _) =>
ValueListenableBuilder<NavPillStyle>(
valueListenable: AppNavPillStyle.current,
builder: (context, navStyle, _) {
final frost =
style == VisualStyle.glossy &&
navStyle == NavPillStyle.frostBlur;
return SlidingPillNav(
items: navItems,
position: _currentPageT(),
geometry: geometry,
onTap: _onSectionTap,
backgroundColor: frost
? AppFrost.navPillTint(cs)
: _composerColor(cs),
borderColor: _composerBorderColor(cs),
);
},
),
);
},
),
+51 -10
View File
@@ -1,5 +1,9 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import '../../core/config/app_frost.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';
@@ -50,6 +54,7 @@ class SlidingPillNav extends StatelessWidget {
final Color? backgroundColor;
final Color? borderColor;
final bool iconsOnly;
final BackdropKey? backdropKey;
const SlidingPillNav({
super.key,
@@ -64,6 +69,7 @@ class SlidingPillNav extends StatelessWidget {
this.backgroundColor,
this.borderColor,
this.iconsOnly = false,
this.backdropKey,
});
static const double height = 68;
@@ -85,12 +91,25 @@ class SlidingPillNav extends StatelessWidget {
valueListenable: AppVisualStyle.current,
builder: (context, style, _) {
if (style != VisualStyle.glossy) {
return _buildNav(context, glossy: false, gradient: false);
return _buildNav(
context,
glossy: false,
gradient: false,
frost: false,
);
}
return ValueListenableBuilder<bool>(
valueListenable: AppPillGradient.current,
builder: (context, gradient, _) =>
_buildNav(context, glossy: true, gradient: gradient),
ValueListenableBuilder<NavPillStyle>(
valueListenable: AppNavPillStyle.current,
builder: (context, navStyle, _) => _buildNav(
context,
glossy: true,
gradient: gradient,
frost: navStyle == NavPillStyle.frostBlur,
),
),
);
},
);
@@ -100,11 +119,15 @@ class SlidingPillNav extends StatelessWidget {
BuildContext context, {
required bool glossy,
required bool gradient,
required bool frost,
}) {
final cs = Theme.of(context).colorScheme;
final visualSel = position.round().clamp(0, items.length - 1);
final base = backgroundColor ?? cs.surfaceContainerHigh;
final base =
backgroundColor ??
(frost ? AppFrost.navPillTint(cs) : cs.surfaceContainerHigh);
final useGradient = glossy && gradient;
final frosted = frost && base.a < 1;
return Container(
height: height,
@@ -118,17 +141,35 @@ class SlidingPillNav extends StatelessWidget {
: (borderColor != null
? Border.all(color: borderColor!, width: 0.5)
: null),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
boxShadow: frosted
? null
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
if (frosted)
Positioned.fill(
child: IgnorePointer(
child: ClipRRect(
borderRadius: BorderRadius.circular(34),
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: AppFrost.sigma,
sigmaY: AppFrost.sigma,
),
backdropGroupKey: backdropKey,
child: const SizedBox.expand(),
),
),
),
),
if (useGradient)
Positioned.fill(
child: IgnorePointer(
+4
View File
@@ -300,6 +300,10 @@
"appearanceComposerSubtitle": "Style and background of the message input bar",
"appearanceComposerBackgroundStandard": "Default",
"appearanceComposerBackgroundFrost": "Frost blur",
"appearanceNavPillTitle": "Switcher style",
"appearanceNavPillSubtitle": "Section switcher on the chats screen",
"appearanceNavPillGlossy": "Glossy",
"appearanceNavPillFrost": "G-FrostBlur",
"appearanceGradientTitle": "Gradient",
"appearanceGradientSubtitle": "Depth and highlights in Glossy capsules",
"appearanceAccentColorTitle": "Accent color",
+24
View File
@@ -1586,6 +1586,30 @@ abstract class AppLocalizations {
/// **'Frost blur'**
String get appearanceComposerBackgroundFrost;
/// No description provided for @appearanceNavPillTitle.
///
/// In en, this message translates to:
/// **'Switcher style'**
String get appearanceNavPillTitle;
/// No description provided for @appearanceNavPillSubtitle.
///
/// In en, this message translates to:
/// **'Section switcher on the chats screen'**
String get appearanceNavPillSubtitle;
/// No description provided for @appearanceNavPillGlossy.
///
/// In en, this message translates to:
/// **'Glossy'**
String get appearanceNavPillGlossy;
/// No description provided for @appearanceNavPillFrost.
///
/// In en, this message translates to:
/// **'G-FrostBlur'**
String get appearanceNavPillFrost;
/// No description provided for @appearanceGradientTitle.
///
/// In en, this message translates to:
+13
View File
@@ -790,6 +790,19 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appearanceComposerBackgroundFrost => 'Frost blur';
@override
String get appearanceNavPillTitle => 'Switcher style';
@override
String get appearanceNavPillSubtitle =>
'Section switcher on the chats screen';
@override
String get appearanceNavPillGlossy => 'Glossy';
@override
String get appearanceNavPillFrost => 'G-FrostBlur';
@override
String get appearanceGradientTitle => 'Gradient';
+13
View File
@@ -792,6 +792,19 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get appearanceComposerBackgroundFrost => 'Frost blur';
@override
String get appearanceNavPillTitle => 'Вид переключателей';
@override
String get appearanceNavPillSubtitle =>
'Переключатель разделов на экране чатов';
@override
String get appearanceNavPillGlossy => 'Glossy';
@override
String get appearanceNavPillFrost => 'G-FrostBlur';
@override
String get appearanceGradientTitle => 'Градиент';
+4
View File
@@ -265,6 +265,10 @@
"appearanceComposerSubtitle": "Стиль и фон панели ввода сообщений",
"appearanceComposerBackgroundStandard": "Default",
"appearanceComposerBackgroundFrost": "Frost blur",
"appearanceNavPillTitle": "Вид переключателей",
"appearanceNavPillSubtitle": "Переключатель разделов на экране чатов",
"appearanceNavPillGlossy": "Glossy",
"appearanceNavPillFrost": "G-FrostBlur",
"appearanceGradientTitle": "Градиент",
"appearanceGradientSubtitle": "Объём и блики в Glossy-капсулах",
"appearanceAccentColorTitle": "Акцентный цвет",
+12 -5
View File
@@ -40,6 +40,7 @@ import 'core/config/app_visual_style.dart';
import 'core/config/app_chat_chrome.dart';
import 'core/config/app_composer_background.dart';
import 'core/config/app_composer_style.dart';
import 'core/config/app_nav_pill_style.dart';
import 'core/config/app_wallpaper_tint.dart';
import 'core/storage/chat_wallpaper_store.dart';
import 'core/utils/wallpaper_seed.dart';
@@ -194,6 +195,7 @@ void main(List<String> args) async {
final chatChromeFuture = AppChatChrome.load();
final composerStyleFuture = AppComposerStyle.load();
final composerBackgroundFuture = AppComposerBackground.load();
final navPillStyleFuture = AppNavPillStyle.load();
final wallpaperTintFuture = AppWallpaperTint.load();
final themeScheduleFuture = AppThemeSchedule.load();
final messageActionsFuture = AppMessageActionsStyle.load();
@@ -247,6 +249,7 @@ void main(List<String> args) async {
chatChromeFuture,
composerStyleFuture,
composerBackgroundFuture,
navPillStyleFuture,
wallpaperTintFuture,
themeScheduleFuture,
messageActionsFuture,
@@ -527,7 +530,9 @@ class KometAppState extends State<KometApp>
AppAmoled.current.removeListener(_onAmoledChanged);
AppThemeSchedule.current.removeListener(_onScheduleChanged);
AppWallpaperTint.current.removeListener(_onWallpaperTintChanged);
ChatWallpaperStore.instance.revision.removeListener(_onWallpaperTintChanged);
ChatWallpaperStore.instance.revision.removeListener(
_onWallpaperTintChanged,
);
WidgetsBinding.instance.removeObserver(this);
_profileUpdateController.close();
fpsOverlayEnabled.dispose();
@@ -752,8 +757,10 @@ class KometAppState extends State<KometApp>
return;
}
await ChatWallpaperStore.instance.load();
final wallpaper =
ChatWallpaperStore.instance.get(accountId, kGlobalWallpaperChatId);
final wallpaper = ChatWallpaperStore.instance.get(
accountId,
kGlobalWallpaperChatId,
);
final seed = await computeWallpaperSeed(wallpaper);
if (!mounted) return;
wallpaperSeed.value = seed;
@@ -897,8 +904,8 @@ class KometAppState extends State<KometApp>
AppWallpaperTint.current,
]),
builder: (context, _) {
final seed = AppWallpaperTint.current.value &&
wallpaperSeed.value != null
final seed =
AppWallpaperTint.current.value && wallpaperSeed.value != null
? wallpaperSeed.value
: accentSeed.value;
final ColorScheme lightBase;