feat: extend sync offset limits

close #282
This commit is contained in:
edde746
2026-01-17 22:34:15 +01:00
parent 0005d70bde
commit ec74aadca7
2 changed files with 121 additions and 11 deletions
+16 -4
View File
@@ -132,13 +132,25 @@ String formatDurationTimestamp(Duration duration) {
}
}
/// Formats a sync offset in milliseconds with sign indicator (e.g., "+150ms", "-250ms").
/// This format is used for audio/subtitle synchronization adjustments.
/// Formats a sync offset in milliseconds with sign indicator (e.g., "+150ms", "-15.1s").
/// Shows milliseconds for values < 10s, decimal seconds for larger values.
///
/// Used for: audio sync sheet, sync offset controls.
String formatSyncOffset(double offsetMs) {
final sign = offsetMs >= 0 ? '+' : '';
return '$sign${offsetMs.round()}ms';
final sign = offsetMs >= 0 ? '+' : '-';
final absMs = offsetMs.abs().round();
final durationLocale = _getDurationLocale();
if (absMs >= 10000) {
// For values >= 10s, show decimal seconds (e.g., "+15.1s")
final seconds = (offsetMs.abs() / 1000).toStringAsFixed(1);
final unit = durationLocale.second(1, true);
return '$sign$seconds$unit';
}
// For values < 10s, show milliseconds (e.g., "+7300ms")
final unit = durationLocale.millisecond(1, true);
return '$sign$absMs$unit';
}
/// Gets the duration package locale based on the current app locale.
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -28,7 +30,17 @@ class SyncOffsetControl extends StatefulWidget {
}
class _SyncOffsetControlState extends State<SyncOffsetControl> {
// Range constants
static const double _sliderMin = -5000; // ±5s for slider
static const double _sliderMax = 5000;
static const double _absoluteMin = -60000; // ±60s absolute limit
static const double _absoluteMax = 60000;
static const double _tapStep = 100; // 100ms per tap
static const double _longPressStep = 1000; // 1s per long-press tick
static const int _sliderDivisions = 200; // 50ms steps for ±5s range
late double _currentOffset;
Timer? _longPressTimer;
@override
void initState() {
@@ -46,6 +58,12 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
}
}
@override
void dispose() {
_longPressTimer?.cancel();
super.dispose();
}
Future<void> _applyOffset(double offsetMs) async {
// Convert milliseconds to seconds for mpv
final offsetSeconds = offsetMs / 1000.0;
@@ -64,6 +82,49 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
_applyOffset(0);
}
void _incrementOffset() {
final newOffset = (_currentOffset + _tapStep).clamp(_absoluteMin, _absoluteMax);
setState(() {
_currentOffset = newOffset;
});
_applyOffset(newOffset);
}
void _decrementOffset() {
final newOffset = (_currentOffset - _tapStep).clamp(_absoluteMin, _absoluteMax);
setState(() {
_currentOffset = newOffset;
});
_applyOffset(newOffset);
}
void _startLongPressIncrement() {
_longPressTimer?.cancel();
_longPressTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
final newOffset = (_currentOffset + _longPressStep).clamp(_absoluteMin, _absoluteMax);
setState(() {
_currentOffset = newOffset;
});
_applyOffset(newOffset);
});
}
void _startLongPressDecrement() {
_longPressTimer?.cancel();
_longPressTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
final newOffset = (_currentOffset - _longPressStep).clamp(_absoluteMin, _absoluteMax);
setState(() {
_currentOffset = newOffset;
});
_applyOffset(newOffset);
});
}
void _stopLongPress() {
_longPressTimer?.cancel();
_longPressTimer = null;
}
String _getDescriptionText() {
if (_currentOffset > 0) {
return t.videoControls.playsLater(label: widget.labelText);
@@ -74,8 +135,30 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
}
}
Widget _buildStepButton({
required IconData icon,
required VoidCallback onTap,
required VoidCallback onLongPressStart,
}) {
return GestureDetector(
onTap: onTap,
onLongPressStart: (_) => onLongPressStart(),
onLongPressEnd: (_) => _stopLongPress(),
onLongPressCancel: _stopLongPress,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(color: Colors.grey[800], borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: Colors.white, size: 28),
),
);
}
@override
Widget build(BuildContext context) {
// Clamp the slider value to its range, but display the actual offset
final sliderValue = _currentOffset.clamp(_sliderMin, _sliderMax);
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -89,19 +172,27 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
const SizedBox(height: 8),
Text(_getDescriptionText(), style: const TextStyle(color: Colors.white70, fontSize: 16)),
const SizedBox(height: 48),
// Slider
// Slider with +/- buttons
Row(
children: [
// Decrement button
_buildStepButton(
icon: Symbols.remove_rounded,
onTap: _decrementOffset,
onLongPressStart: _startLongPressDecrement,
),
const SizedBox(width: 12),
// Slider section
Text(
t.videoControls.minusTime(amount: "2", unit: "s"),
t.videoControls.minusTime(amount: "5", unit: "s"),
style: const TextStyle(color: Colors.white70),
),
Expanded(
child: Slider(
value: _currentOffset,
min: -2000,
max: 2000,
divisions: 80, // 50ms steps
value: sliderValue,
min: _sliderMin,
max: _sliderMax,
divisions: _sliderDivisions,
activeColor: Colors.blue,
inactiveColor: Colors.white24,
onChanged: (value) {
@@ -115,9 +206,16 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
),
),
Text(
t.videoControls.addTime(amount: "2", unit: "s"),
t.videoControls.addTime(amount: "5", unit: "s"),
style: const TextStyle(color: Colors.white70),
),
const SizedBox(width: 12),
// Increment button
_buildStepButton(
icon: Symbols.add_rounded,
onTap: _incrementOffset,
onLongPressStart: _startLongPressIncrement,
),
],
),
const SizedBox(height: 24),