mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
tool bar: RtL slider & button swap (#50)
This commit is contained in:
224
lib/components/custom_slider.dart
Normal file
224
lib/components/custom_slider.dart
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// patched slider.dart with RtL support
|
||||||
|
class _SliderDefaultsM3 extends SliderThemeData {
|
||||||
|
_SliderDefaultsM3(this.context)
|
||||||
|
: super(trackHeight: 4.0);
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get activeTrackColor => _colors.primary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get inactiveTrackColor => _colors.surfaceContainerHighest;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.38);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withOpacity(0.38);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get disabledActiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get thumbColor => _colors.primary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get overlayColor => WidgetStateColor.resolveWith((Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.dragged)) {
|
||||||
|
return _colors.primary.withOpacity(0.1);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.hovered)) {
|
||||||
|
return _colors.primary.withOpacity(0.08);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.focused)) {
|
||||||
|
return _colors.primary.withOpacity(0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Colors.transparent;
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelMedium!.copyWith(
|
||||||
|
color: _colors.onPrimary,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomSlider extends StatefulWidget {
|
||||||
|
const CustomSlider({required this.min, required this.max, required this.value, required this.divisions, required this.onChanged, required this.focusNode, this.reversed = false, super.key});
|
||||||
|
|
||||||
|
final double min;
|
||||||
|
|
||||||
|
final double max;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
|
||||||
|
final int divisions;
|
||||||
|
|
||||||
|
final void Function(double) onChanged;
|
||||||
|
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
|
final bool reversed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomSlider> createState() => _CustomSliderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomSliderState extends State<CustomSlider> {
|
||||||
|
late double value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
value = widget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(CustomSlider oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != oldWidget.value) {
|
||||||
|
setState(() {
|
||||||
|
value = widget.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final theme = _SliderDefaultsM3(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 12),
|
||||||
|
child: widget.max - widget.min > 0 ? LayoutBuilder(
|
||||||
|
builder: (context, constraints) => MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTapDown: (details){
|
||||||
|
var dx = details.localPosition.dx;
|
||||||
|
if(widget.reversed){
|
||||||
|
dx = constraints.maxWidth - dx;
|
||||||
|
}
|
||||||
|
var gap = constraints.maxWidth / widget.divisions;
|
||||||
|
var gapValue = (widget.max - widget.min) / widget.divisions;
|
||||||
|
widget.onChanged.call((dx / gap).round() * gapValue + widget.min);
|
||||||
|
},
|
||||||
|
onVerticalDragUpdate: (details){
|
||||||
|
var dx = details.localPosition.dx;
|
||||||
|
if(dx > constraints.maxWidth || dx < 0) return;
|
||||||
|
if(widget.reversed){
|
||||||
|
dx = constraints.maxWidth - dx;
|
||||||
|
}
|
||||||
|
var gap = constraints.maxWidth / widget.divisions;
|
||||||
|
var gapValue = (widget.max - widget.min) / widget.divisions;
|
||||||
|
widget.onChanged.call((dx / gap).round() * gapValue + widget.min);
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.inactiveTrackColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(10))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if(constraints.maxWidth / widget.divisions > 10)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Row(
|
||||||
|
children: (){
|
||||||
|
var res = <Widget>[];
|
||||||
|
for(int i = 0; i<widget.divisions-1; i++){
|
||||||
|
res.add(const Spacer());
|
||||||
|
res.add(Container(
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surface.withRed(10),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
res.add(const Spacer());
|
||||||
|
return res;
|
||||||
|
}.call(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: widget.reversed ? null : 0,
|
||||||
|
right: widget.reversed ? 0 : null,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: constraints.maxWidth * ((value - widget.min) / (widget.max - widget.min)),
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.activeTrackColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(10))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: widget.reversed ? null : constraints.maxWidth * ((value - widget.min) / (widget.max - widget.min))-11,
|
||||||
|
right: !widget.reversed ? null : constraints.maxWidth * ((value - widget.min) / (widget.max - widget.min))-11,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.activeTrackColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -12,6 +12,7 @@ import 'package:photo_view/photo_view.dart';
|
|||||||
import 'package:photo_view/photo_view_gallery.dart';
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/components/custom_slider.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/cache_manager.dart';
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
|
@@ -18,6 +18,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
|
|
||||||
bool get isOpen => _isOpen;
|
bool get isOpen => _isOpen;
|
||||||
|
|
||||||
|
bool get isReversed => context.reader.mode == ReaderMode.galleryRightToLeft ||
|
||||||
|
context.reader.mode == ReaderMode.continuousRightToLeft;
|
||||||
|
|
||||||
int showFloatingButtonValue = 0;
|
int showFloatingButtonValue = 0;
|
||||||
|
|
||||||
var lastValue = 0;
|
var lastValue = 0;
|
||||||
@@ -217,34 +220,26 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
onPressed: () {
|
onPressed: () => !isReversed
|
||||||
if (!context.reader.toPrevChapter()) {
|
? context.reader.chapter > 1
|
||||||
context.reader.toPage(1);
|
? context.reader.toPrevChapter()
|
||||||
} else {
|
: context.reader.toPage(1)
|
||||||
if (showFloatingButtonValue != 0) {
|
: context.reader.chapter < context.reader.maxChapter
|
||||||
setState(() {
|
? context.reader.toNextChapter()
|
||||||
showFloatingButtonValue = 0;
|
: context.reader.toPage(context.reader.maxPage),
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.first_page),
|
icon: const Icon(Icons.first_page),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: buildSlider(),
|
child: buildSlider(),
|
||||||
),
|
),
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
onPressed: () {
|
onPressed: () => !isReversed
|
||||||
if (!context.reader.toNextChapter()) {
|
? context.reader.chapter < context.reader.maxChapter
|
||||||
context.reader.toPage(context.reader.maxPage);
|
? context.reader.toNextChapter()
|
||||||
} else {
|
: context.reader.toPage(context.reader.maxPage)
|
||||||
if (showFloatingButtonValue != 0) {
|
: context.reader.chapter > 1
|
||||||
setState(() {
|
? context.reader.toPrevChapter()
|
||||||
showFloatingButtonValue = 0;
|
: context.reader.toPage(1),
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.last_page)),
|
icon: const Icon(Icons.last_page)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
@@ -379,12 +374,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
var sliderFocus = FocusNode();
|
var sliderFocus = FocusNode();
|
||||||
|
|
||||||
Widget buildSlider() {
|
Widget buildSlider() {
|
||||||
return Slider(
|
return CustomSlider(
|
||||||
focusNode: sliderFocus,
|
focusNode: sliderFocus,
|
||||||
value: context.reader.page.toDouble(),
|
value: context.reader.page.toDouble(),
|
||||||
min: 1,
|
min: 1,
|
||||||
max:
|
max:
|
||||||
context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
|
context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
|
||||||
|
reversed: isReversed,
|
||||||
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
||||||
onChanged: (i) {
|
onChanged: (i) {
|
||||||
context.reader.toPage(i.toInt());
|
context.reader.toPage(i.toInt());
|
||||||
|
Reference in New Issue
Block a user