From ec74aadca739333796f7c9cd8c7630360af14ead Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:34:15 +0100 Subject: [PATCH] feat: extend sync offset limits close #282 --- lib/utils/formatters.dart | 20 +++- .../widgets/sync_offset_control.dart | 112 ++++++++++++++++-- 2 files changed, 121 insertions(+), 11 deletions(-) diff --git a/lib/utils/formatters.dart b/lib/utils/formatters.dart index 69a345e1..81983846 100644 --- a/lib/utils/formatters.dart +++ b/lib/utils/formatters.dart @@ -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. diff --git a/lib/widgets/video_controls/widgets/sync_offset_control.dart b/lib/widgets/video_controls/widgets/sync_offset_control.dart index 22d1efb6..1bdd408c 100644 --- a/lib/widgets/video_controls/widgets/sync_offset_control.dart +++ b/lib/widgets/video_controls/widgets/sync_offset_control.dart @@ -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 { + // 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 { } } + @override + void dispose() { + _longPressTimer?.cancel(); + super.dispose(); + } + Future _applyOffset(double offsetMs) async { // Convert milliseconds to seconds for mpv final offsetSeconds = offsetMs / 1000.0; @@ -64,6 +82,49 @@ class _SyncOffsetControlState extends State { _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 { } } + 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 { 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 { ), ), 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),