feat: багофикс по запросам волбачия
This commit is contained in:
@@ -55,6 +55,8 @@ class PhotoBubble extends StatelessWidget {
|
||||
);
|
||||
} else if (count == 2) {
|
||||
photosWidget = _buildTwoPhotos(ctx, photos[0], photos[1]);
|
||||
} else if (count == 3) {
|
||||
photosWidget = _buildThreePhotos(ctx, photos);
|
||||
} else {
|
||||
photosWidget = _buildPhotoGrid(ctx, photos);
|
||||
}
|
||||
@@ -307,6 +309,39 @@ class PhotoBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThreePhotos(BubbleContext ctx, List<PhotoAttachment> photos) {
|
||||
final matchTop =
|
||||
ctx.hasMultiplePhotosNoCaption && ctx.shape == BubbleShape.singleTop;
|
||||
final matchBottom =
|
||||
ctx.hasMultiplePhotosNoCaption && ctx.shape == BubbleShape.singleBottom;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: _multiPhotoCornerRadius(
|
||||
matchTop: matchTop,
|
||||
matchBottom: matchBottom,
|
||||
isMe: ctx.isMe,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 3 / 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(flex: 2, child: _buildFillTile(ctx, photos[0], 0)),
|
||||
const SizedBox(width: 2),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: _buildFillTile(ctx, photos[1], 1)),
|
||||
const SizedBox(height: 2),
|
||||
Expanded(child: _buildFillTile(ctx, photos[2], 2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhotoGrid(BubbleContext ctx, List<PhotoAttachment> photos) {
|
||||
final displayCount = photos.length > 4 ? 4 : photos.length;
|
||||
final remaining = photos.length - 4;
|
||||
@@ -361,30 +396,30 @@ class PhotoBubble extends StatelessWidget {
|
||||
return _buildPhotoTile(ctx, photos[index], index);
|
||||
}
|
||||
|
||||
Widget _buildPhotoTile(BubbleContext ctx, PhotoAttachment photo, int index) {
|
||||
Widget _buildPhotoTile(BubbleContext ctx, PhotoAttachment photo, int index) =>
|
||||
AspectRatio(aspectRatio: 1, child: _buildFillTile(ctx, photo, index));
|
||||
|
||||
Widget _buildFillTile(BubbleContext ctx, PhotoAttachment photo, int index) {
|
||||
final cachePx =
|
||||
(BubbleContext.photoMaxSize /
|
||||
2 *
|
||||
MediaQuery.of(ctx.context).devicePixelRatio)
|
||||
.round();
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildPhotoImage(
|
||||
ctx,
|
||||
photo,
|
||||
double.infinity,
|
||||
double.infinity,
|
||||
memWidth: cachePx,
|
||||
memHeight: cachePx,
|
||||
),
|
||||
if (ctx.uploadProgress != null)
|
||||
_buildUploadOverlay(ctx.uploadProgress!, index),
|
||||
if (ctx.uploadProgress == null)
|
||||
_buildTileTapTarget(ctx, index, cachePx),
|
||||
],
|
||||
),
|
||||
return Stack(
|
||||
children: [
|
||||
_buildPhotoImage(
|
||||
ctx,
|
||||
photo,
|
||||
double.infinity,
|
||||
double.infinity,
|
||||
memWidth: cachePx,
|
||||
memHeight: cachePx,
|
||||
),
|
||||
if (ctx.uploadProgress != null)
|
||||
_buildUploadOverlay(ctx.uploadProgress!, index),
|
||||
if (ctx.uploadProgress == null)
|
||||
_buildTileTapTarget(ctx, index, cachePx),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ class VoiceMessageBubble extends StatefulWidget {
|
||||
State<VoiceMessageBubble> createState() => _VoiceMessageBubbleState();
|
||||
}
|
||||
|
||||
const double _transcriptionMaxHeight = 132;
|
||||
|
||||
class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
bool _transcriptionVisible = false;
|
||||
String? _transcriptionText;
|
||||
@@ -82,15 +84,39 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
fallbackDuration: Duration(seconds: widget.duration),
|
||||
);
|
||||
_audio.failure.addListener(_onFailure);
|
||||
TranscriptionCache.listen(_sourceMessageId, _onTranscriptionPush);
|
||||
_adoptCachedTranscription();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
TranscriptionCache.unlisten(_sourceMessageId, _onTranscriptionPush);
|
||||
_audio.failure.removeListener(_onFailure);
|
||||
MediaPlayback.instance.releaseVoice(_audio);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _adoptCachedTranscription() {
|
||||
final cached = TranscriptionCache.get(_sourceMessageId);
|
||||
if (cached == null || cached.status != 1) return;
|
||||
_transcriptionText = cached.text ?? 'не удалось распознать текст';
|
||||
_transcriptionVisible = TranscriptionCache.isExpanded(_sourceMessageId);
|
||||
}
|
||||
|
||||
void _onTranscriptionPush() {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_transcriptionLoading = false;
|
||||
_adoptCachedTranscription();
|
||||
});
|
||||
}
|
||||
|
||||
void _showTranscription(String text) {
|
||||
_transcriptionText = text;
|
||||
_transcriptionVisible = true;
|
||||
TranscriptionCache.setExpanded(_sourceMessageId, true);
|
||||
}
|
||||
|
||||
String get _cacheName => '${widget.audioId ?? _sourceMessageId}.ogg';
|
||||
|
||||
int get _sourceChatId => widget.sourceChatId ?? widget.chatId;
|
||||
@@ -359,15 +385,21 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
curve: Curves.easeOut,
|
||||
alignment: Alignment.topLeft,
|
||||
child: _transcriptionVisible
|
||||
? Text(
|
||||
_transcriptionText ?? '',
|
||||
style: TextStyle(
|
||||
color: widget.textColor.withValues(alpha: 0.8),
|
||||
fontSize: 12,
|
||||
height: 1.3,
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: _transcriptionMaxHeight,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Text(
|
||||
_transcriptionText ?? '',
|
||||
style: TextStyle(
|
||||
color: widget.textColor.withValues(alpha: 0.8),
|
||||
fontSize: 12,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
maxLines: 10,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
@@ -438,16 +470,16 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
if (_transcriptionVisible && _transcriptionText != null) {
|
||||
setState(() {
|
||||
_transcriptionVisible = false;
|
||||
TranscriptionCache.setExpanded(_sourceMessageId, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (TranscriptionCache.has(_sourceMessageId)) {
|
||||
final cached = TranscriptionCache.get(_sourceMessageId)!;
|
||||
setState(() {
|
||||
_transcriptionText = cached.text ?? 'не удалось распознать текст';
|
||||
_transcriptionVisible = true;
|
||||
});
|
||||
setState(
|
||||
() => _showTranscription(cached.text ?? 'не удалось распознать текст'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -468,13 +500,13 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
setState(() {
|
||||
_transcriptionLoading = false;
|
||||
if (result.status == 1) {
|
||||
_transcriptionText = (result.text == null || result.text!.isEmpty)
|
||||
? 'не удалось распознать текст'
|
||||
: result.text;
|
||||
_transcriptionVisible = true;
|
||||
_showTranscription(
|
||||
(result.text == null || result.text!.isEmpty)
|
||||
? 'не удалось распознать текст'
|
||||
: result.text!,
|
||||
);
|
||||
} else if (result.status == 0) {
|
||||
_transcriptionText = 'транскрибация...';
|
||||
_transcriptionVisible = true;
|
||||
_showTranscription('транскрибация...');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -482,8 +514,7 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_transcriptionLoading = false;
|
||||
_transcriptionText = 'ошибка транскрибации';
|
||||
_transcriptionVisible = true;
|
||||
_showTranscription('ошибка транскрибации');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,15 @@ class _AvatarHistoryScreenState extends State<AvatarHistoryScreen> {
|
||||
final current = widget.currentAvatarUrl;
|
||||
_current = (current != null && current.isNotEmpty) ? current : null;
|
||||
_rebuildPages();
|
||||
_load();
|
||||
if (_hasHistory) {
|
||||
_load();
|
||||
} else {
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get _hasHistory => widget.contactId > 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
@@ -100,7 +106,9 @@ class _AvatarHistoryScreenState extends State<AvatarHistoryScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadMore() async {
|
||||
if (_loadingMore || _history.length >= _historyTotal) return;
|
||||
if (!_hasHistory || _loadingMore || _history.length >= _historyTotal) {
|
||||
return;
|
||||
}
|
||||
_loadingMore = true;
|
||||
final photos = await ContactsModule.fetchPhotos(
|
||||
api,
|
||||
@@ -286,9 +294,8 @@ class _AvatarHistoryScreenState extends State<AvatarHistoryScreen> {
|
||||
imageUrl: _pages[i],
|
||||
fit: BoxFit.contain,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
placeholder: (_, _) => const Center(
|
||||
child: SmallSpinner(size: 36, color: Colors.white),
|
||||
),
|
||||
placeholder: (_, _) =>
|
||||
const Center(child: SmallSpinner(size: 36, color: Colors.white)),
|
||||
errorWidget: (_, _, _) =>
|
||||
const Icon(Symbols.broken_image, color: Colors.white54, size: 64),
|
||||
),
|
||||
|
||||
@@ -29,6 +29,21 @@ class HeaderPullScrollPhysics extends ScrollPhysics {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value) {
|
||||
if (delta <= 0) {
|
||||
if (value < position.pixels &&
|
||||
position.pixels <= position.minScrollExtent) {
|
||||
return value - position.pixels;
|
||||
}
|
||||
if (value < position.minScrollExtent &&
|
||||
position.minScrollExtent < position.pixels) {
|
||||
return value - position.minScrollExtent;
|
||||
}
|
||||
}
|
||||
return super.applyBoundaryConditions(position, value);
|
||||
}
|
||||
|
||||
@override
|
||||
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
|
||||
if (delta <= 0 || offset <= 0 || position.pixels <= 0) {
|
||||
@@ -107,8 +122,10 @@ class MorphHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
double get maxExtent => expandedExtent;
|
||||
|
||||
@override
|
||||
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
|
||||
OverScrollHeaderStretchConfiguration();
|
||||
OverScrollHeaderStretchConfiguration? get stretchConfiguration =>
|
||||
expandedExtent > collapsedExtent
|
||||
? OverScrollHeaderStretchConfiguration()
|
||||
: null;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
|
||||
@@ -33,9 +33,12 @@ class ProfileHeroAvatar extends StatelessWidget {
|
||||
final from = _AvatarHeroChild.of(fromHeroContext);
|
||||
final to = _AvatarHeroChild.of(toHeroContext);
|
||||
final sharpest = from.size >= to.size ? from : to;
|
||||
return FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: SizedBox.square(dimension: sharpest.size, child: sharpest.child),
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: SizedBox.square(dimension: sharpest.size, child: sharpest.child),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user