mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
opt video seek preview (#1026)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -1,29 +1,34 @@
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
|
||||
class VideoShotData {
|
||||
String? pvdata;
|
||||
int? imgXLen;
|
||||
int? imgYLen;
|
||||
int? imgXSize;
|
||||
int? imgYSize;
|
||||
List<String>? image;
|
||||
List<int>? index;
|
||||
int imgXLen;
|
||||
int imgYLen;
|
||||
double imgXSize;
|
||||
double imgYSize;
|
||||
late final int totalPerImage = imgXLen * imgYLen;
|
||||
List<String> image;
|
||||
List<int> index;
|
||||
|
||||
VideoShotData({
|
||||
this.pvdata,
|
||||
this.imgXLen,
|
||||
this.imgYLen,
|
||||
this.imgXSize,
|
||||
this.imgYSize,
|
||||
this.image,
|
||||
this.index,
|
||||
required this.imgXLen,
|
||||
required this.imgYLen,
|
||||
required this.imgXSize,
|
||||
required this.imgYSize,
|
||||
required this.image,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
factory VideoShotData.fromJson(Map<String, dynamic> json) => VideoShotData(
|
||||
pvdata: json["pvdata"],
|
||||
imgXLen: json["img_x_len"],
|
||||
imgYLen: json["img_y_len"],
|
||||
imgXSize: json["img_x_size"],
|
||||
imgYSize: json["img_y_size"],
|
||||
image: (json["image"] as List?)?.cast(),
|
||||
index: (json["index"] as List?)?.cast(),
|
||||
imgXSize: (json["img_x_size"] as num).toDouble(),
|
||||
imgYSize: (json["img_y_size"] as num).toDouble(),
|
||||
image: (json["image"] as List)
|
||||
.map((e) => (e as String).http2https)
|
||||
.toList(),
|
||||
index: (json["index"] as List).cast(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' show max;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/ua_type.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/common/account_type.dart';
|
||||
import 'package:PiliPlus/models/common/audio_normalization.dart';
|
||||
@@ -33,6 +37,7 @@ import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:canvas_danmaku/canvas_danmaku.dart';
|
||||
import 'package:crclib/catalog.dart';
|
||||
import 'package:dio/dio.dart' show Options;
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -561,9 +566,7 @@ class PlPlayerController {
|
||||
_pgcType = pgcType;
|
||||
|
||||
if (showSeekPreview) {
|
||||
videoShot = null;
|
||||
showPreview.value = false;
|
||||
previewDx.value = 0;
|
||||
_clearPreview();
|
||||
}
|
||||
|
||||
if (_videoPlayerController != null &&
|
||||
@@ -1542,6 +1545,7 @@ class PlPlayerController {
|
||||
}
|
||||
dmState.clear();
|
||||
_playerCount = 0;
|
||||
_clearPreview();
|
||||
Utils.channel.setMethodCallHandler(null);
|
||||
pause();
|
||||
try {
|
||||
@@ -1600,17 +1604,48 @@ class PlPlayerController {
|
||||
);
|
||||
}
|
||||
|
||||
late final showSeekPreview = Pref.showSeekPreview;
|
||||
late bool _isQueryingVideoShot = false;
|
||||
Map? videoShot;
|
||||
Map<String, WeakReference<ui.Image>>? previewCache;
|
||||
LoadingState<VideoShotData>? videoShot;
|
||||
late final RxBool showPreview = false.obs;
|
||||
late final RxDouble previewDx = 0.0.obs;
|
||||
late final showSeekPreview = Pref.showSeekPreview;
|
||||
late final Rx<int?> previewIndex = Rx<int?>(null);
|
||||
|
||||
Future<void> getVideoShot() async {
|
||||
if (_isQueryingVideoShot) {
|
||||
void updatePreviewIndex(int seconds) {
|
||||
if (videoShot == null) {
|
||||
videoShot = LoadingState.loading();
|
||||
getVideoShot();
|
||||
return;
|
||||
}
|
||||
_isQueryingVideoShot = true;
|
||||
if (videoShot case Success<VideoShotData> success) {
|
||||
final data = success.response;
|
||||
if (data.index.isNullOrEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!showPreview.value) {
|
||||
showPreview.value = true;
|
||||
}
|
||||
previewIndex.value = max(
|
||||
0,
|
||||
(data.index.where((item) => item <= seconds).length - 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _clearPreview() {
|
||||
showPreview.value = false;
|
||||
previewIndex.value = null;
|
||||
videoShot = null;
|
||||
previewCache
|
||||
?..forEach((_, ref) {
|
||||
try {
|
||||
ref.target?.dispose();
|
||||
} catch (_) {}
|
||||
})
|
||||
..clear();
|
||||
previewCache = null;
|
||||
}
|
||||
|
||||
Future<void> getVideoShot() async {
|
||||
try {
|
||||
var res = await Request().get(
|
||||
'/x/player/videoshot',
|
||||
@@ -1620,20 +1655,22 @@ class PlPlayerController {
|
||||
'cid': _cid,
|
||||
'index': 1,
|
||||
},
|
||||
options: Options(
|
||||
headers: {
|
||||
'user-agent': UaType.pc.ua,
|
||||
'referer': 'https://www.bilibili.com/video/$bvid',
|
||||
},
|
||||
),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
videoShot = {
|
||||
'status': true,
|
||||
'data': VideoShotData.fromJson(res.data['data']),
|
||||
};
|
||||
videoShot = Success(VideoShotData.fromJson(res.data['data']));
|
||||
} else {
|
||||
videoShot = {'status': false};
|
||||
videoShot = const Error(null);
|
||||
}
|
||||
} catch (e) {
|
||||
videoShot = {'status': false};
|
||||
videoShot = const Error(null);
|
||||
if (kDebugMode) debugPrint('getVideoShot: $e');
|
||||
}
|
||||
_isQueryingVideoShot = false;
|
||||
}
|
||||
|
||||
late final RxList<double> dmTrend = <double>[].obs;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/episode.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/section.dart';
|
||||
@@ -26,13 +28,13 @@ import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/forward_seek.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/play_pause_btn.dart';
|
||||
import 'package:PiliPlus/utils/duration_util.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
@@ -253,7 +255,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
}
|
||||
|
||||
// 动态构建底部控制条
|
||||
Widget buildBottomControl(bool isLandscape, double maxWidth) {
|
||||
Widget buildBottomControl(bool isLandscape) {
|
||||
final videoDetail = introController.videoDetail.value;
|
||||
final isSeason = videoDetail.ugcSeason != null;
|
||||
final isPart = videoDetail.pages != null && videoDetail.pages!.length > 1;
|
||||
@@ -780,16 +782,17 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
|
||||
final int curSliderPosition =
|
||||
plPlayerController.sliderPosition.value.inMilliseconds;
|
||||
final Duration pos = Duration(
|
||||
milliseconds:
|
||||
curSliderPosition +
|
||||
(plPlayerController.sliderScale * delta.dx / maxWidth)
|
||||
.round(),
|
||||
);
|
||||
final Duration result = pos.clamp(
|
||||
Duration.zero,
|
||||
plPlayerController.duration.value,
|
||||
);
|
||||
final int newPos =
|
||||
(curSliderPosition +
|
||||
(plPlayerController.sliderScale *
|
||||
delta.dx /
|
||||
maxWidth)
|
||||
.round())
|
||||
.clamp(
|
||||
0,
|
||||
plPlayerController.duration.value.inMilliseconds,
|
||||
);
|
||||
final Duration result = Duration(milliseconds: newPos);
|
||||
final height = maxHeight * 0.125;
|
||||
if (details.localFocalPoint.dy <= height &&
|
||||
(details.localFocalPoint.dx >= maxWidth * 0.875 ||
|
||||
@@ -837,18 +840,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
..onChangedSliderStart();
|
||||
if (plPlayerController.showSeekPreview &&
|
||||
plPlayerController.cancelSeek != true) {
|
||||
try {
|
||||
plPlayerController.previewDx.value =
|
||||
result.inMilliseconds /
|
||||
plPlayerController
|
||||
.durationSeconds
|
||||
.value
|
||||
.inMilliseconds *
|
||||
maxWidth;
|
||||
if (!plPlayerController.showPreview.value) {
|
||||
plPlayerController.showPreview.value = true;
|
||||
}
|
||||
} catch (_) {}
|
||||
plPlayerController.updatePreviewIndex(newPos ~/ 1000);
|
||||
}
|
||||
} else if (_gestureType == GestureType.left) {
|
||||
// 左边区域 👈
|
||||
@@ -1102,7 +1094,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: FractionalTranslation(
|
||||
translation: const Offset(0.0, 1.0), // 上下偏移量(负数向上偏移)
|
||||
translation: const Offset(0.0, 0.6),
|
||||
child: Obx(
|
||||
() => AnimatedOpacity(
|
||||
curve: Curves.easeInOut,
|
||||
@@ -1271,11 +1263,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
widget.bottomControl ??
|
||||
BottomControl(
|
||||
controller: plPlayerController,
|
||||
buildBottomControl: (bottomMaxWidth) =>
|
||||
buildBottomControl(
|
||||
maxWidth > maxHeight,
|
||||
bottomMaxWidth,
|
||||
),
|
||||
buildBottomControl: () =>
|
||||
buildBottomControl(maxWidth > maxHeight),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1448,22 +1437,19 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
if (plPlayerController.dmTrend.isNotEmpty &&
|
||||
plPlayerController.showDmTreandChart.value)
|
||||
buildDmChart(theme, plPlayerController),
|
||||
if (plPlayerController.showSeekPreview)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 12,
|
||||
child: buildSeekPreviewWidget(
|
||||
plPlayerController,
|
||||
maxWidth,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
if (plPlayerController.showSeekPreview)
|
||||
buildSeekPreviewWidget(
|
||||
plPlayerController,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
),
|
||||
|
||||
// 锁
|
||||
SafeArea(
|
||||
child: Obx(
|
||||
@@ -1781,81 +1767,66 @@ Widget buildDmChart(
|
||||
Widget buildSeekPreviewWidget(
|
||||
PlPlayerController plPlayerController,
|
||||
double maxWidth,
|
||||
double maxHeight,
|
||||
) {
|
||||
return Obx(
|
||||
() {
|
||||
if (!plPlayerController.showPreview.value ||
|
||||
plPlayerController.videoShot?['status'] != true) {
|
||||
if (plPlayerController.videoShot == null) {
|
||||
plPlayerController.getVideoShot();
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
VideoShotData data = plPlayerController.videoShot!['data'];
|
||||
|
||||
if (data.index.isNullOrEmpty) {
|
||||
if (!plPlayerController.showPreview.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
try {
|
||||
double scale =
|
||||
plPlayerController.isFullScreen.value &&
|
||||
!plPlayerController.isVertical
|
||||
VideoShotData data = plPlayerController.videoShot!.data;
|
||||
|
||||
final isFullScreen = plPlayerController.isFullScreen.value;
|
||||
final double scale = isFullScreen && !plPlayerController.isVertical
|
||||
? 4
|
||||
: 2.5;
|
||||
// offset
|
||||
double left = (plPlayerController.previewDx.value - 48 * scale / 2)
|
||||
.clamp(8, maxWidth - 48 * scale - 8);
|
||||
|
||||
// index
|
||||
// int index = plPlayerController.sliderPositionSeconds.value ~/ 5;
|
||||
int index = max(
|
||||
0,
|
||||
(data.index!
|
||||
.where(
|
||||
(item) =>
|
||||
item <= plPlayerController.sliderPositionSeconds.value,
|
||||
)
|
||||
.length -
|
||||
2),
|
||||
);
|
||||
|
||||
// pageIndex
|
||||
int pageIndex = (index ~/ 100).clamp(0, data.image!.length - 1);
|
||||
|
||||
// alignment
|
||||
double cal(m) {
|
||||
return -1 + 2 / 9 * m;
|
||||
: 3;
|
||||
double height = 27 * scale;
|
||||
final compatHeight = maxHeight - 140;
|
||||
if (compatHeight > 50) {
|
||||
height = min(height, compatHeight);
|
||||
}
|
||||
|
||||
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);
|
||||
final int imgXLen = data.imgXLen;
|
||||
final int imgYLen = data.imgYLen;
|
||||
final int totalPerImage = data.totalPerImage;
|
||||
double imgXSize = data.imgXSize;
|
||||
double imgYSize = data.imgYSize;
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.only(left: left),
|
||||
child: UnconstrainedBox(
|
||||
child: ClipRRect(
|
||||
borderRadius: scale == 2.5
|
||||
? const BorderRadius.all(Radius.circular(6))
|
||||
: StyleString.mdRadius,
|
||||
child: Align(
|
||||
widthFactor: 0.1,
|
||||
heightFactor: 0.1,
|
||||
alignment: alignment,
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.fill,
|
||||
width: 480 * scale,
|
||||
height: 270 * scale,
|
||||
imageUrl: data.image![pageIndex].http2https,
|
||||
return Align(
|
||||
alignment: isFullScreen ? Alignment.center : Alignment.center,
|
||||
child: Obx(
|
||||
() {
|
||||
final index = plPlayerController.previewIndex.value!;
|
||||
int pageIndex = (index ~/ totalPerImage).clamp(
|
||||
0,
|
||||
data.image.length - 1,
|
||||
);
|
||||
int align = index % totalPerImage;
|
||||
int x = align % imgXLen;
|
||||
int y = align ~/ imgYLen;
|
||||
final url = data.image[pageIndex];
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: VideoShotImage(
|
||||
url: url,
|
||||
x: x,
|
||||
y: y,
|
||||
imgXSize: imgXSize,
|
||||
imgYSize: imgYSize,
|
||||
height: height,
|
||||
image: plPlayerController.previewCache?[url]?.target,
|
||||
onCacheImg: (img) =>
|
||||
(plPlayerController.previewCache ??= {})[url] ??=
|
||||
WeakReference(img),
|
||||
onSetSize: (xSize, ySize) => data
|
||||
..imgXSize = imgXSize = xSize
|
||||
..imgYSize = imgYSize = ySize,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -1866,6 +1837,193 @@ Widget buildSeekPreviewWidget(
|
||||
);
|
||||
}
|
||||
|
||||
class VideoShotImage extends StatefulWidget {
|
||||
const VideoShotImage({
|
||||
super.key,
|
||||
this.image,
|
||||
required this.url,
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.imgXSize,
|
||||
required this.imgYSize,
|
||||
required this.height,
|
||||
required this.onCacheImg,
|
||||
required this.onSetSize,
|
||||
});
|
||||
|
||||
final ui.Image? image;
|
||||
final String url;
|
||||
final int x;
|
||||
final int y;
|
||||
final double imgXSize;
|
||||
final double imgYSize;
|
||||
final double height;
|
||||
final ValueChanged<ui.Image> onCacheImg;
|
||||
final Function(double imgXSize, double imgYSize) onSetSize;
|
||||
|
||||
@override
|
||||
State<VideoShotImage> createState() => _VideoShotImageState();
|
||||
}
|
||||
|
||||
Future<ui.Image?> _getImg(String url) async {
|
||||
final cacheManager = DefaultCacheManager();
|
||||
final cacheKey = url.hashCode.toString();
|
||||
final fileInfo = await cacheManager.getFileFromCache(cacheKey);
|
||||
if (fileInfo != null) {
|
||||
final bytes = await fileInfo.file.readAsBytes();
|
||||
final codec = await ui.instantiateImageCodec(bytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
codec.dispose();
|
||||
return frame.image;
|
||||
} else {
|
||||
final res = await Request().get(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
if (res.statusCode == 200) {
|
||||
final data = res.data;
|
||||
cacheManager.putFile(cacheKey, data, fileExtension: 'jpg');
|
||||
final codec = await ui.instantiateImageCodec(data);
|
||||
final frame = await codec.getNextFrame();
|
||||
return frame.image;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class _VideoShotImageState extends State<VideoShotImage> {
|
||||
late Size _size;
|
||||
late Rect _dstRect;
|
||||
late RRect _rrect;
|
||||
ui.Image? _image;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initSize();
|
||||
_loadImg();
|
||||
}
|
||||
|
||||
void _initSizeIfNeeded() {
|
||||
if (_size.width.isNaN) {
|
||||
_initSize();
|
||||
}
|
||||
}
|
||||
|
||||
void _initSize() {
|
||||
if (widget.imgXSize == 0) {
|
||||
if (_image != null) {
|
||||
final imgXSize = _image!.width / 10;
|
||||
final imgYSize = _image!.height / 10;
|
||||
final height = widget.height;
|
||||
final width = height * imgXSize / imgYSize;
|
||||
_size = Size(width, height);
|
||||
_setRect(width, height);
|
||||
widget.onSetSize(imgXSize, imgYSize);
|
||||
} else {
|
||||
_size = const Size(double.nan, double.nan);
|
||||
_setRect(double.nan, double.nan);
|
||||
}
|
||||
} else {
|
||||
final height = widget.height;
|
||||
final width = height * widget.imgXSize / widget.imgYSize;
|
||||
_size = Size(width, height);
|
||||
_setRect(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
void _setRect(double width, double height) {
|
||||
_dstRect = Rect.fromLTWH(0, 0, width, height);
|
||||
_rrect = RRect.fromRectAndRadius(_dstRect, const Radius.circular(10));
|
||||
}
|
||||
|
||||
Future<void> _loadImg() async {
|
||||
_image = widget.image;
|
||||
if (_image != null) {
|
||||
_initSizeIfNeeded();
|
||||
setState(() {});
|
||||
} else {
|
||||
final image = await _getImg(widget.url);
|
||||
if (mounted && image != null) {
|
||||
_image = image;
|
||||
widget.onCacheImg(image);
|
||||
_initSizeIfNeeded();
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(VideoShotImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.url != widget.url) {
|
||||
_loadImg();
|
||||
}
|
||||
}
|
||||
|
||||
late final _imgPaint = Paint()..filterQuality = FilterQuality.medium;
|
||||
late final _borderPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_image != null) {
|
||||
return RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: _CroppedImagePainter(
|
||||
image: _image!,
|
||||
x: widget.x,
|
||||
y: widget.y,
|
||||
imgXSize: widget.imgXSize,
|
||||
imgYSize: widget.imgYSize,
|
||||
dstRect: _dstRect,
|
||||
rrect: _rrect,
|
||||
imgPaint: _imgPaint,
|
||||
borderPaint: _borderPaint,
|
||||
),
|
||||
size: _size,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class _CroppedImagePainter extends CustomPainter {
|
||||
final ui.Image image;
|
||||
final Rect srcRect;
|
||||
final Rect dstRect;
|
||||
final RRect rrect;
|
||||
final Paint imgPaint;
|
||||
final Paint borderPaint;
|
||||
|
||||
_CroppedImagePainter({
|
||||
required this.image,
|
||||
required int x,
|
||||
required int y,
|
||||
required double imgXSize,
|
||||
required double imgYSize,
|
||||
required this.dstRect,
|
||||
required this.rrect,
|
||||
required this.imgPaint,
|
||||
required this.borderPaint,
|
||||
}) : srcRect = Rect.fromLTWH(x * imgXSize, y * imgYSize, imgXSize, imgYSize);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas
|
||||
..drawImageRect(image, srcRect, dstRect, imgPaint)
|
||||
..drawRRect(rrect, borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_CroppedImagePainter oldDelegate) {
|
||||
return oldDelegate.image != image || oldDelegate.srcRect != srcRect;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildViewPointWidget(
|
||||
PlPlayerController plPlayerController,
|
||||
double offset,
|
||||
|
||||
@@ -17,7 +17,7 @@ class BottomControl extends StatelessWidget {
|
||||
});
|
||||
|
||||
final PlPlayerController controller;
|
||||
final Widget Function(double maxWidth) buildBottomControl;
|
||||
final Widget Function() buildBottomControl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -28,15 +28,15 @@ class BottomControl extends StatelessWidget {
|
||||
double lastAnnouncedValue = -1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 12),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 7),
|
||||
child: Obx(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 7),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
return Obx(
|
||||
() => Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.bottomCenter,
|
||||
@@ -63,20 +63,16 @@ class BottomControl extends StatelessWidget {
|
||||
thumbRadius: 7,
|
||||
onDragStart: (duration) {
|
||||
feedBack();
|
||||
controller.onChangedSliderStart(
|
||||
duration.timeStamp,
|
||||
);
|
||||
controller.onChangedSliderStart(duration.timeStamp);
|
||||
},
|
||||
onDragUpdate: (duration) {
|
||||
if (controller.showSeekPreview) {
|
||||
controller.updatePreviewIndex(
|
||||
duration.timeStamp.inSeconds,
|
||||
);
|
||||
}
|
||||
double newProgress =
|
||||
duration.timeStamp.inSeconds / max;
|
||||
if (controller.showSeekPreview) {
|
||||
if (!controller.showPreview.value) {
|
||||
controller.showPreview.value = true;
|
||||
}
|
||||
controller.previewDx.value =
|
||||
duration.localPosition.dx;
|
||||
}
|
||||
if ((newProgress - lastAnnouncedValue).abs() >
|
||||
0.02) {
|
||||
accessibilityDebounce?.cancel();
|
||||
@@ -151,31 +147,20 @@ class BottomControl extends StatelessWidget {
|
||||
buildViewPointWidget(
|
||||
controller,
|
||||
8.75,
|
||||
maxWidth - 20,
|
||||
maxWidth,
|
||||
),
|
||||
],
|
||||
if (controller.dmTrend.isNotEmpty &&
|
||||
controller.showDmTreandChart.value)
|
||||
buildDmChart(theme, controller, 4.5),
|
||||
if (controller.showSeekPreview &&
|
||||
controller.showControls.value)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 18,
|
||||
child: buildSeekPreviewWidget(
|
||||
controller,
|
||||
maxWidth - 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
buildBottomControl(maxWidth),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
buildBottomControl(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -620,7 +620,7 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
@@ -924,7 +924,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
|
||||
@@ -201,6 +201,7 @@ dependencies:
|
||||
ref: master
|
||||
crclib: ^3.0.0
|
||||
web_socket_channel: ^3.0.3
|
||||
image: ^4.5.4
|
||||
|
||||
vector_math: any
|
||||
fixnum: any
|
||||
@@ -212,6 +213,7 @@ dependencies:
|
||||
path: any
|
||||
collection: any
|
||||
material_color_utilities: any
|
||||
flutter_cache_manager: any
|
||||
|
||||
dependency_overrides:
|
||||
screen_brightness: ^2.0.1
|
||||
|
||||
Reference in New Issue
Block a user