fix: мелкие фиксы и работа с прочтением.

This commit is contained in:
Jganenokk
2026-08-07 20:19:19 +07:00
parent b9d69ea37b
commit 7a9e08f5c7
11 changed files with 185 additions and 89 deletions
+5 -2
View File
@@ -5,10 +5,13 @@ import 'persisted_setting.dart';
class AppAmoled {
static const prefKey = 'app_amoled';
static bool get systemIsDark =>
PlatformDispatcher.instance.platformBrightness == Brightness.dark;
static final _setting = PersistedSetting<bool>(
prefKey: prefKey,
defaultValue: false,
read: (prefs, key) => prefs.getBool(key),
defaultValue: systemIsDark,
read: (prefs, key) => prefs.getBool(key) ?? systemIsDark,
write: (prefs, key, value) async {
await prefs.setBool(key, value);
},
+6 -3
View File
@@ -19,7 +19,7 @@ class AppComposerBackground {
static final _setting = PersistedEnum<ComposerBackground>(
prefKey: prefKey,
defaultValue: ComposerBackground.standard,
defaultValue: ComposerBackground.frostBlur,
encode: (value) => value.name,
decode: _parse,
);
@@ -30,6 +30,9 @@ class AppComposerBackground {
static Future<void> save(ComposerBackground value) => _setting.save(value);
static ComposerBackground _parse(String? val) =>
enumFromName(ComposerBackground.values, val, ComposerBackground.standard);
static ComposerBackground _parse(String? val) => enumFromName(
ComposerBackground.values,
val,
ComposerBackground.frostBlur,
);
}
+5 -14
View File
@@ -3,22 +3,13 @@ import 'package:flutter/material.dart';
class AppFrost {
static const double sigma = 34;
static const double panelSigma = 24;
static const double glassAlpha = 0.28;
static const double blurPanelAlpha = 0.55;
static Color panelTint(ColorScheme cs) => cs.surface.withValues(alpha: 0.38);
static Color glassTint(ColorScheme cs, [double alpha = glassAlpha]) =>
cs.surfaceContainerHigh.withValues(alpha: alpha);
static Color blurPanelTint(ColorScheme cs) =>
cs.surfaceContainerHigh.withValues(alpha: 0.55);
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);
static Color blurPanelTint(ColorScheme cs) => glassTint(cs, blurPanelAlpha);
static BorderSide hairline(ColorScheme cs) =>
BorderSide(color: cs.outlineVariant.withValues(alpha: 0.4), width: 0.5);
+2 -2
View File
@@ -19,7 +19,7 @@ class AppNavPillStyle {
static final _setting = PersistedEnum<NavPillStyle>(
prefKey: prefKey,
defaultValue: NavPillStyle.glossy,
defaultValue: NavPillStyle.frostBlur,
encode: (value) => value.name,
decode: _parse,
);
@@ -31,5 +31,5 @@ class AppNavPillStyle {
static Future<void> save(NavPillStyle value) => _setting.save(value);
static NavPillStyle _parse(String? val) =>
enumFromName(NavPillStyle.values, val, NavPillStyle.glossy);
enumFromName(NavPillStyle.values, val, NavPillStyle.frostBlur);
}
@@ -20,6 +20,7 @@ import 'package:komet/models/story.dart';
class ChatHeaderRow extends StatelessWidget {
final bool glossy;
final bool frosted;
final bool backdropVisible;
final bool liquid;
final BackdropKey? backdropKey;
final ColorScheme cs;
@@ -46,6 +47,7 @@ class ChatHeaderRow extends StatelessWidget {
super.key,
required this.glossy,
required this.frosted,
this.backdropVisible = true,
this.liquid = false,
this.backdropKey,
required this.cs,
@@ -73,9 +75,10 @@ class ChatHeaderRow extends StatelessWidget {
Widget build(BuildContext context) =>
glossy ? _glossyRow(context) : _materialRow(context);
Color? get _pillColor => frosted || liquid ? AppFrost.pillTint(cs) : null;
Color? get _pillColor => frosted || liquid ? AppFrost.glassTint(cs) : null;
double? get _pillBlur => frosted && !liquid ? AppFrost.sigma : null;
double? get _pillBlur =>
frosted && !liquid && backdropVisible ? AppFrost.sigma : null;
Widget _glossyRow(BuildContext context) {
final nameStyle = TextStyle(
@@ -408,7 +408,7 @@ class ComposerInputBar extends StatelessWidget {
: recording
? cs.error
: _frost
? AppFrost.inputTint(cs)
? AppFrost.glassTint(cs)
: cs.surfaceContainerHighest,
onTap:
(hasText ||
@@ -523,7 +523,7 @@ class ComposerInputBar extends StatelessWidget {
if (_flat) return child;
return GlossyPill(
color: _translucent
? AppFrost.inputTint(cs)
? AppFrost.glassTint(cs)
: Color.alphaBlend(
cs.surfaceContainerHighest.withValues(alpha: 0.92),
cs.surface,
@@ -741,7 +741,7 @@ class ComposerInputBar extends StatelessWidget {
if (!_translucent && chrome != ChatChromeStyle.transparent) return child;
return GlassSurface(
liquid: _liquid,
frostTint: AppFrost.panelTint(cs),
frostTint: AppFrost.glassTint(cs),
border: Border(top: AppFrost.hairline(cs)),
backdropKey: backdropKey,
child: child,
@@ -2318,7 +2318,7 @@ class _ChatListScreenState extends State<ChatListScreen>
return GlossyPill(
onTap: _toggleFab,
color: frost || liquid
? AppFrost.fabTint(cs)
? AppFrost.glassTint(cs)
: cs.primaryContainer,
blurSigma: frost
? AppFrost.sigma
+155 -59
View File
@@ -560,6 +560,12 @@ class _ChatScreenState extends State<ChatScreen>
static const double _glossyHeaderHeight = 76.0;
static const double _glossySearchHeight = 58.0;
static const double _pinnedBannerLift = 6.0;
static const double _unreadSeparatorHeight = 30.0;
static const double _unreadSeparatorInset = 72.0;
static const double _unreadAnchorFallbackAlignment = 0.3;
static const int _jumpStallLimit = 8;
static const int _jumpFrameLimit = 240;
static const double _jumpStepMaxScreens = 4.0;
final BackdropKey _barBackdrop = BackdropKey();
final BackdropKey _pillBackdrop = BackdropKey();
@@ -1038,18 +1044,39 @@ class _ChatScreenState extends State<ChatScreen>
}
}
void _positionToMessage(String messageId, double alignment) {
double _unreadAnchorAlignment() {
final listBox = _listKey.currentContext?.findRenderObject();
if (listBox is! RenderBox || listBox.size.height <= 0) {
return _unreadAnchorFallbackAlignment;
}
final glossy = AppVisualStyle.current.value.glossyChrome;
final chromeBottom = _effectiveChrome == ChatChromeStyle.color
? 0.0
: MediaQuery.paddingOf(context).top +
(glossy ? _glossyHeaderHeight : kToolbarHeight) +
_pinnedBannerHeight.value;
final desiredTop =
chromeBottom + _unreadSeparatorHeight + _unreadSeparatorInset;
return (desiredTop / listBox.size.height).clamp(0.0, 0.5);
}
void _positionToMessage(String messageId) {
_pinnedMessageId = messageId;
_pinnedAlignment = alignment.clamp(0.0, 1.0);
_jumpCacheExtent.value = _jumpCacheExtentPx;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_pinnedAlignment = _unreadAnchorAlignment();
_scrollToLoadedMessage(
messageId,
alignment: _pinnedAlignment,
highlight: false,
notifyIfMissing: false,
onSettled: () {
if (mounted) setState(_markPositioned);
if (!mounted) return;
setState(_markPositioned);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _jumpCacheExtent.value = null;
});
},
);
});
@@ -1105,7 +1132,7 @@ class _ChatScreenState extends State<ChatScreen>
}
if (firstUnread > 0 || !_hasMoreHistory) {
_initialPositionDone = true;
_positionToMessage(_messages[firstUnread].id, 0.15);
_positionToMessage(_messages[firstUnread].id);
} else {
_positioningInFlight = true;
unawaited(_loadUntilUnreadReady());
@@ -1222,7 +1249,7 @@ class _ChatScreenState extends State<ChatScreen>
_positioningInFlight = false;
_initialPositionDone = true;
if (idx >= 0) {
_positionToMessage(_messages[idx].id, 0.15);
_positionToMessage(_messages[idx].id);
} else {
setState(_markPositioned);
}
@@ -2673,7 +2700,7 @@ class _ChatScreenState extends State<ChatScreen>
}
return _FrostedPanel(
sigma: AppFrost.sigma,
tint: AppFrost.panelTint(cs),
tint: AppFrost.glassTint(cs),
border: Border(top: AppFrost.hairline(cs)),
backdropKey: _barBackdrop,
child: child,
@@ -3095,7 +3122,7 @@ class _ChatScreenState extends State<ChatScreen>
: (chrome == ChatChromeStyle.transparent && !glossy)
? _FrostedPanel(
sigma: AppFrost.sigma,
tint: AppFrost.panelTint(cs),
tint: AppFrost.glassTint(cs),
border: Border(bottom: AppFrost.hairline(cs)),
backdropKey: _barBackdrop,
child: const SizedBox.expand(),
@@ -3136,6 +3163,7 @@ class _ChatScreenState extends State<ChatScreen>
glossy: glossy,
frosted:
glossy && chrome == ChatChromeStyle.transparent,
backdropVisible: t == 0 && s == 0,
liquid: _liquidChrome,
backdropKey: _pillBackdrop,
cs: cs,
@@ -4835,12 +4863,7 @@ class _ChatScreenState extends State<ChatScreen>
messageId,
).currentContext?.findRenderObject();
if (laidOut is! RenderBox || !laidOut.attached) {
var below = 0.0;
for (var i = pos + 1; i < items.length; i++) {
below += items[i] is _MessageItem ? _avgMessageHeight : 44.0;
}
final maxExtent = _scrollController.position.maxScrollExtent;
_scrollController.jumpTo(below.clamp(0.0, maxExtent).toDouble());
_jumpNearMessage(messageId);
}
if (highlight) {
@@ -4858,10 +4881,72 @@ class _ChatScreenState extends State<ChatScreen>
);
}
({int oldest, int newest}) _visibleItemRange(
List<Object> items,
RenderBox listBox,
) {
var oldest = -1;
var newest = -1;
final viewportBottom = listBox.size.height;
for (var i = 0; i < items.length; i++) {
final item = items[i];
if (item is! _MessageItem) continue;
final box = _messageKeys[item.message.id]?.currentContext
?.findRenderObject();
if (box is! RenderBox || !box.attached) continue;
final top = box.localToGlobal(Offset.zero, ancestor: listBox).dy;
if (top + box.size.height <= 0 || top >= viewportBottom) continue;
if (oldest == -1) oldest = i;
newest = i;
}
return (oldest: oldest, newest: newest);
}
double _jumpStepScreens(int index, ({int oldest, int newest}) visible) {
if (visible.oldest == -1) return 1;
final perScreen = visible.newest - visible.oldest + 1;
if (perScreen <= 0) return 1;
final away = index < visible.oldest
? visible.oldest - index
: index - visible.newest;
return (away / perScreen).clamp(1.0, _jumpStepMaxScreens);
}
bool _jumpNearMessage(String messageId) {
if (!_scrollController.hasClients) return false;
final listBox = _listKey.currentContext?.findRenderObject();
if (listBox is! RenderBox || listBox.size.height <= 0) return false;
final items = _buildCombinedItems();
final index = items.indexWhere(
(it) => it is _MessageItem && it.message.id == messageId,
);
if (index == -1) return false;
final visible = _visibleItemRange(items, listBox);
final position = _scrollController.position;
final step = position.viewportDimension * _jumpStepScreens(index, visible);
final double next;
if (visible.oldest == -1 || index < visible.oldest) {
next = position.pixels + step;
} else if (index > visible.newest) {
next = position.pixels - step;
} else {
return false;
}
final clamped = next.clamp(
position.minScrollExtent,
position.maxScrollExtent,
);
if ((clamped - position.pixels).abs() < 0.5) return false;
_scrollController.jumpTo(clamped);
return true;
}
void _alignLoadedMessage(
String messageId,
double alignment,
int attempt, {
int frames = 0,
VoidCallback? onSettled,
}) {
if (!mounted || !_scrollController.hasClients) {
@@ -4871,15 +4956,17 @@ class _ChatScreenState extends State<ChatScreen>
final listBox = _listKey.currentContext?.findRenderObject();
final box = _keyForMessage(messageId).currentContext?.findRenderObject();
if (listBox is! RenderBox || box is! RenderBox || !box.attached) {
if (attempt >= 8) {
if (attempt >= _jumpStallLimit || frames >= _jumpFrameLimit) {
onSettled?.call();
return;
}
final moved = _jumpNearMessage(messageId);
WidgetsBinding.instance.addPostFrameCallback(
(_) => _alignLoadedMessage(
messageId,
alignment,
attempt + 1,
moved ? 0 : attempt + 1,
frames: frames + 1,
onSettled: onSettled,
),
);
@@ -4899,7 +4986,8 @@ class _ChatScreenState extends State<ChatScreen>
if (viewportHeight <= 0 ||
delta.abs() <= 0.5 ||
(target - pos.pixels).abs() <= 0.5 ||
attempt >= 8) {
attempt >= _jumpStallLimit ||
frames >= _jumpFrameLimit) {
onSettled?.call();
return;
}
@@ -4910,6 +4998,7 @@ class _ChatScreenState extends State<ChatScreen>
messageId,
alignment,
attempt + 1,
frames: frames + 1,
onSettled: onSettled,
),
);
@@ -5790,52 +5879,59 @@ class _ChatScreenState extends State<ChatScreen>
builder: (context, child) {
final t = _scrollDownCurved.value;
if (t == 0) return const SizedBox.shrink();
final backdropVisible = t >= 1;
return Opacity(
opacity: t,
child: Transform.scale(scale: 0.82 + 0.18 * t, child: child),
child: Transform.scale(
scale: 0.82 + 0.18 * t,
child: SizedBox(
width: 46,
height: 46,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: GlossyPill(
color: frosted || _liquidChrome
? AppFrost.glassTint(cs)
: null,
blurSigma: frosted && !_liquidChrome && backdropVisible
? AppFrost.sigma
: null,
liquid: _liquidChrome,
backdropKey: _pillBackdrop,
elevated: true,
onTap: _onScrollDownTap,
child: child!,
),
),
Positioned(
top: -5,
right: -3,
child: ValueListenableBuilder<int>(
valueListenable: _newMessageCount,
builder: (context, count, _) => count <= 0
? const SizedBox.shrink()
: AnimatedValueSwap<int>(
value: count > 99 ? 100 : count,
alignment: Alignment.centerRight,
builder: (context, value) =>
_unreadBadge(cs, value),
),
),
),
],
),
),
),
);
},
child: SizedBox(
width: 46,
height: 46,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: GlossyPill(
color: frosted || _liquidChrome
? AppFrost.pillTint(cs)
: null,
blurSigma: frosted && !_liquidChrome ? AppFrost.sigma : null,
liquid: _liquidChrome,
backdropKey: _pillBackdrop,
elevated: true,
onTap: _onScrollDownTap,
child: Center(
child: Icon(
Symbols.keyboard_arrow_down,
color: cs.onSurface,
weight: 500,
size: 26,
),
),
),
),
Positioned(
top: -5,
right: -3,
child: ValueListenableBuilder<int>(
valueListenable: _newMessageCount,
builder: (context, count, _) => count <= 0
? const SizedBox.shrink()
: AnimatedValueSwap<int>(
value: count > 99 ? 100 : count,
alignment: Alignment.centerRight,
builder: (context, value) => _unreadBadge(cs, value),
),
),
),
],
child: Center(
child: Icon(
Symbols.keyboard_arrow_down,
color: cs.onSurface,
weight: 500,
size: 26,
),
),
),
@@ -6862,7 +6958,7 @@ class _PinnedMessageBanner extends StatelessWidget {
final cs = Theme.of(context).colorScheme;
final content = Material(
color: frosted
? AppFrost.panelTint(cs)
? AppFrost.glassTint(cs)
: floating
? cs.surfaceContainerHigh.withValues(alpha: 0.92)
: cs.surfaceContainerHigh,
@@ -797,7 +797,7 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
backgroundColor: liquid
? null
: (frost
? AppFrost.navPillTint(cs)
? AppFrost.glassTint(cs)
: _composerColor(cs)),
borderColor: _composerBorderColor(cs),
);
+1 -1
View File
@@ -132,7 +132,7 @@ class SlidingPillNav extends StatelessWidget {
final base = liquid
? (translucent ? backgroundColor! : AppLiquidGlass.navTint(cs))
: (backgroundColor ??
(frost ? AppFrost.navPillTint(cs) : cs.surfaceContainerHigh));
(frost ? AppFrost.glassTint(cs) : cs.surfaceContainerHigh));
final useGradient = glossy && gradient && !liquid;
final frosted = frost && !liquid && base.a < 1;
+1 -1
View File
@@ -254,7 +254,7 @@ class _StickerPanelState extends State<StickerPanel>
height: widget.height,
child: GlassSurface(
liquid: false,
frostTint: AppFrost.panelTint(cs),
frostTint: AppFrost.glassTint(cs),
border: Border(top: AppFrost.hairline(cs)),
child: SafeArea(
top: false,