починил вход, выход с ака, сделал вход сразу после кода, задержку перед появлением менюшки с эмодзи чтобы мозги не ебало

This commit is contained in:
jganenok
2025-12-04 07:54:30 +07:00
parent 9e07617293
commit 4729c16a13
2 changed files with 158 additions and 41 deletions

View File

@@ -28,6 +28,7 @@ import 'package:gwid/utils/user_id_lookup_screen.dart';
import 'package:gwid/screens/music_library_screen.dart'; import 'package:gwid/screens/music_library_screen.dart';
import 'package:gwid/widgets/message_preview_dialog.dart'; import 'package:gwid/widgets/message_preview_dialog.dart';
import 'package:gwid/services/chat_read_settings_service.dart'; import 'package:gwid/services/chat_read_settings_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gwid/services/local_profile_manager.dart'; import 'package:gwid/services/local_profile_manager.dart';
import 'package:gwid/widgets/contact_name_widget.dart'; import 'package:gwid/widgets/contact_name_widget.dart';
import 'package:gwid/widgets/contact_avatar_widget.dart'; import 'package:gwid/widgets/contact_avatar_widget.dart';
@@ -1849,6 +1850,25 @@ class _ChatsScreenState extends State<ChatsScreen>
print('🌐 URL веб-приложения: $webUrl'); print('🌐 URL веб-приложения: $webUrl');
if (!mounted) return;
// На десктопах WebView ведёт себя нестабильно (чёрный экран),
// поэтому открываем Сферум во внешнем браузере.
if (!Platform.isAndroid && !Platform.isIOS) {
final uri = Uri.tryParse(webUrl);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Не удалось открыть Сферум: $webUrl'),
backgroundColor: Colors.red,
),
);
}
return;
}
if (mounted) { if (mounted) {
_showSferumWebView(context, webUrl); _showSferumWebView(context, webUrl);
} }

View File

