refactor: extract FocusableSlider widget

This commit is contained in:
edde746
2026-04-04 19:22:23 +02:00
parent 8f326e692d
commit a262c94f88
2 changed files with 93 additions and 24 deletions
+86
View File
@@ -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<double>? 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<FocusableSlider> createState() => _FocusableSliderState();
}
class _FocusableSliderState extends State<FocusableSlider> {
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,
),
),
);
}
}
@@ -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<AppearanceSettingsScreen> {
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)),