mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-29 22:00:16 +08:00
feat: video seek preview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -1922,6 +1922,14 @@ List<SettingsModel> get extraSettings => [
|
|||||||
setKey: SettingBoxKey.enableLivePhoto,
|
setKey: SettingBoxKey.enableLivePhoto,
|
||||||
defaultVal: true,
|
defaultVal: true,
|
||||||
),
|
),
|
||||||
|
SettingsModel(
|
||||||
|
settingsType: SettingsType.sw1tch,
|
||||||
|
title: '视频进度条缩略图',
|
||||||
|
subtitle: '滑动进度条时显示视频缩略图',
|
||||||
|
leading: Icon(Icons.preview_outlined),
|
||||||
|
setKey: SettingBoxKey.showSeekPreview,
|
||||||
|
defaultVal: true,
|
||||||
|
),
|
||||||
SettingsModel(
|
SettingsModel(
|
||||||
settingsType: SettingsType.sw1tch,
|
settingsType: SettingsType.sw1tch,
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
|
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
|
||||||
|
import 'package:PiliPlus/http/init.dart';
|
||||||
import 'package:PiliPlus/models/common/audio_normalization.dart';
|
import 'package:PiliPlus/models/common/audio_normalization.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
|
import 'package:PiliPlus/utils/id_utils.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:canvas_danmaku/canvas_danmaku.dart';
|
import 'package:canvas_danmaku/canvas_danmaku.dart';
|
||||||
import 'package:easy_debounce/easy_throttle.dart';
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
@@ -509,6 +511,12 @@ class PlPlayerController {
|
|||||||
_subType = subType;
|
_subType = subType;
|
||||||
_enableHeart = enableHeart;
|
_enableHeart = enableHeart;
|
||||||
|
|
||||||
|
if (showSeekPreview) {
|
||||||
|
videoShot = null;
|
||||||
|
showPreview.value = false;
|
||||||
|
localPosition.value = Offset.zero;
|
||||||
|
}
|
||||||
|
|
||||||
if (_videoPlayerController != null &&
|
if (_videoPlayerController != null &&
|
||||||
_videoPlayerController!.state.playing) {
|
_videoPlayerController!.state.playing) {
|
||||||
await pause(notify: false);
|
await pause(notify: false);
|
||||||
@@ -1564,4 +1572,33 @@ class PlPlayerController {
|
|||||||
videoPlayerController?.setVideoTrack(
|
videoPlayerController?.setVideoTrack(
|
||||||
_onlyPlayAudio.value ? VideoTrack.no() : VideoTrack.auto());
|
_onlyPlayAudio.value ? VideoTrack.no() : VideoTrack.auto());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late final showSeekPreview = GStorage.showSeekPreview;
|
||||||
|
late bool _isQueryingVideoShot = false;
|
||||||
|
Map? videoShot;
|
||||||
|
late final RxBool showPreview = false.obs;
|
||||||
|
late final Rx<Offset> localPosition = Offset.zero.obs;
|
||||||
|
|
||||||
|
void getVideoShot() async {
|
||||||
|
if (_isQueryingVideoShot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isQueryingVideoShot = true;
|
||||||
|
dynamic res = await Request().get(
|
||||||
|
'https://api.bilibili.com/x/player/videoshot',
|
||||||
|
queryParameters: {
|
||||||
|
'aid': IdUtils.bv2av(_bvid),
|
||||||
|
'cid': _cid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
videoShot = {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
videoShot = {'status': false};
|
||||||
|
}
|
||||||
|
_isQueryingVideoShot = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
|
|||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
||||||
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
|
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:PiliPlus/utils/id_utils.dart';
|
import 'package:PiliPlus/utils/id_utils.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:easy_debounce/easy_throttle.dart';
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
@@ -1090,6 +1092,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
value: '${(value / max * 100).round()}%',
|
value: '${(value / max * 100).round()}%',
|
||||||
// enabled: false,
|
// enabled: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
children: [
|
children: [
|
||||||
if (plPlayerController.viewPointList.isNotEmpty &&
|
if (plPlayerController.viewPointList.isNotEmpty &&
|
||||||
@@ -1137,35 +1140,28 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
thumbColor: colorTheme,
|
thumbColor: colorTheme,
|
||||||
barHeight: 3.5,
|
barHeight: 3.5,
|
||||||
thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5,
|
thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5,
|
||||||
// onDragStart: (duration) {
|
onDragStart: (duration) {
|
||||||
// draggingFixedProgressBar.value = true;
|
feedBack();
|
||||||
// feedBack();
|
plPlayerController.onChangedSliderStart();
|
||||||
// _.onChangedSliderStart();
|
},
|
||||||
// },
|
onDragUpdate: (duration) {
|
||||||
// onDragUpdate: (duration) {
|
plPlayerController
|
||||||
// double newProgress = duration.timeStamp.inSeconds / max;
|
.onUpdatedSliderProgress(duration.timeStamp);
|
||||||
// if ((newProgress - _lastAnnouncedValue).abs() > 0.02) {
|
if (plPlayerController.showPreview.value.not) {
|
||||||
// _accessibilityDebounce?.cancel();
|
plPlayerController.showPreview.value = true;
|
||||||
// _accessibilityDebounce =
|
}
|
||||||
// Timer(const Duration(milliseconds: 200), () {
|
plPlayerController.localPosition.value =
|
||||||
// SemanticsService.announce(
|
duration.localPosition;
|
||||||
// "${(newProgress * 100).round()}%",
|
},
|
||||||
// TextDirection.ltr);
|
onSeek: (duration) {
|
||||||
// _lastAnnouncedValue = newProgress;
|
plPlayerController.showPreview.value = false;
|
||||||
// });
|
plPlayerController.onChangedSliderEnd();
|
||||||
// }
|
plPlayerController
|
||||||
// _.onUpdatedSliderProgress(duration.timeStamp);
|
.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
// },
|
plPlayerController.seekTo(
|
||||||
// onSeek: (duration) {
|
Duration(seconds: duration.inSeconds),
|
||||||
// draggingFixedProgressBar.value = false;
|
type: 'slider');
|
||||||
// _.onChangedSliderEnd();
|
},
|
||||||
// _.onChangedSlider(duration.inSeconds.toDouble());
|
|
||||||
// _.seekTo(Duration(seconds: duration.inSeconds),
|
|
||||||
// type: 'slider');
|
|
||||||
// SemanticsService.announce(
|
|
||||||
// "${(duration.inSeconds / max * 100).round()}%",
|
|
||||||
// TextDirection.ltr);
|
|
||||||
// },
|
|
||||||
),
|
),
|
||||||
if (plPlayerController.segmentList.isNotEmpty)
|
if (plPlayerController.segmentList.isNotEmpty)
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -1196,6 +1192,13 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (plPlayerController.showSeekPreview)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 12,
|
||||||
|
child: buildSeekPreviewWidget(plPlayerController),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// SlideTransition(
|
// SlideTransition(
|
||||||
@@ -1478,3 +1481,90 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildSeekPreviewWidget(PlPlayerController plPlayerController) {
|
||||||
|
return Obx(() {
|
||||||
|
if (plPlayerController.showPreview.value.not) {
|
||||||
|
return SizedBox.shrink(
|
||||||
|
key: ValueKey(plPlayerController.localPosition.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (plPlayerController.videoShot == null) {
|
||||||
|
plPlayerController.getVideoShot();
|
||||||
|
return SizedBox.shrink(
|
||||||
|
key: ValueKey(plPlayerController.localPosition.value),
|
||||||
|
);
|
||||||
|
} else if (plPlayerController.videoShot!['status'] == false) {
|
||||||
|
return SizedBox.shrink(
|
||||||
|
key: ValueKey(plPlayerController.localPosition.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
key: ValueKey(plPlayerController.localPosition.value),
|
||||||
|
builder: (context, constraints) {
|
||||||
|
try {
|
||||||
|
double scale = 1.5;
|
||||||
|
// offset
|
||||||
|
double left =
|
||||||
|
(plPlayerController.localPosition.value.dx - 48 * scale / 2)
|
||||||
|
.clamp(8, constraints.maxWidth - 48 * scale - 8);
|
||||||
|
|
||||||
|
// index
|
||||||
|
int index = plPlayerController.sliderPositionSeconds.value ~/ 5;
|
||||||
|
|
||||||
|
// pageIndex
|
||||||
|
int pageIndex = (index ~/ 100).clamp(
|
||||||
|
0,
|
||||||
|
(plPlayerController.videoShot!['data']['image'] as List).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
// alignment
|
||||||
|
double cal(m) {
|
||||||
|
return -1 + 2 / 9 * m;
|
||||||
|
}
|
||||||
|
|
||||||
|
int align = index % 100;
|
||||||
|
int x = align % 10;
|
||||||
|
int y = align ~/ 10;
|
||||||
|
double dx = cal(x);
|
||||||
|
double dy = cal(y);
|
||||||
|
Alignment alignment = Alignment(dx, dy);
|
||||||
|
|
||||||
|
// url
|
||||||
|
String parseUrl(String url) {
|
||||||
|
return url.startsWith('//') ? 'https:$url' : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: EdgeInsets.only(left: left),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 48 * scale,
|
||||||
|
height: 27 * scale,
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Align(
|
||||||
|
widthFactor: 0.1,
|
||||||
|
heightFactor: 0.1,
|
||||||
|
alignment: alignment,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
width: 480 * scale,
|
||||||
|
height: 270 * scale,
|
||||||
|
imageUrl: parseUrl(plPlayerController.videoShot!['data']
|
||||||
|
['image'][pageIndex]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return SizedBox.shrink(
|
||||||
|
key: ValueKey(plPlayerController.localPosition.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
|
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:nil/nil.dart';
|
import 'package:nil/nil.dart';
|
||||||
import 'package:PiliPlus/plugin/pl_player/index.dart';
|
import 'package:PiliPlus/plugin/pl_player/index.dart'
|
||||||
|
show PlPlayerController, buildSeekPreviewWidget;
|
||||||
import 'package:PiliPlus/utils/feed_back.dart';
|
import 'package:PiliPlus/utils/feed_back.dart';
|
||||||
|
|
||||||
import '../../../common/widgets/audio_video_progress_bar.dart';
|
import '../../../common/widgets/audio_video_progress_bar.dart';
|
||||||
@@ -30,7 +32,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
double lastAnnouncedValue = -1;
|
double lastAnnouncedValue = -1;
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
height: 90,
|
height: 120,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@@ -50,6 +52,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
value: '${(value / max * 100).round()}%',
|
value: '${(value / max * 100).round()}%',
|
||||||
// enabled: false,
|
// enabled: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
children: [
|
children: [
|
||||||
if (controller?.viewPointList.isNotEmpty == true &&
|
if (controller?.viewPointList.isNotEmpty == true &&
|
||||||
@@ -101,6 +104,11 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
onDragUpdate: (duration) {
|
onDragUpdate: (duration) {
|
||||||
double newProgress =
|
double newProgress =
|
||||||
duration.timeStamp.inSeconds / max;
|
duration.timeStamp.inSeconds / max;
|
||||||
|
if (controller!.showPreview.value.not) {
|
||||||
|
controller!.showPreview.value = true;
|
||||||
|
}
|
||||||
|
controller!.localPosition.value =
|
||||||
|
duration.localPosition;
|
||||||
if ((newProgress - lastAnnouncedValue).abs() > 0.02) {
|
if ((newProgress - lastAnnouncedValue).abs() > 0.02) {
|
||||||
accessibilityDebounce?.cancel();
|
accessibilityDebounce?.cancel();
|
||||||
accessibilityDebounce =
|
accessibilityDebounce =
|
||||||
@@ -115,6 +123,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
.onUpdatedSliderProgress(duration.timeStamp);
|
.onUpdatedSliderProgress(duration.timeStamp);
|
||||||
},
|
},
|
||||||
onSeek: (duration) {
|
onSeek: (duration) {
|
||||||
|
controller!.showPreview.value = false;
|
||||||
controller!.onChangedSliderEnd();
|
controller!.onChangedSliderEnd();
|
||||||
controller!
|
controller!
|
||||||
.onChangedSlider(duration.inSeconds.toDouble());
|
.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
@@ -155,6 +164,13 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (controller?.showSeekPreview == true)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 16,
|
||||||
|
child: buildSeekPreviewWidget(controller!),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -357,6 +357,9 @@ class GStorage {
|
|||||||
static bool get enableLivePhoto =>
|
static bool get enableLivePhoto =>
|
||||||
GStorage.setting.get(SettingBoxKey.enableLivePhoto, defaultValue: true);
|
GStorage.setting.get(SettingBoxKey.enableLivePhoto, defaultValue: true);
|
||||||
|
|
||||||
|
static bool get showSeekPreview =>
|
||||||
|
GStorage.setting.get(SettingBoxKey.showSeekPreview, defaultValue: true);
|
||||||
|
|
||||||
static List<double> get dynamicDetailRatio => List<double>.from(setting
|
static List<double> get dynamicDetailRatio => List<double>.from(setting
|
||||||
.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]));
|
.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]));
|
||||||
|
|
||||||
@@ -585,6 +588,7 @@ class SettingBoxKey {
|
|||||||
searchSuggestion = 'searchSuggestion',
|
searchSuggestion = 'searchSuggestion',
|
||||||
showDynDecorate = 'showDynDecorate',
|
showDynDecorate = 'showDynDecorate',
|
||||||
enableLivePhoto = 'enableLivePhoto',
|
enableLivePhoto = 'enableLivePhoto',
|
||||||
|
showSeekPreview = 'showSeekPreview',
|
||||||
|
|
||||||
// Sponsor Block
|
// Sponsor Block
|
||||||
enableSponsorBlock = 'enableSponsorBlock',
|
enableSponsorBlock = 'enableSponsorBlock',
|
||||||
|
|||||||
Reference in New Issue
Block a user