refactor: extract FocusableSlider widget
This commit is contained in:
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user