From b6352a6a43a17a38f249c522496cf37adeb07382 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Fri, 12 Sep 2025 18:42:06 +0800 Subject: [PATCH] opt ui opt video keyboard event opt code Signed-off-by: bggRGjQaUbCoE --- assets/images/logo/app_icon.ico | Bin 0 -> 175680 bytes lib/common/constants.dart | 3 + lib/common/widgets/image/image_save.dart | 18 +- .../interactive_viewer.dart | 442 +++++------------- .../interactive_viewer_boundary.dart | 6 +- .../interactiveviewer_gallery.dart | 32 +- lib/main.dart | 10 +- lib/pages/about/view.dart | 14 +- lib/pages/dynamics/widgets/content_panel.dart | 2 +- lib/pages/fan/view.dart | 16 +- lib/pages/follow/widgets/follow_item.dart | 133 +++--- lib/pages/live_room/controller.dart | 14 +- lib/pages/live_room/view.dart | 5 +- lib/pages/login/controller.dart | 2 +- lib/pages/login/view.dart | 30 +- lib/pages/main/controller.dart | 1 + lib/pages/main/view.dart | 70 ++- lib/pages/member/widget/user_info_card.dart | 2 +- lib/pages/member_home/view.dart | 2 +- lib/pages/save_panel/view.dart | 19 +- lib/pages/setting/models/extra_settings.dart | 14 + lib/pages/setting/pages/logs.dart | 7 +- lib/pages/video/controller.dart | 18 +- .../video/reply/widgets/reply_item_grpc.dart | 2 +- lib/pages/video/view.dart | 18 +- lib/pages/video/widgets/focus.dart | 16 + lib/pages/video/widgets/header_control.dart | 37 +- lib/pages/webdav/webdav.dart | 3 +- lib/pages/whisper_detail/controller.dart | 4 + lib/plugin/pl_player/controller.dart | 8 +- lib/plugin/pl_player/view.dart | 90 +++- lib/services/audio_handler.dart | 3 +- .../accounts/account_manager/account_mgr.dart | 15 +- lib/utils/image_utils.dart | 15 +- lib/utils/permission_handler.dart | 201 ++++++++ lib/utils/storage_key.dart | 3 +- lib/utils/storage_pref.dart | 3 + lib/utils/update.dart | 7 +- lib/utils/utils.dart | 19 +- pubspec.lock | 54 +-- pubspec.yaml | 6 +- windows/runner/resources/app_icon.ico | Bin 9697 -> 175680 bytes 42 files changed, 763 insertions(+), 601 deletions(-) create mode 100644 assets/images/logo/app_icon.ico create mode 100644 lib/pages/video/widgets/focus.dart create mode 100644 lib/utils/permission_handler.dart diff --git a/assets/images/logo/app_icon.ico b/assets/images/logo/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9fba6e49218ec1bc4add31ea70801b85acd81806 GIT binary patch literal 175680 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

hLmlvbI+F-= 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 literal 0 HcmV?d00001 diff --git a/lib/common/constants.dart b/lib/common/constants.dart index aafff233..85128447 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 b6a46f94..380644b8 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 a5fab49b..4b0606d6 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 cf6673ab..55c3a02e 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 b867a83e..f7e9b294 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 1c635121..439306e4 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 78299a3e..eda652ec 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 af7a4b5b..86a9a146 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 208a891e..f8564e6f 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 4a916ac6..2f95d029 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 3e823b06..b0871b93 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 e177dc46..8b45fb65 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 4b5b77b3..1b6779a0 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 e2f10d65..f38bd8bd 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 bd7a70ec..05dd777b 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 a4d16085..e6d9cde0 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 9de20460..3c6d55cd 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 9f743cf9..2e188f60 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 d5d843b3..4b39d4fe 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 a876ad84..0be40ec3 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 e579e355..4a75c2d9 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 74af3f98..0f4bf433 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 3649d782..a35feeaf 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 966742e5..b837e6eb 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 00000000..b89b463e --- /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 474e2c7e..50ed9502 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 c7f8c18c..7a6d82ce 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 5c705b7f..c1ca4af6 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 43f3345d..9441d011 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 a2741976..6497d883 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 7c9429d1..2bd17d35 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 1b4cb7d4..84d02cb6 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 d79657ac..3d1e39f0 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 00000000..4025997e --- /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 c3131350..f6de978d 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 70b60f2a..a601aad1 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 d73071d1..3908d131 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 53f76baf..a426af89 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 522187a8..a8988637 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 37fc915c..daef74ce 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 aae59560b274a054b28dad9220fdd1fcf06e157c..9fba6e49218ec1bc4add31ea70801b85acd81806 100644 GIT binary patch literal 175680 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

hLmlvbI+F-= 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 literal 9697 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