mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Tweak AVIF tuning and preferences
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user