diff --git a/lib/focus/focusable_slider.dart b/lib/focus/focusable_slider.dart new file mode 100644 index 00000000..4c8c0db9 --- /dev/null +++ b/lib/focus/focusable_slider.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import 'dpad_navigator.dart'; +import 'focusable_wrapper.dart'; +import 'input_mode_tracker.dart'; + +/// A D-pad friendly slider that uses LEFT/RIGHT to adjust value +/// and lets UP/DOWN pass through for focus traversal. +/// +/// Hides the thumb in keyboard mode when not focused, matching +/// the timeline slider pattern. +class FocusableSlider extends StatefulWidget { + final double value; + final double min; + final double max; + final int? divisions; + final ValueChanged? onChanged; + final FocusNode? focusNode; + final bool autofocus; + + const FocusableSlider({ + super.key, + required this.value, + this.min = 0.0, + this.max = 1.0, + this.divisions, + this.onChanged, + this.focusNode, + this.autofocus = false, + }); + + @override + State createState() => _FocusableSliderState(); +} + +class _FocusableSliderState extends State { + bool _isFocused = false; + + double get _step { + if (widget.divisions != null && widget.divisions! > 0) { + return (widget.max - widget.min) / widget.divisions!; + } + return (widget.max - widget.min) * 0.05; + } + + KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { + final key = event.logicalKey; + if (key.isLeftKey || key.isRightKey) { + if (event.isActionable && widget.onChanged != null) { + final delta = key.isRightKey ? _step : -_step; + final newValue = (widget.value + delta).clamp(widget.min, widget.max); + widget.onChanged!(newValue); + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + + @override + Widget build(BuildContext context) { + return FocusableWrapper( + focusNode: widget.focusNode, + autofocus: widget.autofocus, + descendantsAreFocusable: false, + disableScale: true, + focusColor: Colors.transparent, + onFocusChange: (focused) => setState(() => _isFocused = focused), + onKeyEvent: _handleKeyEvent, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), + thumbSize: WidgetStatePropertyAll( + (!InputModeTracker.isKeyboardMode(context) || _isFocused) ? const Size(4, 20) : Size.zero, + ), + ), + child: Slider( + value: widget.value, + min: widget.min, + max: widget.max, + divisions: widget.divisions, + onChanged: widget.onChanged, + ), + ), + ); + } +} diff --git a/lib/screens/settings/appearance_settings_screen.dart b/lib/screens/settings/appearance_settings_screen.dart index 50e9ff84..4964db92 100644 --- a/lib/screens/settings/appearance_settings_screen.dart +++ b/lib/screens/settings/appearance_settings_screen.dart @@ -7,8 +7,7 @@ import '../../providers/settings_provider.dart'; import '../../providers/theme_provider.dart'; import '../../providers/user_profile_provider.dart'; import '../../services/settings_service.dart' as settings; -import '../../focus/dpad_navigator.dart'; -import '../../focus/focusable_wrapper.dart'; +import '../../focus/focusable_slider.dart'; import '../../utils/platform_detector.dart'; import '../../widgets/app_icon.dart'; import '../../widgets/focused_scroll_scaffold.dart'; @@ -150,28 +149,12 @@ class _AppearanceSettingsScreenState extends State { const SizedBox(width: 16), Text(t.settings.compact, style: const TextStyle(fontSize: 12, color: Colors.grey)), Expanded( - child: FocusableWrapper( - descendantsAreFocusable: false, - disableScale: true, - onKeyEvent: (node, event) { - final key = event.logicalKey; - if (key.isLeftKey || key.isRightKey) { - if (event.isActionable) { - final delta = key.isRightKey ? 1 : -1; - final newValue = (settingsProvider.libraryDensity + delta).clamp(1, 5); - settingsProvider.setLibraryDensity(newValue); - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - child: Slider( - value: settingsProvider.libraryDensity.toDouble(), - min: 1, - max: 5, - divisions: 4, - onChanged: (value) => settingsProvider.setLibraryDensity(value.round()), - ), + child: FocusableSlider( + value: settingsProvider.libraryDensity.toDouble(), + min: 1, + max: 5, + divisions: 4, + onChanged: (value) => settingsProvider.setLibraryDensity(value.round()), ), ), Text(t.settings.comfortable, style: const TextStyle(fontSize: 12, color: Colors.grey)),