diff --git a/.fvmrc b/.fvmrc index 1670fb70..1d108d23 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { "flutter": "3.35.5" -} +} \ No newline at end of file diff --git a/lib/common/widgets/gesture/interactive_viewer.dart b/lib/common/widgets/gesture/interactive_viewer.dart new file mode 100644 index 00000000..80d80fa8 --- /dev/null +++ b/lib/common/widgets/gesture/interactive_viewer.dart @@ -0,0 +1,905 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' show Platform; +import 'dart:math' as math; + +import 'package:flutter/foundation.dart' show clampDouble; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:vector_math/vector_math_64.dart' show Quad, Vector3; + +class MouseInteractiveViewer extends StatefulWidget { + const MouseInteractiveViewer({ + super.key, + this.clipBehavior = Clip.hardEdge, + this.panAxis = PanAxis.free, + this.boundaryMargin = EdgeInsets.zero, + this.constrained = true, + this.maxScale = 2.5, + this.minScale = 0.8, + this.interactionEndFrictionCoefficient = _kDrag, + this.pointerSignalFallback, + this.onPointerPanZoomUpdate, + this.onPointerPanZoomEnd, + this.onPointerDown, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.panEnabled = true, + this.scaleEnabled = true, + this.scaleFactor = kDefaultMouseScrollToScaleFactor, + this.transformationController, + this.alignment, + this.trackpadScrollCausesScale = false, + + required this.childKey, + required this.child, + }) : assert(minScale > 0), + assert(interactionEndFrictionCoefficient > 0), + assert(maxScale > 0), + assert(maxScale >= minScale); + + final Alignment? alignment; + final Clip clipBehavior; + final PanAxis panAxis; + final EdgeInsets boundaryMargin; + final Widget child; + final bool constrained; + final bool panEnabled; + final bool scaleEnabled; + final bool trackpadScrollCausesScale; + final double scaleFactor; + final double maxScale; + final double minScale; + final double interactionEndFrictionCoefficient; + final PointerSignalEventListener? pointerSignalFallback; + final PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate; + final PointerPanZoomEndEventListener? onPointerPanZoomEnd; + final PointerDownEventListener? onPointerDown; + final GestureScaleEndCallback? onInteractionEnd; + final GestureScaleStartCallback? onInteractionStart; + final GestureScaleUpdateCallback? onInteractionUpdate; + final TransformationController? transformationController; + final GlobalKey childKey; + + static const double _kDrag = 0.0000135; + + @override + State createState() => _MouseInteractiveViewerState(); +} + +class _MouseInteractiveViewerState extends State + with TickerProviderStateMixin { + late TransformationController _transformer = + widget.transformationController ?? TransformationController(); + + final GlobalKey _parentKey = GlobalKey(); + Animation? _animation; + Animation? _scaleAnimation; + late Offset _scaleAnimationFocalPoint; + late AnimationController _controller; + late AnimationController _scaleController; + Axis? _currentAxis; + Offset? _referenceFocalPoint; + double? _scaleStart; + double? _rotationStart = 0.0; + double _currentRotation = 0.0; + _GestureType? _gestureType; + + static final gestureSettings = DeviceGestureSettings( + touchSlop: Platform.isIOS ? 9 : 4, + ); + + late final _scaleGestureRecognizer = + ScaleGestureRecognizer( + debugOwner: this, + allowedButtonsFilter: (buttons) => buttons == kPrimaryButton, + trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), + trackpadScrollCausesScale: widget.trackpadScrollCausesScale, + ) + ..gestureSettings = gestureSettings + ..onStart = _onScaleStart + ..onUpdate = _onScaleUpdate + ..onEnd = _onScaleEnd; + + final bool _rotateEnabled = false; + + Rect get _boundaryRect { + assert(widget.childKey.currentContext != null); + final RenderBox childRenderBox = + widget.childKey.currentContext!.findRenderObject()! as RenderBox; + final Size childSize = childRenderBox.size; + final Rect boundaryRect = widget.boundaryMargin.inflateRect( + Offset.zero & childSize, + ); + assert( + !boundaryRect.isEmpty, + "InteractiveViewer's child must have nonzero dimensions.", + ); + assert( + boundaryRect.isFinite || + (boundaryRect.left.isInfinite && + boundaryRect.top.isInfinite && + boundaryRect.right.isInfinite && + boundaryRect.bottom.isInfinite), + 'boundaryRect must either be infinite in all directions or finite in all directions.', + ); + return boundaryRect; + } + + Rect get _viewport { + assert(_parentKey.currentContext != null); + final RenderBox parentRenderBox = + _parentKey.currentContext!.findRenderObject()! as RenderBox; + return Offset.zero & parentRenderBox.size; + } + + Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { + if (translation == Offset.zero) { + return matrix.clone(); + } + + final Offset alignedTranslation; + + if (_currentAxis != null) { + alignedTranslation = switch (widget.panAxis) { + PanAxis.horizontal => _alignAxis(translation, Axis.horizontal), + PanAxis.vertical => _alignAxis(translation, Axis.vertical), + PanAxis.aligned => _alignAxis(translation, _currentAxis!), + PanAxis.free => translation, + }; + } else { + alignedTranslation = translation; + } + + final Matrix4 nextMatrix = matrix.clone() + ..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1); + + final Quad nextViewport = _transformViewport(nextMatrix, _viewport); + + if (_boundaryRect.isInfinite) { + return nextMatrix; + } + + final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation( + _boundaryRect, + _currentRotation, + ); + + final Offset offendingDistance = _exceedsBy( + boundariesAabbQuad, + nextViewport, + ); + if (offendingDistance == Offset.zero) { + return nextMatrix; + } + + final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix); + final double currentScale = matrix.getMaxScaleOnAxis(); + final Offset correctedTotalTranslation = Offset( + nextTotalTranslation.dx - offendingDistance.dx * currentScale, + nextTotalTranslation.dy - offendingDistance.dy * currentScale, + ); + final Matrix4 correctedMatrix = matrix.clone() + ..setTranslation( + Vector3( + correctedTotalTranslation.dx, + correctedTotalTranslation.dy, + 0.0, + ), + ); + + final Quad correctedViewport = _transformViewport( + correctedMatrix, + _viewport, + ); + final Offset offendingCorrectedDistance = _exceedsBy( + boundariesAabbQuad, + correctedViewport, + ); + if (offendingCorrectedDistance == Offset.zero) { + return correctedMatrix; + } + + if (offendingCorrectedDistance.dx != 0.0 && + offendingCorrectedDistance.dy != 0.0) { + return matrix.clone(); + } + + final Offset unidirectionalCorrectedTotalTranslation = Offset( + offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, + offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, + ); + return matrix.clone()..setTranslation( + Vector3( + unidirectionalCorrectedTotalTranslation.dx, + unidirectionalCorrectedTotalTranslation.dy, + 0.0, + ), + ); + } + + Matrix4 _matrixScale(Matrix4 matrix, double scale) { + if (scale == 1.0) { + return matrix.clone(); + } + assert(scale != 0.0); + + final double currentScale = _transformer.value.getMaxScaleOnAxis(); + final double totalScale = math.max( + currentScale * scale, + math.max( + _viewport.width / _boundaryRect.width, + _viewport.height / _boundaryRect.height, + ), + ); + final double clampedTotalScale = clampDouble( + totalScale, + widget.minScale, + widget.maxScale, + ); + final double clampedScale = clampedTotalScale / currentScale; + return matrix.clone() + ..scaleByDouble(clampedScale, clampedScale, clampedScale, 1); + } + + Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) { + if (rotation == 0) { + return matrix.clone(); + } + final Offset focalPointScene = _transformer.toScene(focalPoint); + return matrix.clone() + ..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1) + ..rotateZ(-rotation) + ..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1); + } + + bool _gestureIsSupported(_GestureType? gestureType) { + return switch (gestureType) { + _GestureType.rotate => _rotateEnabled, + _GestureType.scale => widget.scaleEnabled, + _GestureType.pan || null => widget.panEnabled, + }; + } + + _GestureType _getGestureType(ScaleUpdateDetails details) { + final double scale = !widget.scaleEnabled ? 1.0 : details.scale; + final double rotation = !_rotateEnabled ? 0.0 : details.rotation; + if ((scale - 1).abs() > rotation.abs()) { + return _GestureType.scale; + } else if (rotation != 0.0) { + return _GestureType.rotate; + } else { + return _GestureType.pan; + } + } + + // Handle the start of a gesture. All of pan, scale, and rotate are handled + // with GestureDetector's scale gesture. + void _onScaleStart(ScaleStartDetails details) { + widget.onInteractionStart?.call(details); + + if (_controller.isAnimating) { + _controller + ..stop() + ..reset(); + _animation?.removeListener(_handleInertiaAnimation); + _animation = null; + } + if (_scaleController.isAnimating) { + _scaleController + ..stop() + ..reset(); + _scaleAnimation?.removeListener(_handleScaleAnimation); + _scaleAnimation = null; + } + + _gestureType = null; + _currentAxis = null; + _scaleStart = _transformer.value.getMaxScaleOnAxis(); + _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); + _rotationStart = _currentRotation; + } + + // Handle an update to an ongoing gesture. All of pan, scale, and rotate are + // handled with GestureDetector's scale gesture. + void _onScaleUpdate(ScaleUpdateDetails details) { + final double scale = _transformer.value.getMaxScaleOnAxis(); + _scaleAnimationFocalPoint = details.localFocalPoint; + final Offset focalPointScene = _transformer.toScene( + details.localFocalPoint, + ); + + if (_gestureType == _GestureType.pan) { + // When a gesture first starts, it sometimes has no change in scale and + // rotation despite being a two-finger gesture. Here the gesture is + // allowed to be reinterpreted as its correct type after originally + // being marked as a pan. + _gestureType = _getGestureType(details); + } else { + _gestureType ??= _getGestureType(details); + } + if (!_gestureIsSupported(_gestureType)) { + widget.onInteractionUpdate?.call(details); + return; + } + + switch (_gestureType!) { + case _GestureType.scale: + assert(_scaleStart != null); + // details.scale gives us the amount to change the scale as of the + // start of this gesture, so calculate the amount to scale as of the + // previous call to _onScaleUpdate. + final double desiredScale = _scaleStart! * details.scale; + final double scaleChange = desiredScale / scale; + _transformer.value = _matrixScale(_transformer.value, scaleChange); + + // While scaling, translate such that the user's two fingers stay on + // the same places in the scene. That means that the focal point of + // the scale should be on the same place in the scene before and after + // the scale. + final Offset focalPointSceneScaled = _transformer.toScene( + details.localFocalPoint, + ); + _transformer.value = _matrixTranslate( + _transformer.value, + focalPointSceneScaled - _referenceFocalPoint!, + ); + + // details.localFocalPoint should now be at the same location as the + // original _referenceFocalPoint point. If it's not, that's because + // the translate came in contact with a boundary. In that case, update + // _referenceFocalPoint so subsequent updates happen in relation to + // the new effective focal point. + final Offset focalPointSceneCheck = _transformer.toScene( + details.localFocalPoint, + ); + if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) { + _referenceFocalPoint = focalPointSceneCheck; + } + + case _GestureType.rotate: + if (details.rotation == 0.0) { + widget.onInteractionUpdate?.call(details); + return; + } + final double desiredRotation = _rotationStart! + details.rotation; + _transformer.value = _matrixRotate( + _transformer.value, + _currentRotation - desiredRotation, + details.localFocalPoint, + ); + _currentRotation = desiredRotation; + + case _GestureType.pan: + assert(_referenceFocalPoint != null); + // details may have a change in scale here when scaleEnabled is false. + // In an effort to keep the behavior similar whether or not scaleEnabled + // is true, these gestures are thrown away. + if (details.scale != 1.0) { + widget.onInteractionUpdate?.call(details); + return; + } + _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); + // Translate so that the same point in the scene is underneath the + // focal point before and after the movement. + final Offset translationChange = + focalPointScene - _referenceFocalPoint!; + _transformer.value = _matrixTranslate( + _transformer.value, + translationChange, + ); + _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); + } + widget.onInteractionUpdate?.call(details); + } + + // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate + // are handled with GestureDetector's scale gesture. + void _onScaleEnd(ScaleEndDetails details) { + widget.onInteractionEnd?.call(details); + _scaleStart = null; + _rotationStart = null; + _referenceFocalPoint = null; + + _animation?.removeListener(_handleInertiaAnimation); + _scaleAnimation?.removeListener(_handleScaleAnimation); + _controller.reset(); + _scaleController.reset(); + + if (!_gestureIsSupported(_gestureType)) { + _currentAxis = null; + return; + } + + switch (_gestureType) { + case _GestureType.pan: + if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { + _currentAxis = null; + return; + } + final Vector3 translationVector = _transformer.value.getTranslation(); + final Offset translation = Offset( + translationVector.x, + translationVector.y, + ); + final FrictionSimulation frictionSimulationX = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dx, + details.velocity.pixelsPerSecond.dx, + ); + final FrictionSimulation frictionSimulationY = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dy, + details.velocity.pixelsPerSecond.dy, + ); + final double tFinal = _getFinalTime( + details.velocity.pixelsPerSecond.distance, + widget.interactionEndFrictionCoefficient, + ); + _animation = + Tween( + begin: translation, + end: Offset( + frictionSimulationX.finalX, + frictionSimulationY.finalX, + ), + ).animate( + CurvedAnimation(parent: _controller, curve: Curves.decelerate), + ) + ..addListener(_handleInertiaAnimation); + _controller + ..duration = Duration(milliseconds: (tFinal * 1000).round()) + ..forward(); + case _GestureType.scale: + if (details.scaleVelocity.abs() < 0.1) { + _currentAxis = null; + return; + } + final double scale = _transformer.value.getMaxScaleOnAxis(); + final FrictionSimulation frictionSimulation = FrictionSimulation( + widget.interactionEndFrictionCoefficient * widget.scaleFactor, + scale, + details.scaleVelocity / 10, + ); + final double tFinal = _getFinalTime( + details.scaleVelocity.abs(), + widget.interactionEndFrictionCoefficient, + effectivelyMotionless: 0.1, + ); + _scaleAnimation = + Tween( + begin: scale, + end: frictionSimulation.x(tFinal), + ).animate( + CurvedAnimation( + parent: _scaleController, + curve: Curves.decelerate, + ), + ) + ..addListener(_handleScaleAnimation); + _scaleController + ..duration = Duration(milliseconds: (tFinal * 1000).round()) + ..forward(); + case _GestureType.rotate || null: + break; + } + } + + void _receivedPointerSignal(PointerSignalEvent event) { + final Offset local = event.localPosition; + final Offset global = event.position; + final double scaleChange; + if (event is PointerScrollEvent) { + if (event.kind == PointerDeviceKind.trackpad) { + widget.onInteractionStart?.call( + ScaleStartDetails(focalPoint: global, localFocalPoint: local), + ); + + final Offset localDelta = PointerEvent.transformDeltaViaPositions( + untransformedEndPosition: global + event.scrollDelta, + untransformedDelta: event.scrollDelta, + transform: event.transform, + ); + + final Offset focalPointScene = _transformer.toScene(local); + final Offset newFocalPointScene = _transformer.toScene( + local - localDelta, + ); + + _transformer.value = _matrixTranslate( + _transformer.value, + newFocalPointScene - focalPointScene, + ); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: global - event.scrollDelta, + localFocalPoint: local - localDelta, + focalPointDelta: -localDelta, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + _handlePointerScrollEvent(event); + return; + } else if (event is PointerScaleEvent) { + scaleChange = event.scale; + } else { + return; + } + widget.onInteractionStart?.call( + ScaleStartDetails(focalPoint: global, localFocalPoint: local), + ); + + if (!_gestureIsSupported(_GestureType.scale)) { + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: global, + localFocalPoint: local, + scale: scaleChange, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final Offset focalPointScene = _transformer.toScene(local); + _transformer.value = _matrixScale(_transformer.value, scaleChange); + + // After scaling, translate such that the event's position is at the + // same scene point before and after the scale. + final Offset focalPointSceneScaled = _transformer.toScene(local); + _transformer.value = _matrixTranslate( + _transformer.value, + focalPointSceneScaled - focalPointScene, + ); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: global, + localFocalPoint: local, + scale: scaleChange, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + } + + void _handlePointerScrollEvent(PointerScrollEvent event) { + final Offset local = event.localPosition; + final Offset global = event.position; + + if (_gestureIsSupported(_GestureType.scale)) { + late final shift = HardwareKeyboard.instance.isShiftPressed; + if (HardwareKeyboard.instance.isControlPressed) { + _handleMouseWheelScale(event, local, global); + return; + } else if (shift || HardwareKeyboard.instance.isAltPressed) { + _handleMouseWheelPanAsScale(event, local, global, shift); + return; + } else { + widget.pointerSignalFallback?.call(event); + } + } + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: global, + localFocalPoint: local, + scale: math.exp(-event.scrollDelta.dy / widget.scaleFactor), + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + } + + void _handleMouseWheelScale( + PointerScrollEvent event, + Offset local, + Offset global, + ) { + final double scaleChange = math.exp( + -event.scrollDelta.dy / widget.scaleFactor, + ); + final Offset focalPointScene = _transformer.toScene(local); + _transformer.value = _matrixScale(_transformer.value, scaleChange); + + final Offset focalPointSceneScaled = _transformer.toScene(local); + _transformer.value = _matrixTranslate( + _transformer.value, + focalPointSceneScaled - focalPointScene, + ); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: global, + localFocalPoint: local, + scale: scaleChange, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + } + + void _handleMouseWheelPanAsScale( + PointerScrollEvent event, + Offset local, + Offset global, + bool flip, + ) { + final Offset translation = flip + ? event.scrollDelta.flip + : event.scrollDelta; + + final Offset focalPointScene = _transformer.toScene(local); + final Offset newFocalPointScene = _transformer.toScene(local - translation); + + _transformer.value = _matrixTranslate( + _transformer.value, + newFocalPointScene - focalPointScene, + ); + } + + void _handleInertiaAnimation() { + if (!_controller.isAnimating) { + _currentAxis = null; + _animation?.removeListener(_handleInertiaAnimation); + _animation = null; + _controller.reset(); + return; + } + final Vector3 translationVector = _transformer.value.getTranslation(); + final Offset translation = Offset(translationVector.x, translationVector.y); + _transformer.value = _matrixTranslate( + _transformer.value, + _transformer.toScene(_animation!.value) - + _transformer.toScene(translation), + ); + } + + void _handleScaleAnimation() { + if (!_scaleController.isAnimating) { + _currentAxis = null; + _scaleAnimation?.removeListener(_handleScaleAnimation); + _scaleAnimation = null; + _scaleController.reset(); + return; + } + final double desiredScale = _scaleAnimation!.value; + final double scaleChange = + desiredScale / _transformer.value.getMaxScaleOnAxis(); + final Offset referenceFocalPoint = _transformer.toScene( + _scaleAnimationFocalPoint, + ); + _transformer.value = _matrixScale(_transformer.value, scaleChange); + + final Offset focalPointSceneScaled = _transformer.toScene( + _scaleAnimationFocalPoint, + ); + _transformer.value = _matrixTranslate( + _transformer.value, + focalPointSceneScaled - referenceFocalPoint, + ); + } + + void _handleTransformation() { + setState(() {}); + } + + void _onPointerDown(PointerDownEvent event) { + widget.onPointerDown?.call(event); + _scaleGestureRecognizer.addPointer(event); + } + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this); + _scaleController = AnimationController(vsync: this); + + _transformer.addListener(_handleTransformation); + } + + @override + void didUpdateWidget(MouseInteractiveViewer oldWidget) { + super.didUpdateWidget(oldWidget); + + final TransformationController? newController = + widget.transformationController; + if (newController == oldWidget.transformationController) { + return; + } + _transformer.removeListener(_handleTransformation); + if (oldWidget.transformationController == null) { + _transformer.dispose(); + } + _transformer = newController ?? TransformationController(); + _transformer.addListener(_handleTransformation); + } + + @override + void dispose() { + _scaleGestureRecognizer.dispose(); + _controller.dispose(); + _scaleController.dispose(); + _transformer.removeListener(_handleTransformation); + if (widget.transformationController == null) { + _transformer.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + assert(widget.child.key == widget.childKey); + + return Listener( + key: _parentKey, + behavior: HitTestBehavior.opaque, + onPointerSignal: _receivedPointerSignal, + onPointerDown: _onPointerDown, + onPointerPanZoomStart: _scaleGestureRecognizer.addPointerPanZoom, + onPointerPanZoomUpdate: widget.onPointerPanZoomUpdate, + onPointerPanZoomEnd: widget.onPointerPanZoomEnd, + child: _InteractiveViewerBuilt( + childKey: widget.childKey, + clipBehavior: widget.clipBehavior, + constrained: widget.constrained, + matrix: _transformer.value, + alignment: widget.alignment, + child: widget.child, + ), + ); + } +} + +class _InteractiveViewerBuilt extends StatelessWidget { + const _InteractiveViewerBuilt({ + required this.child, + required this.childKey, + required this.clipBehavior, + required this.constrained, + required this.matrix, + required this.alignment, + }); + + final Widget child; + final GlobalKey childKey; + final Clip clipBehavior; + final bool constrained; + final Matrix4 matrix; + final Alignment? alignment; + + @override + Widget build(BuildContext context) { + Widget child = Transform( + transform: matrix, + alignment: alignment, + child: this.child, + ); + + if (!constrained) { + child = OverflowBox( + alignment: Alignment.topLeft, + minWidth: 0.0, + minHeight: 0.0, + maxWidth: double.infinity, + maxHeight: double.infinity, + child: child, + ); + } + + if (clipBehavior != Clip.none) { + child = ClipRect(clipBehavior: clipBehavior, child: child); + } + + return child; + } +} + +enum _GestureType { pan, scale, rotate } + +double _getFinalTime( + double velocity, + double drag, { + double effectivelyMotionless = 10, +}) { + return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); +} + +Offset _getMatrixTranslation(Matrix4 matrix) { + final Vector3 nextTranslation = matrix.getTranslation(); + return Offset(nextTranslation.x, nextTranslation.y); +} + +Quad _transformViewport(Matrix4 matrix, Rect viewport) { + final Matrix4 inverseMatrix = matrix.clone()..invert(); + return Quad.points( + inverseMatrix.transform3( + Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0), + ), + inverseMatrix.transform3( + Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0), + ), + inverseMatrix.transform3( + Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0), + ), + inverseMatrix.transform3( + Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0), + ), + ); +} + +Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { + final Matrix4 rotationMatrix = Matrix4.identity() + ..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1) + ..rotateZ(rotation) + ..translateByDouble(-rect.size.width / 2, -rect.size.height / 2, 0, 1); + final Quad boundariesRotated = Quad.points( + rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)), + rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)), + rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)), + rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)), + ); + // ignore: invalid_use_of_visible_for_testing_member + return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated); +} + +Offset _exceedsBy(Quad boundary, Quad viewport) { + final List viewportPoints = [ + viewport.point0, + viewport.point1, + viewport.point2, + viewport.point3, + ]; + Offset largestExcess = Offset.zero; + for (final Vector3 point in viewportPoints) { + // ignore: invalid_use_of_visible_for_testing_member + final Vector3 pointInside = InteractiveViewer.getNearestPointInside( + point, + boundary, + ); + final Offset excess = Offset( + pointInside.x - point.x, + pointInside.y - point.y, + ); + if (excess.dx.abs() > largestExcess.dx.abs()) { + largestExcess = Offset(excess.dx, largestExcess.dy); + } + if (excess.dy.abs() > largestExcess.dy.abs()) { + largestExcess = Offset(largestExcess.dx, excess.dy); + } + } + + return _round(largestExcess); +} + +Offset _round(Offset offset) { + return Offset( + double.parse(offset.dx.toStringAsFixed(9)), + double.parse(offset.dy.toStringAsFixed(9)), + ); +} + +Offset _alignAxis(Offset offset, Axis axis) { + return switch (axis) { + Axis.horizontal => Offset(offset.dx, 0.0), + Axis.vertical => Offset(0.0, offset.dy), + }; +} + +Axis? _getPanAxis(Offset point1, Offset point2) { + if (point1 == point2) { + return null; + } + final double x = point2.dx - point1.dx; + final double y = point2.dy - point1.dy; + return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; +} + +extension on Offset { + Offset get flip => Offset(dy, dx); +} diff --git a/lib/main.dart b/lib/main.dart index 51d51838..c47bea35 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -277,7 +277,7 @@ class MyApp extends StatelessWidget { final plCtr = PlPlayerController.instance; if (plCtr != null) { - if (plCtr.isFullScreen.value == true) { + if (plCtr.isFullScreen.value) { plCtr ..triggerFullScreen(status: false) ..controlsLock.value = false; diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 2d8b98ac..773489ae 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -131,7 +131,6 @@ class _PlDanmakuState extends State { e.colorful == DmColorfulType.VipGradualColor, count: e.hasCount() ? e.count : null, selfSend: e.isSelf, - extra: VideoDanmaku(id: e.id.toInt(), mid: e.midHash), ), ); } diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 99cc41c7..01239683 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -371,11 +371,6 @@ class LiveRoomController extends GetxController { : DmUtils.decimalToColor(extra['color']), type: DmUtils.getPosition(extra['mode']), selfSend: extra['send_from_me'] ?? false, - extra: LiveDanmaku( - id: extra['id_str'], - mid: uid, - uname: user['base']['name'], - ), ), ); if (!disableAutoScroll.value) { diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 2b60f176..b35d1004 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -211,8 +211,8 @@ class _LiveRoomPageState extends State required double width, required double height, bool isPipMode = false, - Color? fill, - Alignment? alignment, + Color fill = Colors.black, + Alignment alignment = Alignment.center, bool needDm = true, }) { if (!isFullScreen && !plPlayerController.isDesktopPip) { @@ -472,7 +472,7 @@ class _LiveRoomPageState extends State height: videoHeight, isFullScreen, needDm: isFullScreen, - alignment: isFullScreen ? null : Alignment.topCenter, + alignment: isFullScreen ? Alignment.center : Alignment.topCenter, ), ), Positioned( diff --git a/lib/pages/setting/models/play_settings.dart b/lib/pages/setting/models/play_settings.dart index cdd165a0..d6e92202 100644 --- a/lib/pages/setting/models/play_settings.dart +++ b/lib/pages/setting/models/play_settings.dart @@ -29,13 +29,6 @@ List get playSettings => [ setKey: SettingBoxKey.enableShowDanmaku, defaultVal: true, ), - // const SettingsModel( - // settingsType: SettingsType.sw1tch, - // title: '启用点击弹幕', - // leading: Icon(Icons.touch_app_outlined), - // setKey: SettingBoxKey.enableTapDm, - // defaultVal: false, - // ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) => Get.toNamed('/playSpeedSet'), diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 587cc971..742100d4 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -165,17 +165,18 @@ class _VideoDetailPageVState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { + late final ctr = videoDetailController.plPlayerController; if (state == AppLifecycleState.resumed) { - if (!videoDetailController.plPlayerController.showDanmaku) { + if (!ctr.showDanmaku) { introController.startTimer(); - videoDetailController.plPlayerController.showDanmaku = true; + ctr.showDanmaku = true; // 修复从后台恢复时全屏状态下屏幕方向错误的问题 if (isFullScreen && Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((_) { // 根据视频方向重新设置屏幕方向 final isVertical = videoDetailController.isVertical.value; - final mode = plPlayerController?.mode; + final mode = ctr.mode; if (!(mode == FullScreenMode.vertical || (mode == FullScreenMode.auto && isVertical) || @@ -188,7 +189,7 @@ class _VideoDetailPageVState extends State } } else if (state == AppLifecycleState.paused) { introController.canelTimer(); - videoDetailController.plPlayerController.showDanmaku = false; + ctr.showDanmaku = false; } } diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 1c936436..fa6a56b9 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -10,6 +10,7 @@ import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/http/danmaku_block.dart'; +import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/cdn_type.dart'; @@ -30,6 +31,7 @@ import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -987,16 +989,19 @@ class HeaderControlState extends State { onTap: () async { Get.back(); try { - final res = await Dio().get( + final res = await Request.dio.get( item.subtitleUrl!.http2https, - options: Options(responseType: ResponseType.bytes), + options: Options( + responseType: ResponseType.bytes, + extra: {'account': const NoAccount()}, + ), ); if (res.statusCode == 200) { - final Uint8List bytes = res.data; + final bytes = res.data!; final name = '${introController.videoDetail.value.title}-${videoDetailCtr.bvid}-${videoDetailCtr.cid.value}-${item.lanDoc}.json'; final path = await FilePicker.platform.saveFile( - allowedExtensions: ['json'], + allowedExtensions: const ['json'], type: FileType.custom, fileName: name, bytes: Utils.isDesktop ? null : bytes, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 653ced15..83d147cc 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -119,10 +119,6 @@ class PlPlayerController { late final RxBool _continuePlayInBackground = Pref.continuePlayInBackground.obs; - late final RxBool _flipX = false.obs; - - late final RxBool _flipY = false.obs; - /// final RxBool _isSliderMoving = false.obs; PlaylistMode _looping = PlaylistMode.none; @@ -231,9 +227,9 @@ class PlPlayerController { late final RxBool onlyPlayAudio = false.obs; /// 镜像 - RxBool get flipX => _flipX; + late final RxBool flipX = false.obs; - RxBool get flipY => _flipY; + late final RxBool flipY = false.obs; /// 是否长按倍速 RxBool get longPressStatus => _longPressStatus; @@ -324,7 +320,6 @@ class PlPlayerController { } /// 弹幕权重 - late final enableTapDm = Pref.enableTapDm; late int danmakuWeight = Pref.danmakuWeight; late RuleFilter filters = Pref.danmakuFilterRule; // 关联弹幕控制器 diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 564a93f2..655a136d 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'dart:ui' as ui; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/gesture/interactive_viewer.dart'; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart'; @@ -21,12 +22,10 @@ import 'package:PiliPlus/models_new/video/video_detail/section.dart'; import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart'; import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; -import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart'; import 'package:PiliPlus/pages/video/post_panel/view.dart'; -import 'package:PiliPlus/pages/video/widgets/header_control.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_control_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart'; @@ -48,7 +47,6 @@ import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:dio/dio.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:fl_chart/fl_chart.dart'; @@ -64,6 +62,7 @@ import 'package:get/get.dart' hide ContextExtensionss; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; class PLVideoPlayer extends StatefulWidget { @@ -76,12 +75,10 @@ class PLVideoPlayer extends StatefulWidget { required this.headerControl, this.bottomControl, this.danmuWidget, - this.customWidget, - this.customWidgets, this.showEpisodes, this.showViewPoints, - this.fill, - this.alignment, + this.fill = Colors.black, + this.alignment = Alignment.center, super.key, }); @@ -93,31 +90,26 @@ class PLVideoPlayer extends StatefulWidget { final Widget headerControl; final Widget? bottomControl; final Widget? danmuWidget; - - // List or Widget - - final Widget? customWidget; - final List? customWidgets; final void Function([int?, UgcSeason?, dynamic, String?, int?, int?])? showEpisodes; final VoidCallback? showViewPoints; - final Color? fill; - final Alignment? alignment; + final Color fill; + final Alignment alignment; @override State createState() => _PLVideoPlayerState(); } class _PLVideoPlayerState extends State - with TickerProviderStateMixin { + with WidgetsBindingObserver, TickerProviderStateMixin { late AnimationController animationController; late VideoController videoController; late final CommonIntroController introController = widget.introController!; late final VideoDetailController videoDetailController = widget.videoDetailController!; - final GlobalKey _playerKey = GlobalKey(); - final GlobalKey key = GlobalKey(); + final _playerKey = GlobalKey(); + final _videoKey = GlobalKey(); final RxDouble _brightnessValue = 0.0.obs; final RxBool _brightnessIndicator = false.obs; @@ -139,19 +131,28 @@ class _PLVideoPlayerState extends State StreamSubscription? _listener; StreamSubscription? _controlsListener; - @override - void didUpdateWidget(PLVideoPlayer oldWidget) { - super.didUpdateWidget(oldWidget); - if (plPlayerController.enableTapDm && - (widget.maxWidth != oldWidget.maxWidth || - widget.maxHeight != maxHeight)) { - _removeOverlay(); - } - } + bool _pauseDueToPauseUponEnteringBackgroundMode = false; + StreamSubscription? wakeLock; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); + + late final player = plPlayerController.videoController?.player; + if (player != null && player.state.playing) { + WakelockPlus.enable(); + } + wakeLock = player?.stream.playing.listen( + (value) { + if (value) { + WakelockPlus.enable(); + } else { + WakelockPlus.disable(); + } + }, + ); + _controlsListener = plPlayerController.showControls.listen((bool val) { final visible = val && !plPlayerController.controlsLock.value; if (widget.videoDetailController?.headerCtrKey.currentState?.provider @@ -218,6 +219,27 @@ class _PLVideoPlayerState extends State ..onDoubleTapDown = onDoubleTapDown; } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (!plPlayerController.continuePlayInBackground.value) { + late final player = plPlayerController.videoController?.player; + if (const [ + AppLifecycleState.paused, + AppLifecycleState.detached, + ].contains(state)) { + if (player != null && player.state.playing) { + _pauseDueToPauseUponEnteringBackgroundMode = true; + player.pause(); + } + } else { + if (_pauseDueToPauseUponEnteringBackgroundMode) { + _pauseDueToPauseUponEnteringBackgroundMode = false; + player?.play(); + } + } + } + } + Future setBrightness(double value) async { try { await ScreenBrightnessPlatform.instance.setApplicationScreenBrightness( @@ -236,6 +258,11 @@ class _PLVideoPlayerState extends State @override void dispose() { + WidgetsBinding.instance.removeObserver(this); + wakeLock?.cancel(); + WakelockPlus.enabled.then((i) { + if (i) WakelockPlus.disable(); + }); _tapGestureRecognizer.dispose(); _longPressRecognizer?.dispose(); _doubleTapGestureRecognizer.dispose(); @@ -831,7 +858,7 @@ class _PLVideoPlayerState extends State if (details.localFocalPoint.dx < 40) return; if (details.localFocalPoint.dx > maxWidth - 40) return; if (details.localFocalPoint.dy > maxHeight - 40) return; - if (details.pointerCount == 2) { + if (details.pointerCount > 1) { interacting = true; } plPlayerController.initialFocalPoint = details.localFocalPoint; @@ -848,7 +875,7 @@ class _PLVideoPlayerState extends State } Offset cumulativeDelta = details.localFocalPoint - plPlayerController.initialFocalPoint; - if (details.pointerCount == 2 && cumulativeDelta.distance < 1.5) { + if (details.pointerCount > 1 && cumulativeDelta.distance < 1.5) { interacting = true; _gestureType = null; return; @@ -1072,25 +1099,10 @@ class _PLVideoPlayerState extends State void onTapUp(TapUpDetails details) { switch (details.kind) { - case ui.PointerDeviceKind.mouse when (Utils.isDesktop): + case ui.PointerDeviceKind.mouse when Utils.isDesktop: onTapDesktop(); break; default: - if (kDebugMode && isMobile) { - final ctr = plPlayerController.danmakuController; - if (ctr != null) { - final item = ctr.findSingleDanmaku(details.localPosition); - if (item == null) { - if (_suspendedDm.value != null) { - _removeOverlay(); - break; - } - } else if (item != _suspendedDm.value?.item) { - _showOverlay(item, details, ctr); - break; - } - } - } plPlayerController.controls = !plPlayerController.showControls.value; break; } @@ -1098,7 +1110,7 @@ class _PLVideoPlayerState extends State void onDoubleTapDown(TapDownDetails details) { switch (details.kind) { - case ui.PointerDeviceKind.mouse when !isMobile: + case ui.PointerDeviceKind.mouse when Utils.isDesktop: onDoubleTapDesktop(); break; default: @@ -1113,12 +1125,11 @@ class _PLVideoPlayerState extends State _longPressRecognizer ??= LongPressGestureRecognizer() ..onLongPressStart = ((_) => plPlayerController.setLongPressStatus(true)) - ..onLongPressEnd = ((_) => - plPlayerController.setLongPressStatus(false)); + ..onLongPressEnd = (_) => plPlayerController.setLongPressStatus(false); late final TapGestureRecognizer _tapGestureRecognizer; late final DoubleTapGestureRecognizer _doubleTapGestureRecognizer; - void onPointerDown(PointerDownEvent event) { + void _onPointerDown(PointerDownEvent event) { if (!isMobile) { final buttons = event.buttons; final isSecondaryBtn = buttons == kSecondaryMouseButton; @@ -1135,11 +1146,11 @@ class _PLVideoPlayerState extends State } } + _tapGestureRecognizer.addPointer(event); + _doubleTapGestureRecognizer.addPointer(event); if (!plPlayerController.isLive) { longPressRecognizer.addPointer(event); } - _tapGestureRecognizer.addPointer(event); - _doubleTapGestureRecognizer.addPointer(event); } void _showControlsIfNeeded() { @@ -1155,7 +1166,7 @@ class _PLVideoPlayerState extends State } } - void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent event) { + void _onPointerPanZoomUpdate(PointerPanZoomUpdateEvent event) { if (plPlayerController.controlsLock.value) return; if (_gestureType == null) { final pan = event.pan; @@ -1213,11 +1224,11 @@ class _PLVideoPlayerState extends State } } - void onPointerPanZoomEnd(PointerPanZoomEndEvent event) { + void _onPointerPanZoomEnd(PointerPanZoomEndEvent event) { _gestureType = null; } - void onPointerSignal(PointerSignalEvent event) { + void _onPointerSignal(PointerSignalEvent event) { if (event is PointerScrollEvent) { final offset = -event.scrollDelta.dy / 4000; final volume = clampDouble( @@ -1245,58 +1256,29 @@ class _PLVideoPlayerState extends State final isFullScreen = this.isFullScreen; final isLive = plPlayerController.isLive; - final gestureWidget = Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: onPointerDown, - onPointerPanZoomUpdate: isMobile ? null : onPointerPanZoomUpdate, - onPointerPanZoomEnd: isMobile ? null : onPointerPanZoomEnd, - onPointerSignal: isMobile ? null : onPointerSignal, - ); - final child = Stack( fit: StackFit.passthrough, key: _playerKey, children: [ - Obx( - () { - final videoFit = plPlayerController.videoFit.value; - return Video( - key: key, - width: maxWidth, - height: maxHeight, - fill: widget.fill ?? Colors.black, - alignment: widget.alignment ?? Alignment.center, - controller: videoController, - controls: NoVideoControls, - pauseUponEnteringBackgroundMode: - !plPlayerController.continuePlayInBackground.value, - resumeUponEnteringForegroundMode: true, - // 字幕尺寸调节 - subtitleViewConfiguration: isLive - ? const SubtitleViewConfiguration() - : plPlayerController.subtitleConfig.value, - fit: videoFit.boxFit, - aspectRatio: videoFit.aspectRatio, - dmWidget: widget.danmuWidget, - transformationController: transformationController, - scaleEnabled: isMobile && !plPlayerController.controlsLock.value, - enableShrinkVideoSize: - isMobile && plPlayerController.enableShrinkVideoSize, - onInteractionStart: _onInteractionStart, // TODO: refa gesture - onInteractionUpdate: _onInteractionUpdate, - onInteractionEnd: _onInteractionEnd, - flipX: plPlayerController.flipX.value, - flipY: plPlayerController.flipY.value, - gestureWidget: gestureWidget, - enableDragSubtitle: plPlayerController.enableDragSubtitle, - onUpdatePadding: plPlayerController.onUpdatePadding, - ); - }, - ), + _videoWidget, - // /// 弹幕面板 - // if (widget.danmuWidget != null) - // Positioned.fill(top: 4, child: widget.danmuWidget!), + if (widget.danmuWidget case final danmaku?) + Positioned.fill(child: danmaku), + + if (!isLive) + Positioned.fill( + child: IgnorePointer( + ignoring: !plPlayerController.enableDragSubtitle, + child: Obx( + () => SubtitleView( + controller: videoController, + configuration: plPlayerController.subtitleConfig.value, + enableDragSubtitle: plPlayerController.enableDragSubtitle, + onUpdatePadding: plPlayerController.onUpdatePadding, + ), + ), + ), + ), /// 长按倍速 toast if (!isLive) @@ -1928,102 +1910,9 @@ class _PLVideoPlayerState extends State ) : const SizedBox.shrink(); }), - - Obx(() { - if (_suspendedDm.value case final suspendedDm?) { - final offset = suspendedDm.offset; - final item = suspendedDm.item; - final extra = item.content.extra as VideoDanmaku; - return Positioned( - left: offset.dx, - top: offset.dy, - child: Column( - children: [ - const CustomPaint( - painter: _TrianglePainter(Colors.black54), - size: Size(12, 6), - ), - Container( - width: overlayWidth, - height: overlayHeight, - decoration: const BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.all(Radius.circular(18)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _overlayItem( - Icon( - size: 20, - extra.isLike - ? Icons.thumb_up_off_alt_sharp - : Icons.thumb_up_off_alt_outlined, - color: Colors.white, - ), - onTap: () { - _removeOverlay(); - HeaderControl.likeDanmaku( - extra, - plPlayerController.cid!, - ); - }, - ), - _overlayItem( - const Icon( - size: 20, - Icons.copy, - color: Colors.white, - ), - onTap: () { - _removeOverlay(); - Utils.copyText(item.content.text); - }, - ), - if (item.content.selfSend) - _overlayItem( - const Icon( - size: 20, - Icons.delete, - color: Colors.white, - ), - onTap: () { - _removeOverlay(); - HeaderControl.deleteDanmaku( - extra.id, - plPlayerController.cid!, - ); - }, - ) - else - _overlayItem( - const Icon( - size: 20, - Icons.report_problem_outlined, - color: Colors.white, - ), - onTap: () { - _removeOverlay(); - HeaderControl.reportDanmaku( - extra, - context, - plPlayerController, - ); - }, - ), - ], - ), - ), - ], - ), - ); - } - return const SizedBox.shrink(); - }), ], ); - if (!isMobile) { + if (Utils.isDesktop) { return Obx( () => MouseRegion( cursor: !plPlayerController.showControls.value && isFullScreen @@ -2040,6 +1929,58 @@ class _PLVideoPlayerState extends State return child; } + Widget get _videoWidget { + return Container( + clipBehavior: Clip.none, + width: maxWidth, + height: maxHeight, + color: widget.fill, + child: Obx( + () => MouseInteractiveViewer( + scaleEnabled: !plPlayerController.controlsLock.value, + pointerSignalFallback: _onPointerSignal, + onPointerPanZoomUpdate: _onPointerPanZoomUpdate, + onPointerPanZoomEnd: _onPointerPanZoomEnd, + onPointerDown: _onPointerDown, + onInteractionStart: _onInteractionStart, + onInteractionUpdate: _onInteractionUpdate, + onInteractionEnd: _onInteractionEnd, + panEnabled: false, + minScale: plPlayerController.enableShrinkVideoSize ? 0.75 : 1, + maxScale: 2.0, + boundaryMargin: plPlayerController.enableShrinkVideoSize + ? const EdgeInsets.all(double.infinity) + : EdgeInsets.zero, + panAxis: PanAxis.aligned, + transformationController: transformationController, + childKey: _videoKey, + child: RepaintBoundary( + key: _videoKey, + child: Obx( + () { + final videoFit = plPlayerController.videoFit.value; + return Transform.flip( + flipX: plPlayerController.flipX.value, + flipY: plPlayerController.flipY.value, + filterQuality: FilterQuality.low, + child: FittedBox( + fit: videoFit.boxFit, + alignment: widget.alignment, + child: SimpleVideo( + controller: plPlayerController.videoController!, + fill: widget.fill, + aspectRatio: videoFit.aspectRatio, + ), + ), + ); + }, + ), + ), + ), + ), + ); + } + late final segment = Pair( first: plPlayerController.position.value.inMilliseconds / 1000.0, second: plPlayerController.position.value.inMilliseconds / 1000.0, @@ -2183,54 +2124,6 @@ class _PLVideoPlayerState extends State }, ); } - - static const overlaySpacing = 10.0; - static const overlayWidth = 130.0; - static const overlayHeight = 35.0; - - final Rx<({Offset offset, DanmakuItem item})?> _suspendedDm = - Rx<({Offset offset, DanmakuItem item})?>(null); - - void _removeOverlay() { - _suspendedDm - ..value?.item.suspend = false - ..value = null; - } - - Widget _overlayItem(Widget child, {required VoidCallback onTap}) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: SizedBox( - height: overlayHeight, - width: overlayWidth / 3, - child: Center( - child: child, - ), - ), - ); - } - - void _showOverlay( - DanmakuItem item, - TapUpDetails event, - DanmakuController ctr, - ) { - _removeOverlay(); - item.suspend = true; - - final dy = item.content.type == DanmakuItemType.bottom - ? ctr.viewHeight - item.yPosition - item.height - : item.yPosition; - - final top = dy + item.height; - final left = clampDouble( - event.localPosition.dx - overlayWidth / 2, - overlaySpacing, - ctr.viewWidth - overlayWidth - overlaySpacing, - ); - _suspendedDm.value = (offset: Offset(left, top), item: item); - } } Widget buildDmChart( @@ -2575,27 +2468,3 @@ Widget buildViewPointWidget( ), ); } - -class _TrianglePainter extends CustomPainter { - const _TrianglePainter(this.color); - final Color color; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - - final path = Path() - ..moveTo(0, size.height) - ..lineTo(size.width, size.height) - ..lineTo(size.width / 2, 0) - ..close(); - - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(covariant _TrianglePainter oldDelegate) => - color != oldDelegate.color; -} diff --git a/lib/tcp/live.dart b/lib/tcp/live.dart index 89e11f95..22ee12f5 100644 --- a/lib/tcp/live.dart +++ b/lib/tcp/live.dart @@ -226,7 +226,7 @@ class LiveMessageStream { } _processingData(decompressedData); } catch (e) { - if (kDebugMode) logger.i(e); + if (kDebugMode) rethrow; } } }, @@ -256,7 +256,7 @@ class LiveMessageStream { } } } catch (e) { - if (kDebugMode) logger.i('ParseHeader错误: $e'); + if (kDebugMode) rethrow; } } diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index 483d0d14..574a5f46 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -141,8 +141,7 @@ abstract class SettingBoxKey { showFsLockBtn = 'showFsLockBtn', silentDownImg = 'silentDownImg', showMemberShop = 'showMemberShop', - enablePlayAll = 'enablePlayAll', - enableTapDm = 'enableTapDm'; + enablePlayAll = 'enablePlayAll'; static const String minimizeOnExit = 'minimizeOnExit', windowSize = 'windowSize', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index f309cefd..c7ba9865 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -858,7 +858,4 @@ abstract class Pref { static bool get enablePlayAll => _setting.get(SettingBoxKey.enablePlayAll, defaultValue: true); - - static bool get enableTapDm => - _setting.get(SettingBoxKey.enableTapDm, defaultValue: false); } diff --git a/pubspec.lock b/pubspec.lock index 77becacb..6353b4fc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -223,7 +223,7 @@ packages: description: path: "." ref: main - resolved-ref: "454790117e05e96782b05ee3d28d0283741b3cde" + resolved-ref: "5bbf0b0903447862fb04927f13530db701e2cfcf" url: "https://github.com/bggRGjQaUbCoE/canvas_danmaku.git" source: git version: "0.2.6" @@ -1097,7 +1097,7 @@ packages: description: path: media_kit ref: "version_1.2.5" - resolved-ref: "8f15c273f3e5c05476d8b19881719a1bd906c4f2" + resolved-ref: ebc4b1a4a3ec3170898e2a502e17c0a25289eb53 url: "https://github.com/bggRGjQaUbCoE/media-kit.git" source: git version: "1.1.11" @@ -1106,7 +1106,7 @@ packages: description: path: "libs/android/media_kit_libs_android_video" ref: "version_1.2.5" - resolved-ref: "8f15c273f3e5c05476d8b19881719a1bd906c4f2" + resolved-ref: ebc4b1a4a3ec3170898e2a502e17c0a25289eb53 url: "https://github.com/bggRGjQaUbCoE/media-kit.git" source: git version: "1.3.7" @@ -1139,7 +1139,7 @@ packages: description: path: "libs/universal/media_kit_libs_video" ref: "version_1.2.5" - resolved-ref: "8f15c273f3e5c05476d8b19881719a1bd906c4f2" + resolved-ref: ebc4b1a4a3ec3170898e2a502e17c0a25289eb53 url: "https://github.com/bggRGjQaUbCoE/media-kit.git" source: git version: "1.0.5" @@ -1148,7 +1148,7 @@ packages: description: path: "libs/windows/media_kit_libs_windows_video" ref: "version_1.2.5" - resolved-ref: "8f15c273f3e5c05476d8b19881719a1bd906c4f2" + resolved-ref: ebc4b1a4a3ec3170898e2a502e17c0a25289eb53 url: "https://github.com/bggRGjQaUbCoE/media-kit.git" source: git version: "1.0.10" @@ -1157,7 +1157,7 @@ packages: description: path: media_kit_native_event_loop ref: "version_1.2.5" - resolved-ref: "8f15c273f3e5c05476d8b19881719a1bd906c4f2" + resolved-ref: ebc4b1a4a3ec3170898e2a502e17c0a25289eb53 url: "https://github.com/bggRGjQaUbCoE/media-kit.git" source: git version: "1.0.9" @@ -1166,7 +1166,7 @@ packages: description: path: media_kit_video ref: "version_1.2.5" - resolved-ref: "8f15c273f3e5c05476d8b19881719a1bd906c4f2" + resolved-ref: ebc4b1a4a3ec3170898e2a502e17c0a25289eb53 url: "https://github.com/bggRGjQaUbCoE/media-kit.git" source: git version: "1.2.5"