Tweak AVIF tuning and preferences

This commit is contained in:
Janez T
2026-03-02 09:42:14 +01:00
parent 4d0c45c11c
commit c176bd92a9
12 changed files with 972 additions and 47 deletions

View File

@@ -34,6 +34,15 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
@override
Widget build(BuildContext context) {
final radioBw = context.select<ConnectionProvider, int?>(
(p) => p.deviceInfo.radioBw,
);
final radioSf = context.select<ConnectionProvider, int?>(
(p) => p.deviceInfo.radioSf,
);
final radioCr = context.select<ConnectionProvider, int?>(
(p) => p.deviceInfo.radioCr,
);
final envelope = ImageEnvelope.tryParse(widget.message.text);
if (envelope == null) return const SizedBox.shrink();
@@ -84,6 +93,10 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
received: received,
total: total,
envelope: envelope,
pathLen: widget.message.pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
error: _errorText,
isSentByMe: widget.isSentByMe,
),
@@ -219,17 +232,40 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
required int received,
required int total,
required ImageEnvelope envelope,
required int pathLen,
required int? radioBw,
required int? radioSf,
required int? radioCr,
required String? error,
required bool isSentByMe,
}) {
final txEstimate = estimateImageTransmitDuration(
fragmentCount: envelope.total,
sizeBytes: envelope.sizeBytes,
pathLen: pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
);
final txEstimateLabel = _formatTransmitEstimate(txEstimate);
if (error != null) return error;
if (isRequesting) return '📥 Loading… $received/$total';
if (isRequesting) return '📥 Loading… $received/$total · $txEstimateLabel';
if (isComplete) {
final base =
'🖼️ ${envelope.width}×${envelope.height} ${envelope.format.label}';
return isSentByMe ? '$base · ${envelope.total} seg' : base;
return isSentByMe
? '$base · ${envelope.total} seg · $txEstimateLabel'
: '$base · $txEstimateLabel';
}
return '🖼️ Tap to load · ${envelope.width}×${envelope.height}';
return '🖼️ Tap to load · ${envelope.width}×${envelope.height} · $txEstimateLabel';
}
static String _formatTransmitEstimate(Duration value) {
if (value.inSeconds < 60) return '~${value.inSeconds}s tx';
final minutes = value.inMinutes;
final seconds = value.inSeconds % 60;
return '~${minutes}m ${seconds}s tx';
}
void _showFullScreen(BuildContext context, Uint8List imageBytes) {

View File

@@ -298,6 +298,9 @@ class _MessageBubbleState extends State<MessageBubble> {
void _showTechnicalDetails(BuildContext context) {
final connectionProvider = context.read<ConnectionProvider>();
final radioBw = connectionProvider.deviceInfo.radioBw;
final radioSf = connectionProvider.deviceInfo.radioSf;
final radioCr = connectionProvider.deviceInfo.radioCr;
final contactsProvider = context.read<ContactsProvider>();
final voiceProvider = context.read<VoiceProvider>();
final imageProvider = context.read<ip.ImageProvider>();
@@ -350,6 +353,45 @@ class _MessageBubbleState extends State<MessageBubble> {
final imageSession = imageEnvelope != null
? imageProvider.session(imageEnvelope.sessionId)
: null;
final imageTxEstimate = imageEnvelope != null
? estimateImageTransmitDuration(
fragmentCount: imageEnvelope.total,
sizeBytes: imageEnvelope.sizeBytes,
pathLen: widget.message.pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
)
: Duration.zero;
final voiceTxEstimate = voiceSession != null
? estimateVoiceTransmitDurationFromPackets(
packets: voiceSession.packets,
pathLen: widget.message.pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
)
: envelope != null
? estimateVoiceTransmitDuration(
mode: envelope.mode,
packetCount: envelope.total,
durationMs: envelope.durationMs,
pathLen: widget.message.pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
)
: legacyVoicePacket != null
? estimateVoiceTransmitDuration(
mode: legacyVoicePacket.mode,
packetCount: legacyVoicePacket.total,
durationMs: legacyVoicePacket.durationMs * legacyVoicePacket.total,
pathLen: widget.message.pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
)
: Duration.zero;
final senderPrefixHex = widget.message.senderPublicKeyPrefix
?.map((b) => b.toRadixString(16).padLeft(2, '0'))
@@ -431,8 +473,16 @@ class _MessageBubbleState extends State<MessageBubble> {
rawLines.add(
'Session estimated duration s: ${voiceSession.estimatedDurationSeconds.toStringAsFixed(2)}',
);
rawLines.add(
'Estimated voice tx: ~${voiceTxEstimate.inSeconds}s (current radio)',
);
} else {
rawLines.add('Session present locally: no');
if (voiceTxEstimate > Duration.zero) {
rawLines.add(
'Estimated voice tx: ~${voiceTxEstimate.inSeconds}s (current radio)',
);
}
}
}
@@ -448,6 +498,9 @@ class _MessageBubbleState extends State<MessageBubble> {
);
rawLines.add('Fragments total (envelope): ${imageEnvelope.total}');
rawLines.add('Compressed size (envelope): ${imageEnvelope.sizeBytes} B');
rawLines.add(
'Estimated image tx: ~${imageTxEstimate.inSeconds}s (current radio)',
);
rawLines.add('Envelope senderKey6: ${imageEnvelope.senderKey6}');
rawLines.add('Envelope ts: ${imageEnvelope.timestampSec}');
rawLines.add('Envelope ver: ${imageEnvelope.version}');
@@ -588,7 +641,9 @@ class _MessageBubbleState extends State<MessageBubble> {
_detailRow(
context,
label: l10n.floodFallback,
value: widget.message.usedFloodFallback ? l10n.yes : l10n.no,
value: widget.message.usedFloodFallback
? l10n.yes
: l10n.no,
),
],
),
@@ -670,6 +725,59 @@ class _MessageBubbleState extends State<MessageBubble> {
label: l10n.complete,
value: voiceSession.isComplete ? l10n.yes : l10n.no,
),
if (voiceTxEstimate > Duration.zero)
_detailRow(
context,
label: 'Estimated tx',
value: voiceTxEstimate.inSeconds < 60
? '~${voiceTxEstimate.inSeconds}s'
: '~${voiceTxEstimate.inMinutes}m ${voiceTxEstimate.inSeconds % 60}s',
),
],
),
),
],
if (imageEnvelope != null) ...[
const SizedBox(height: 12),
_techSection(
context,
icon: Icons.image_outlined,
title: 'Image',
child: Column(
children: [
_detailRow(context, label: l10n.envelope, value: 'IE1'),
_detailRow(
context,
label: 'Format',
value: imageEnvelope.format.label,
),
_detailRow(
context,
label: 'Dimensions',
value:
'${imageEnvelope.width}×${imageEnvelope.height}',
),
_detailRow(
context,
label: 'Segments',
value: imageSession != null
? '${imageSession.receivedCount}/${imageSession.total}'
: '${imageEnvelope.total}',
),
if (imageSession != null)
_detailRow(
context,
label: l10n.complete,
value: imageSession.isComplete ? l10n.yes : l10n.no,
),
if (imageTxEstimate > Duration.zero)
_detailRow(
context,
label: 'Estimated tx',
value: imageTxEstimate.inSeconds < 60
? '~${imageTxEstimate.inSeconds}s'
: '~${imageTxEstimate.inMinutes}m ${imageTxEstimate.inSeconds % 60}s',
),
],
),
),
@@ -681,7 +789,10 @@ class _MessageBubbleState extends State<MessageBubble> {
visualDensity: VisualDensity.compact,
title: Text(
l10n.rawDump,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
children: [
Container(
@@ -1834,7 +1945,8 @@ class _MessageBubbleState extends State<MessageBubble> {
!widget.isCompact)
VoiceMessageBubble(message: message, isSentByMe: isOwnMessage)
// Image message content (IE1 envelope)
else if (ImageEnvelope.isEnvelope(message.text) && !widget.isCompact)
else if (ImageEnvelope.isEnvelope(message.text) &&
!widget.isCompact)
ImageMessageBubble(message: message, isSentByMe: isOwnMessage)
// Regular message content
else if (!message.isDrawing || widget.isCompact)

View File

@@ -31,6 +31,15 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
@override
Widget build(BuildContext context) {
final radioBw = context.select<ConnectionProvider, int?>(
(p) => p.deviceInfo.radioBw,
);
final radioSf = context.select<ConnectionProvider, int?>(
(p) => p.deviceInfo.radioSf,
);
final radioCr = context.select<ConnectionProvider, int?>(
(p) => p.deviceInfo.radioCr,
);
final voiceId = widget.message.voiceId;
if (voiceId == null) return const SizedBox.shrink();
@@ -73,6 +82,16 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
session: session,
messageText: widget.message.text,
);
final txEstimate = _resolveVoiceTransmitEstimate(
session: session,
envelope: envelope,
messageText: widget.message.text,
pathLen: widget.message.pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
);
final txEstimateLabel = _formatTransmitEstimate(txEstimate);
return Row(
mainAxisSize: MainAxisSize.min,
@@ -124,21 +143,21 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
),
)
else
_WaveformBar(
isComplete: isComplete,
bars: waveformBars,
),
_WaveformBar(isComplete: isComplete, bars: waveformBars),
const SizedBox(height: 4),
Text(
_buildStatusText(
durationLabel: durationLabel,
modeLabel: modeLabel,
txEstimateLabel: txEstimateLabel,
received: received,
total: total,
isComplete: isComplete,
isRequesting: _isRequesting,
errorText: _errorText,
requestingLabel: AppLocalizations.of(context)!.requestingVoice,
requestingLabel: AppLocalizations.of(
context,
)!.requestingVoice,
),
style: TextStyle(
fontSize: 11,
@@ -236,6 +255,7 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
static String _buildStatusText({
required String durationLabel,
required String modeLabel,
required String txEstimateLabel,
required int received,
required int total,
required bool isComplete,
@@ -246,12 +266,12 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
if (errorText != null) return errorText;
final progress = total > 0 ? ' ($received/$total)' : '';
if (isRequesting) {
return '$requestingLabel$progress';
return '$requestingLabel$progress · $txEstimateLabel';
}
if (!isComplete && total > 0) {
return '🎙️ $durationLabel · $modeLabel$progress';
return '🎙️ $durationLabel · $modeLabel$progress · $txEstimateLabel';
}
return '🎙️ $durationLabel · $modeLabel';
return '🎙️ $durationLabel · $modeLabel · $txEstimateLabel';
}
List<double> _resolveWaveformBars({
@@ -271,16 +291,68 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
return const [];
}
Duration _resolveVoiceTransmitEstimate({
required VoiceSession? session,
required VoiceEnvelope? envelope,
required String messageText,
required int pathLen,
required int? radioBw,
required int? radioSf,
required int? radioCr,
}) {
if (session != null) {
final fromSession = estimateVoiceTransmitDurationFromPackets(
packets: session.packets,
pathLen: pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
);
if (fromSession > Duration.zero) return fromSession;
}
if (envelope != null) {
return estimateVoiceTransmitDuration(
mode: envelope.mode,
packetCount: envelope.total,
durationMs: envelope.durationMs,
pathLen: pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
);
}
final legacyPacket = VoicePacket.tryParseText(messageText);
if (legacyPacket != null) {
return estimateVoiceTransmitDuration(
mode: legacyPacket.mode,
packetCount: legacyPacket.total,
durationMs: legacyPacket.durationMs * legacyPacket.total,
pathLen: pathLen,
radioBw: radioBw,
radioSf: radioSf,
radioCr: radioCr,
);
}
return Duration.zero;
}
static String _formatTransmitEstimate(Duration value) {
if (value <= Duration.zero) return '~0s tx';
if (value.inSeconds < 60) return '~${value.inSeconds}s tx';
final minutes = value.inMinutes;
final seconds = value.inSeconds % 60;
return '~${minutes}m ${seconds}s tx';
}
}
/// Voice waveform rendered as a row of bars.
class _WaveformBar extends StatelessWidget {
final bool isComplete;
final List<double> bars;
const _WaveformBar({
required this.isComplete,
required this.bars,
});
const _WaveformBar({required this.isComplete, required this.bars});
@override
Widget build(BuildContext context) {