mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-06 10:07:48 +08:00
add audio volume button & slider on desktop
Closes #1950 Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
@@ -23,6 +23,7 @@ import 'package:PiliPlus/pages/sponsor_block/block_mixin.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_mixin.dart';
|
||||
import 'package:PiliPlus/pages/video/pay_coins/view.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/play_status.dart';
|
||||
import 'package:PiliPlus/services/service_locator.dart';
|
||||
@@ -36,6 +37,8 @@ import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:PiliPlus/utils/share_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/video_utils.dart';
|
||||
@@ -94,6 +97,34 @@ class AudioController extends GetxController
|
||||
|
||||
ListOrder order = ListOrder.ORDER_NORMAL;
|
||||
|
||||
double? _lastVolume;
|
||||
late final RxDouble desktopVolume = RxDouble(Pref.desktopVolume);
|
||||
|
||||
void toggleVolume() {
|
||||
if (_lastVolume == null) {
|
||||
_lastVolume = desktopVolume.value;
|
||||
setVolume(0, clearLastVolme: false);
|
||||
} else {
|
||||
setVolume(_lastVolume!);
|
||||
}
|
||||
}
|
||||
|
||||
void setVolume(double volume, {bool clearLastVolme = true}) {
|
||||
if (clearLastVolme) {
|
||||
_lastVolume = null;
|
||||
}
|
||||
desktopVolume.value = volume;
|
||||
player?.setVolume(volume * 100);
|
||||
}
|
||||
|
||||
void syncVolume([_]) {
|
||||
final volume = desktopVolume.value;
|
||||
PlPlayerController.instance
|
||||
?..volume.value = volume
|
||||
..videoPlayerController?.setVolume(volume * 100);
|
||||
GStorage.setting.put(SettingBoxKey.desktopVolume, volume.toPrecision(3));
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@@ -296,7 +327,13 @@ class AudioController extends GetxController
|
||||
if (_hasInit) return;
|
||||
_hasInit = true;
|
||||
assert(player == null, _subscriptions = null);
|
||||
player = await Player.create();
|
||||
player = await Player.create(
|
||||
configuration: PlatformUtils.isDesktop
|
||||
? PlayerConfiguration(
|
||||
options: {'volume': (desktopVolume.value * 100).toString()},
|
||||
)
|
||||
: const PlayerConfiguration(),
|
||||
);
|
||||
if (isClosed) {
|
||||
player!.dispose();
|
||||
player = null;
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/pages/audio/controller.dart';
|
||||
import 'package:PiliPlus/pages/audio/volume_button.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:PiliPlus/services/shutdown_timer_service.dart';
|
||||
@@ -29,6 +30,7 @@ import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -795,6 +797,15 @@ class _AudioPageState extends State<AudioPage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
if (kDebugMode || PlatformUtils.isDesktop) {
|
||||
child = Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Expanded(child: child),
|
||||
VolumeButton(controller: _controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
|
||||
243
lib/pages/audio/volume_button.dart
Normal file
243
lib/pages/audio/volume_button.dart
Normal file
@@ -0,0 +1,243 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user