feat/fix/refactor: Кнопка добавления человека в контакты, возможность развертывания аватарки на экране профиля, включение сторисов по умолчанию, показ сторисов на экране чатов, чата, и при просмотре списка участников, миниапы у ботов. Фикс прогресса просмотра сториса, фикс предпросмотра медиав пикере, прогресс бар отправки фото/видео/файлов + фото продолжат отправку даже если выйти с чата. Вынес превью фото, добавил отдельный модуль отправки медиа

This commit is contained in:
Jganenokk
2026-08-05 20:04:12 +07:00
parent e87d3d57f7
commit 3915cb957e
33 changed files with 2522 additions and 680 deletions
@@ -160,6 +160,10 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
_thumbKeys.putIfAbsent(id, () => GlobalKey<_ThumbnailState>());
void _openPreview(GalleryItem item) {
if (item.isVideo) {
_toggleSelection(item);
return;
}
final thumbKey = _thumbKey(item.id);
final hero = PhotoHeroController(
origin: () => photoHeroRect(thumbKey),
@@ -1089,7 +1093,7 @@ class _ThumbnailState extends State<_Thumbnail> {
}
void _resolveProvider() {
final file = widget.editedFile ?? widget.item.localFile;
final file = widget.editedFile ?? (widget.item.isVideo ? null : widget.item.localFile);
if (file != null) {
_provider = ResizeImage(
FileImage(file),
@@ -1102,7 +1106,7 @@ class _ThumbnailState extends State<_Thumbnail> {
final id = widget.item.id;
widget.item.thumbnail(_pixelSize).then((data) {
if (!mounted || data == null || widget.item.id != id) return;
if (widget.editedFile != null || widget.item.localFile != null) return;
if (widget.editedFile != null) return;
setState(() => _provider = MemoryImage(data));
});
}
@@ -1119,5 +1123,16 @@ class _ThumbnailState extends State<_Thumbnail> {
);
}
Widget _placeholder() => ColoredBox(color: widget.cs.surfaceContainerHighest);
Widget _placeholder() => ColoredBox(
color: widget.cs.surfaceContainerHighest,
child: widget.item.isVideo
? Center(
child: Icon(
Symbols.movie,
size: 28,
color: widget.cs.onSurfaceVariant,
),
)
: null,
);
}
@@ -8,6 +8,7 @@ import '../../../../core/config/komet_settings.dart';
import '../../../../core/utils/format.dart';
import '../../../../models/attachment.dart';
import '../../formatted_message_text.dart';
import '../../sending_clock_icon.dart';
import '../../photo_viewer.dart';
enum MessageType { text, attachment, voice, control }
@@ -202,6 +203,9 @@ class BubbleContext {
Widget _statusIconFor(String? status, {Color? color, double size = 14}) {
final v = messageStatusVisual(status, dimColor: color ?? dim);
if (isSendingStatus(status)) {
return SendingClockIcon(color: v.color, size: size);
}
return Icon(v.icon, size: size, color: v.color);
}
}
@@ -17,6 +17,7 @@ import '../../../../models/attachment.dart';
import '../../custom_notification.dart';
import '../../decrypted_photo.dart';
import '../../photo_viewer.dart';
import '../../upload_progress_ring.dart';
import 'bubble_context.dart';
class FileBubble extends StatelessWidget {
@@ -69,11 +70,24 @@ class FileBubble extends StatelessWidget {
color: isMe ? ctx.systemTint : ctx.cs.primaryContainer,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Symbols.description,
color: isMe ? ctx.cs.onPrimaryContainer : ctx.cs.primary,
size: 20,
),
child: ctx.uploadProgress == null
? Icon(
Symbols.description,
color: isMe
? ctx.cs.onPrimaryContainer
: ctx.cs.primary,
size: 20,
)
: UploadProgressRing(
progress: ctx.uploadProgress!,
color: isMe
? ctx.cs.onPrimaryContainer
: ctx.cs.primary,
size: 38,
strokeWidth: 2.4,
iconSize: 14,
padding: const EdgeInsets.all(4),
),
),
const SizedBox(width: 10),
Flexible(
@@ -3,6 +3,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../../../models/attachment.dart';
import '../../lottie_image.dart';
import '../../sending_clock_icon.dart';
import 'bubble_context.dart';
class StickerBubble extends StatelessWidget {
@@ -86,6 +87,9 @@ class StickerBubble extends StatelessWidget {
Widget _buildStickerStatusIcon() {
final status = ctx.overrideStatus ?? ctx.message.status;
final v = messageStatusVisual(status, dimColor: Colors.white);
if (isSendingStatus(status)) {
return SendingClockIcon(color: v.color, size: 13);
}
return Icon(v.icon, size: 13, color: v.color);
}
}
@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/main.dart';
import '../../../../core/media/preview_image.dart';
import '../../../../core/utils/format.dart';
import '../../../../core/utils/haptics.dart';
import '../../../../models/attachment.dart';
import '../../custom_notification.dart';
import '../../upload_progress_ring.dart';
import '../../photo_viewer.dart';
import 'bubble_context.dart';
import 'video_note_bubble.dart';
@@ -63,39 +65,78 @@ class VideoBubble extends StatelessWidget {
child: Icon(Symbols.videocam, size: 48, color: ctx.cs.onSurfaceVariant),
);
final localThumb = dataUriImage(video, video.previewData);
final uploading = ctx.uploadProgress;
Widget previewImage() {
if (previewUrl.isNotEmpty && !previewUrl.startsWith('data:')) {
return CachedNetworkImage(
imageUrl: previewUrl,
width: width,
height: height,
fit: BoxFit.cover,
memCacheWidth: (width * dpr).round(),
fadeInDuration: Duration.zero,
placeholderFadeInDuration: Duration.zero,
errorWidget: (_, _, _) => localThumb == null
? placeholder()
: Image(
image: localThumb,
width: width,
height: height,
fit: BoxFit.cover,
),
);
}
if (localThumb != null) {
return Image(
image: localThumb,
width: width,
height: height,
fit: BoxFit.cover,
gaplessPlayback: true,
errorBuilder: (_, _, _) => placeholder(),
);
}
return placeholder();
}
final preview = ClipRRect(
borderRadius: BorderRadius.circular(BubbleContext.photoBorderRadius),
child: Stack(
children: [
previewUrl.isEmpty
? placeholder()
: CachedNetworkImage(
imageUrl: previewUrl,
width: width,
height: height,
fit: BoxFit.cover,
memCacheWidth: (width * dpr).round(),
fadeInDuration: Duration.zero,
placeholderFadeInDuration: Duration.zero,
errorWidget: (_, _, _) => placeholder(),
previewImage(),
if (uploading != null)
Positioned.fill(
child: ColoredBox(
color: Colors.black.withValues(alpha: 0.35),
child: Center(
child: UploadProgressRing(
progress: uploading,
color: Colors.white,
trackColor: Colors.white24,
),
),
Positioned.fill(
child: Center(
child: Container(
width: 48,
height: 48,
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
Symbols.play_arrow,
color: Colors.white,
size: 30,
),
)
else
Positioned.fill(
child: Center(
child: Container(
width: 48,
height: 48,
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
Symbols.play_arrow,
color: Colors.white,
size: 30,
),
),
),
),
),
if (durationMs != null && durationMs > 0)
Positioned(
left: 6,
@@ -112,12 +153,13 @@ class VideoBubble extends StatelessWidget {
),
),
),
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _playVideo(ctx.context, video),
if (uploading == null)
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _playVideo(ctx.context, video),
),
),
),
],
),
);