mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-08 11:07:41 +08:00
add audio volume button & slider on desktop
Closes #1950 Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
2613
lib/common/widgets/flutter/vertical_slider.dart
Normal file
2613
lib/common/widgets/flutter/vertical_slider.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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/controller.dart';
|
||||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_mixin.dart';
|
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_mixin.dart';
|
||||||
import 'package:PiliPlus/pages/video/pay_coins/view.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_repeat.dart';
|
||||||
import 'package:PiliPlus/plugin/pl_player/models/play_status.dart';
|
import 'package:PiliPlus/plugin/pl_player/models/play_status.dart';
|
||||||
import 'package:PiliPlus/services/service_locator.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/page_utils.dart';
|
||||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||||
import 'package:PiliPlus/utils/share_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/storage_pref.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:PiliPlus/utils/video_utils.dart';
|
import 'package:PiliPlus/utils/video_utils.dart';
|
||||||
@@ -94,6 +97,34 @@ class AudioController extends GetxController
|
|||||||
|
|
||||||
ListOrder order = ListOrder.ORDER_NORMAL;
|
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
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@@ -296,7 +327,13 @@ class AudioController extends GetxController
|
|||||||
if (_hasInit) return;
|
if (_hasInit) return;
|
||||||
_hasInit = true;
|
_hasInit = true;
|
||||||
assert(player == null, _subscriptions = null);
|
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) {
|
if (isClosed) {
|
||||||
player!.dispose();
|
player!.dispose();
|
||||||
player = null;
|
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_preview_type.dart';
|
||||||
import 'package:PiliPlus/models/common/image_type.dart';
|
import 'package:PiliPlus/models/common/image_type.dart';
|
||||||
import 'package:PiliPlus/pages/audio/controller.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/pages/video/introduction/ugc/widgets/action_item.dart';
|
||||||
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
|
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
|
||||||
import 'package:PiliPlus/services/shutdown_timer_service.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.dart';
|
||||||
import 'package:PiliPlus/utils/storage_key.dart';
|
import 'package:PiliPlus/utils/storage_key.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||||
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
|
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.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;
|
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