ну немножечко так оптимизировал чутка, добавил раздел 'производительность' в насторйки, немного сделал чуточку в кастомизации, пару мелочей переделал
This commit is contained in:
@@ -29,6 +29,7 @@ class CachedChat {
|
|||||||
final int? lastMsgId;
|
final int? lastMsgId;
|
||||||
final int? lastMsgTime;
|
final int? lastMsgTime;
|
||||||
final String? lastMsgText;
|
final String? lastMsgText;
|
||||||
|
final String? lastMsgTextOneLine;
|
||||||
final int? lastMsgSenderId;
|
final int? lastMsgSenderId;
|
||||||
final int unreadCount;
|
final int unreadCount;
|
||||||
final int lastEventTime;
|
final int lastEventTime;
|
||||||
@@ -40,7 +41,7 @@ class CachedChat {
|
|||||||
final Map<int, int> participants;
|
final Map<int, int> participants;
|
||||||
final Set<String> options;
|
final Set<String> options;
|
||||||
|
|
||||||
const CachedChat({
|
CachedChat({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.accountId,
|
required this.accountId,
|
||||||
required this.type,
|
required this.type,
|
||||||
@@ -59,7 +60,9 @@ class CachedChat {
|
|||||||
required this.seenTime,
|
required this.seenTime,
|
||||||
required this.participants,
|
required this.participants,
|
||||||
this.options = const {},
|
this.options = const {},
|
||||||
});
|
}) : lastMsgTextOneLine = lastMsgText != null && lastMsgText.contains('\n')
|
||||||
|
? lastMsgText.replaceAll('\n', ' ')
|
||||||
|
: lastMsgText;
|
||||||
|
|
||||||
bool get isOfficial => options.contains('OFFICIAL');
|
bool get isOfficial => options.contains('OFFICIAL');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class AppAccent {
|
||||||
|
static const prefKey = 'app_accent_seed';
|
||||||
|
|
||||||
|
static const List<({String label, Color? seed})> presets = [
|
||||||
|
(label: 'Системный', seed: null),
|
||||||
|
(label: 'Сиреневый', seed: Color(0xFFC1C4FF)),
|
||||||
|
(label: 'Синий', seed: Color(0xFF4F8EFF)),
|
||||||
|
(label: 'Бирюзовый', seed: Color(0xFF00BFA5)),
|
||||||
|
(label: 'Зелёный', seed: Color(0xFF43A047)),
|
||||||
|
(label: 'Янтарный', seed: Color(0xFFFFB300)),
|
||||||
|
(label: 'Розовый', seed: Color(0xFFE91E63)),
|
||||||
|
(label: 'Красный', seed: Color(0xFFE53935)),
|
||||||
|
(label: 'Фиолетовый', seed: Color(0xFF7E57C2)),
|
||||||
|
];
|
||||||
|
|
||||||
|
static Future<Color?> load() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final val = prefs.getInt(prefKey);
|
||||||
|
if (val == null) return null;
|
||||||
|
return Color(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> save(Color? color) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (color == null) {
|
||||||
|
await prefs.remove(prefKey);
|
||||||
|
} else {
|
||||||
|
await prefs.setInt(prefKey, color.toARGB32());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
enum BubbleStyle { mobile, desktop }
|
||||||
|
|
||||||
|
class AppBubbleShape {
|
||||||
|
static const prefKey = 'app_bubble_shape';
|
||||||
|
static final ValueNotifier<BubbleStyle> current = ValueNotifier(
|
||||||
|
BubbleStyle.mobile,
|
||||||
|
);
|
||||||
|
|
||||||
|
static Future<BubbleStyle> load() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final val = prefs.getString(prefKey);
|
||||||
|
return _parse(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> save(BubbleStyle style) async {
|
||||||
|
current.value = style;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(prefKey, style.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BubbleStyle _parse(String? val) {
|
||||||
|
if (val == BubbleStyle.desktop.name) return BubbleStyle.desktop;
|
||||||
|
return BubbleStyle.mobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String label(BubbleStyle style) {
|
||||||
|
switch (style) {
|
||||||
|
case BubbleStyle.mobile:
|
||||||
|
return 'TG Mobile';
|
||||||
|
case BubbleStyle.desktop:
|
||||||
|
return 'TG Desktop';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class AppCacheExtent {
|
||||||
|
static const prefKey = 'app_cache_extent';
|
||||||
|
static const double defaultValue = 5000;
|
||||||
|
static const double min = 1000;
|
||||||
|
static const double max = 10000;
|
||||||
|
static const double lowWarnThreshold = 2500;
|
||||||
|
static const double highWarnThreshold = 7000;
|
||||||
|
|
||||||
|
static final ValueNotifier<double> current = ValueNotifier(defaultValue);
|
||||||
|
|
||||||
|
static Future<double> load() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final raw = prefs.getDouble(prefKey);
|
||||||
|
if (raw == null) return defaultValue;
|
||||||
|
return clamp(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> save(double value) async {
|
||||||
|
final clamped = clamp(value);
|
||||||
|
current.value = clamped;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setDouble(prefKey, clamped);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double clamp(double v) {
|
||||||
|
if (v < min) return min;
|
||||||
|
if (v > max) return max;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,6 +170,8 @@ class _CallsTabState extends State<CallsTab> {
|
|||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: call.avatarUrl!,
|
imageUrl: call.avatarUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
memCacheWidth: 144,
|
||||||
|
memCacheHeight: 144,
|
||||||
fadeInDuration: const Duration(milliseconds: 120),
|
fadeInDuration: const Duration(milliseconds: 120),
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
_buildPlaceholderAvatar(cs, call.name),
|
_buildPlaceholderAvatar(cs, call.name),
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
|||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: widget.imageUrl,
|
imageUrl: widget.imageUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
memCacheWidth: 360,
|
||||||
|
memCacheHeight: 360,
|
||||||
errorWidget: (context, error, stack) => _avatarLetters(cs),
|
errorWidget: (context, error, stack) => _avatarLetters(cs),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -394,7 +396,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: const Color(0xFF007AFF), size: 22),
|
Icon(icon, color: cs.primary, size: 22),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
@@ -806,7 +808,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
|||||||
(avatar != null && avatar.isNotEmpty)
|
(avatar != null && avatar.isNotEmpty)
|
||||||
? CircleAvatar(
|
? CircleAvatar(
|
||||||
radius: 22,
|
radius: 22,
|
||||||
backgroundImage: CachedNetworkImageProvider(avatar),
|
backgroundImage: CachedNetworkImageProvider(avatar, maxWidth: 144, maxHeight: 144),
|
||||||
backgroundColor: cs.primaryContainer,
|
backgroundColor: cs.primaryContainer,
|
||||||
)
|
)
|
||||||
: CircleAvatar(
|
: CircleAvatar(
|
||||||
|
|||||||
@@ -74,16 +74,24 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
double _navDragBaseLeft = 0;
|
double _navDragBaseLeft = 0;
|
||||||
double _revealAnimBegin = 0.0;
|
double _revealAnimBegin = 0.0;
|
||||||
double _closeAnimBegin = 0.0;
|
double _closeAnimBegin = 0.0;
|
||||||
double _pullRatio = 0.0;
|
|
||||||
static const double _kStoriesPullTriggerPx = 16.0;
|
static const double _kStoriesPullTriggerPx = 16.0;
|
||||||
|
|
||||||
|
final _StoriesUi _storiesUi = _StoriesUi();
|
||||||
|
double get _pullRatio => _storiesUi.pullRatio;
|
||||||
|
set _pullRatio(double v) => _storiesUi.pullRatio = v;
|
||||||
|
bool get _storiesDockedOpen => _storiesUi.dockedOpen;
|
||||||
|
set _storiesDockedOpen(bool v) => _storiesUi.dockedOpen = v;
|
||||||
|
bool get _storiesOverscrollRevealArmed => _storiesUi.overscrollRevealArmed;
|
||||||
|
set _storiesOverscrollRevealArmed(bool v) =>
|
||||||
|
_storiesUi.overscrollRevealArmed = v;
|
||||||
|
bool get _shouldCollapseSearch => _storiesUi.shouldCollapseSearch;
|
||||||
|
set _shouldCollapseSearch(bool v) => _storiesUi.shouldCollapseSearch = v;
|
||||||
|
|
||||||
bool _navDragging = false;
|
bool _navDragging = false;
|
||||||
bool _isFabOpen = false;
|
bool _isFabOpen = false;
|
||||||
bool _showCacheWarning = false;
|
bool _showCacheWarning = false;
|
||||||
bool _storiesAnimClosing = false;
|
bool _storiesAnimClosing = false;
|
||||||
bool _storiesDockedOpen = false;
|
Timer? _contactRebuildTimer;
|
||||||
bool _storiesOverscrollRevealArmed = true;
|
|
||||||
bool _shouldCollapseSearch = false;
|
|
||||||
bool get _isSelectionMode => _selectedChats.isNotEmpty;
|
bool get _isSelectionMode => _selectedChats.isNotEmpty;
|
||||||
bool? _foldersListKnown;
|
bool? _foldersListKnown;
|
||||||
|
|
||||||
@@ -370,11 +378,19 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
messagesModule.searchContactById(id).whenComplete(() {
|
messagesModule.searchContactById(id).whenComplete(() {
|
||||||
_inflightContactIds.remove(id);
|
_inflightContactIds.remove(id);
|
||||||
if (mounted) setState(() {});
|
_scheduleContactRebuild();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scheduleContactRebuild() {
|
||||||
|
if (!mounted) return;
|
||||||
|
_contactRebuildTimer?.cancel();
|
||||||
|
_contactRebuildTimer = Timer(const Duration(milliseconds: 120), () {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
List<CachedChat> _chatsForPageIndex(int pageIndex) {
|
List<CachedChat> _chatsForPageIndex(int pageIndex) {
|
||||||
List<CachedChat> base;
|
List<CachedChat> base;
|
||||||
if (_folders.isEmpty) {
|
if (_folders.isEmpty) {
|
||||||
@@ -436,9 +452,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
if (!c.hasClients) return;
|
if (!c.hasClients) return;
|
||||||
final double offset = c.offset;
|
final double offset = c.offset;
|
||||||
if (_isSelectionMode && !_shouldCollapseSearch && offset < 132) {
|
if (_isSelectionMode && !_shouldCollapseSearch && offset < 132) {
|
||||||
setState(() {
|
_shouldCollapseSearch = true;
|
||||||
_shouldCollapseSearch = true;
|
_storiesUi.notify();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
@@ -453,9 +468,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
_startStoriesAutoReveal(dragRatio);
|
_startStoriesAutoReveal(dragRatio);
|
||||||
} else if (!_storiesDockedOpen) {
|
} else if (!_storiesDockedOpen) {
|
||||||
if (dragRatio != _pullRatio) {
|
if (dragRatio != _pullRatio) {
|
||||||
setState(() {
|
_pullRatio = dragRatio;
|
||||||
_pullRatio = dragRatio;
|
_storiesUi.notify();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -470,14 +484,9 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final disarm = offset > 3 && _storiesOverscrollRevealArmed;
|
final disarm = offset > 3 && _storiesOverscrollRevealArmed;
|
||||||
final clearPull = _pullRatio > 0;
|
final clearPull = _pullRatio > 0;
|
||||||
if (disarm || clearPull) {
|
if (disarm || clearPull) {
|
||||||
setState(() {
|
if (disarm) _storiesOverscrollRevealArmed = false;
|
||||||
if (disarm) {
|
if (clearPull) _pullRatio = 0.0;
|
||||||
_storiesOverscrollRevealArmed = false;
|
_storiesUi.notify();
|
||||||
}
|
|
||||||
if (clearPull) {
|
|
||||||
_pullRatio = 0.0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,85 +527,81 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
animation: _shimmerController,
|
animation: _shimmerController,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final opacity = 0.3 + 0.3 * sin(_shimmerController.value * pi * 2);
|
final opacity = 0.3 + 0.3 * sin(_shimmerController.value * pi * 2);
|
||||||
return Opacity(
|
return Opacity(opacity: opacity, child: child);
|
||||||
opacity: opacity,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cs.surfaceContainerHighest,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 48,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 120,
|
|
||||||
height: 14,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cs.surfaceContainerHighest,
|
|
||||||
borderRadius: BorderRadius.circular(7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 12,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cs.surfaceContainerHighest,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 120,
|
||||||
|
height: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStoriesRevealTick() {
|
void _onStoriesRevealTick() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final t = Curves.easeOutCubic.transform(_storiesRevealController.value);
|
final t = Curves.easeOutCubic.transform(_storiesRevealController.value);
|
||||||
setState(() {
|
if (_storiesAnimClosing) {
|
||||||
if (_storiesAnimClosing) {
|
_pullRatio = _closeAnimBegin * (1.0 - t);
|
||||||
_pullRatio = _closeAnimBegin * (1.0 - t);
|
} else {
|
||||||
} else {
|
_pullRatio = _revealAnimBegin + (1.0 - _revealAnimBegin) * t;
|
||||||
_pullRatio = _revealAnimBegin + (1.0 - _revealAnimBegin) * t;
|
}
|
||||||
}
|
_storiesUi.notify();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStoriesRevealStatus(AnimationStatus status) {
|
void _onStoriesRevealStatus(AnimationStatus status) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (status == AnimationStatus.completed) {
|
if (status == AnimationStatus.completed) {
|
||||||
setState(() {
|
if (_storiesAnimClosing) {
|
||||||
if (_storiesAnimClosing) {
|
_pullRatio = 0.0;
|
||||||
_pullRatio = 0.0;
|
_storiesDockedOpen = false;
|
||||||
_storiesDockedOpen = false;
|
_storiesAnimClosing = false;
|
||||||
_storiesAnimClosing = false;
|
_storiesOverscrollRevealArmed = true;
|
||||||
_storiesOverscrollRevealArmed = true;
|
} else {
|
||||||
} else {
|
_pullRatio = 1.0;
|
||||||
_pullRatio = 1.0;
|
_storiesDockedOpen = true;
|
||||||
_storiesDockedOpen = true;
|
_storiesRevealLayoutSettleUntil = DateTime.now().add(
|
||||||
_storiesRevealLayoutSettleUntil = DateTime.now().add(
|
const Duration(milliseconds: 520),
|
||||||
const Duration(milliseconds: 520),
|
);
|
||||||
);
|
}
|
||||||
}
|
_storiesUi.notify();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,10 +612,9 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
_storiesAnimClosing = false;
|
_storiesAnimClosing = false;
|
||||||
final from = max(_pullRatio, suggestedFrom.clamp(0.0, 1.0));
|
final from = max(_pullRatio, suggestedFrom.clamp(0.0, 1.0));
|
||||||
if (from >= 1.0) {
|
if (from >= 1.0) {
|
||||||
setState(() {
|
_pullRatio = 1.0;
|
||||||
_pullRatio = 1.0;
|
_storiesDockedOpen = true;
|
||||||
_storiesDockedOpen = true;
|
_storiesUi.notify();
|
||||||
});
|
|
||||||
_storiesRevealLayoutSettleUntil = DateTime.now().add(
|
_storiesRevealLayoutSettleUntil = DateTime.now().add(
|
||||||
const Duration(milliseconds: 520),
|
const Duration(milliseconds: 520),
|
||||||
);
|
);
|
||||||
@@ -638,12 +642,11 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
_storiesAnimClosing = true;
|
_storiesAnimClosing = true;
|
||||||
final from = _pullRatio.clamp(0.0, 1.0);
|
final from = _pullRatio.clamp(0.0, 1.0);
|
||||||
if (from <= 0) {
|
if (from <= 0) {
|
||||||
setState(() {
|
_pullRatio = 0.0;
|
||||||
_pullRatio = 0.0;
|
_storiesDockedOpen = false;
|
||||||
_storiesDockedOpen = false;
|
_storiesAnimClosing = false;
|
||||||
_storiesAnimClosing = false;
|
_storiesOverscrollRevealArmed = true;
|
||||||
_storiesOverscrollRevealArmed = true;
|
_storiesUi.notify();
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_closeAnimBegin = from;
|
_closeAnimBegin = from;
|
||||||
@@ -659,9 +662,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
|
|
||||||
if (n is ScrollEndNotification) {
|
if (n is ScrollEndNotification) {
|
||||||
if (n.metrics.pixels <= 0.5) {
|
if (n.metrics.pixels <= 0.5) {
|
||||||
setState(() {
|
_storiesOverscrollRevealArmed = true;
|
||||||
_storiesOverscrollRevealArmed = true;
|
_storiesUi.notify();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -714,6 +716,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
c.removeListener(fn);
|
c.removeListener(fn);
|
||||||
c.dispose();
|
c.dispose();
|
||||||
}
|
}
|
||||||
|
_contactRebuildTimer?.cancel();
|
||||||
|
_storiesUi.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,15 +778,17 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
ClipRect(
|
ListenableBuilder(
|
||||||
clipBehavior: Clip.hardEdge,
|
listenable: _storiesUi,
|
||||||
child: AnimatedSize(
|
builder: (context, _) => ClipRect(
|
||||||
duration: const Duration(milliseconds: 200),
|
clipBehavior: Clip.hardEdge,
|
||||||
curve: Curves.easeOutCubic,
|
child: AnimatedSize(
|
||||||
alignment: Alignment.topCenter,
|
duration: const Duration(milliseconds: 200),
|
||||||
child: _shouldCollapseSearch
|
curve: Curves.easeOutCubic,
|
||||||
? const SizedBox(width: double.infinity, height: 52)
|
alignment: Alignment.topCenter,
|
||||||
: Column(
|
child: _shouldCollapseSearch
|
||||||
|
? const SizedBox(width: double.infinity, height: 52)
|
||||||
|
: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@@ -933,7 +939,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 3, 20, 14),
|
padding: const EdgeInsets.fromLTRB(20, 3, 20, 4),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -974,13 +980,14 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_folders.length > 1)
|
if (_folders.length > 1)
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
height: 48,
|
height: 34,
|
||||||
color: cs.surface,
|
color: cs.surface,
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(
|
behavior: ScrollConfiguration.of(context).copyWith(
|
||||||
@@ -1007,7 +1014,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 20,
|
horizontal: 20,
|
||||||
vertical: 4,
|
vertical: 2,
|
||||||
),
|
),
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
@@ -1024,7 +1031,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 20,
|
horizontal: 20,
|
||||||
vertical: 4,
|
vertical: 2,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -1054,6 +1061,13 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final chats = _chatsForPageIndex(pageIndex);
|
final chats = _chatsForPageIndex(pageIndex);
|
||||||
final sc = _folderChatScrollControllers[pageIndex];
|
final sc = _folderChatScrollControllers[pageIndex];
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final pinnedCount = _isInitialLoading
|
||||||
|
? 0
|
||||||
|
: chats.where((c) => (c.favIndex ?? 0) > 0).length;
|
||||||
|
final hasSeparator = pinnedCount > 0 && pinnedCount < chats.length;
|
||||||
|
final totalItems = _isInitialLoading
|
||||||
|
? 10
|
||||||
|
: chats.length + (hasSeparator ? 1 : 0);
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
onNotification: (ScrollNotification n) {
|
onNotification: (ScrollNotification n) {
|
||||||
if (_currentNavIndex != 0) return false;
|
if (_currentNavIndex != 0) return false;
|
||||||
@@ -1098,10 +1112,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
return _buildChatShimmer();
|
return _buildChatShimmer();
|
||||||
}
|
}
|
||||||
|
|
||||||
final pinnedCount = chats.where((c) => (c.favIndex ?? 0) > 0).length;
|
|
||||||
final hasSeparator = pinnedCount > 0 && pinnedCount < chats.length;
|
|
||||||
|
|
||||||
// Insert separator row between pinned and regular sections
|
|
||||||
if (hasSeparator && index == pinnedCount) {
|
if (hasSeparator && index == pinnedCount) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
@@ -1131,7 +1141,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
return _buildChatItem(
|
return _buildChatItem(
|
||||||
chat.id.toString(),
|
chat.id.toString(),
|
||||||
name ?? "Пользователь",
|
name ?? "Пользователь",
|
||||||
chat.lastMsgText?.replaceAll('\n', ' ') ?? '',
|
chat.lastMsgTextOneLine ?? '',
|
||||||
_formatTime(chat.lastMsgTime),
|
_formatTime(chat.lastMsgTime),
|
||||||
avatar ?? "",
|
avatar ?? "",
|
||||||
isOnline: chat.isOnline,
|
isOnline: chat.isOnline,
|
||||||
@@ -1172,7 +1182,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
chatType: chat.type,
|
chatType: chat.type,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, childCount: _isInitialLoading ? 10 : chats.length + (chats.any((c) => (c.favIndex ?? 0) > 0) && chats.any((c) => (c.favIndex ?? 0) <= 0) ? 1 : 0)),
|
}, childCount: totalItems),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
@@ -1673,7 +1683,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
),
|
),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 26,
|
radius: 26,
|
||||||
backgroundImage: CachedNetworkImageProvider(imageUrl),
|
backgroundImage: CachedNetworkImageProvider(imageUrl, maxWidth: 144, maxHeight: 144),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
@@ -1742,33 +1752,36 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final isSelected = _selectedFolderId == folderId;
|
final isSelected = _selectedFolderId == folderId;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final i = _folders.indexWhere((f) => f.id == folderId);
|
final target = _folders.indexWhere((f) => f.id == folderId);
|
||||||
if (i < 0) return;
|
if (target < 0) return;
|
||||||
setState(() => _selectedFolderId = folderId);
|
setState(() => _selectedFolderId = folderId);
|
||||||
if (_folderPageController.hasClients) {
|
if (_folderPageController.hasClients) {
|
||||||
final cur = _folderPageController.page?.round();
|
final cur = _folderPageController.page?.round() ?? 0;
|
||||||
if (cur != i) {
|
if (cur == target) return;
|
||||||
_folderPageController.animateToPage(
|
if ((target - cur).abs() > 1) {
|
||||||
i,
|
final neighbor = target > cur ? target - 1 : target + 1;
|
||||||
duration: const Duration(milliseconds: 320),
|
_folderPageController.jumpToPage(neighbor);
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
_folderPageController.animateToPage(
|
||||||
|
target,
|
||||||
|
duration: const Duration(milliseconds: 280),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? cs.primaryContainer : cs.surfaceContainerHigh,
|
color: isSelected ? cs.primaryContainer : cs.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isSelected ? cs.onPrimaryContainer : cs.primary,
|
color: isSelected ? cs.onPrimaryContainer : cs.primary,
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1829,7 +1842,7 @@ Navigator.push(
|
|||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: cs.surfaceContainerHighest,
|
backgroundColor: cs.surfaceContainerHighest,
|
||||||
backgroundImage: imageUrl.isNotEmpty
|
backgroundImage: imageUrl.isNotEmpty
|
||||||
? CachedNetworkImageProvider(imageUrl)
|
? CachedNetworkImageProvider(imageUrl, maxWidth: 144, maxHeight: 144)
|
||||||
: null,
|
: null,
|
||||||
child: imageUrl.isEmpty
|
child: imageUrl.isEmpty
|
||||||
? Text(
|
? Text(
|
||||||
@@ -2158,9 +2171,18 @@ Navigator.push(
|
|||||||
),
|
),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 12,
|
radius: 12,
|
||||||
backgroundImage: CachedNetworkImageProvider(imageUrl),
|
backgroundImage: CachedNetworkImageProvider(imageUrl, maxWidth: 144, maxHeight: 144),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StoriesUi extends ChangeNotifier {
|
||||||
|
double pullRatio = 0.0;
|
||||||
|
bool dockedOpen = false;
|
||||||
|
bool overscrollRevealArmed = true;
|
||||||
|
bool shouldCollapseSearch = false;
|
||||||
|
|
||||||
|
void notify() => notifyListeners();
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../../backend/api.dart';
|
|||||||
import '../../../backend/modules/messages.dart';
|
import '../../../backend/modules/messages.dart';
|
||||||
import '../../../core/storage/app_database.dart';
|
import '../../../core/storage/app_database.dart';
|
||||||
import '../../../core/utils/haptics.dart';
|
import '../../../core/utils/haptics.dart';
|
||||||
|
import '../../../core/config/app_cache_extent.dart';
|
||||||
import '../../../models/attachment.dart';
|
import '../../../models/attachment.dart';
|
||||||
import '../../widgets/message_bubble.dart';
|
import '../../widgets/message_bubble.dart';
|
||||||
import '../../widgets/attachment_panel.dart';
|
import '../../widgets/attachment_panel.dart';
|
||||||
@@ -49,18 +50,18 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
final TextEditingController _messageController = TextEditingController();
|
final TextEditingController _messageController = TextEditingController();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
final GlobalKey _listKey = GlobalKey();
|
final GlobalKey _listKey = GlobalKey();
|
||||||
bool _hasText = false;
|
final ValueNotifier<bool> _hasText = ValueNotifier(false);
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _showAttachmentPanel = false;
|
final ValueNotifier<bool> _showAttachmentPanel = ValueNotifier(false);
|
||||||
late AnimationController _shimmerController;
|
late AnimationController _shimmerController;
|
||||||
List<CachedMessage> _messages = [];
|
List<CachedMessage> _messages = [];
|
||||||
int _myId = 0;
|
int _myId = 0;
|
||||||
CachedChat? chat;
|
CachedChat? chat;
|
||||||
|
|
||||||
DateTime? _floatingDate;
|
final ValueNotifier<DateTime?> _floatingDate = ValueNotifier(null);
|
||||||
DateTime? _lastFloatingDate;
|
|
||||||
Timer? _floatingDateTimer;
|
Timer? _floatingDateTimer;
|
||||||
late final AnimationController _floatingDateAnimController;
|
late final AnimationController _floatingDateAnimController;
|
||||||
|
late final CurvedAnimation _floatingDateCurved;
|
||||||
final Map<int, GlobalKey> _separatorKeys = {};
|
final Map<int, GlobalKey> _separatorKeys = {};
|
||||||
double _lastScrollOffset = 0;
|
double _lastScrollOffset = 0;
|
||||||
String? _lastSentId;
|
String? _lastSentId;
|
||||||
@@ -79,6 +80,11 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
duration: const Duration(milliseconds: 220),
|
duration: const Duration(milliseconds: 220),
|
||||||
reverseDuration: const Duration(milliseconds: 380),
|
reverseDuration: const Duration(milliseconds: 380),
|
||||||
);
|
);
|
||||||
|
_floatingDateCurved = CurvedAnimation(
|
||||||
|
parent: _floatingDateAnimController,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
reverseCurve: Curves.easeIn,
|
||||||
|
);
|
||||||
|
|
||||||
_loadHistory();
|
_loadHistory();
|
||||||
}
|
}
|
||||||
@@ -139,7 +145,11 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_messageController.removeListener(_onTextChanged);
|
_messageController.removeListener(_onTextChanged);
|
||||||
_scrollController.removeListener(_onScrollForDate);
|
_scrollController.removeListener(_onScrollForDate);
|
||||||
_floatingDateTimer?.cancel();
|
_floatingDateTimer?.cancel();
|
||||||
|
_floatingDateCurved.dispose();
|
||||||
_floatingDateAnimController.dispose();
|
_floatingDateAnimController.dispose();
|
||||||
|
_floatingDate.dispose();
|
||||||
|
_hasText.dispose();
|
||||||
|
_showAttachmentPanel.dispose();
|
||||||
_messageController.dispose();
|
_messageController.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
_shimmerController.dispose();
|
_shimmerController.dispose();
|
||||||
@@ -147,11 +157,9 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTextChanged() {
|
void _onTextChanged() {
|
||||||
final bool newHasText = _messageController.text.trim().isNotEmpty;
|
final newHasText = _messageController.text.trim().isNotEmpty;
|
||||||
if (newHasText != _hasText) {
|
if (newHasText != _hasText.value) {
|
||||||
setState(() {
|
_hasText.value = newHasText;
|
||||||
_hasText = newHasText;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,11 +197,11 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
status: 'sending',
|
status: 'sending',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_hasText.value = false;
|
||||||
setState(() {
|
setState(() {
|
||||||
_lastSentId = tempId;
|
_lastSentId = tempId;
|
||||||
_messages.add(tempMessage);
|
_messages.add(tempMessage);
|
||||||
_messageController.clear();
|
_messageController.clear();
|
||||||
_hasText = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Instant tactile "whoosh" the moment the message leaves the composer,
|
// Instant tactile "whoosh" the moment the message leaves the composer,
|
||||||
@@ -386,12 +394,8 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
|
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
final bool dateChanged = result != _lastFloatingDate;
|
final bool dateChanged = result != _floatingDate.value;
|
||||||
_lastFloatingDate = result;
|
_floatingDate.value = result;
|
||||||
|
|
||||||
if (result != _floatingDate) {
|
|
||||||
setState(() => _floatingDate = result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateChanged) {
|
if (dateChanged) {
|
||||||
_floatingDateAnimController.forward(from: 0);
|
_floatingDateAnimController.forward(from: 0);
|
||||||
@@ -420,7 +424,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDateSeparatorWidget(BuildContext context, DateTime date,
|
Widget _buildDateSeparatorWidget(BuildContext context, DateTime date,
|
||||||
{Key? key}) {
|
{Key? key, bool floating = false}) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
key: key,
|
key: key,
|
||||||
@@ -429,7 +433,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cs.surfaceContainerHighest.withValues(alpha: 0.6),
|
color: cs.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -437,7 +441,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurfaceVariant,
|
color: cs.onSurfaceVariant,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: floating ? FontStyle.normal : FontStyle.italic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -482,7 +486,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
if (widget.imageUrl.isNotEmpty)
|
if (widget.imageUrl.isNotEmpty)
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 18,
|
radius: 18,
|
||||||
backgroundImage: CachedNetworkImageProvider(widget.imageUrl),
|
backgroundImage: CachedNetworkImageProvider(widget.imageUrl, maxWidth: 144, maxHeight: 144),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
@@ -563,16 +567,21 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_buildInputArea(context),
|
_buildInputArea(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_showAttachmentPanel)
|
ValueListenableBuilder<bool>(
|
||||||
Positioned(
|
valueListenable: _showAttachmentPanel,
|
||||||
left: 0,
|
builder: (context, open, _) {
|
||||||
right: 0,
|
if (!open) return const SizedBox.shrink();
|
||||||
bottom: 0,
|
return Positioned(
|
||||||
child: AttachmentPanel(
|
left: 0,
|
||||||
chatId: widget.chatId,
|
right: 0,
|
||||||
onClose: () => setState(() => _showAttachmentPanel = false),
|
bottom: 0,
|
||||||
),
|
child: AttachmentPanel(
|
||||||
),
|
chatId: widget.chatId,
|
||||||
|
onClose: () => _showAttachmentPanel.value = false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -595,11 +604,13 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
return Stack(
|
return Stack(
|
||||||
key: _listKey,
|
key: _listKey,
|
||||||
children: [
|
children: [
|
||||||
ListView.builder(
|
ValueListenableBuilder<double>(
|
||||||
|
valueListenable: AppCacheExtent.current,
|
||||||
|
builder: (context, cacheExtent, _) => ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
cacheExtent: 9999,
|
cacheExtent: cacheExtent,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = items[items.length - 1 - index];
|
final item = items[items.length - 1 - index];
|
||||||
@@ -641,32 +652,34 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
return bubble;
|
return bubble;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_lastFloatingDate != null)
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: AnimatedBuilder(
|
child: ValueListenableBuilder<DateTime?>(
|
||||||
animation: _floatingDateAnimController,
|
valueListenable: _floatingDate,
|
||||||
builder: (context, child) {
|
builder: (context, date, _) {
|
||||||
final t = CurvedAnimation(
|
if (date == null) return const SizedBox.shrink();
|
||||||
parent: _floatingDateAnimController,
|
return AnimatedBuilder(
|
||||||
curve: Curves.easeOut,
|
animation: _floatingDateCurved,
|
||||||
reverseCurve: Curves.easeIn,
|
builder: (context, child) {
|
||||||
).value;
|
final t = _floatingDateCurved.value;
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: t,
|
opacity: t,
|
||||||
child: Transform.scale(
|
child: Transform.scale(
|
||||||
scale: 0.82 + 0.18 * t,
|
scale: 0.82 + 0.18 * t,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: _buildDateSeparatorWidget(context, _lastFloatingDate!),
|
child: _buildDateSeparatorWidget(context, date, floating: true),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -842,7 +855,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
if (event is KeyDownEvent &&
|
if (event is KeyDownEvent &&
|
||||||
event.logicalKey == LogicalKeyboardKey.enter &&
|
event.logicalKey == LogicalKeyboardKey.enter &&
|
||||||
!HardwareKeyboard.instance.isShiftPressed) {
|
!HardwareKeyboard.instance.isShiftPressed) {
|
||||||
if (_hasText) _sendMessage();
|
if (_hasText.value) _sendMessage();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
@@ -868,64 +881,35 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AnimatedContainer(
|
_AttachButton(
|
||||||
duration: const Duration(milliseconds: 200),
|
hasText: _hasText,
|
||||||
width: _hasText ? 0 : 36,
|
panelOpen: _showAttachmentPanel,
|
||||||
child: AnimatedOpacity(
|
mutedIcon: mutedIcon,
|
||||||
duration: const Duration(milliseconds: 200),
|
cs: cs,
|
||||||
opacity: _hasText ? 0 : 1,
|
|
||||||
child: _hasText
|
|
||||||
? const SizedBox.shrink()
|
|
||||||
: GestureDetector(
|
|
||||||
onTap: _showAttachmentPanel ? null : () => setState(() => _showAttachmentPanel = true),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 12),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
if (_showAttachmentPanel)
|
|
||||||
SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: cs.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Symbols.attachment,
|
|
||||||
color: _showAttachmentPanel
|
|
||||||
? cs.onSurfaceVariant.withValues(alpha: 0.3)
|
|
||||||
: mutedIcon,
|
|
||||||
size: 24,
|
|
||||||
weight: 400,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
ValueListenableBuilder<bool>(
|
||||||
width: 54,
|
valueListenable: _hasText,
|
||||||
height: 54,
|
builder: (context, hasText, _) => Container(
|
||||||
alignment: Alignment.center,
|
width: 54,
|
||||||
decoration: BoxDecoration(
|
height: 54,
|
||||||
color: _hasText ? cs.primary : cs.surfaceContainerHighest,
|
alignment: Alignment.center,
|
||||||
shape: BoxShape.circle,
|
decoration: BoxDecoration(
|
||||||
),
|
color: hasText ? cs.primary : cs.surfaceContainerHighest,
|
||||||
child: GestureDetector(
|
shape: BoxShape.circle,
|
||||||
onTap: _hasText ? _sendMessage : null,
|
),
|
||||||
child: Icon(
|
child: GestureDetector(
|
||||||
_hasText ? Symbols.send : Symbols.mic,
|
onTap: hasText ? _sendMessage : null,
|
||||||
color: _hasText ? cs.onPrimary : cs.onSurface,
|
child: Icon(
|
||||||
size: 24,
|
hasText ? Symbols.send : Symbols.mic,
|
||||||
weight: 400,
|
color: hasText ? cs.onPrimary : cs.onSurface,
|
||||||
|
size: 24,
|
||||||
|
weight: 400,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -936,6 +920,70 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AttachButton extends StatelessWidget {
|
||||||
|
final ValueNotifier<bool> hasText;
|
||||||
|
final ValueNotifier<bool> panelOpen;
|
||||||
|
final Color mutedIcon;
|
||||||
|
final ColorScheme cs;
|
||||||
|
|
||||||
|
const _AttachButton({
|
||||||
|
required this.hasText,
|
||||||
|
required this.panelOpen,
|
||||||
|
required this.mutedIcon,
|
||||||
|
required this.cs,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: hasText,
|
||||||
|
builder: (context, isText, _) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
width: isText ? 0 : 36,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
opacity: isText ? 0 : 1,
|
||||||
|
child: isText
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: panelOpen,
|
||||||
|
builder: (context, open, _) => GestureDetector(
|
||||||
|
onTap: open ? null : () => panelOpen.value = true,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
if (open)
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: cs.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Symbols.attachment,
|
||||||
|
color: open
|
||||||
|
? cs.onSurfaceVariant.withValues(alpha: 0.3)
|
||||||
|
: mutedIcon,
|
||||||
|
size: 24,
|
||||||
|
weight: 400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SentMessageAnimation extends StatefulWidget {
|
class _SentMessageAnimation extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final VoidCallback onComplete;
|
final VoidCallback onComplete;
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: contact.baseUrl!,
|
imageUrl: contact.baseUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
memCacheWidth: 144,
|
||||||
|
memCacheHeight: 144,
|
||||||
fadeInDuration: const Duration(milliseconds: 120),
|
fadeInDuration: const Duration(milliseconds: 120),
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
_buildPlaceholderAvatar(cs, nameToDisplay),
|
_buildPlaceholderAvatar(cs, nameToDisplay),
|
||||||
|
|||||||
@@ -0,0 +1,526 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:m3e_collection/m3e_collection.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
import '../../../core/config/app_bubble_shape.dart';
|
||||||
|
import '../../../core/utils/haptics.dart';
|
||||||
|
import '../../../main.dart';
|
||||||
|
|
||||||
|
class AppearanceScreen extends StatefulWidget {
|
||||||
|
const AppearanceScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppearanceScreen> createState() => _AppearanceScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppearanceScreenState extends State<AppearanceScreen> {
|
||||||
|
static const _fallback = Color(0xFFC1C4FF);
|
||||||
|
|
||||||
|
final ValueNotifier<Color> _color = ValueNotifier(_fallback);
|
||||||
|
final ValueNotifier<bool> _isSystem = ValueNotifier(false);
|
||||||
|
bool _initialized = false;
|
||||||
|
bool _accentExpanded = false;
|
||||||
|
Timer? _debounce;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (!_initialized) {
|
||||||
|
_initialized = true;
|
||||||
|
final seed = KometApp.stateOf(context)?.accentSeed.value;
|
||||||
|
_isSystem.value = seed == null;
|
||||||
|
_color.value = seed ?? _fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_debounce?.cancel();
|
||||||
|
_color.dispose();
|
||||||
|
_isSystem.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onColorChanged(Color color) {
|
||||||
|
_color.value = color;
|
||||||
|
_isSystem.value = false;
|
||||||
|
_debounce?.cancel();
|
||||||
|
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||||
|
if (mounted) KometApp.stateOf(context)?.applyAccentColor(color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetToSystem() {
|
||||||
|
Haptics.selection();
|
||||||
|
_debounce?.cancel();
|
||||||
|
_isSystem.value = true;
|
||||||
|
_color.value = _fallback;
|
||||||
|
KometApp.stateOf(context)?.applyAccentColor(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleAccentExpanded() {
|
||||||
|
Haptics.tap();
|
||||||
|
setState(() => _accentExpanded = !_accentExpanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStyleChanged(BubbleStyle style) {
|
||||||
|
Haptics.selection();
|
||||||
|
AppBubbleShape.save(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
appBar: AppBarM3E(
|
||||||
|
titleText: 'Внешний вид',
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: ListView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
|
||||||
|
children: [
|
||||||
|
_PreviewSection(color: _color, isSystem: _isSystem),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ColorPickerCard(
|
||||||
|
color: _color,
|
||||||
|
isSystem: _isSystem,
|
||||||
|
expanded: _accentExpanded,
|
||||||
|
onToggle: _toggleAccentExpanded,
|
||||||
|
onColorChanged: _onColorChanged,
|
||||||
|
onReset: _resetToSystem,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_BubbleShapeCard(onChanged: _onStyleChanged),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreviewSection extends StatefulWidget {
|
||||||
|
final ValueNotifier<Color> color;
|
||||||
|
final ValueNotifier<bool> isSystem;
|
||||||
|
|
||||||
|
const _PreviewSection({required this.color, required this.isSystem});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PreviewSection> createState() => _PreviewSectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreviewSectionState extends State<_PreviewSection> {
|
||||||
|
ColorScheme? _cachedScheme;
|
||||||
|
Color? _cachedColor;
|
||||||
|
Brightness? _cachedBrightness;
|
||||||
|
|
||||||
|
ColorScheme _schemeFor(Color color, Brightness brightness) {
|
||||||
|
if (_cachedScheme != null &&
|
||||||
|
_cachedColor == color &&
|
||||||
|
_cachedBrightness == brightness) {
|
||||||
|
return _cachedScheme!;
|
||||||
|
}
|
||||||
|
_cachedColor = color;
|
||||||
|
_cachedBrightness = brightness;
|
||||||
|
_cachedScheme = ColorScheme.fromSeed(
|
||||||
|
seedColor: color,
|
||||||
|
brightness: brightness,
|
||||||
|
);
|
||||||
|
return _cachedScheme!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final outerCs = Theme.of(context).colorScheme;
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
|
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: widget.isSystem,
|
||||||
|
builder: (context, isSystem, _) {
|
||||||
|
if (isSystem) {
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(colorScheme: outerCs),
|
||||||
|
child: const _ChatPreview(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ValueListenableBuilder<Color>(
|
||||||
|
valueListenable: widget.color,
|
||||||
|
builder: (context, color, _) {
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
colorScheme: _schemeFor(color, brightness),
|
||||||
|
),
|
||||||
|
child: const _ChatPreview(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatPreview extends StatelessWidget {
|
||||||
|
const _ChatPreview();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return ValueListenableBuilder<BubbleStyle>(
|
||||||
|
valueListenable: AppBubbleShape.current,
|
||||||
|
builder: (context, style, _) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_PreviewBubble(text: 'Как тебе?', isMe: true, style: style),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_PreviewBubble(text: 'отлично выглядит!', isMe: false, style: style),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreviewBubble extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final bool isMe;
|
||||||
|
final BubbleStyle style;
|
||||||
|
|
||||||
|
const _PreviewBubble({
|
||||||
|
required this.text,
|
||||||
|
required this.isMe,
|
||||||
|
required this.style,
|
||||||
|
});
|
||||||
|
|
||||||
|
BorderRadius get _radius {
|
||||||
|
const big = Radius.circular(20);
|
||||||
|
const small = Radius.circular(4);
|
||||||
|
final outside = style == BubbleStyle.mobile ? big : small;
|
||||||
|
return BorderRadius.only(
|
||||||
|
topLeft: isMe ? outside : big,
|
||||||
|
topRight: isMe ? big : outside,
|
||||||
|
bottomLeft: isMe ? outside : big,
|
||||||
|
bottomRight: isMe ? big : outside,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final bg = isMe ? cs.primaryContainer : cs.surfaceContainerHighest;
|
||||||
|
final fg = Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black;
|
||||||
|
|
||||||
|
return Align(
|
||||||
|
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 220),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(color: bg, borderRadius: _radius),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(color: fg, fontSize: 15, height: 1.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorPickerCard extends StatelessWidget {
|
||||||
|
final ValueNotifier<Color> color;
|
||||||
|
final ValueNotifier<bool> isSystem;
|
||||||
|
final bool expanded;
|
||||||
|
final VoidCallback onToggle;
|
||||||
|
final ValueChanged<Color> onColorChanged;
|
||||||
|
final VoidCallback onReset;
|
||||||
|
|
||||||
|
const _ColorPickerCard({
|
||||||
|
required this.color,
|
||||||
|
required this.isSystem,
|
||||||
|
required this.expanded,
|
||||||
|
required this.onToggle,
|
||||||
|
required this.onColorChanged,
|
||||||
|
required this.onReset,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: isSystem,
|
||||||
|
builder: (context, sys, _) {
|
||||||
|
return ValueListenableBuilder<Color>(
|
||||||
|
valueListenable: color,
|
||||||
|
builder: (context, col, _) => _buildBody(cs, col, sys),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody(ColorScheme cs, Color col, bool sys) {
|
||||||
|
final swatchColor = sys ? cs.primary : col;
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: onToggle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 18, 16, 18),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: swatchColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: cs.outlineVariant.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Акцентный цвет',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
sys
|
||||||
|
? 'Системный'
|
||||||
|
: 'Основной цвет интерфейса и пузырей',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedRotation(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
turns: expanded ? 0.5 : 0,
|
||||||
|
child: Icon(
|
||||||
|
Symbols.expand_more,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 220),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: expanded
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_HueStripPicker(
|
||||||
|
color: col,
|
||||||
|
onChanged: onColorChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton.tonal(
|
||||||
|
onPressed: sys ? null : onReset,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.auto_awesome, size: 18, weight: 500),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(sys
|
||||||
|
? 'Системный цвет активен'
|
||||||
|
: 'Сбросить на системный'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(width: double.infinity),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BubbleShapeCard extends StatelessWidget {
|
||||||
|
final ValueChanged<BubbleStyle> onChanged;
|
||||||
|
|
||||||
|
const _BubbleShapeCard({required this.onChanged});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Форма сообщения',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Скругление углов пузырей',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ValueListenableBuilder<BubbleStyle>(
|
||||||
|
valueListenable: AppBubbleShape.current,
|
||||||
|
builder: (context, current, _) {
|
||||||
|
return SegmentedButton<BubbleStyle>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: BubbleStyle.mobile,
|
||||||
|
label: Text('TG Mobile'),
|
||||||
|
icon: Icon(Symbols.smartphone),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: BubbleStyle.desktop,
|
||||||
|
label: Text('TG Desktop'),
|
||||||
|
icon: Icon(Symbols.desktop_windows),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {current},
|
||||||
|
onSelectionChanged: (set) {
|
||||||
|
if (set.isNotEmpty) onChanged(set.first);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HueStripPicker extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
final ValueChanged<Color> onChanged;
|
||||||
|
|
||||||
|
const _HueStripPicker({required this.color, required this.onChanged});
|
||||||
|
|
||||||
|
static const _gradient = LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFF0000),
|
||||||
|
Color(0xFFFFFF00),
|
||||||
|
Color(0xFF00FF00),
|
||||||
|
Color(0xFF00FFFF),
|
||||||
|
Color(0xFF0000FF),
|
||||||
|
Color(0xFFFF00FF),
|
||||||
|
Color(0xFFFF0000),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hue = HSVColor.fromColor(color).hue;
|
||||||
|
const trackHeight = 26.0;
|
||||||
|
const thumbDiameter = 30.0;
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final width = constraints.maxWidth;
|
||||||
|
void emit(double dx) {
|
||||||
|
final clamped = dx.clamp(0.0, width);
|
||||||
|
final newHue = (clamped / width) * 360;
|
||||||
|
onChanged(HSVColor.fromAHSV(1, newHue, 1, 1).toColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
final thumbLeft = (hue / 360) * width - thumbDiameter / 2;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onPanDown: (d) => emit(d.localPosition.dx),
|
||||||
|
onPanUpdate: (d) => emit(d.localPosition.dx),
|
||||||
|
child: SizedBox(
|
||||||
|
height: thumbDiameter + 4,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
height: trackHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(trackHeight / 2),
|
||||||
|
gradient: _gradient,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: thumbLeft.clamp(0, width - thumbDiameter),
|
||||||
|
top: (thumbDiameter + 4 - thumbDiameter) / 2,
|
||||||
|
child: Container(
|
||||||
|
width: thumbDiameter,
|
||||||
|
height: thumbDiameter,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: HSVColor.fromAHSV(1, hue, 1, 1).toColor(),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 3),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.18),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:m3e_collection/m3e_collection.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
import '../../../core/utils/haptics.dart';
|
import '../../../core/utils/haptics.dart';
|
||||||
|
import 'appearance_screen.dart';
|
||||||
import 'font_settings_screen.dart';
|
import 'font_settings_screen.dart';
|
||||||
|
|
||||||
class _CustomizationCategory {
|
class _CustomizationCategory {
|
||||||
@@ -23,6 +24,12 @@ class CustomizationScreen extends StatelessWidget {
|
|||||||
const CustomizationScreen({super.key});
|
const CustomizationScreen({super.key});
|
||||||
|
|
||||||
static const List<_CustomizationCategory> _categories = [
|
static const List<_CustomizationCategory> _categories = [
|
||||||
|
_CustomizationCategory(
|
||||||
|
icon: Symbols.palette,
|
||||||
|
title: 'Внешний вид',
|
||||||
|
subtitle: 'Акцентный цвет интерфейса',
|
||||||
|
builder: _buildAppearance,
|
||||||
|
),
|
||||||
_CustomizationCategory(
|
_CustomizationCategory(
|
||||||
icon: Symbols.text_fields,
|
icon: Symbols.text_fields,
|
||||||
title: 'Шрифты',
|
title: 'Шрифты',
|
||||||
@@ -31,6 +38,9 @@ class CustomizationScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static Widget _buildAppearance(BuildContext context) =>
|
||||||
|
const AppearanceScreen();
|
||||||
|
|
||||||
static Widget _buildFontSettings(BuildContext context) =>
|
static Widget _buildFontSettings(BuildContext context) =>
|
||||||
const FontSettingsScreen();
|
const FontSettingsScreen();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:m3e_collection/m3e_collection.dart';
|
||||||
|
|
||||||
|
import '../../../core/config/app_cache_extent.dart';
|
||||||
|
import '../../../core/utils/haptics.dart';
|
||||||
|
|
||||||
|
class PerformanceScreen extends StatefulWidget {
|
||||||
|
const PerformanceScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PerformanceScreen> createState() => _PerformanceScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PerformanceScreenState extends State<PerformanceScreen> {
|
||||||
|
late double _value;
|
||||||
|
late double _preZoneValue;
|
||||||
|
bool _lowWarnDismissed = false;
|
||||||
|
bool _highWarnDismissed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_value = AppCacheExtent.current.value;
|
||||||
|
_preZoneValue = _value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isInSafeZone(double v) =>
|
||||||
|
v >= AppCacheExtent.lowWarnThreshold && v < AppCacheExtent.highWarnThreshold;
|
||||||
|
|
||||||
|
void _onChanged(double v) {
|
||||||
|
setState(() {
|
||||||
|
_value = v;
|
||||||
|
if (_isInSafeZone(v)) _preZoneValue = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onChangeEnd(double v) async {
|
||||||
|
Haptics.selection();
|
||||||
|
final inLow = v < AppCacheExtent.lowWarnThreshold;
|
||||||
|
final inHigh = v >= AppCacheExtent.highWarnThreshold;
|
||||||
|
|
||||||
|
if (inLow && !_lowWarnDismissed) {
|
||||||
|
final ok = await _showWarning(
|
||||||
|
text:
|
||||||
|
'Производительность приложения может снизиться, вы уверены?',
|
||||||
|
);
|
||||||
|
if (ok) {
|
||||||
|
_lowWarnDismissed = true;
|
||||||
|
await AppCacheExtent.save(v);
|
||||||
|
} else {
|
||||||
|
setState(() => _value = _preZoneValue);
|
||||||
|
await AppCacheExtent.save(_preZoneValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inHigh && !_highWarnDismissed) {
|
||||||
|
final ok = await _showWarning(
|
||||||
|
text:
|
||||||
|
'Это врядли даст хотя-бы немного заметный прирост к FPS, '
|
||||||
|
'но может потреблять больше памяти. Вы уверены?',
|
||||||
|
);
|
||||||
|
if (ok) {
|
||||||
|
_highWarnDismissed = true;
|
||||||
|
await AppCacheExtent.save(v);
|
||||||
|
} else {
|
||||||
|
setState(() => _value = _preZoneValue);
|
||||||
|
await AppCacheExtent.save(_preZoneValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AppCacheExtent.save(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _showWarning({required String text}) async {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final res = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: cs.surfaceContainerHigh,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(color: cs.onSurface, fontSize: 15, height: 1.35),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text(
|
||||||
|
'Нет',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Да'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return res ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final hint = cs.onSurfaceVariant;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
appBar: AppBarM3E(
|
||||||
|
titleText: 'Производительность',
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: ListView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Кеш сообщений',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Сколько пикселей сообщений держать построенными за пределами видимой области.',
|
||||||
|
style: TextStyle(color: hint, fontSize: 13, height: 1.3),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
Text(
|
||||||
|
'Текущий cacheExtent: ${_value.round()}',
|
||||||
|
style: TextStyle(color: hint, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Slider(
|
||||||
|
value: _value,
|
||||||
|
min: AppCacheExtent.min,
|
||||||
|
max: AppCacheExtent.max,
|
||||||
|
onChanged: _onChanged,
|
||||||
|
onChangeEnd: _onChangeEnd,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Меньше потребление',
|
||||||
|
style: TextStyle(color: hint, fontSize: 11),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Больше FPS',
|
||||||
|
style: TextStyle(color: hint, fontSize: 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import '../../../l10n/app_localizations.dart';
|
|||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../auth/proxy_settings_sheet.dart';
|
import '../auth/proxy_settings_sheet.dart';
|
||||||
import 'customization_screen.dart';
|
import 'customization_screen.dart';
|
||||||
|
import 'performance_screen.dart';
|
||||||
import 'debug_menu_screen.dart';
|
import 'debug_menu_screen.dart';
|
||||||
import 'devices_screen.dart';
|
import 'devices_screen.dart';
|
||||||
import 'edit_profile_screen.dart';
|
import 'edit_profile_screen.dart';
|
||||||
@@ -162,6 +163,18 @@ child: _buildSection(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_SettingsItem(
|
||||||
|
icon: Symbols.speed,
|
||||||
|
label: 'Производительность',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const PerformanceScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -393,6 +406,8 @@ child: _buildSection(
|
|||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: _profile!.baseUrl!,
|
imageUrl: _profile!.baseUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
memCacheWidth: 240,
|
||||||
|
memCacheHeight: 240,
|
||||||
fadeInDuration: const Duration(milliseconds: 120),
|
fadeInDuration: const Duration(milliseconds: 120),
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
_buildPlaceholderAvatar(cs, name),
|
_buildPlaceholderAvatar(cs, name),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+97
-50
@@ -7,6 +7,9 @@ import 'package:m3e_collection/m3e_collection.dart';
|
|||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'backend/api.dart';
|
import 'backend/api.dart';
|
||||||
|
import 'core/config/app_accent.dart';
|
||||||
|
import 'core/config/app_bubble_shape.dart';
|
||||||
|
import 'core/config/app_cache_extent.dart';
|
||||||
import 'core/config/app_fonts.dart';
|
import 'core/config/app_fonts.dart';
|
||||||
import 'backend/modules/account.dart';
|
import 'backend/modules/account.dart';
|
||||||
import 'backend/modules/contacts.dart';
|
import 'backend/modules/contacts.dart';
|
||||||
@@ -65,6 +68,9 @@ void main() async {
|
|||||||
final initialFontScale = AppFonts.clampScale(
|
final initialFontScale = AppFonts.clampScale(
|
||||||
prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale,
|
prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale,
|
||||||
);
|
);
|
||||||
|
final initialAccentSeed = await AppAccent.load();
|
||||||
|
AppBubbleShape.current.value = await AppBubbleShape.load();
|
||||||
|
AppCacheExtent.current.value = await AppCacheExtent.load();
|
||||||
runApp(
|
runApp(
|
||||||
KometApp(
|
KometApp(
|
||||||
initialLocale: initialLocale,
|
initialLocale: initialLocale,
|
||||||
@@ -72,6 +78,7 @@ void main() async {
|
|||||||
initialVpnBypass: initialVpnBypass,
|
initialVpnBypass: initialVpnBypass,
|
||||||
initialFontId: initialFontId,
|
initialFontId: initialFontId,
|
||||||
initialFontScale: initialFontScale,
|
initialFontScale: initialFontScale,
|
||||||
|
initialAccentSeed: initialAccentSeed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -84,6 +91,7 @@ class KometApp extends StatefulWidget {
|
|||||||
this.initialVpnBypass = false,
|
this.initialVpnBypass = false,
|
||||||
required this.initialFontId,
|
required this.initialFontId,
|
||||||
required this.initialFontScale,
|
required this.initialFontScale,
|
||||||
|
this.initialAccentSeed,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Locale initialLocale;
|
final Locale initialLocale;
|
||||||
@@ -91,6 +99,7 @@ class KometApp extends StatefulWidget {
|
|||||||
final bool initialVpnBypass;
|
final bool initialVpnBypass;
|
||||||
final String initialFontId;
|
final String initialFontId;
|
||||||
final double initialFontScale;
|
final double initialFontScale;
|
||||||
|
final Color? initialAccentSeed;
|
||||||
static final navigatorKey = GlobalKey<NavigatorState>();
|
static final navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
static KometAppState? stateOf(BuildContext context) {
|
static KometAppState? stateOf(BuildContext context) {
|
||||||
@@ -107,6 +116,9 @@ class KometAppState extends State<KometApp> {
|
|||||||
late Locale _locale;
|
late Locale _locale;
|
||||||
late String _fontId;
|
late String _fontId;
|
||||||
bool _isLoggingOut = false;
|
bool _isLoggingOut = false;
|
||||||
|
late final ValueNotifier<Color?> accentSeed = ValueNotifier(
|
||||||
|
widget.initialAccentSeed,
|
||||||
|
);
|
||||||
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
||||||
StreamSubscription<LoginStatus>? _loginStatusSub;
|
StreamSubscription<LoginStatus>? _loginStatusSub;
|
||||||
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
||||||
@@ -205,6 +217,7 @@ class KometAppState extends State<KometApp> {
|
|||||||
fpsOverlayEnabled.dispose();
|
fpsOverlayEnabled.dispose();
|
||||||
vpnBypassEnabled.dispose();
|
vpnBypassEnabled.dispose();
|
||||||
fontScale.dispose();
|
fontScale.dispose();
|
||||||
|
accentSeed.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +250,11 @@ class KometAppState extends State<KometApp> {
|
|||||||
|
|
||||||
String get fontId => _fontId;
|
String get fontId => _fontId;
|
||||||
|
|
||||||
|
Future<void> applyAccentColor(Color? seed) async {
|
||||||
|
await AppAccent.save(seed);
|
||||||
|
accentSeed.value = seed;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> applyAppFont(String fontId) async {
|
Future<void> applyAppFont(String fontId) async {
|
||||||
if (_fontId == fontId) return;
|
if (_fontId == fontId) return;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -265,6 +283,28 @@ class KometAppState extends State<KometApp> {
|
|||||||
ThemeData? _lightTheme;
|
ThemeData? _lightTheme;
|
||||||
ThemeData? _darkTheme;
|
ThemeData? _darkTheme;
|
||||||
|
|
||||||
|
Color? _seedCacheKey;
|
||||||
|
ColorScheme? _seedCacheLight;
|
||||||
|
ColorScheme? _seedCacheDark;
|
||||||
|
|
||||||
|
({ColorScheme light, ColorScheme dark}) _schemesForSeed(Color seed) {
|
||||||
|
if (_seedCacheKey == seed &&
|
||||||
|
_seedCacheLight != null &&
|
||||||
|
_seedCacheDark != null) {
|
||||||
|
return (light: _seedCacheLight!, dark: _seedCacheDark!);
|
||||||
|
}
|
||||||
|
_seedCacheKey = seed;
|
||||||
|
_seedCacheLight = ColorScheme.fromSeed(
|
||||||
|
seedColor: seed,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
);
|
||||||
|
_seedCacheDark = ColorScheme.fromSeed(
|
||||||
|
seedColor: seed,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
|
return (light: _seedCacheLight!, dark: _seedCacheDark!);
|
||||||
|
}
|
||||||
|
|
||||||
void _rebuildThemesIfNeeded(ColorScheme light, ColorScheme dark) {
|
void _rebuildThemesIfNeeded(ColorScheme light, ColorScheme dark) {
|
||||||
if (_themeCacheFontId == _fontId &&
|
if (_themeCacheFontId == _fontId &&
|
||||||
_themeCacheLight == light &&
|
_themeCacheLight == light &&
|
||||||
@@ -334,65 +374,72 @@ class KometAppState extends State<KometApp> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||||
final lightBase =
|
return ValueListenableBuilder<Color?>(
|
||||||
lightDynamic ??
|
valueListenable: accentSeed,
|
||||||
ColorScheme.fromSeed(
|
builder: (context, seed, _) {
|
||||||
seedColor: _fallbackSeed,
|
final ColorScheme lightBase;
|
||||||
brightness: Brightness.light,
|
final ColorScheme darkBase;
|
||||||
);
|
if (seed != null) {
|
||||||
final darkBase =
|
final s = _schemesForSeed(seed);
|
||||||
darkDynamic ??
|
lightBase = s.light;
|
||||||
ColorScheme.fromSeed(
|
darkBase = s.dark;
|
||||||
seedColor: _fallbackSeed,
|
} else if (lightDynamic != null && darkDynamic != null) {
|
||||||
brightness: Brightness.dark,
|
lightBase = lightDynamic;
|
||||||
);
|
darkBase = darkDynamic;
|
||||||
|
} else {
|
||||||
|
final s = _schemesForSeed(_fallbackSeed);
|
||||||
|
lightBase = lightDynamic ?? s.light;
|
||||||
|
darkBase = darkDynamic ?? s.dark;
|
||||||
|
}
|
||||||
|
|
||||||
final lightScheme = _adjustLightScheme(lightBase);
|
final lightScheme = _adjustLightScheme(lightBase);
|
||||||
final darkScheme = _adjustDarkScheme(darkBase);
|
final darkScheme = _adjustDarkScheme(darkBase);
|
||||||
|
|
||||||
_rebuildThemesIfNeeded(lightScheme, darkScheme);
|
_rebuildThemesIfNeeded(lightScheme, darkScheme);
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Komet',
|
title: 'Komet',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
theme: _lightTheme,
|
theme: _lightTheme,
|
||||||
darkTheme: _darkTheme,
|
darkTheme: _darkTheme,
|
||||||
navigatorKey: KometApp.navigatorKey,
|
navigatorKey: KometApp.navigatorKey,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return ValueListenableBuilder<double>(
|
return ValueListenableBuilder<double>(
|
||||||
valueListenable: fontScale,
|
valueListenable: fontScale,
|
||||||
child: child ?? const SizedBox.shrink(),
|
child: child ?? const SizedBox.shrink(),
|
||||||
builder: (context, scale, appChild) {
|
builder: (context, scale, appChild) {
|
||||||
Widget scaledChild = appChild!;
|
Widget scaledChild = appChild!;
|
||||||
if ((scale - 1.0).abs() > 0.001) {
|
if ((scale - 1.0).abs() > 0.001) {
|
||||||
scaledChild = MediaQuery.withClampedTextScaling(
|
scaledChild = MediaQuery.withClampedTextScaling(
|
||||||
minScaleFactor: scale,
|
minScaleFactor: scale,
|
||||||
maxScaleFactor: scale,
|
maxScaleFactor: scale,
|
||||||
child: scaledChild,
|
child: scaledChild,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ValueListenableBuilder<bool>(
|
return ValueListenableBuilder<bool>(
|
||||||
valueListenable: fpsOverlayEnabled,
|
valueListenable: fpsOverlayEnabled,
|
||||||
child: scaledChild,
|
child: scaledChild,
|
||||||
builder: (context, fpsOn, sChild) {
|
builder: (context, fpsOn, sChild) {
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
sChild!,
|
sChild!,
|
||||||
if (fpsOn) const FpsOverlayLayer(),
|
if (fpsOn) const FpsOverlayLayer(),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
home: const _StartupScreen(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
home: const _StartupScreen(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-58
@@ -129,14 +129,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
cupertino_icons:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: cupertino_icons
|
|
||||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.9"
|
|
||||||
dart_lz4:
|
dart_lz4:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -355,54 +347,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.34"
|
version: "2.0.34"
|
||||||
flutter_secure_storage:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage
|
|
||||||
sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "10.0.0"
|
|
||||||
flutter_secure_storage_darwin:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_darwin
|
|
||||||
sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0"
|
|
||||||
flutter_secure_storage_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_linux
|
|
||||||
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.0"
|
|
||||||
flutter_secure_storage_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_platform_interface
|
|
||||||
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
flutter_secure_storage_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_web
|
|
||||||
sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
flutter_secure_storage_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_windows
|
|
||||||
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.0"
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -446,7 +390,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
@@ -454,7 +398,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.0"
|
version: "1.6.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ dependencies:
|
|||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
|
||||||
dart_lz4: ^1.0.0
|
dart_lz4: ^1.0.0
|
||||||
libcompress: ^1.0.0
|
libcompress: ^1.0.0
|
||||||
msgpack_dart: ^1.0.1
|
msgpack_dart: ^1.0.1
|
||||||
@@ -44,9 +43,6 @@ dependencies:
|
|||||||
device_info_plus: 12.3.0
|
device_info_plus: 12.3.0
|
||||||
flutter_timezone: ^5.0.1
|
flutter_timezone: ^5.0.1
|
||||||
timezone: ^0.11.0
|
timezone: ^0.11.0
|
||||||
flutter_secure_storage: ^10.0.0
|
|
||||||
http: ^1.4.0
|
|
||||||
http_parser: ^4.1.0
|
|
||||||
file_picker: ^8.0.0
|
file_picker: ^8.0.0
|
||||||
sqflite: ^2.4.2
|
sqflite: ^2.4.2
|
||||||
sqflite_common_ffi: ^2.4.0+2
|
sqflite_common_ffi: ^2.4.0+2
|
||||||
|
|||||||
Reference in New Issue
Block a user