add audio volume button & slider on desktop

Closes #1950

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-05-03 21:23:03 +08:00
parent 9259e84d5c
commit d5bf3487f8
4 changed files with 2905 additions and 1 deletions

View File

@@ -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;

View File

@@ -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;
}

View 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);
}
}