diff --git a/assets/images/logo/app_icon.ico b/assets/images/logo/app_icon.ico new file mode 100644 index 000000000..9fba6e492 Binary files /dev/null and b/assets/images/logo/app_icon.ico differ diff --git a/lib/common/constants.dart b/lib/common/constants.dart index aafff233e..851284472 100644 --- a/lib/common/constants.dart +++ b/lib/common/constants.dart @@ -13,6 +13,9 @@ class StyleString { } class Constants { + static const appName = 'PiliPlus'; + static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus'; + // 27eb53fc9058f8c3 移动端 Android // 4409e2ce8ffd12b8 HD版 static const String appKey = 'dfca71928277209b'; diff --git a/lib/common/widgets/image/image_save.dart b/lib/common/widgets/image/image_save.dart index b6a46f94a..380644b8f 100644 --- a/lib/common/widgets/image/image_save.dart +++ b/lib/common/widgets/image/image_save.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/utils/image_utils.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -107,14 +108,15 @@ void imageSaveDialog({ icon: Icons.watch_later_outlined, ), if (cover?.isNotEmpty == true) ...[ - iconBtn( - tooltip: '分享', - onPressed: () { - SmartDialog.dismiss(); - ImageUtils.onShareImg(cover!); - }, - icon: Icons.share, - ), + if (Utils.isMobile) + iconBtn( + tooltip: '分享', + onPressed: () { + SmartDialog.dismiss(); + ImageUtils.onShareImg(cover!); + }, + icon: Icons.share, + ), iconBtn( tooltip: '保存封面图', onPressed: () async { diff --git a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart index a5fab49bd..4b0606d61 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart @@ -2,28 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: uri_does_not_exist_in_doc_import + +/// @docImport 'editable_text.dart'; +/// @docImport 'scroll_view.dart'; +/// @docImport 'table.dart'; +library; + 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:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3; - -// Examples can assume: -// late BuildContext context; -// late Offset? _childWasTappedAt; -// late TransformationController _transformationController; -// Widget child = const Placeholder(); - -/// A signature for widget builders that take a [Quad] of the current viewport. -/// -/// See also: -/// -/// * [InteractiveViewer.builder], whose builder is of this type. -/// * [WidgetBuilder], which is similar, but takes no viewport. -typedef InteractiveViewerWidgetBuilder = - Widget Function(BuildContext context, Quad viewport); +import 'package:vector_math/vector_math_64.dart' show Quad, Vector3; /// A widget that enables pan and zoom interactions with its child. /// @@ -428,43 +420,19 @@ class InteractiveViewer extends StatefulWidget { static Quad getAxisAlignedBoundingBox(Quad quad) { final double minX = math.min( quad.point0.x, - math.min( - quad.point1.x, - math.min( - quad.point2.x, - quad.point3.x, - ), - ), + math.min(quad.point1.x, math.min(quad.point2.x, quad.point3.x)), ); final double minY = math.min( quad.point0.y, - math.min( - quad.point1.y, - math.min( - quad.point2.y, - quad.point3.y, - ), - ), + math.min(quad.point1.y, math.min(quad.point2.y, quad.point3.y)), ); final double maxX = math.max( quad.point0.x, - math.max( - quad.point1.x, - math.max( - quad.point2.x, - quad.point3.x, - ), - ), + math.max(quad.point1.x, math.max(quad.point2.x, quad.point3.x)), ); final double maxY = math.max( quad.point0.y, - math.max( - quad.point1.y, - math.max( - quad.point2.y, - quad.point3.y, - ), - ), + math.max(quad.point1.y, math.max(quad.point2.y, quad.point3.y)), ); return Quad.points( Vector3(minX, minY, 0), @@ -529,7 +497,8 @@ class InteractiveViewer extends StatefulWidget { class _InteractiveViewerState extends State with TickerProviderStateMixin { - TransformationController? _transformationController; + late TransformationController _transformer = + widget.transformationController ?? TransformationController(); final GlobalKey _childKey = GlobalKey(); final GlobalKey _parentKey = GlobalKey(); @@ -611,10 +580,7 @@ class _InteractiveViewerState extends State } final Matrix4 nextMatrix = matrix.clone() - ..translate( - alignedTranslation.dx, - alignedTranslation.dy, - ); + ..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1); // Transform the viewport to determine where its four corners will be after // the child has been transformed. @@ -712,8 +678,7 @@ class _InteractiveViewerState extends State // Don't allow a scale that results in an overall scale beyond min/max // scale. - final double currentScale = _transformationController!.value - .getMaxScaleOnAxis(); + final double currentScale = _transformer.value.getMaxScaleOnAxis(); final double totalScale = math.max( currentScale * scale, // Ensure that the scale cannot make the child so big that it can't fit @@ -729,7 +694,8 @@ class _InteractiveViewerState extends State widget.maxScale, ); final double clampedScale = clampedTotalScale / currentScale; - return matrix.clone()..scale(clampedScale); + return matrix.clone() + ..scaleByDouble(clampedScale, clampedScale, clampedScale, 1); } // Return a new matrix representing the given matrix after applying the given @@ -738,13 +704,11 @@ class _InteractiveViewerState extends State if (rotation == 0) { return matrix.clone(); } - final Offset focalPointScene = _transformationController!.toScene( - focalPoint, - ); + final Offset focalPointScene = _transformer.toScene(focalPoint); return matrix.clone() - ..translate(focalPointScene.dx, focalPointScene.dy) + ..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1) ..rotateZ(-rotation) - ..translate(-focalPointScene.dx, -focalPointScene.dy); + ..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1); } // Returns true iff the given _GestureType is enabled. @@ -776,8 +740,7 @@ class _InteractiveViewerState extends State // with GestureDetector's scale gesture. void _onScaleStart(ScaleStartDetails details) { if (widget.isAnimating?.call() == true || - (details.pointerCount < 2 && - _transformationController?.value.row0.x == 1.0)) { + (details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) { widget.onPanStart?.call(details); return; } @@ -788,23 +751,21 @@ class _InteractiveViewerState extends State _controller ..stop() ..reset(); - _animation?.removeListener(_onAnimate); + _animation?.removeListener(_handleInertiaAnimation); _animation = null; } if (_scaleController.isAnimating) { _scaleController ..stop() ..reset(); - _scaleAnimation?.removeListener(_onScaleAnimate); + _scaleAnimation?.removeListener(_handleScaleAnimation); _scaleAnimation = null; } _gestureType = null; _currentAxis = null; - _scaleStart = _transformationController!.value.getMaxScaleOnAxis(); - _referenceFocalPoint = _transformationController!.toScene( - details.localFocalPoint, - ); + _scaleStart = _transformer.value.getMaxScaleOnAxis(); + _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); _rotationStart = _currentRotation; } @@ -812,15 +773,14 @@ class _InteractiveViewerState extends State // handled with GestureDetector's scale gesture. void _onScaleUpdate(ScaleUpdateDetails details) { if (widget.isAnimating?.call() == true || - (details.pointerCount < 2 && - _transformationController?.value.row0.x == 1.0)) { + (details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) { widget.onPanUpdate?.call(details); return; } - final double scale = _transformationController!.value.getMaxScaleOnAxis(); + final double scale = _transformer.value.getMaxScaleOnAxis(); _scaleAnimationFocalPoint = details.localFocalPoint; - final Offset focalPointScene = _transformationController!.toScene( + final Offset focalPointScene = _transformer.toScene( details.localFocalPoint, ); @@ -846,20 +806,17 @@ class _InteractiveViewerState extends State // previous call to _onScaleUpdate. final double desiredScale = _scaleStart! * details.scale; final double scaleChange = desiredScale / scale; - _transformationController!.value = _matrixScale( - _transformationController!.value, - scaleChange, - ); + _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 = _transformationController!.toScene( + final Offset focalPointSceneScaled = _transformer.toScene( details.localFocalPoint, ); - _transformationController!.value = _matrixTranslate( - _transformationController!.value, + _transformer.value = _matrixTranslate( + _transformer.value, focalPointSceneScaled - _referenceFocalPoint!, ); @@ -868,7 +825,7 @@ class _InteractiveViewerState extends State // 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 = _transformationController!.toScene( + final Offset focalPointSceneCheck = _transformer.toScene( details.localFocalPoint, ); if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) { @@ -881,8 +838,8 @@ class _InteractiveViewerState extends State return; } final double desiredRotation = _rotationStart! + details.rotation; - _transformationController!.value = _matrixRotate( - _transformationController!.value, + _transformer.value = _matrixRotate( + _transformer.value, _currentRotation - desiredRotation, details.localFocalPoint, ); @@ -902,13 +859,11 @@ class _InteractiveViewerState extends State // focal point before and after the movement. final Offset translationChange = focalPointScene - _referenceFocalPoint!; - _transformationController!.value = _matrixTranslate( - _transformationController!.value, + _transformer.value = _matrixTranslate( + _transformer.value, translationChange, ); - _referenceFocalPoint = _transformationController!.toScene( - details.localFocalPoint, - ); + _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); } widget.onInteractionUpdate?.call(details); } @@ -916,12 +871,11 @@ class _InteractiveViewerState extends State // 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) { - if (_transformationController?.value.row0.x == 1.0) { + if (_transformer.value.row0.x == 1.0) { widget.onReset?.call(); } if (widget.isAnimating?.call() == true || - (details.pointerCount < 2 && - _transformationController?.value.row0.x == 1.0)) { + (details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) { widget.onPanEnd?.call(details); return; } @@ -931,8 +885,8 @@ class _InteractiveViewerState extends State _rotationStart = null; _referenceFocalPoint = null; - _animation?.removeListener(_onAnimate); - _scaleAnimation?.removeListener(_onScaleAnimate); + _animation?.removeListener(_handleInertiaAnimation); + _scaleAnimation?.removeListener(_handleScaleAnimation); _controller.reset(); _scaleController.reset(); @@ -947,8 +901,7 @@ class _InteractiveViewerState extends State _currentAxis = null; return; } - final Vector3 translationVector = _transformationController!.value - .getTranslation(); + final Vector3 translationVector = _transformer.value.getTranslation(); final Offset translation = Offset( translationVector.x, translationVector.y, @@ -975,21 +928,17 @@ class _InteractiveViewerState extends State frictionSimulationY.finalX, ), ).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.decelerate, - ), + CurvedAnimation(parent: _controller, curve: Curves.decelerate), ); _controller.duration = Duration(milliseconds: (tFinal * 1000).round()); - _animation!.addListener(_onAnimate); + _animation!.addListener(_handleInertiaAnimation); _controller.forward(); case _GestureType.scale: if (details.scaleVelocity.abs() < 0.1) { _currentAxis = null; return; } - final double scale = _transformationController!.value - .getMaxScaleOnAxis(); + final double scale = _transformer.value.getMaxScaleOnAxis(); final FrictionSimulation frictionSimulation = FrictionSimulation( widget.interactionEndFrictionCoefficient * widget.scaleFactor, scale, @@ -1013,7 +962,7 @@ class _InteractiveViewerState extends State _scaleController.duration = Duration( milliseconds: (tFinal * 1000).round(), ); - _scaleAnimation!.addListener(_onScaleAnimate); + _scaleAnimation!.addListener(_handleScaleAnimation); _scaleController.forward(); case _GestureType.rotate || null: break; @@ -1022,20 +971,19 @@ class _InteractiveViewerState extends State // Handle mousewheel and web trackpad scroll events. 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.trackpadScrollCausesScale) { // Trackpad scroll, so treat it as a pan. widget.onInteractionStart?.call( - ScaleStartDetails( - focalPoint: event.position, - localFocalPoint: event.localPosition, - ), + ScaleStartDetails(focalPoint: global, localFocalPoint: local), ); final Offset localDelta = PointerEvent.transformDeltaViaPositions( - untransformedEndPosition: event.position + event.scrollDelta, + untransformedEndPosition: global + event.scrollDelta, untransformedDelta: event.scrollDelta, transform: event.transform, ); @@ -1043,8 +991,8 @@ class _InteractiveViewerState extends State if (!_gestureIsSupported(_GestureType.pan)) { widget.onInteractionUpdate?.call( ScaleUpdateDetails( - focalPoint: event.position - event.scrollDelta, - localFocalPoint: event.localPosition - event.scrollDelta, + focalPoint: global - event.scrollDelta, + localFocalPoint: local - event.scrollDelta, focalPointDelta: -localDelta, ), ); @@ -1052,23 +1000,20 @@ class _InteractiveViewerState extends State return; } - final Offset focalPointScene = _transformationController!.toScene( - event.localPosition, + final Offset focalPointScene = _transformer.toScene(local); + final Offset newFocalPointScene = _transformer.toScene( + local - localDelta, ); - final Offset newFocalPointScene = _transformationController!.toScene( - event.localPosition - localDelta, - ); - - _transformationController!.value = _matrixTranslate( - _transformationController!.value, + _transformer.value = _matrixTranslate( + _transformer.value, newFocalPointScene - focalPointScene, ); widget.onInteractionUpdate?.call( ScaleUpdateDetails( - focalPoint: event.position - event.scrollDelta, - localFocalPoint: event.localPosition - localDelta, + focalPoint: global - event.scrollDelta, + localFocalPoint: local - localDelta, focalPointDelta: -localDelta, ), ); @@ -1086,17 +1031,14 @@ class _InteractiveViewerState extends State return; } widget.onInteractionStart?.call( - ScaleStartDetails( - focalPoint: event.position, - localFocalPoint: event.localPosition, - ), + ScaleStartDetails(focalPoint: global, localFocalPoint: local), ); if (!_gestureIsSupported(_GestureType.scale)) { widget.onInteractionUpdate?.call( ScaleUpdateDetails( - focalPoint: event.position, - localFocalPoint: event.localPosition, + focalPoint: global, + localFocalPoint: local, scale: scaleChange, ), ); @@ -1104,95 +1046,75 @@ class _InteractiveViewerState extends State return; } - final Offset focalPointScene = _transformationController!.toScene( - event.localPosition, - ); - - _transformationController!.value = _matrixScale( - _transformationController!.value, - scaleChange, - ); + 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 = _transformationController!.toScene( - event.localPosition, - ); - _transformationController!.value = _matrixTranslate( - _transformationController!.value, + final Offset focalPointSceneScaled = _transformer.toScene(local); + _transformer.value = _matrixTranslate( + _transformer.value, focalPointSceneScaled - focalPointScene, ); widget.onInteractionUpdate?.call( ScaleUpdateDetails( - focalPoint: event.position, - localFocalPoint: event.localPosition, + focalPoint: global, + localFocalPoint: local, scale: scaleChange, ), ); widget.onInteractionEnd?.call(ScaleEndDetails()); } - // Handle inertia drag animation. - void _onAnimate() { + void _handleInertiaAnimation() { if (!_controller.isAnimating) { _currentAxis = null; - _animation?.removeListener(_onAnimate); + _animation?.removeListener(_handleInertiaAnimation); _animation = null; _controller.reset(); return; } // Translate such that the resulting translation is _animation.value. - final Vector3 translationVector = _transformationController!.value - .getTranslation(); + final Vector3 translationVector = _transformer.value.getTranslation(); final Offset translation = Offset(translationVector.x, translationVector.y); - final Offset translationScene = _transformationController!.toScene( - translation, - ); - final Offset animationScene = _transformationController!.toScene( - _animation!.value, - ); - final Offset translationChangeScene = animationScene - translationScene; - _transformationController!.value = _matrixTranslate( - _transformationController!.value, - translationChangeScene, + _transformer.value = _matrixTranslate( + _transformer.value, + _transformer.toScene(_animation!.value) - + _transformer.toScene(translation), ); } - // Handle inertia scale animation. - void _onScaleAnimate() { + void _handleScaleAnimation() { if (!_scaleController.isAnimating) { _currentAxis = null; - _scaleAnimation?.removeListener(_onScaleAnimate); + _scaleAnimation?.removeListener(_handleScaleAnimation); _scaleAnimation = null; _scaleController.reset(); return; } final double desiredScale = _scaleAnimation!.value; final double scaleChange = - desiredScale / _transformationController!.value.getMaxScaleOnAxis(); - final Offset referenceFocalPoint = _transformationController!.toScene( + desiredScale / _transformer.value.getMaxScaleOnAxis(); + final Offset referenceFocalPoint = _transformer.toScene( _scaleAnimationFocalPoint, ); - _transformationController!.value = _matrixScale( - _transformationController!.value, - scaleChange, - ); + _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 = _transformationController!.toScene( + final Offset focalPointSceneScaled = _transformer.toScene( _scaleAnimationFocalPoint, ); - _transformationController!.value = _matrixTranslate( - _transformationController!.value, + _transformer.value = _matrixTranslate( + _transformer.value, focalPointSceneScaled - referenceFocalPoint, ); } - void _onTransformationControllerChange() { + void _handleTransformation() { // A change to the TransformationController's value is a change to the // state. setState(() {}); @@ -1201,63 +1123,36 @@ class _InteractiveViewerState extends State @override void initState() { super.initState(); - - _transformationController = - widget.transformationController ?? TransformationController(); - _transformationController!.addListener(_onTransformationControllerChange); - _controller = AnimationController( - vsync: this, - ); + _controller = AnimationController(vsync: this); _scaleController = AnimationController(vsync: this); + + _transformer.addListener(_handleTransformation); } @override void didUpdateWidget(InteractiveViewer oldWidget) { super.didUpdateWidget(oldWidget); - // Handle all cases of needing to dispose and initialize - // transformationControllers. - if (oldWidget.transformationController == null) { - if (widget.transformationController != null) { - _transformationController!.removeListener( - _onTransformationControllerChange, - ); - _transformationController!.dispose(); - _transformationController = widget.transformationController; - _transformationController!.addListener( - _onTransformationControllerChange, - ); - } - } else { - if (widget.transformationController == null) { - _transformationController!.removeListener( - _onTransformationControllerChange, - ); - _transformationController = TransformationController(); - _transformationController!.addListener( - _onTransformationControllerChange, - ); - } else if (widget.transformationController != - oldWidget.transformationController) { - _transformationController!.removeListener( - _onTransformationControllerChange, - ); - _transformationController = widget.transformationController; - _transformationController!.addListener( - _onTransformationControllerChange, - ); - } + + 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() { _controller.dispose(); _scaleController.dispose(); - _transformationController!.removeListener( - _onTransformationControllerChange, - ); + _transformer.removeListener(_handleTransformation); if (widget.transformationController == null) { - _transformationController!.dispose(); + _transformer.dispose(); } super.dispose(); } @@ -1270,7 +1165,7 @@ class _InteractiveViewerState extends State childKey: _childKey, clipBehavior: widget.clipBehavior, constrained: widget.constrained, - matrix: _transformationController!.value, + matrix: _transformer.value, alignment: widget.alignment, child: widget.child!, ); @@ -1281,7 +1176,7 @@ class _InteractiveViewerState extends State assert(!widget.constrained); child = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - final Matrix4 matrix = _transformationController!.value; + final Matrix4 matrix = _transformer.value; return _InteractiveViewerBuilt( childKey: _childKey, clipBehavior: widget.clipBehavior, @@ -1301,8 +1196,7 @@ class _InteractiveViewerState extends State key: _parentKey, onPointerSignal: _receivedPointerSignal, child: GestureDetector( - behavior: - HitTestBehavior.translucent, // Necessary when panning off screen. + behavior: HitTestBehavior.opaque, // Necessary when panning off screen. onScaleEnd: _onScaleEnd, onScaleStart: _onScaleStart, onScaleUpdate: _onScaleUpdate, @@ -1338,10 +1232,7 @@ class _InteractiveViewerBuilt extends StatelessWidget { Widget child = Transform( transform: matrix, alignment: alignment, - child: KeyedSubtree( - key: childKey, - child: this.child, - ), + child: KeyedSubtree(key: childKey, child: this.child), ); if (!constrained) { @@ -1355,83 +1246,13 @@ class _InteractiveViewerBuilt extends StatelessWidget { ); } - return ClipRect( - clipBehavior: clipBehavior, - child: child, - ); - } -} - -/// A thin wrapper on [ValueNotifier] whose value is a [Matrix4] representing a -/// transformation. -/// -/// The [value] defaults to the identity matrix, which corresponds to no -/// transformation. -/// -/// See also: -/// -/// * [InteractiveViewer.transformationController] for detailed documentation -/// on how to use TransformationController with [InteractiveViewer]. -class TransformationController extends ValueNotifier { - /// Create an instance of [TransformationController]. - /// - /// The [value] defaults to the identity matrix, which corresponds to no - /// transformation. - TransformationController([Matrix4? value]) - : super(value ?? Matrix4.identity()); - - /// Return the scene point at the given viewport point. - /// - /// A viewport point is relative to the parent while a scene point is relative - /// to the child, regardless of transformation. Calling toScene with a - /// viewport point essentially returns the scene coordinate that lies - /// underneath the viewport point given the transform. - /// - /// The viewport transforms as the inverse of the child (i.e. moving the child - /// left is equivalent to moving the viewport right). - /// - /// This method is often useful when determining where an event on the parent - /// occurs on the child. This example shows how to determine where a tap on - /// the parent occurred on the child. - /// - /// ```dart - /// @override - /// Widget build(BuildContext context) { - /// return GestureDetector( - /// onTapUp: (TapUpDetails details) { - /// _childWasTappedAt = _transformationController.toScene( - /// details.localPosition, - /// ); - /// }, - /// child: InteractiveViewer( - /// transformationController: _transformationController, - /// child: child, - /// ), - /// ); - /// } - /// ``` - Offset toScene(Offset viewportPoint) { - // On viewportPoint, perform the inverse transformation of the scene to get - // where the point would be in the scene before the transformation. - final Matrix4 inverseMatrix = Matrix4.inverted(value); - final Vector3 untransformed = inverseMatrix.transform3( - Vector3( - viewportPoint.dx, - viewportPoint.dy, - 0, - ), - ); - return Offset(untransformed.x, untransformed.y); + return ClipRect(clipBehavior: clipBehavior, child: child); } } // A classification of relevant user gestures. Each contiguous user gesture is // represented by exactly one _GestureType. -enum _GestureType { - pan, - scale, - rotate, -} +enum _GestureType { pan, scale, rotate } // Given a velocity and drag, calculate the time at which motion will come to // a stop, within the margin of effectivelyMotionless. @@ -1457,32 +1278,16 @@ 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, - ), + Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0), ), inverseMatrix.transform3( - Vector3( - viewport.topRight.dx, - viewport.topRight.dy, - 0.0, - ), + Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0), ), inverseMatrix.transform3( - Vector3( - viewport.bottomRight.dx, - viewport.bottomRight.dy, - 0.0, - ), + Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0), ), inverseMatrix.transform3( - Vector3( - viewport.bottomLeft.dx, - viewport.bottomLeft.dy, - 0.0, - ), + Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0), ), ); } @@ -1491,9 +1296,9 @@ Quad _transformViewport(Matrix4 matrix, Rect viewport) { // the given amount. Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { final Matrix4 rotationMatrix = Matrix4.identity() - ..translate(rect.size.width / 2, rect.size.height / 2) + ..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1) ..rotateZ(rotation) - ..translate(-rect.size.width / 2, -rect.size.height / 2); + ..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)), @@ -1562,20 +1367,3 @@ Axis? _getPanAxis(Offset point1, Offset point2) { final double y = point2.dy - point1.dy; return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; } - -/// This enum is used to specify the behavior of the [InteractiveViewer] when -/// the user drags the viewport. -enum PanAxis { - /// The user can only pan the viewport along the horizontal axis. - horizontal, - - /// The user can only pan the viewport along the vertical axis. - vertical, - - /// The user can pan the viewport along the horizontal and vertical axes - /// but not diagonally. - aligned, - - /// The user can pan the viewport freely in any direction. - free, -} diff --git a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart index cf6673ab4..55c3a02e7 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart @@ -43,7 +43,7 @@ class InteractiveViewerBoundary extends StatefulWidget { final double boundaryWidth; /// The [TransformationController] for the [InteractiveViewer]. - final custom.TransformationController? controller; + final TransformationController? controller; /// Called when the scale changed after an interaction ended. final ScaleChanged? onScaleChanged; @@ -68,7 +68,7 @@ class InteractiveViewerBoundary extends StatefulWidget { class InteractiveViewerBoundaryState extends State with SingleTickerProviderStateMixin { - custom.TransformationController? _controller; + TransformationController? _controller; double? _scale; @@ -86,7 +86,7 @@ class InteractiveViewerBoundaryState extends State void initState() { super.initState(); - _controller = widget.controller ?? custom.TransformationController(); + _controller = widget.controller ?? TransformationController(); _animateController = AnimationController( duration: const Duration(milliseconds: 300), diff --git a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart index b867a83ec..f7e9b294e 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart' - as custom; import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/utils/extension.dart'; @@ -81,7 +79,7 @@ class InteractiveviewerGallery extends StatefulWidget { class _InteractiveviewerGalleryState extends State with SingleTickerProviderStateMixin { PageController? _pageController; - custom.TransformationController? _transformationController; + TransformationController? _transformationController; /// The controller to animate the transformation value of the /// [InteractiveViewer] when it should reset. @@ -104,7 +102,7 @@ class _InteractiveviewerGalleryState extends State _pageController = PageController(initialPage: widget.initIndex); - _transformationController = custom.TransformationController(); + _transformationController = TransformationController(); _animationController = AnimationController( vsync: this, @@ -351,10 +349,11 @@ class _InteractiveviewerGalleryState extends State itemBuilder: (context) { final item = widget.sources[currentIndex.value]; return [ - PopupMenuItem( - onTap: () => ImageUtils.onShareImg(item.url), - child: const Text("分享图片"), - ), + if (Utils.isMobile) + PopupMenuItem( + onTap: () => ImageUtils.onShareImg(item.url), + child: const Text("分享图片"), + ), PopupMenuItem( onTap: () => Utils.copyText(item.url), child: const Text("复制链接"), @@ -500,14 +499,15 @@ class _InteractiveviewerGalleryState extends State content: Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - onTap: () { - Get.back(); - ImageUtils.onShareImg(item.url); - }, - dense: true, - title: const Text('分享', style: TextStyle(fontSize: 14)), - ), + if (Utils.isMobile) + ListTile( + onTap: () { + Get.back(); + ImageUtils.onShareImg(item.url); + }, + dense: true, + title: const Text('分享', style: TextStyle(fontSize: 14)), + ), ListTile( onTap: () { Get.back(); diff --git a/lib/main.dart b/lib/main.dart index 1c635121c..439306e4f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:PiliPlus/build_config.dart'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/custom_toast.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/models/common/theme/theme_color_type.dart'; @@ -57,7 +58,9 @@ void main() async { Request(); Request.setCookie(); RequestUtils.syncHistoryStatus(); - PiliScheme.init(); + if (Utils.isMobile) { + PiliScheme.init(); + } SmartDialog.config.toast = SmartConfigToast( displayType: SmartToastType.onlyRefresh, @@ -83,7 +86,7 @@ void main() async { titleBarStyle: Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal, - title: 'PiliPlus', + title: Constants.appName, ); windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.show(); @@ -194,8 +197,7 @@ class MyApp extends StatelessWidget { // 图片缓存 // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20; return GetMaterialApp( - // showSemanticsDebugger: true, - title: 'PiliPlus', + title: Constants.appName, theme: ThemeUtils.getThemeData( colorScheme: lightColorScheme, isDynamic: lightDynamic != null && isDynamicColor, diff --git a/lib/pages/about/view.dart b/lib/pages/about/view.dart index 78299a3ea..eda652ec5 100644 --- a/lib/pages/about/view.dart +++ b/lib/pages/about/view.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:PiliPlus/build_config.dart'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/list_tile.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; @@ -40,8 +41,6 @@ class AboutPage extends StatefulWidget { } class _AboutPageState extends State { - final String _sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus'; - RxString currentVersion = ''.obs; RxString cacheSize = ''.obs; @@ -124,7 +123,7 @@ class _AboutPageState extends State { ), ListTile( title: Text( - 'PiliPlus', + Constants.appName, textAlign: TextAlign.center, style: theme.textTheme.titleMedium!.copyWith(height: 2), ), @@ -165,7 +164,7 @@ Commit Hash: ${BuildConfig.commitHash}''', ), leading: const Icon(Icons.info_outline), onTap: () => PageUtils.launchURL( - 'https://github.com/bggRGjQaUbCoE/PiliPlus/commit/${BuildConfig.commitHash}', + '${Constants.sourceCodeUrl}/commit/${BuildConfig.commitHash}', ), onLongPress: () => Utils.copyText(BuildConfig.commitHash), ), @@ -175,10 +174,10 @@ Commit Hash: ${BuildConfig.commitHash}''', color: theme.colorScheme.outlineVariant, ), ListTile( - onTap: () => PageUtils.launchURL(_sourceCodeUrl), + onTap: () => PageUtils.launchURL(Constants.sourceCodeUrl), leading: const Icon(Icons.code), title: const Text('Source Code'), - subtitle: Text(_sourceCodeUrl, style: subTitleStyle), + subtitle: Text(Constants.sourceCodeUrl, style: subTitleStyle), ), if (Platform.isAndroid) ListTile( @@ -192,7 +191,8 @@ Commit Hash: ${BuildConfig.commitHash}''', ), ), ListTile( - onTap: () => PageUtils.launchURL('$_sourceCodeUrl/issues'), + onTap: () => + PageUtils.launchURL('${Constants.sourceCodeUrl}/issues'), leading: const Icon(Icons.feedback_outlined), title: const Text('问题反馈'), trailing: Icon( diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index af7a4b5b2..86a9a1463 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -48,7 +48,7 @@ Widget content( TextSpan( children: [ WidgetSpan( - alignment: PlaceholderAlignment.bottom, + alignment: PlaceholderAlignment.middle, child: Padding( padding: const EdgeInsets.only(right: 4), child: Icon( diff --git a/lib/pages/fan/view.dart b/lib/pages/fan/view.dart index 208a891ea..f8564e6f1 100644 --- a/lib/pages/fan/view.dart +++ b/lib/pages/fan/view.dart @@ -143,7 +143,6 @@ class _FansPageState extends State { ), child: Row( spacing: 10, - crossAxisAlignment: CrossAxisAlignment.start, children: [ NetworkImgLayer( width: 45, @@ -152,19 +151,20 @@ class _FansPageState extends State { src: item.face, ), Column( - spacing: 4, + spacing: 3, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.uname!, style: const TextStyle(fontSize: 14), ), - Text( - item.sign ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 13, color: theme.outline), - ), + if (item.sign != null) + Text( + item.sign!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 13, color: theme.outline), + ), ], ), ], diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart index 4a916ac63..2f95d029e 100644 --- a/lib/pages/follow/widgets/follow_item.dart +++ b/lib/pages/follow/widgets/follow_item.dart @@ -1,5 +1,4 @@ -import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; -import 'package:PiliPlus/models/common/image_type.dart'; +import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/models_new/follow/list.dart'; import 'package:PiliPlus/pages/share/view.dart' show UserModel; import 'package:PiliPlus/utils/feed_back.dart'; @@ -23,10 +22,10 @@ class FollowItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); return Material( type: MaterialType.transparency, - child: ListTile( + child: InkWell( onTap: () { if (onSelect != null) { onSelect!.call( @@ -41,74 +40,72 @@ class FollowItem extends StatelessWidget { Get.toNamed('/member?mid=${item.mid}'); } }, - leading: Stack( - clipBehavior: Clip.none, - children: [ - NetworkImgLayer( - width: 45, - height: 45, - type: ImageType.avatar, - src: item.face, - ), - if (item.officialVerify?.type == 0 || - item.officialVerify?.type == 1) - Positioned( - bottom: 0, - right: 0, - child: DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: theme.colorScheme.surface, - ), - child: Icon( - Icons.offline_bolt, - color: item.officialVerify?.type == 0 - ? const Color(0xFFFFCC00) - : Colors.lightBlueAccent, - size: 14, - ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + child: Row( + children: [ + PendantAvatar( + size: 45, + badgeSize: 14, + avatar: item.face, + officialType: item.officialVerify?.type, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + spacing: 3, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.uname!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + if (item.sign != null) + Text( + item.sign!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], ), ), - ], - ), - title: Text( - item.uname!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - item.sign!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - dense: true, - trailing: isOwner == true - ? SizedBox( - height: 34, - child: FilledButton.tonal( - onPressed: () => RequestUtils.actionRelationMod( - context: context, - mid: item.mid, - isFollow: item.attribute != -1, - callback: callback, - ), - style: FilledButton.styleFrom( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), - foregroundColor: item.attribute == -1 - ? null - : theme.colorScheme.outline, - backgroundColor: item.attribute == -1 - ? null - : theme.colorScheme.onInverseSurface, - ), - child: Text( - '${item.attribute == -1 ? '' : '已'}关注', - style: const TextStyle(fontSize: 12), + if (isOwner ?? false) + SizedBox( + height: 34, + child: FilledButton.tonal( + onPressed: () => RequestUtils.actionRelationMod( + context: context, + mid: item.mid, + isFollow: item.attribute != -1, + callback: callback, + ), + style: FilledButton.styleFrom( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), + foregroundColor: item.attribute == -1 + ? null + : colorScheme.outline, + backgroundColor: item.attribute == -1 + ? null + : colorScheme.onInverseSurface, + ), + child: Text( + '${item.attribute == -1 ? '' : '已'}关注', + style: const TextStyle(fontSize: 12), + ), ), ), - ) - : null, + ], + ), + ), ), ); } diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 3e823b06d..b0871b933 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -21,9 +21,9 @@ import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/danmaku_utils.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -74,7 +74,7 @@ class LiveRoomController extends GetxController { late final RxInt pageIndex = 0.obs; PageController? pageController; - int? currentQn; + int? currentQn = Utils.isMobile ? null : Pref.liveQuality; RxString currentQnDesc = ''.obs; final RxBool isPortrait = false.obs; late List<({int code, String desc})> acceptQnList = []; @@ -126,13 +126,9 @@ class LiveRoomController extends GetxController { } Future queryLiveUrl() async { - if (currentQn == null) { - await Connectivity().checkConnectivity().then((res) { - currentQn = res.contains(ConnectivityResult.wifi) - ? Pref.liveQuality - : Pref.liveQualityCellular; - }); - } + currentQn ??= await Utils.isWiFi + ? Pref.liveQuality + : Pref.liveQualityCellular; var res = await LiveHttp.liveRoomInfo( roomId: roomId, qn: currentQn, diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index e177dc46a..8b45fb65e 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -15,6 +15,7 @@ import 'package:PiliPlus/pages/live_room/superchat/superchat_panel.dart'; import 'package:PiliPlus/pages/live_room/widgets/bottom_control.dart'; import 'package:PiliPlus/pages/live_room/widgets/chat_panel.dart'; import 'package:PiliPlus/pages/live_room/widgets/header_control.dart'; +import 'package:PiliPlus/pages/video/widgets/focus.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; @@ -189,9 +190,9 @@ class _LiveRoomPageState extends State isPipMode: true, needDm: !plPlayerController.pipNoDanmaku, ) - : childWhenDisabled; + : focus(childWhenDisabled); } else { - return childWhenDisabled; + return focus(childWhenDisabled); } } diff --git a/lib/pages/login/controller.dart b/lib/pages/login/controller.dart index 4b5b77b37..1b6779a0f 100644 --- a/lib/pages/login/controller.dart +++ b/lib/pages/login/controller.dart @@ -29,7 +29,7 @@ class LoginPageController extends GetxController late TabController tabController; - final Gt3FlutterPlugin captcha = Gt3FlutterPlugin(); + late final Gt3FlutterPlugin captcha = Gt3FlutterPlugin(); CaptchaDataModel captchaData = CaptchaDataModel(); RxInt qrCodeLeftTime = 180.obs; diff --git a/lib/pages/login/view.dart b/lib/pages/login/view.dart index e2f10d658..f38bd8bd9 100644 --- a/lib/pages/login/view.dart +++ b/lib/pages/login/view.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/pages/login/controller.dart'; import 'package:PiliPlus/utils/extension.dart'; @@ -24,6 +25,7 @@ class _LoginPageState extends State { // 二维码生成时间 bool showPassword = false; GlobalKey globalKey = GlobalKey(); + final isMobile = Utils.isMobile; Widget loginByQRCode(ThemeData theme) { return Column( @@ -61,7 +63,8 @@ class _LoginPageState extends State { ); Uint8List pngBytes = byteData!.buffer.asUint8List(); SmartDialog.dismiss(); - String picName = "PiliPlus_loginQRCode_${ImageUtils.time}"; + String picName = + "${Constants.appName}_loginQRCode_${ImageUtils.time}"; ImageUtils.saveByteImg(bytes: pngBytes, fileName: picName); }, icon: const Icon(Icons.save), @@ -188,6 +191,7 @@ class _LoginPageState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: TextField( + enabled: isMobile, controller: _loginPageCtr.usernameTextController, inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r"\s"))], decoration: InputDecoration( @@ -205,6 +209,7 @@ class _LoginPageState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: TextField( + enabled: isMobile, obscureText: !showPassword, keyboardType: TextInputType.visiblePassword, inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r"\s"))], @@ -226,11 +231,9 @@ class _LoginPageState extends State { const SizedBox(width: 10), Checkbox( value: showPassword, - onChanged: (value) { - setState(() { - showPassword = value!; - }); - }, + onChanged: isMobile + ? (value) => setState(() => showPassword = value!) + : null, ), const Text('显示密码'), const Spacer(), @@ -309,7 +312,7 @@ class _LoginPageState extends State { ], ), OutlinedButton.icon( - onPressed: _loginPageCtr.loginByPassword, + onPressed: isMobile ? _loginPageCtr.loginByPassword : null, icon: const Icon(Icons.login), label: const Text('登录'), ), @@ -351,6 +354,7 @@ class _LoginPageState extends State { Builder( builder: (context) { return PopupMenuButton>( + enabled: isMobile, padding: EdgeInsets.zero, tooltip: '选择国际冠码,' @@ -402,6 +406,7 @@ class _LoginPageState extends State { const SizedBox(width: 6), Expanded( child: TextField( + enabled: isMobile, controller: _loginPageCtr.telTextController, keyboardType: TextInputType.number, inputFormatters: [ @@ -433,6 +438,7 @@ class _LoginPageState extends State { children: [ Expanded( child: TextField( + enabled: isMobile, controller: _loginPageCtr.smsCodeTextController, decoration: const InputDecoration( prefixIcon: Icon(Icons.sms_outlined), @@ -447,9 +453,11 @@ class _LoginPageState extends State { ), Obx( () => TextButton.icon( - onPressed: _loginPageCtr.smsSendCooldown > 0 - ? null - : _loginPageCtr.sendSmsCode, + onPressed: isMobile + ? (_loginPageCtr.smsSendCooldown > 0 + ? null + : _loginPageCtr.sendSmsCode) + : null, icon: const Icon(Icons.send), label: Text( _loginPageCtr.smsSendCooldown > 0 @@ -464,7 +472,7 @@ class _LoginPageState extends State { ), const SizedBox(height: 20), OutlinedButton.icon( - onPressed: _loginPageCtr.loginBySmsCode, + onPressed: isMobile ? _loginPageCtr.loginBySmsCode : null, icon: const Icon(Icons.login), label: const Text('登录'), ), diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index bd7a70ecc..05dd777b7 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -58,6 +58,7 @@ class MainController extends GetxController late final optTabletNav = Pref.optTabletNav; late bool directExitOnBack = Pref.directExitOnBack; + late bool minimizeOnExit = Pref.minimizeOnExit; static const _period = 5 * 60 * 1000; late int _lastSelectTime = 0; diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index a4d160859..e6d9cde0a 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/tabs.dart'; import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart'; @@ -18,6 +19,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart' hide ContextExtensionss; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; class MainApp extends StatefulWidget { const MainApp({super.key}); @@ -27,13 +30,20 @@ class MainApp extends StatefulWidget { } class _MainAppState extends State - with RouteAware, WidgetsBindingObserver { + with RouteAware, WidgetsBindingObserver, WindowListener, TrayListener { final MainController _mainController = Get.put(MainController()); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + if (Utils.isDesktop) { + windowManager + ..addListener(this) + ..setPreventClose(true); + trayManager.addListener(this); + _handleTray(); + } } @override @@ -75,13 +85,69 @@ class _MainAppState extends State @override void dispose() { + if (Utils.isDesktop) { + trayManager.removeListener(this); + windowManager.removeListener(this); + } PageUtils.routeObserver.unsubscribe(this); WidgetsBinding.instance.removeObserver(this); - GStorage.close(); PiliScheme.listener?.cancel(); + GStorage.close(); super.dispose(); } + @override + void onWindowClose() { + if (_mainController.minimizeOnExit) { + windowManager.hide(); + } else { + exit(0); + } + } + + @override + Future onTrayIconMouseDown() async { + if (await windowManager.isVisible()) { + windowManager.hide(); + } else { + windowManager.show(); + } + } + + @override + Future onTrayIconRightMouseDown() async { + // ignore: deprecated_member_use + trayManager.popUpContextMenu(bringAppToFront: true); + } + + @override + void onTrayMenuItemClick(MenuItem menuItem) { + switch (menuItem.key) { + case 'show': + windowManager.show(); + case 'exit': + exit(0); + } + } + + Future _handleTray() async { + if (Platform.isWindows) { + await trayManager.setIcon('assets/images/logo/app_icon.ico'); + } + if (!Platform.isLinux) { + await trayManager.setToolTip(Constants.appName); + } + + Menu trayMenu = Menu( + items: [ + MenuItem(key: 'show', label: '显示窗口'), + MenuItem.separator(), + MenuItem(key: 'exit', label: '退出 ${Constants.appName}'), + ], + ); + await trayManager.setContextMenu(trayMenu); + } + void onBack() { if (Platform.isAndroid) { Utils.channel.invokeMethod('back'); diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 9de204605..3c6d55cdc 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -384,7 +384,7 @@ class UserInfoCard extends StatelessWidget { children: [ if (relation != 0 && relation != 128) ...[ WidgetSpan( - alignment: PlaceholderAlignment.top, + alignment: PlaceholderAlignment.middle, child: Icon( Icons.sort, size: 16, diff --git a/lib/pages/member_home/view.dart b/lib/pages/member_home/view.dart index 9f743cf9a..2e188f60a 100644 --- a/lib/pages/member_home/view.dart +++ b/lib/pages/member_home/view.dart @@ -369,7 +369,7 @@ class _MemberHomeState extends State style: TextStyle(color: color), ), WidgetSpan( - alignment: PlaceholderAlignment.top, + alignment: PlaceholderAlignment.middle, child: Icon( Icons.arrow_forward_ios, size: 14, diff --git a/lib/pages/save_panel/view.dart b/lib/pages/save_panel/view.dart index d5d843b38..4b39d4feb 100644 --- a/lib/pages/save_panel/view.dart +++ b/lib/pages/save_panel/view.dart @@ -551,6 +551,7 @@ class _SavePanelState extends State { bottom: 25 + padding.bottom, ), child: Row( + spacing: 40, mainAxisAlignment: MainAxisAlignment.center, children: [ iconButton( @@ -562,7 +563,6 @@ class _SavePanelState extends State { bgColor: theme.colorScheme.onInverseSurface, iconColor: theme.colorScheme.onSurfaceVariant, ), - const SizedBox(width: 40), iconButton( size: 42, tooltip: showBottom ? '隐藏' : '显示', @@ -574,15 +574,14 @@ class _SavePanelState extends State { showBottom = !showBottom; }), ), - const SizedBox(width: 40), - iconButton( - size: 42, - tooltip: '分享', - context: context, - icon: Icons.share, - onPressed: () => _onSaveOrSharePic(true), - ), - const SizedBox(width: 40), + if (Utils.isMobile) + iconButton( + size: 42, + tooltip: '分享', + context: context, + icon: Icons.share, + onPressed: () => _onSaveOrSharePic(true), + ), iconButton( size: 42, tooltip: '保存', diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index a876ad84b..0be40ec3c 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -30,6 +30,7 @@ import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/update.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -38,6 +39,19 @@ import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; List get extraSettings => [ + if (Utils.isDesktop) + SettingsModel( + settingsType: SettingsType.sw1tch, + title: '退出时最小化', + leading: const Icon(Icons.exit_to_app), + setKey: SettingBoxKey.minimizeOnExit, + defaultVal: true, + onChanged: (value) { + try { + Get.find().minimizeOnExit = value; + } catch (_) {} + }, + ), SettingsModel( settingsType: SettingsType.sw1tch, title: '空降助手', diff --git a/lib/pages/setting/pages/logs.dart b/lib/pages/setting/pages/logs.dart index e579e3552..4a75c2d9c 100644 --- a/lib/pages/setting/pages/logs.dart +++ b/lib/pages/setting/pages/logs.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/services/logger.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -57,7 +58,7 @@ class _LogsPageState extends State { return item .replaceAll( '============================== CATCHER 2 LOG ==============================', - 'PiliPlus错误日志\n********************', + '${Constants.appName}错误日志\n********************', ) .replaceAll('DEVICE INFO', '设备信息') .replaceAll('APP INFO', '应用信息') @@ -133,9 +134,7 @@ class _LogsPageState extends State { copyLogs(); break; case 'feedback': - PageUtils.launchURL( - 'https://github.com/bggRGjQaUbCoE/PiliPlus/issues', - ); + PageUtils.launchURL('${Constants.sourceCodeUrl}/issues'); break; case 'clear': clearLogsHandle(); diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 74af3f980..0f4bf4330 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -49,7 +49,6 @@ import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart' show Options; import 'package:easy_debounce/easy_throttle.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; @@ -1129,15 +1128,14 @@ class VideoDetailController extends GetxController _querySponsorBlock(); } if (plPlayerController.cacheVideoQa == null) { - await Connectivity().checkConnectivity().then((res) { - plPlayerController - ..cacheVideoQa = res.contains(ConnectivityResult.wifi) - ? Pref.defaultVideoQa - : Pref.defaultVideoQaCellular - ..cacheAudioQa = res.contains(ConnectivityResult.wifi) - ? Pref.defaultAudioQa - : Pref.defaultAudioQaCellular; - }); + final isWiFi = await Utils.isWiFi; + plPlayerController + ..cacheVideoQa = isWiFi + ? Pref.defaultVideoQa + : Pref.defaultVideoQaCellular + ..cacheAudioQa = isWiFi + ? Pref.defaultAudioQa + : Pref.defaultAudioQaCellular; } var result = await VideoHttp.videoUrl( diff --git a/lib/pages/video/reply/widgets/reply_item_grpc.dart b/lib/pages/video/reply/widgets/reply_item_grpc.dart index 3649d7820..a35feeaf9 100644 --- a/lib/pages/video/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/reply/widgets/reply_item_grpc.dart @@ -278,7 +278,7 @@ class ReplyItemGrpc extends StatelessWidget { children: [ if (replyItem.replyControl.isUpTop) ...[ const WidgetSpan( - alignment: PlaceholderAlignment.top, + alignment: PlaceholderAlignment.middle, child: PBadge( text: 'TOP', size: PBadgeSize.small, diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 966742e5b..b837e6eb3 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -35,6 +35,7 @@ import 'package:PiliPlus/pages/video/reply/controller.dart'; import 'package:PiliPlus/pages/video/reply/view.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/view_point/view.dart'; +import 'package:PiliPlus/pages/video/widgets/focus.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/fullscreen_mode.dart'; @@ -59,8 +60,7 @@ import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart' - show SystemUiOverlayStyle, KeyDownEvent, LogicalKeyboardKey; +import 'package:flutter/services.dart' show SystemUiOverlayStyle; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart' hide ContextExtensionss; @@ -1506,19 +1506,7 @@ class _VideoDetailPageVState extends State } else { child = autoChoose(childWhenDisabledAlmostSquare); } - child = Focus( - onKeyEvent: (node, event) { - if (event is KeyDownEvent && - (event.logicalKey == LogicalKeyboardKey.arrowLeft || - event.logicalKey == LogicalKeyboardKey.arrowRight || - event.logicalKey == LogicalKeyboardKey.arrowUp || - event.logicalKey == LogicalKeyboardKey.arrowDown)) { - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - child: child, - ); + child = focus(child); return videoDetailController.plPlayerController.darkVideoPage ? Theme(data: themeData, child: child) : child; diff --git a/lib/pages/video/widgets/focus.dart b/lib/pages/video/widgets/focus.dart new file mode 100644 index 000000000..b89b463e3 --- /dev/null +++ b/lib/pages/video/widgets/focus.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show LogicalKeyboardKey; + +Widget focus(Widget child) => Focus( + onKeyEvent: (node, event) { + if (event.logicalKey == LogicalKeyboardKey.tab || + event.logicalKey == LogicalKeyboardKey.arrowLeft || + event.logicalKey == LogicalKeyboardKey.arrowRight || + event.logicalKey == LogicalKeyboardKey.arrowUp || + event.logicalKey == LogicalKeyboardKey.arrowDown) { + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: child, +); diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 474e2c7e9..50ed95028 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -34,7 +34,6 @@ import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; import 'package:floating/floating.dart'; @@ -678,18 +677,12 @@ class HeaderControlState extends TripleState { // update if (!plPlayerController.tempPlayerConf) { - final res = await Connectivity().checkConnectivity(); - if (res.contains(ConnectivityResult.wifi)) { - setting.put( - SettingBoxKey.defaultVideoQa, - quality, - ); - } else { - setting.put( - SettingBoxKey.defaultVideoQaCellular, - quality, - ); - } + setting.put( + await Utils.isWiFi + ? SettingBoxKey.defaultVideoQa + : SettingBoxKey.defaultVideoQaCellular, + quality, + ); } }, // 可能包含会员解锁画质 @@ -755,18 +748,12 @@ class HeaderControlState extends TripleState { // update if (!plPlayerController.tempPlayerConf) { - final res = await Connectivity().checkConnectivity(); - if (res.contains(ConnectivityResult.wifi)) { - setting.put( - SettingBoxKey.defaultAudioQa, - quality, - ); - } else { - setting.put( - SettingBoxKey.defaultAudioQaCellular, - quality, - ); - } + setting.put( + await Utils.isWiFi + ? SettingBoxKey.defaultAudioQa + : SettingBoxKey.defaultAudioQaCellular, + quality, + ); } }, contentPadding: const EdgeInsets.only(left: 20, right: 20), diff --git a/lib/pages/webdav/webdav.dart b/lib/pages/webdav/webdav.dart index c7f8c18c9..7a6d82ce0 100644 --- a/lib/pages/webdav/webdav.dart +++ b/lib/pages/webdav/webdav.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/utils/context_ext.dart'; import 'package:PiliPlus/utils/storage.dart'; @@ -26,7 +27,7 @@ class WebDav { if (!_webdavDirectory.endsWith('/')) { _webdavDirectory += '/'; } - _webdavDirectory += 'PiliPlus'; + _webdavDirectory += Constants.appName; try { _client = null; diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 5c705b7fb..c1ca4af63 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -65,6 +65,7 @@ class WhisperDetailController extends CommonListController { } } + late bool _isSending = false; Future sendMsg({ String? message, Map? picMsg, @@ -73,6 +74,8 @@ class WhisperDetailController extends CommonListController { int? index, }) async { assert((message != null) ^ (picMsg != null)); + if (_isSending) return; + _isSending = true; feedBack(); SmartDialog.dismiss(); if (!accountService.isLogin.value) { @@ -102,6 +105,7 @@ class WhisperDetailController extends CommonListController { } else { result.toast(); } + _isSending = false; } @override diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 43f3345d5..9441d0112 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -208,6 +208,7 @@ class PlPlayerController { /// 音量控制条 RxDouble get volume => _currentVolume; Stream get onVolumeChanged => _currentVolume.stream; + late bool isMuted = false; /// 亮度控制条 RxDouble get brightness => _currentBrightness; @@ -335,8 +336,8 @@ class PlPlayerController { late final bool tempPlayerConf = Pref.tempPlayerConf; - int? cacheVideoQa; - late int cacheAudioQa; + late int? cacheVideoQa = Utils.isMobile ? null : Pref.defaultVideoQa; + late int cacheAudioQa = Pref.defaultAudioQa; bool enableHeart = true; late final bool enableHA = Pref.enableHA; @@ -1207,6 +1208,9 @@ class PlPlayerController { } Future setVolume(double volume) async { + if (this.volume.value == volume) { + return; + } this.volume.value = volume; try { if (Utils.isDesktop) { diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index a27419767..6497d8834 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'dart:math'; +import 'dart:math' as math; import 'dart:ui' as ui; import 'package:PiliPlus/common/constants.dart'; @@ -46,7 +46,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:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:fl_chart/fl_chart.dart'; @@ -247,9 +246,6 @@ class _PLVideoPlayerState extends State } Future setVolume(double value) async { - if (plPlayerController.volume.value == value) { - return; - } plPlayerController.setVolume(value); _volumeIndicator.value = true; _volumeInterceptEventStream.value = true; @@ -454,7 +450,7 @@ class _PLVideoPlayerState extends State width: widgetWidth, height: 30, icon: Transform.rotate( - angle: pi / 2, + angle: math.pi / 2, child: const Icon( MdiIcons.viewHeadline, semanticLabel: '分段信息', @@ -696,18 +692,12 @@ class _PLVideoPlayerState extends State // update if (!plPlayerController.tempPlayerConf) { - final res = await Connectivity().checkConnectivity(); - if (res.contains(ConnectivityResult.wifi)) { - GStorage.setting.put( - SettingBoxKey.defaultVideoQa, - quality, - ); - } else { - GStorage.setting.put( - SettingBoxKey.defaultVideoQaCellular, - quality, - ); - } + GStorage.setting.put( + await Utils.isWiFi + ? SettingBoxKey.defaultVideoQa + : SettingBoxKey.defaultVideoQaCellular, + quality, + ); } }, child: Text( @@ -1143,15 +1133,19 @@ class _PLVideoPlayerState extends State case LogicalKeyboardKey.space: onDoubleTapCenter(); break; + case LogicalKeyboardKey.keyF: plPlayerController.triggerFullScreen(status: !isFullScreen); break; + case LogicalKeyboardKey.arrowLeft when (!plPlayerController.isLive): onDoubleTapSeekBackward(); break; + case LogicalKeyboardKey.arrowRight when (!plPlayerController.isLive): onDoubleTapSeekForward(); break; + case LogicalKeyboardKey.escape: if (isFullScreen) { plPlayerController.triggerFullScreen(status: false); @@ -1159,6 +1153,7 @@ class _PLVideoPlayerState extends State Get.back(); } break; + case LogicalKeyboardKey.keyD: final newVal = !plPlayerController.enableShowDanmaku.value; plPlayerController.enableShowDanmaku.value = newVal; @@ -1166,6 +1161,63 @@ class _PLVideoPlayerState extends State GStorage.setting.put(SettingBoxKey.enableShowDanmaku, newVal); } break; + + case LogicalKeyboardKey.arrowUp: + final volume = math.min( + 1.0, + plPlayerController.volume.value + 0.1, + ); + setVolume(volume); + break; + + case LogicalKeyboardKey.arrowDown: + final volume = math.max( + 0.0, + plPlayerController.volume.value - 0.1, + ); + setVolume(volume); + break; + + case LogicalKeyboardKey.keyM: + final isMuted = !plPlayerController.isMuted; + plPlayerController.videoPlayerController!.setVolume( + isMuted ? 0 : plPlayerController.volume.value * 100, + ); + plPlayerController.isMuted = isMuted; + SmartDialog.showToast('${isMuted ? '' : '取消'}静音'); + break; + + case LogicalKeyboardKey.keyQ when (!plPlayerController.isLive): + introController.actionLikeVideo(); + break; + + case LogicalKeyboardKey.keyW when (!plPlayerController.isLive): + introController.actionCoinVideo(); + break; + + case LogicalKeyboardKey.keyE when (!plPlayerController.isLive): + introController.actionFavVideo(isQuick: true); + break; + + case LogicalKeyboardKey.keyR when (!plPlayerController.isLive): + introController.viewLater(); + break; + + case LogicalKeyboardKey.bracketLeft when (!plPlayerController.isLive): + if (!introController.prevPlay()) { + SmartDialog.showToast('已经是第一集了'); + } + break; + + case LogicalKeyboardKey.bracketRight when (!plPlayerController.isLive): + if (!introController.nextPlay()) { + SmartDialog.showToast('已经是最后一集了'); + } + break; + + case LogicalKeyboardKey.enter when (!plPlayerController.isLive): + widget.videoDetailController?.showShootDanmakuSheet(); + break; } } } @@ -2136,7 +2188,7 @@ Widget buildSeekPreviewWidget( double height = 27 * scale; final compatHeight = maxHeight - 140; if (compatHeight > 50) { - height = min(height, compatHeight); + height = math.min(height, compatHeight); } final int imgXLen = data.imgXLen; diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index 7c9429d11..2bd17d350 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/episode.dart'; import 'package:PiliPlus/models_new/video/video_detail/data.dart'; @@ -13,7 +14,7 @@ Future initAudioService() async { builder: VideoPlayerServiceHandler.new, config: const AudioServiceConfig( androidNotificationChannelId: 'com.example.piliplus.audio', - androidNotificationChannelName: 'Audio Service PiliPlus', + androidNotificationChannelName: 'Audio Service ${Constants.appName}', androidNotificationOngoing: true, androidStopForegroundOnPause: true, fastForwardInterval: Duration(seconds: 10), diff --git a/lib/utils/accounts/account_manager/account_mgr.dart b/lib/utils/accounts/account_manager/account_mgr.dart index 1b4cb7d49..84d02cb6e 100644 --- a/lib/utils/accounts/account_manager/account_mgr.dart +++ b/lib/utils/accounts/account_manager/account_mgr.dart @@ -10,6 +10,7 @@ import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/app_sign.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart' show kDebugMode; @@ -312,13 +313,19 @@ class AccountManager extends Interceptor { case DioExceptionType.sendTimeout: return '发送请求超时,请检查网络设置'; case DioExceptionType.unknown: - final String res = - (await Connectivity().checkConnectivity()).first.title; - return '$res网络异常 ${error.error}'; + String desc; + try { + desc = Utils.isMobile + ? (await Connectivity().checkConnectivity()).first.desc + : ''; + } catch (_) { + desc = ''; + } + return '$desc网络异常 ${error.error}'; } } } extension _ConnectivityResultExt on ConnectivityResult { - String get title => const ['蓝牙', 'Wi-Fi', '局域', '流量', '无', '代理', '其他'][index]; + String get desc => const ['蓝牙', 'Wi-Fi', '局域', '流量', '无', '代理', '其他'][index]; } diff --git a/lib/utils/image_utils.dart b/lib/utils/image_utils.dart index d79657ac5..3d1e39f0b 100644 --- a/lib/utils/image_utils.dart +++ b/lib/utils/image_utils.dart @@ -1,9 +1,11 @@ import 'dart:io'; import 'dart:typed_data'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/global_data.dart'; +import 'package:PiliPlus/utils/permission_handler.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:dio/dio.dart'; @@ -12,7 +14,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:intl/intl.dart' show DateFormat; import 'package:live_photo_maker/live_photo_maker.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:saver_gallery/saver_gallery.dart'; import 'package:share_plus/share_plus.dart'; @@ -106,7 +107,7 @@ class ImageUtils { required int height, }) async { try { - if (!await checkPermissionDependOnSdkInt(context)) { + if (Utils.isMobile && !await checkPermissionDependOnSdkInt(context)) { return false; } if (!silentDownImg) SmartDialog.showLoading(msg: '正在下载'); @@ -165,7 +166,9 @@ class ImageUtils { BuildContext context, List imgList, ) async { - if (!await checkPermissionDependOnSdkInt(context)) return false; + if (Utils.isMobile && !await checkPermissionDependOnSdkInt(context)) { + return false; + } CancelToken? cancelToken; if (!silentDownImg) { cancelToken = CancelToken(); @@ -193,7 +196,7 @@ class ImageUtils { await SaverGallery.saveFile( filePath: filePath, fileName: name, - androidRelativePath: "Pictures/PiliPlus", + androidRelativePath: "Pictures/${Constants.appName}", skipIfExists: false, ).whenComplete(File(filePath).tryDel); } @@ -271,7 +274,7 @@ class ImageUtils { result = await SaverGallery.saveImage( bytes, fileName: fileName, - androidRelativePath: "Pictures/PiliPlus", + androidRelativePath: "Pictures/${Constants.appName}", skipIfExists: false, ); SmartDialog.dismiss(); @@ -313,7 +316,7 @@ class ImageUtils { result = await SaverGallery.saveFile( filePath: filePath, fileName: fileName, - androidRelativePath: "Pictures/PiliPlus", + androidRelativePath: "Pictures/${Constants.appName}", skipIfExists: false, ).whenComplete(file.tryDel); } else { diff --git a/lib/utils/permission_handler.dart b/lib/utils/permission_handler.dart new file mode 100644 index 000000000..4025997eb --- /dev/null +++ b/lib/utils/permission_handler.dart @@ -0,0 +1,201 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; + +export 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart' + show + Permission, + PermissionStatus, + PermissionStatusGetters, + PermissionWithService, + FuturePermissionStatusGetters, + ServiceStatus, + ServiceStatusGetters, + FutureServiceStatusGetters; + +PermissionHandlerPlatform get _handler => PermissionHandlerPlatform.instance; + +/// Opens the app settings page. +/// +/// Returns [true] if the app settings page could be opened, otherwise [false]. +Future openAppSettings() => _handler.openAppSettings(); + +/// Actions that can be executed on a permission. +extension PermissionActions on Permission { + /// Callback for when permission is denied. + static FutureOr? Function()? _onDenied; + + /// Callback for when permission is granted. + static FutureOr? Function()? _onGranted; + + /// Callback for when permission is permanently denied. + static FutureOr? Function()? _onPermanentlyDenied; + + /// Callback for when permission is restricted. + static FutureOr? Function()? _onRestricted; + + /// Callback for when permission is limited. + static FutureOr? Function()? _onLimited; + + /// Callback for when permission is Provisional. + static FutureOr? Function()? _onProvisional; + + /// Method to set a callback for when permission is denied. + Permission onDeniedCallback(FutureOr? Function()? callback) { + _onDenied = callback; + return this; + } + + /// Method to set a callback for when permission is granted. + Permission onGrantedCallback(FutureOr? Function()? callback) { + _onGranted = callback; + return this; + } + + /// Method to set a callback for when permission is permanently denied. + Permission onPermanentlyDeniedCallback(FutureOr? Function()? callback) { + _onPermanentlyDenied = callback; + return this; + } + + /// Method to set a callback for when permission is restricted. + Permission onRestrictedCallback(FutureOr? Function()? callback) { + _onRestricted = callback; + return this; + } + + /// Method to set a callback for when permission is limited. + Permission onLimitedCallback(FutureOr? Function()? callback) { + _onLimited = callback; + return this; + } + + /// Method to set a callback for when permission is provisional. + Permission onProvisionalCallback(FutureOr? Function()? callback) { + _onProvisional = callback; + return this; + } + + /// Checks the current status of the given [Permission]. + /// + /// Notes about specific permissions: + /// - **[Permission.bluetooth]** + /// - iOS 13.0 only: + /// - The method will **always** return [PermissionStatus.denied], + /// regardless of the actual status. For the actual permission state, + /// use [Permission.bluetooth.request]. Note that this will show a + /// permission dialog if the permission was not yet requested. + Future get status => _handler.checkPermissionStatus(this); + + /// If you should show a rationale for requesting permission. + /// + /// This is only implemented on Android, calling this on iOS always returns + /// [false]. + Future get shouldShowRequestRationale async { + if (defaultTargetPlatform != TargetPlatform.android) { + return false; + } + + return _handler.shouldShowRequestPermissionRationale(this); + } + + /// Request the user for access to this [Permission], if access hasn't already + /// been grant access before. + /// + /// Returns the new [PermissionStatus]. + Future request() async { + final permissionStatus = + (await [this].request())[this] ?? PermissionStatus.denied; + + if (permissionStatus.isDenied) { + _onDenied?.call(); + } else if (permissionStatus.isGranted) { + _onGranted?.call(); + } else if (permissionStatus.isPermanentlyDenied) { + _onPermanentlyDenied?.call(); + } else if (permissionStatus.isRestricted) { + _onRestricted?.call(); + } else if (permissionStatus.isLimited) { + _onLimited?.call(); + } else if (permissionStatus.isProvisional) { + _onProvisional?.call(); + } + + return permissionStatus; + } +} + +/// Shortcuts for checking the [status] of a [Permission]. +extension PermissionCheckShortcuts on Permission { + /// If the user granted this permission. + Future get isGranted => status.isGranted; + + /// If the user denied this permission. + Future get isDenied => status.isDenied; + + /// If the OS denied this permission. The user cannot change the status, + /// possibly due to active restrictions such as parental controls being in + /// place. + /// *Only supported on iOS.* + Future get isRestricted => status.isRestricted; + + /// User has authorized this application for limited access. + /// *Only supported on iOS.(iOS14+ for photos, ios18+ for contacts)* + Future get isLimited => status.isLimited; + + /// Returns `true` when permissions are denied permanently. + /// + /// When permissions are denied permanently, no new permission dialog will + /// be showed to the user. Consuming Apps should redirect the user to the + /// App settings to change permissions. + Future get isPermanentlyDenied => status.isPermanentlyDenied; + + /// If the application is provisionally authorized to post noninterruptive user notifications. + /// *Only supported on iOS.* + Future get isProvisional => status.isProvisional; +} + +/// Actions that apply only to permissions that have an associated service. +extension ServicePermissionActions on PermissionWithService { + /// Checks the current status of the service associated with the given + /// [Permission]. + /// + /// Notes about specific permissions: + /// - **[Permission.phone]** + /// - Android: + /// - The method will return [ServiceStatus.notApplicable] when: + /// - the device lacks the TELEPHONY feature + /// - TelephonyManager.getPhoneType() returns PHONE_TYPE_NONE + /// - when no Intents can be resolved to handle the `tel:` scheme + /// - The method will return [ServiceStatus.disabled] when: + /// - the SIM card is missing + /// - iOS: + /// - The method will return [ServiceStatus.notApplicable] when: + /// - the native code can not find a handler for the `tel:` scheme + /// - The method will return [ServiceStatus.disabled] when: + /// - the mobile network code (MNC) is either 0 or 65535. See + /// https://stackoverflow.com/a/11595365 for details + /// - **PLEASE NOTE that this is still not a perfect indication** of the + /// device's capability to place & connect phone calls as it also depends + /// on the network condition. + /// - **[Permission.bluetooth]** + /// - iOS: + /// - The method will **always** return [ServiceStatus.disabled] when the + /// Bluetooth permission was denied by the user. It is impossible to + /// obtain the actual Bluetooth service status without having the + /// Bluetooth permission granted. + /// - The method will prompt the user for Bluetooth permission if the + /// permission was not yet requested. + Future get serviceStatus => _handler.checkServiceStatus(this); +} + +/// Actions that can be taken on a [List] of [Permission]s. +extension PermissionListActions on List { + /// Requests the user for access to these permissions, if they haven't already + /// been granted before. + /// + /// Returns a [Map] containing the status per requested [Permission]. + Future> request() => + _handler.requestPermissions(this); +} diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index c31313503..f6de978d3 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -135,7 +135,8 @@ class SettingBoxKey { showFsScreenshotBtn = 'showFsScreenshotBtn', showFsLockBtn = 'showFsLockBtn', silentDownImg = 'silentDownImg', - showMemberShop = 'showMemberShop'; + showMemberShop = 'showMemberShop', + minimizeOnExit = 'minimizeOnExit'; static const String subtitlePreferenceV2 = 'subtitlePreferenceV2', enableDragSubtitle = 'enableDragSubtitle', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 70b60f2ae..a601aad1b 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -809,4 +809,7 @@ abstract class Pref { static bool get showSuperChat => _setting.get(SettingBoxKey.showSuperChat, defaultValue: true); + + static bool get minimizeOnExit => + _setting.get(SettingBoxKey.minimizeOnExit, defaultValue: true); } diff --git a/lib/utils/update.dart b/lib/utils/update.dart index d73071d11..3908d131a 100644 --- a/lib/utils/update.dart +++ b/lib/utils/update.dart @@ -1,6 +1,7 @@ import 'dart:io' show Platform; import 'package:PiliPlus/build_config.dart'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/ua_type.dart'; @@ -61,7 +62,7 @@ class Update { Text('${res.data[0]['body']}'), TextButton( onPressed: () => PageUtils.launchURL( - 'https://github.com/bggRGjQaUbCoE/PiliPlus/commits/main', + '${Constants.sourceCodeUrl}/commits/main', ), child: Text( "点此查看完整更新(即commit)内容", @@ -134,9 +135,7 @@ class Update { download('ios'); } } catch (_) { - PageUtils.launchURL( - 'https://github.com/bggRGjQaUbCoE/PiliPlus/releases/latest', - ); + PageUtils.launchURL('${Constants.sourceCodeUrl}/releases/latest'); } } } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 53f76baf5..a426af899 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:PiliPlus/common/constants.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -14,13 +16,24 @@ import 'package:share_plus/share_plus.dart'; class Utils { static final Random random = Random(); - static const channel = MethodChannel("PiliPlus"); + static const channel = MethodChannel(Constants.appName); static final bool isMobile = Platform.isAndroid || Platform.isIOS; static final bool isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; + static Future get isWiFi async { + try { + return Utils.isMobile && + (await Connectivity().checkConnectivity()).contains( + ConnectivityResult.wifi, + ); + } catch (_) { + return true; + } + } + static Color parseColor(String color) => Color(int.parse(color.replaceFirst('#', 'FF'), radix: 16)); @@ -59,6 +72,10 @@ class Utils { } static Future shareText(String text) async { + if (Utils.isDesktop) { + copyText(text); + return; + } try { await SharePlus.instance.share( ShareParams( diff --git a/pubspec.lock b/pubspec.lock index 522187a8b..a89886375 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1199,6 +1199,14 @@ packages: url: "https://github.com/bggRGjQaUbCoE/media-kit.git" source: git version: "1.2.5" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: @@ -1319,16 +1327,8 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.dev" - source: hosted - version: "12.0.1" permission_handler_android: - dependency: transitive + dependency: "direct main" description: name: permission_handler_android sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" @@ -1336,37 +1336,21 @@ packages: source: hosted version: "13.0.1" permission_handler_apple: - dependency: transitive + dependency: "direct main" description: name: permission_handler_apple sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 url: "https://pub.dev" source: hosted version: "9.4.7" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" - source: hosted - version: "0.1.3+5" permission_handler_platform_interface: - dependency: transitive + dependency: "direct main" description: name: permission_handler_platform_interface sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 url: "https://pub.dev" source: hosted version: "4.3.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" petitparser: dependency: transitive description: @@ -1655,6 +1639,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter @@ -1796,6 +1788,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: "537e539f48cd82d8ee2240d4330158c7b44c7e043e8e18b5811f2f8f6b7df25a" + url: "https://pub.dev" + source: hosted + version: "0.5.1" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 37fc915c2..daef74ce4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,10 @@ dependencies: # 设备信息 device_info_plus: ^11.2.0 # 权限 - permission_handler: ^12.0.0+1 + # permission_handler: ^12.0.0+1 + permission_handler_apple: ^9.4.7 + permission_handler_android: ^13.0.1 + permission_handler_platform_interface: ^4.3.0 # 分享 share_plus: ^12.0.0 # cookie 管理 @@ -208,6 +211,7 @@ dependencies: web_socket_channel: ^3.0.3 image: ^4.5.4 window_manager: ^0.5.1 + tray_manager: ^0.5.1 file_picker: ^10.3.3 vector_math: any diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index aae59560b..9fba6e492 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