mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-06 10:07:48 +08:00
244 lines
6.3 KiB
Dart
244 lines
6.3 KiB
Dart
import 'dart:async' show Timer;
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:PiliPlus/common/widgets/flutter/vertical_slider.dart';
|
|
import 'package:PiliPlus/pages/audio/controller.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
|
|
import 'package:get/get.dart';
|
|
|
|
class VolumeButton extends StatefulWidget {
|
|
const VolumeButton({
|
|
super.key,
|
|
required this.controller,
|
|
});
|
|
|
|
final AudioController controller;
|
|
|
|
@override
|
|
State<VolumeButton> createState() => _VolumeButtonState();
|
|
}
|
|
|
|
class _VolumeButtonState extends State<VolumeButton> {
|
|
final _controller = OverlayPortalController();
|
|
Timer? _timer;
|
|
late CardThemeData cardTheme;
|
|
late ColorScheme theme;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
theme = ColorScheme.of(context);
|
|
cardTheme = CardTheme.of(context);
|
|
}
|
|
|
|
void _stopTimer([_]) {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
}
|
|
|
|
void _show([_]) {
|
|
_stopTimer();
|
|
_controller.show();
|
|
}
|
|
|
|
void _scheduleDismiss([_]) {
|
|
_timer ??= Timer(const Duration(milliseconds: 100), () {
|
|
_controller.hide();
|
|
_timer = null;
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_stopTimer();
|
|
if (_controller.isShowing) {
|
|
_controller.hide();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MouseRegion(
|
|
onEnter: _show,
|
|
onExit: _scheduleDismiss,
|
|
cursor: SystemMouseCursors.click,
|
|
child: OverlayPortal.overlayChildLayoutBuilder(
|
|
controller: _controller,
|
|
overlayChildBuilder: _overlayChildBuilder,
|
|
child: Obx(() {
|
|
final volume = widget.controller.desktopVolume.value;
|
|
return InkWell(
|
|
onTapUp: _onTapUp,
|
|
customBorder: const CircleBorder(),
|
|
child: Padding(
|
|
padding: const .all(10.0),
|
|
child: Icon(
|
|
volume == 0.0
|
|
? Icons.volume_off
|
|
: volume < 0.5
|
|
? Icons.volume_down
|
|
: Icons.volume_up,
|
|
color: theme.onSurfaceVariant,
|
|
size: 22.0,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _overlayChildBuilder(
|
|
BuildContext context,
|
|
OverlayChildLayoutInfo info,
|
|
) {
|
|
final offset = MatrixUtils.transformPoint(
|
|
info.childPaintTransform,
|
|
info.childSize.topCenter(const Offset(0, -6)),
|
|
);
|
|
return _volumeSlider(offset);
|
|
}
|
|
|
|
Widget _volumeSlider(Offset offset) {
|
|
return _VolumeWidget(
|
|
offset: offset,
|
|
child: MouseRegion(
|
|
onEnter: _stopTimer,
|
|
onExit: _scheduleDismiss,
|
|
child: Container(
|
|
padding: const .fromLTRB(6, 8, 6, 2),
|
|
decoration: BoxDecoration(
|
|
color: ElevationOverlay.applySurfaceTint(
|
|
cardTheme.color ?? theme.surfaceContainerLow,
|
|
cardTheme.surfaceTintColor,
|
|
2,
|
|
),
|
|
borderRadius: const .all(.circular(6)),
|
|
),
|
|
child: SliderTheme(
|
|
data: const SliderThemeData(
|
|
trackHeight: 4,
|
|
overlayColor: Colors.transparent,
|
|
thumbShape: RoundSliderThumbShape(
|
|
enabledThumbRadius: 6,
|
|
),
|
|
),
|
|
child: Obx(
|
|
() {
|
|
final volume = widget.controller.desktopVolume.value;
|
|
return Column(
|
|
spacing: 2,
|
|
mainAxisSize: .min,
|
|
children: [
|
|
Text(
|
|
'${(volume * 100).round()}',
|
|
style: const TextStyle(fontSize: 13),
|
|
),
|
|
Expanded(
|
|
child: VerticalSlider(
|
|
year2023: true,
|
|
min: 0.0,
|
|
max: 2.0,
|
|
value: volume,
|
|
showValueIndicator: .never,
|
|
onChanged: widget.controller.setVolume,
|
|
onChangeEnd: widget.controller.syncVolume,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onTapUp(TapUpDetails details) {
|
|
switch (details.kind) {
|
|
case .mouse:
|
|
widget.controller.toggleVolume();
|
|
case _:
|
|
_showVolumeDialog();
|
|
}
|
|
}
|
|
|
|
void _showVolumeDialog() {
|
|
final renderBox = context.findRenderObject() as RenderBox;
|
|
final offset = renderBox.localToGlobal(
|
|
renderBox.size.topCenter(const Offset(0, -6)),
|
|
);
|
|
Get.key.currentState!.push(
|
|
DialogRoute(
|
|
context: context,
|
|
useSafeArea: false,
|
|
barrierColor: Colors.transparent,
|
|
builder: (context) {
|
|
return _volumeSlider(offset);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _VolumeWidget extends SingleChildRenderObjectWidget {
|
|
const _VolumeWidget({
|
|
required this.offset,
|
|
required Widget super.child,
|
|
});
|
|
|
|
final Offset offset;
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return _RenderVolumeWidget(offset: offset);
|
|
}
|
|
}
|
|
|
|
class _RenderVolumeWidget extends RenderProxyBox {
|
|
_RenderVolumeWidget({required this.offset});
|
|
|
|
final Offset offset;
|
|
late Offset _offset;
|
|
|
|
@override
|
|
void performLayout() {
|
|
final childSize =
|
|
(child!..layout(
|
|
const BoxConstraints(maxWidth: 40, maxHeight: 170),
|
|
parentUsesSize: true,
|
|
))
|
|
.size;
|
|
size = constraints.biggest;
|
|
_offset = Offset(
|
|
math.min(offset.dx - (childSize.width / 2), size.width - childSize.width),
|
|
math.min(offset.dy, size.height) - childSize.height,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
super.paint(context, _offset);
|
|
}
|
|
|
|
@override
|
|
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
|
return result.addWithPaintOffset(
|
|
offset: _offset,
|
|
position: position,
|
|
hitTest: (BoxHitTestResult result, Offset transformed) {
|
|
assert(transformed == position - _offset);
|
|
return child!.hitTest(result, position: transformed);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
|
|
transform.translateByDouble(_offset.dx, _offset.dy, 0.0, 1.0);
|
|
}
|
|
}
|