@@ -176,12 +176,7 @@ class _KometColoredSegment {
_KometColoredSegment(this.text, this.color); _KometColoredSegment(this.text, this.color);
} }
enum _KometSegmentType { enum _KometSegmentType { normal, colored, galaxy, pulse }
normal,
colored,
galaxy,
pulse,
}
class _KometSegment { class _KometSegment {
final String text; final String text;
@@ -232,19 +227,13 @@ class _GalaxyAnimatedTextState extends State<_GalaxyAnimatedText>
return LinearGradient( return LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [color, Color.lerp(Colors.white, Colors.black, t)!],
color,
Color.lerp(Colors.white, Colors.black, t)!,
],
).createShader(bounds); ).createShader(bounds);
}, },
blendMode: BlendMode.srcIn, blendMode: BlendMode.srcIn,
child: Text( child: Text(
widget.text, widget.text,
style: const TextStyle( style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
fontSize: 14,
fontWeight: FontWeight.w600,
),
), ),
); );
}, },
@@ -330,7 +319,10 @@ class _PulseAnimatedTextState extends State<_PulseAnimatedText>
return Text(text); return Text(text);
} }
final messageText = afterHash.substring(quoteIndex + 1, afterHash.length - 1); final messageText = afterHash.substring(
quoteIndex + 1,
afterHash.length - 1,
);
final baseColor = _pulseColor ?? Colors.red; final baseColor = _pulseColor ?? Colors.red;
return AnimatedBuilder( return AnimatedBuilder(
@@ -1754,13 +1746,14 @@ class ChatMessageBubble extends StatelessWidget {
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) { if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
if (isMobile) { if (isMobile) {
videoContent = GestureDetector( // На мобильных: короткий тап по видео запускает видео,
onTapDown: (TapDownDetails details) { // а панель появляется только при длинном удержании (~0.7 c).
_showMessageContextMenu(context, details.globalPosition); videoContent = _LongPressContextMenuWrapper(
},
child: videoContent, child: videoContent,
onShowMenu: (offset) => _showMessageContextMenu(context, offset),
); );
} else { } else {
// На десктопе оставляем контекстное меню по правому клику
videoContent = GestureDetector( videoContent = GestureDetector(
onSecondaryTapDown: (TapDownDetails details) { onSecondaryTapDown: (TapDownDetails details) {
_showMessageContextMenu(context, details.globalPosition); _showMessageContextMenu(context, details.globalPosition);
@@ -1849,15 +1842,14 @@ class ChatMessageBubble extends StatelessWidget {
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) { if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
if (isMobile) { if (isMobile) {
photoContent = GestureDetector( // На мобильных: короткий тап открывает фото, панель только по долгому тапу.
onTapDown: (TapDownDetails details) { photoContent = _LongPressContextMenuWrapper(
_showMessageContextMenu(context, details.globalPosition);
},
child: photoContent, child: photoContent,
onShowMenu: (offset) => _showMessageContextMenu(context, offset),
); );
} else { } else {
photoContent = GestureDetector( photoContent = GestureDetector(
onTapDown: (TapDownDetails details) { onSecondaryTapDown: (TapDownDetails details) {
_showMessageContextMenu(context, details.globalPosition); _showMessageContextMenu(context, details.globalPosition);
}, },
child: photoContent, child: photoContent,
@@ -1986,11 +1978,11 @@ class ChatMessageBubble extends StatelessWidget {
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) { if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
if (isMobile) { if (isMobile) {
videoContent = GestureDetector( // На мобильных: короткий тап по видео — воспроизведение,
onTapDown: (TapDownDetails details) { // панель реакций/действий — только по долгому тапу.
_showMessageContextMenu(context, details.globalPosition); videoContent = _LongPressContextMenuWrapper(
},
child: videoContent, child: videoContent,
onShowMenu: (offset) => _showMessageContextMenu(context, offset),
); );
} else { } else {
videoContent = GestureDetector( videoContent = GestureDetector(
@@ -4048,7 +4040,12 @@ class ChatMessageBubble extends StatelessWidget {
), ),
) )
else if (decryptedText != null) else if (decryptedText != null)
_buildMixedMessageContent(decryptedText!, defaultTextStyle, linkStyle, onOpenLink) _buildMixedMessageContent(
decryptedText!,
defaultTextStyle,
linkStyle,
onOpenLink,
)
else if (message.text.contains("welcome.saved.dialog.message")) else if (message.text.contains("welcome.saved.dialog.message"))
Linkify( Linkify(
text: text:
@@ -4061,7 +4058,12 @@ class ChatMessageBubble extends StatelessWidget {
) )
else if (message.text.contains("komet.cosmetic.") || else if (message.text.contains("komet.cosmetic.") ||
message.text.contains("komet.color_")) message.text.contains("komet.color_"))
_buildMixedMessageContent(message.text, defaultTextStyle, linkStyle, onOpenLink) _buildMixedMessageContent(
message.text,
defaultTextStyle,
linkStyle,
onOpenLink,
)
else else
Linkify( Linkify(
text: message.text, text: message.text,
@@ -4219,14 +4221,21 @@ class ChatMessageBubble extends StatelessWidget {
// Если маркер не найден, добавляем оставшийся текст как обычный // Если маркер не найден, добавляем оставшийся текст как обычный
if (markerType == null) { if (markerType == null) {
if (index < text.length) { if (index < text.length) {
segments.add(_KometSegment(text.substring(index), _KometSegmentType.normal)); segments.add(
_KometSegment(text.substring(index), _KometSegmentType.normal),
);
} }
break; break;
} }
// Добавляем текст до маркера как обычный // Добавляем текст до маркера как обычный
if (nextMarker > index) { if (nextMarker > index) {
segments.add(_KometSegment(text.substring(index, nextMarker), _KometSegmentType.normal)); segments.add(
_KometSegment(
text.substring(index, nextMarker),
_KometSegmentType.normal,
),
);
} }
// Обрабатываем найденный маркер // Обрабатываем найденный маркер
@@ -4241,13 +4250,24 @@ class ChatMessageBubble extends StatelessWidget {
if (secondQuote != -1) { if (secondQuote != -1) {
final segmentText = afterHash.substring(textStart, secondQuote); final segmentText = afterHash.substring(textStart, secondQuote);
final color = _parseKometHexColor(hexStr, null); final color = _parseKometHexColor(hexStr, null);
segments.add(_KometSegment(segmentText, _KometSegmentType.pulse, color: color)); segments.add(
index = nextMarker + prefix.length + secondQuote + 2; // +2 для двух кавычек _KometSegment(segmentText, _KometSegmentType.pulse, color: color),
);
index =
nextMarker +
prefix.length +
secondQuote +
2; // +2 для двух кавычек
continue; continue;
} }
} }
// Если парсинг не удался, добавляем как обычный текст // Если парсинг не удался, добавляем как обычный текст
segments.add(_KometSegment(text.substring(nextMarker, nextMarker + prefix.length + 10), _KometSegmentType.normal)); segments.add(
_KometSegment(
text.substring(nextMarker, nextMarker + prefix.length + 10),
_KometSegmentType.normal,
),
);
index = nextMarker + prefix.length + 10; index = nextMarker + prefix.length + 10;
} else if (markerType == "galaxy") { } else if (markerType == "galaxy") {
const prefix = "komet.cosmetic.galaxy'"; const prefix = "komet.cosmetic.galaxy'";
@@ -4260,7 +4280,12 @@ class ChatMessageBubble extends StatelessWidget {
continue; continue;
} }
// Если парсинг не удался, добавляем как обычный текст // Если парсинг не удался, добавляем как обычный текст
segments.add(_KometSegment(text.substring(nextMarker, textStart + 10), _KometSegmentType.normal)); segments.add(
_KometSegment(
text.substring(nextMarker, textStart + 10),
_KometSegmentType.normal,
),
);
index = textStart + 10; index = textStart + 10;
} else if (markerType == "color") { } else if (markerType == "color") {
const marker = 'komet.color_'; const marker = 'komet.color_';
@@ -4273,13 +4298,24 @@ class ChatMessageBubble extends StatelessWidget {
if (secondQuote != -1) { if (secondQuote != -1) {
final segmentText = text.substring(textStart, secondQuote); final segmentText = text.substring(textStart, secondQuote);
final color = _parseKometHexColor(colorStr, null); final color = _parseKometHexColor(colorStr, null);
segments.add(_KometSegment(segmentText, _KometSegmentType.colored, color: color)); segments.add(
_KometSegment(
segmentText,
_KometSegmentType.colored,
color: color,
),
);
index = secondQuote + 1; index = secondQuote + 1;
continue; continue;
} }
} }
// Если парсинг не удался, добавляем как обычный текст // Если парсинг не удался, добавляем как обычный текст
segments.add(_KometSegment(text.substring(nextMarker, colorStart + 10), _KometSegmentType.normal)); segments.add(
_KometSegment(
text.substring(nextMarker, colorStart + 10),
_KometSegmentType.normal,
),
);
index = colorStart + 10; index = colorStart + 10;
} }
} }
@@ -4320,8 +4356,14 @@ class ChatMessageBubble extends StatelessWidget {
return _GalaxyAnimatedText(text: seg.text); return _GalaxyAnimatedText(text: seg.text);
case _KometSegmentType.pulse: case _KometSegmentType.pulse:
// Создаем строку в правильном формате для _PulseAnimatedText // Создаем строку в правильном формате для _PulseAnimatedText
final hexStr = seg.color!.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase(); final hexStr = seg.color!.value
return _PulseAnimatedText(text: "komet.cosmetic.pulse#$hexStr'${seg.text}'"); .toRadixString(16)
.padLeft(8, '0')
.substring(2)
.toUpperCase();
return _PulseAnimatedText(
text: "komet.cosmetic.pulse#$hexStr'${seg.text}'",
);
} }
}).toList(), }).toList(),
); );
@@ -4493,6 +4535,61 @@ class ChatMessageBubble extends StatelessWidget {
} }
} }
/// Обёртка, которая показывает контекстное меню только при долгом удержании.
///
/// - Короткий тап пропускается к дочерним жестам (открытие фото/видео и т.п.).
/// - Долгое удержание (~0.7 секунды) открывает панель реакций/действий.
class _LongPressContextMenuWrapper extends StatefulWidget {
final Widget child;
final void Function(Offset globalPosition) onShowMenu;
const _LongPressContextMenuWrapper({
required this.child,
required this.onShowMenu,
});
@override
State<_LongPressContextMenuWrapper> createState() =>
_LongPressContextMenuWrapperState();
}
class _LongPressContextMenuWrapperState
extends State<_LongPressContextMenuWrapper> {
static const Duration _longPressDuration = Duration(milliseconds: 700);
Timer? _timer;
bool _isLongPressTriggered = false;
void _onPointerDown(PointerDownEvent event) {
_isLongPressTriggered = false;
_timer?.cancel();
_timer = Timer(_longPressDuration, () {
_isLongPressTriggered = true;
widget.onShowMenu(event.position);
});
}
void _onPointerUpOrCancel(PointerEvent event) {
_timer?.cancel();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _onPointerDown,
onPointerUp: _onPointerUpOrCancel,
onPointerCancel: _onPointerUpOrCancel,
child: widget.child,
);
}
}
class GlobalImageStore { class GlobalImageStore {
static final Map<String, Uint8List> _memory = {}; static final Map<String, Uint8List> _memory = {};
static final Map<String, ValueNotifier<double?>> _progress = {}; static final Map<String, ValueNotifier<double?>> _progress = {};