diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 3dd146d3f..a05863692 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -42,7 +42,7 @@
UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
-
+
CADisableMinimumFrameDurationOnPhone
UIApplicationSupportsIndirectInputEvents
diff --git a/lib/common/widgets/article_content.dart b/lib/common/widgets/article_content.dart
index b3671e152..85678454d 100644
--- a/lib/common/widgets/article_content.dart
+++ b/lib/common/widgets/article_content.dart
@@ -16,7 +16,7 @@ Widget articleContent({
.toList();
return SliverList.separated(
itemCount: list.length,
- itemBuilder: (_, index) {
+ itemBuilder: (context, index) {
ArticleContentModel item = list[index];
if (item.text != null) {
List spanList = [];
@@ -55,7 +55,7 @@ Widget articleContent({
);
} else if (item.pic != null) {
return LayoutBuilder(
- builder: (_, constraints) => GestureDetector(
+ builder: (context, constraints) => GestureDetector(
onTap: () {
context.imageView(
initialPage: imgList.indexOf(item.pic!.pics!.first.url!),
diff --git a/lib/common/widgets/imageview.dart b/lib/common/widgets/imageview.dart
index 54a2641a6..88e08f68d 100644
--- a/lib/common/widgets/imageview.dart
+++ b/lib/common/widgets/imageview.dart
@@ -23,10 +23,12 @@ class ImageModel {
bool get isLongPic => _isLongPic ??= (safeHeight / safeWidth) > (22 / 9);
}
-Widget image(
+Widget imageview(
double maxWidth,
- List picArr,
-) {
+ List picArr, [
+ VoidCallback? onViewImage,
+ ValueChanged? onDismissed,
+]) {
double imageWidth = (maxWidth - 2 * 5) / 3;
double imageHeight = imageWidth;
if (picArr.length == 1) {
@@ -53,43 +55,48 @@ Widget image(
height: picArr.length == 1 ? imageHeight : null,
width: picArr.length == 1 ? imageWidth : maxWidth,
itemCount: picArr.length,
- itemBuilder: (context, index) => GestureDetector(
- onTap: () {
- context.imageView(
- initialPage: index,
- imgList: picArr.map((item) => item.url).toList(),
- );
- // showDialog(
- // useSafeArea: false,
- // context: context,
- // builder: (context) {
- // return ImagePreview(
- // initialPage: index,
- // imgList: picArr.map((item) => item.url).toList(),
- // );
- // },
- // );
- },
- child: Stack(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(12),
- child: NetworkImgLayer(
- src: picArr[index].url,
- width: imageWidth,
- height: imageHeight,
- isLongPic: () => picArr[index].isLongPic,
- callback: () =>
- picArr[index].safeWidth <= picArr[index].safeHeight,
+ itemBuilder: (context, index) => Hero(
+ tag: picArr[index].url,
+ child: GestureDetector(
+ onTap: () {
+ onViewImage?.call();
+ context.imageView(
+ initialPage: index,
+ imgList: picArr.map((item) => item.url).toList(),
+ onDismissed: onDismissed,
+ );
+ // showDialog(
+ // useSafeArea: false,
+ // context: context,
+ // builder: (context) {
+ // return ImagePreview(
+ // initialPage: index,
+ // imgList: picArr.map((item) => item.url).toList(),
+ // );
+ // },
+ // );
+ },
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: NetworkImgLayer(
+ src: picArr[index].url,
+ width: imageWidth,
+ height: imageHeight,
+ isLongPic: () => picArr[index].isLongPic,
+ callback: () =>
+ picArr[index].safeWidth <= picArr[index].safeHeight,
+ ),
),
- ),
- if (picArr[index].isLongPic)
- const PBadge(
- text: '长图',
- right: 8,
- bottom: 8,
- ),
- ],
+ if (picArr[index].isLongPic)
+ const PBadge(
+ text: '长图',
+ right: 8,
+ bottom: 8,
+ ),
+ ],
+ ),
),
),
);
diff --git a/lib/common/widgets/interactiveviewer_gallery/hero_dialog_route.dart b/lib/common/widgets/interactiveviewer_gallery/hero_dialog_route.dart
new file mode 100644
index 000000000..238bc3bfe
--- /dev/null
+++ b/lib/common/widgets/interactiveviewer_gallery/hero_dialog_route.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/material.dart';
+
+/// https://github.com/qq326646683/interactiveviewer_gallery
+
+/// A [PageRoute] with a semi transparent background.
+///
+/// Similar to calling [showDialog] except it can be used with a [Navigator] to
+/// show a [Hero] animation.
+class HeroDialogRoute extends PageRoute {
+ HeroDialogRoute({
+ required this.builder,
+ });
+
+ final WidgetBuilder builder;
+
+ @override
+ bool get opaque => false;
+
+ @override
+ bool get barrierDismissible => true;
+
+ @override
+ String? get barrierLabel => null;
+
+ @override
+ Duration get transitionDuration => const Duration(milliseconds: 300);
+
+ @override
+ bool get maintainState => true;
+
+ @override
+ Color? get barrierColor => null;
+
+ @override
+ Widget buildTransitions(
+ BuildContext context,
+ Animation animation,
+ Animation secondaryAnimation,
+ Widget child,
+ ) {
+ return FadeTransition(
+ opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
+ child: child,
+ );
+ }
+
+ @override
+ Widget buildPage(
+ BuildContext context,
+ Animation animation,
+ Animation secondaryAnimation,
+ ) {
+ final Widget child = builder(context);
+ final Widget result = Semantics(
+ scopesRoute: true,
+ explicitChildNodes: true,
+ child: child,
+ );
+ return result;
+ }
+}
diff --git a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart
new file mode 100644
index 000000000..876223034
--- /dev/null
+++ b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart
@@ -0,0 +1,1503 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart: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);
+
+/// A widget that enables pan and zoom interactions with its child.
+///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=zrn7V3bMJvg}
+///
+/// The user can transform the child by dragging to pan or pinching to zoom.
+///
+/// By default, InteractiveViewer clips its child using [Clip.hardEdge].
+/// To prevent this behavior, consider setting [clipBehavior] to [Clip.none].
+/// When [clipBehavior] is [Clip.none], InteractiveViewer may draw outside of
+/// its original area of the screen, such as when a child is zoomed in and
+/// increases in size. However, it will not receive gestures outside of its original area.
+/// To prevent dead areas where InteractiveViewer does not receive gestures,
+/// don't set [clipBehavior] or be sure that the InteractiveViewer widget is the
+/// size of the area that should be interactive.
+///
+/// See also:
+/// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/main/lib/demos/reference/transformations_demo.dart),
+/// which includes the use of InteractiveViewer.
+/// * The [flutter-go demo](https://github.com/justinmc/flutter-go), which includes robust positioning of an InteractiveViewer child
+/// that works for all screen sizes and child sizes.
+/// * The [Lazy Flutter Performance Session](https://www.youtube.com/watch?v=qax_nOpgz7E), which includes the use of an InteractiveViewer to
+/// performantly view subsets of a large set of widgets using the builder constructor.
+///
+/// {@tool dartpad}
+/// This example shows a simple Container that can be panned and zoomed.
+///
+/// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart **
+/// {@end-tool}
+@immutable
+class InteractiveViewer extends StatefulWidget {
+ /// Create an InteractiveViewer.
+ InteractiveViewer({
+ super.key,
+ this.clipBehavior = Clip.hardEdge,
+ this.panAxis = PanAxis.free,
+ this.boundaryMargin = EdgeInsets.zero,
+ this.constrained = true,
+ // These default scale values were eyeballed as reasonable limits for common
+ // use cases.
+ this.maxScale = 2.5,
+ this.minScale = 0.8,
+ this.interactionEndFrictionCoefficient = _kDrag,
+ this.onInteractionEnd,
+ this.onInteractionStart,
+ this.onInteractionUpdate,
+ this.panEnabled = true,
+ this.scaleEnabled = true,
+ this.scaleFactor = kDefaultMouseScrollToScaleFactor,
+ this.transformationController,
+ this.alignment,
+ this.trackpadScrollCausesScale = false,
+ this.onPanStart,
+ this.onPanUpdate,
+ this.onPanEnd,
+ required Widget this.child,
+ }) : assert(minScale > 0),
+ assert(interactionEndFrictionCoefficient > 0),
+ assert(minScale.isFinite),
+ assert(maxScale > 0),
+ assert(!maxScale.isNaN),
+ assert(maxScale >= minScale),
+ // boundaryMargin must be either fully infinite or fully finite, but not
+ // a mix of both.
+ assert(
+ (boundaryMargin.horizontal.isInfinite &&
+ boundaryMargin.vertical.isInfinite) ||
+ (boundaryMargin.top.isFinite &&
+ boundaryMargin.right.isFinite &&
+ boundaryMargin.bottom.isFinite &&
+ boundaryMargin.left.isFinite),
+ ),
+ builder = null;
+
+ /// Creates an InteractiveViewer for a child that is created on demand.
+ ///
+ /// Can be used to render a child that changes in response to the current
+ /// transformation.
+ ///
+ /// See the [builder] attribute docs for an example of using it to optimize a
+ /// large child.
+ InteractiveViewer.builder({
+ super.key,
+ this.clipBehavior = Clip.hardEdge,
+ this.panAxis = PanAxis.free,
+ this.boundaryMargin = EdgeInsets.zero,
+ // These default scale values were eyeballed as reasonable limits for common
+ // use cases.
+ this.maxScale = 2.5,
+ this.minScale = 0.8,
+ this.interactionEndFrictionCoefficient = _kDrag,
+ this.onInteractionEnd,
+ this.onInteractionStart,
+ this.onInteractionUpdate,
+ this.panEnabled = true,
+ this.scaleEnabled = true,
+ this.scaleFactor = 200.0,
+ this.transformationController,
+ this.alignment,
+ this.trackpadScrollCausesScale = false,
+ this.onPanStart,
+ this.onPanUpdate,
+ this.onPanEnd,
+ required InteractiveViewerWidgetBuilder this.builder,
+ }) : assert(minScale > 0),
+ assert(interactionEndFrictionCoefficient > 0),
+ assert(minScale.isFinite),
+ assert(maxScale > 0),
+ assert(!maxScale.isNaN),
+ assert(maxScale >= minScale),
+ // boundaryMargin must be either fully infinite or fully finite, but not
+ // a mix of both.
+ assert(
+ (boundaryMargin.horizontal.isInfinite &&
+ boundaryMargin.vertical.isInfinite) ||
+ (boundaryMargin.top.isFinite &&
+ boundaryMargin.right.isFinite &&
+ boundaryMargin.bottom.isFinite &&
+ boundaryMargin.left.isFinite),
+ ),
+ constrained = false,
+ child = null;
+
+ final ValueChanged? onPanStart;
+ final ValueChanged? onPanUpdate;
+ final ValueChanged? onPanEnd;
+
+ /// The alignment of the child's origin, relative to the size of the box.
+ final Alignment? alignment;
+
+ /// If set to [Clip.none], the child may extend beyond the size of the InteractiveViewer,
+ /// but it will not receive gestures in these areas.
+ /// Be sure that the InteractiveViewer is the desired size when using [Clip.none].
+ ///
+ /// Defaults to [Clip.hardEdge].
+ final Clip clipBehavior;
+
+ /// When set to [PanAxis.aligned], panning is only allowed in the horizontal
+ /// axis or the vertical axis, diagonal panning is not allowed.
+ ///
+ /// When set to [PanAxis.vertical] or [PanAxis.horizontal] panning is only
+ /// allowed in the specified axis. For example, if set to [PanAxis.vertical],
+ /// panning will only be allowed in the vertical axis. And if set to [PanAxis.horizontal],
+ /// panning will only be allowed in the horizontal axis.
+ ///
+ /// When set to [PanAxis.free] panning is allowed in all directions.
+ ///
+ /// Defaults to [PanAxis.free].
+ final PanAxis panAxis;
+
+ /// A margin for the visible boundaries of the child.
+ ///
+ /// Any transformation that results in the viewport being able to view outside
+ /// of the boundaries will be stopped at the boundary. The boundaries do not
+ /// rotate with the rest of the scene, so they are always aligned with the
+ /// viewport.
+ ///
+ /// To produce no boundaries at all, pass infinite [EdgeInsets], such as
+ /// `EdgeInsets.all(double.infinity)`.
+ ///
+ /// No edge can be NaN.
+ ///
+ /// Defaults to [EdgeInsets.zero], which results in boundaries that are the
+ /// exact same size and position as the [child].
+ final EdgeInsets boundaryMargin;
+
+ /// Builds the child of this widget.
+ ///
+ /// Passed with the [InteractiveViewer.builder] constructor. Otherwise, the
+ /// [child] parameter must be passed directly, and this is null.
+ ///
+ /// {@tool dartpad}
+ /// This example shows how to use builder to create a [Table] whose cell
+ /// contents are only built when they are visible. Built and remove cells are
+ /// logged in the console for illustration.
+ ///
+ /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart **
+ /// {@end-tool}
+ ///
+ /// See also:
+ ///
+ /// * [ListView.builder], which follows a similar pattern.
+ final InteractiveViewerWidgetBuilder? builder;
+
+ /// The child [Widget] that is transformed by InteractiveViewer.
+ ///
+ /// If the [InteractiveViewer.builder] constructor is used, then this will be
+ /// null, otherwise it is required.
+ final Widget? child;
+
+ /// Whether the normal size constraints at this point in the widget tree are
+ /// applied to the child.
+ ///
+ /// If set to false, then the child will be given infinite constraints. This
+ /// is often useful when a child should be bigger than the InteractiveViewer.
+ ///
+ /// For example, for a child which is bigger than the viewport but can be
+ /// panned to reveal parts that were initially offscreen, [constrained] must
+ /// be set to false to allow it to size itself properly. If [constrained] is
+ /// true and the child can only size itself to the viewport, then areas
+ /// initially outside of the viewport will not be able to receive user
+ /// interaction events. If experiencing regions of the child that are not
+ /// receptive to user gestures, make sure [constrained] is false and the child
+ /// is sized properly.
+ ///
+ /// Defaults to true.
+ ///
+ /// {@tool dartpad}
+ /// This example shows how to create a pannable table. Because the table is
+ /// larger than the entire screen, setting [constrained] to false is necessary
+ /// to allow it to be drawn to its full size. The parts of the table that
+ /// exceed the screen size can then be panned into view.
+ ///
+ /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart **
+ /// {@end-tool}
+ final bool constrained;
+
+ /// If false, the user will be prevented from panning.
+ ///
+ /// Defaults to true.
+ ///
+ /// See also:
+ ///
+ /// * [scaleEnabled], which is similar but for scale.
+ final bool panEnabled;
+
+ /// If false, the user will be prevented from scaling.
+ ///
+ /// Defaults to true.
+ ///
+ /// See also:
+ ///
+ /// * [panEnabled], which is similar but for panning.
+ final bool scaleEnabled;
+
+ /// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
+ final bool trackpadScrollCausesScale;
+
+ /// Determines the amount of scale to be performed per pointer scroll.
+ ///
+ /// Defaults to [kDefaultMouseScrollToScaleFactor].
+ ///
+ /// Increasing this value above the default causes scaling to feel slower,
+ /// while decreasing it causes scaling to feel faster.
+ ///
+ /// The amount of scale is calculated as the exponential function of the
+ /// [PointerScrollEvent.scrollDelta] to [scaleFactor] ratio. In the Flutter
+ /// engine, the mousewheel [PointerScrollEvent.scrollDelta] is hardcoded to 20
+ /// per scroll, while a trackpad scroll can be any amount.
+ ///
+ /// Affects only pointer device scrolling, not pinch to zoom.
+ final double scaleFactor;
+
+ /// The maximum allowed scale.
+ ///
+ /// The scale will be clamped between this and [minScale] inclusively.
+ ///
+ /// Defaults to 2.5.
+ ///
+ /// Must be greater than zero and greater than [minScale].
+ final double maxScale;
+
+ /// The minimum allowed scale.
+ ///
+ /// The scale will be clamped between this and [maxScale] inclusively.
+ ///
+ /// Scale is also affected by [boundaryMargin]. If the scale would result in
+ /// viewing beyond the boundary, then it will not be allowed. By default,
+ /// boundaryMargin is EdgeInsets.zero, so scaling below 1.0 will not be
+ /// allowed in most cases without first increasing the boundaryMargin.
+ ///
+ /// Defaults to 0.8.
+ ///
+ /// Must be a finite number greater than zero and less than [maxScale].
+ final double minScale;
+
+ /// Changes the deceleration behavior after a gesture.
+ ///
+ /// Defaults to 0.0000135.
+ ///
+ /// Must be a finite number greater than zero.
+ final double interactionEndFrictionCoefficient;
+
+ /// Called when the user ends a pan or scale gesture on the widget.
+ ///
+ /// At the time this is called, the [TransformationController] will have
+ /// already been updated to reflect the change caused by the interaction,
+ /// though a pan may cause an inertia animation after this is called as well.
+ ///
+ /// {@template flutter.widgets.InteractiveViewer.onInteractionEnd}
+ /// Will be called even if the interaction is disabled with [panEnabled] or
+ /// [scaleEnabled] for both touch gestures and mouse interactions.
+ ///
+ /// A [GestureDetector] wrapping the InteractiveViewer will not respond to
+ /// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and
+ /// [GestureDetector.onScaleEnd]. Use [onInteractionStart],
+ /// [onInteractionUpdate], and [onInteractionEnd] to respond to those
+ /// gestures.
+ /// {@endtemplate}
+ ///
+ /// See also:
+ ///
+ /// * [onInteractionStart], which handles the start of the same interaction.
+ /// * [onInteractionUpdate], which handles an update to the same interaction.
+ final GestureScaleEndCallback? onInteractionEnd;
+
+ /// Called when the user begins a pan or scale gesture on the widget.
+ ///
+ /// At the time this is called, the [TransformationController] will not have
+ /// changed due to this interaction.
+ ///
+ /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd}
+ ///
+ /// The coordinates provided in the details' `focalPoint` and
+ /// `localFocalPoint` are normal Flutter event coordinates, not
+ /// InteractiveViewer scene coordinates. See
+ /// [TransformationController.toScene] for how to convert these coordinates to
+ /// scene coordinates relative to the child.
+ ///
+ /// See also:
+ ///
+ /// * [onInteractionUpdate], which handles an update to the same interaction.
+ /// * [onInteractionEnd], which handles the end of the same interaction.
+ final GestureScaleStartCallback? onInteractionStart;
+
+ /// Called when the user updates a pan or scale gesture on the widget.
+ ///
+ /// At the time this is called, the [TransformationController] will have
+ /// already been updated to reflect the change caused by the interaction, if
+ /// the interaction caused the matrix to change.
+ ///
+ /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd}
+ ///
+ /// The coordinates provided in the details' `focalPoint` and
+ /// `localFocalPoint` are normal Flutter event coordinates, not
+ /// InteractiveViewer scene coordinates. See
+ /// [TransformationController.toScene] for how to convert these coordinates to
+ /// scene coordinates relative to the child.
+ ///
+ /// See also:
+ ///
+ /// * [onInteractionStart], which handles the start of the same interaction.
+ /// * [onInteractionEnd], which handles the end of the same interaction.
+ final GestureScaleUpdateCallback? onInteractionUpdate;
+
+ /// A [TransformationController] for the transformation performed on the
+ /// child.
+ ///
+ /// Whenever the child is transformed, the [Matrix4] value is updated and all
+ /// listeners are notified. If the value is set, InteractiveViewer will update
+ /// to respect the new value.
+ ///
+ /// {@tool dartpad}
+ /// This example shows how transformationController can be used to animate the
+ /// transformation back to its starting position.
+ ///
+ /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart **
+ /// {@end-tool}
+ ///
+ /// See also:
+ ///
+ /// * [ValueNotifier], the parent class of TransformationController.
+ /// * [TextEditingController] for an example of another similar pattern.
+ final TransformationController? transformationController;
+
+ // Used as the coefficient of friction in the inertial translation animation.
+ // This value was eyeballed to give a feel similar to Google Photos.
+ static const double _kDrag = 0.0000135;
+
+ /// Returns the closest point to the given point on the given line segment.
+ @visibleForTesting
+ static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) {
+ final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() +
+ math.pow(l2.y - l1.y, 2.0).toDouble();
+
+ // In this case, l1 == l2.
+ if (lengthSquared == 0) {
+ return l1;
+ }
+
+ // Calculate how far down the line segment the closest point is and return
+ // the point.
+ final Vector3 l1P = point - l1;
+ final Vector3 l1L2 = l2 - l1;
+ final double fraction =
+ clampDouble(l1P.dot(l1L2) / lengthSquared, 0.0, 1.0);
+ return l1 + l1L2 * fraction;
+ }
+
+ /// Given a quad, return its axis aligned bounding box.
+ @visibleForTesting
+ 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,
+ ),
+ ),
+ );
+ final double minY = math.min(
+ quad.point0.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,
+ ),
+ ),
+ );
+ final double maxY = math.max(
+ quad.point0.y,
+ math.max(
+ quad.point1.y,
+ math.max(
+ quad.point2.y,
+ quad.point3.y,
+ ),
+ ),
+ );
+ return Quad.points(
+ Vector3(minX, minY, 0),
+ Vector3(maxX, minY, 0),
+ Vector3(maxX, maxY, 0),
+ Vector3(minX, maxY, 0),
+ );
+ }
+
+ /// Returns true iff the point is inside the rectangle given by the Quad,
+ /// inclusively.
+ /// Algorithm from https://math.stackexchange.com/a/190373.
+ @visibleForTesting
+ static bool pointIsInside(Vector3 point, Quad quad) {
+ final Vector3 aM = point - quad.point0;
+ final Vector3 aB = quad.point1 - quad.point0;
+ final Vector3 aD = quad.point3 - quad.point0;
+
+ final double aMAB = aM.dot(aB);
+ final double aBAB = aB.dot(aB);
+ final double aMAD = aM.dot(aD);
+ final double aDAD = aD.dot(aD);
+
+ return 0 <= aMAB && aMAB <= aBAB && 0 <= aMAD && aMAD <= aDAD;
+ }
+
+ /// Get the point inside (inclusively) the given Quad that is nearest to the
+ /// given Vector3.
+ @visibleForTesting
+ static Vector3 getNearestPointInside(Vector3 point, Quad quad) {
+ // If the point is inside the axis aligned bounding box, then it's ok where
+ // it is.
+ if (pointIsInside(point, quad)) {
+ return point;
+ }
+
+ // Otherwise, return the nearest point on the quad.
+ final List closestPoints = [
+ InteractiveViewer.getNearestPointOnLine(point, quad.point0, quad.point1),
+ InteractiveViewer.getNearestPointOnLine(point, quad.point1, quad.point2),
+ InteractiveViewer.getNearestPointOnLine(point, quad.point2, quad.point3),
+ InteractiveViewer.getNearestPointOnLine(point, quad.point3, quad.point0),
+ ];
+ double minDistance = double.infinity;
+ late Vector3 closestOverall;
+ for (final Vector3 closePoint in closestPoints) {
+ final double distance = math.sqrt(
+ math.pow(point.x - closePoint.x, 2) +
+ math.pow(point.y - closePoint.y, 2),
+ );
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestOverall = closePoint;
+ }
+ }
+ return closestOverall;
+ }
+
+ @override
+ State createState() => _InteractiveViewerState();
+}
+
+class _InteractiveViewerState extends State
+ with TickerProviderStateMixin {
+ TransformationController? _transformationController;
+
+ final GlobalKey _childKey = GlobalKey();
+ final GlobalKey _parentKey = GlobalKey();
+ Animation? _animation;
+ Animation? _scaleAnimation;
+ late Offset _scaleAnimationFocalPoint;
+ late AnimationController _controller;
+ late AnimationController _scaleController;
+ Axis? _currentAxis; // Used with panAxis.
+ Offset? _referenceFocalPoint; // Point where the current gesture began.
+ double? _scaleStart; // Scale value at start of scaling gesture.
+ double? _rotationStart = 0.0; // Rotation at start of rotation gesture.
+ double _currentRotation = 0.0; // Rotation of _transformationController.value.
+ _GestureType? _gestureType;
+
+ // TODO(justinmc): Add rotateEnabled parameter to the widget and remove this
+ // hardcoded value when the rotation feature is implemented.
+ // https://github.com/flutter/flutter/issues/57698
+ final bool _rotateEnabled = false;
+
+ // The _boundaryRect is calculated by adding the boundaryMargin to the size of
+ // the child.
+ Rect get _boundaryRect {
+ assert(_childKey.currentContext != null);
+ assert(!widget.boundaryMargin.left.isNaN);
+ assert(!widget.boundaryMargin.right.isNaN);
+ assert(!widget.boundaryMargin.top.isNaN);
+ assert(!widget.boundaryMargin.bottom.isNaN);
+
+ final RenderBox childRenderBox =
+ _childKey.currentContext!.findRenderObject()! as RenderBox;
+ final Size childSize = childRenderBox.size;
+ final Rect boundaryRect =
+ widget.boundaryMargin.inflateRect(Offset.zero & childSize);
+ assert(
+ !boundaryRect.isEmpty,
+ "InteractiveViewer's child must have nonzero dimensions.",
+ );
+ // Boundaries that are partially infinite are not allowed because Matrix4's
+ // rotation and translation methods don't handle infinites well.
+ assert(
+ boundaryRect.isFinite ||
+ (boundaryRect.left.isInfinite &&
+ boundaryRect.top.isInfinite &&
+ boundaryRect.right.isInfinite &&
+ boundaryRect.bottom.isInfinite),
+ 'boundaryRect must either be infinite in all directions or finite in all directions.',
+ );
+ return boundaryRect;
+ }
+
+ // The Rect representing the child's parent.
+ Rect get _viewport {
+ assert(_parentKey.currentContext != null);
+ final RenderBox parentRenderBox =
+ _parentKey.currentContext!.findRenderObject()! as RenderBox;
+ return Offset.zero & parentRenderBox.size;
+ }
+
+ // Return a new matrix representing the given matrix after applying the given
+ // translation.
+ Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
+ if (translation == Offset.zero) {
+ return matrix.clone();
+ }
+
+ final Offset alignedTranslation;
+
+ if (_currentAxis != null) {
+ alignedTranslation = switch (widget.panAxis) {
+ PanAxis.horizontal => _alignAxis(translation, Axis.horizontal),
+ PanAxis.vertical => _alignAxis(translation, Axis.vertical),
+ PanAxis.aligned => _alignAxis(translation, _currentAxis!),
+ PanAxis.free => translation,
+ };
+ } else {
+ alignedTranslation = translation;
+ }
+
+ final Matrix4 nextMatrix = matrix.clone()
+ ..translate(
+ alignedTranslation.dx,
+ alignedTranslation.dy,
+ );
+
+ // Transform the viewport to determine where its four corners will be after
+ // the child has been transformed.
+ final Quad nextViewport = _transformViewport(nextMatrix, _viewport);
+
+ // If the boundaries are infinite, then no need to check if the translation
+ // fits within them.
+ if (_boundaryRect.isInfinite) {
+ return nextMatrix;
+ }
+
+ // Expand the boundaries with rotation. This prevents the problem where a
+ // mismatch in orientation between the viewport and boundaries effectively
+ // limits translation. With this approach, all points that are visible with
+ // no rotation are visible after rotation.
+ final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation(
+ _boundaryRect,
+ _currentRotation,
+ );
+
+ // If the given translation fits completely within the boundaries, allow it.
+ final Offset offendingDistance =
+ _exceedsBy(boundariesAabbQuad, nextViewport);
+ if (offendingDistance == Offset.zero) {
+ return nextMatrix;
+ }
+
+ // Desired translation goes out of bounds, so translate to the nearest
+ // in-bounds point instead.
+ final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix);
+ final double currentScale = matrix.getMaxScaleOnAxis();
+ final Offset correctedTotalTranslation = Offset(
+ nextTotalTranslation.dx - offendingDistance.dx * currentScale,
+ nextTotalTranslation.dy - offendingDistance.dy * currentScale,
+ );
+ // TODO(justinmc): This needs some work to handle rotation properly. The
+ // idea is that the boundaries are axis aligned (boundariesAabbQuad), but
+ // calculating the translation to put the viewport inside that Quad is more
+ // complicated than this when rotated.
+ // https://github.com/flutter/flutter/issues/57698
+ final Matrix4 correctedMatrix = matrix.clone()
+ ..setTranslation(Vector3(
+ correctedTotalTranslation.dx,
+ correctedTotalTranslation.dy,
+ 0.0,
+ ));
+
+ // Double check that the corrected translation fits.
+ final Quad correctedViewport =
+ _transformViewport(correctedMatrix, _viewport);
+ final Offset offendingCorrectedDistance =
+ _exceedsBy(boundariesAabbQuad, correctedViewport);
+ if (offendingCorrectedDistance == Offset.zero) {
+ return correctedMatrix;
+ }
+
+ // If the corrected translation doesn't fit in either direction, don't allow
+ // any translation at all. This happens when the viewport is larger than the
+ // entire boundary.
+ if (offendingCorrectedDistance.dx != 0.0 &&
+ offendingCorrectedDistance.dy != 0.0) {
+ return matrix.clone();
+ }
+
+ // Otherwise, allow translation in only the direction that fits. This
+ // happens when the viewport is larger than the boundary in one direction.
+ final Offset unidirectionalCorrectedTotalTranslation = Offset(
+ offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0,
+ offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0,
+ );
+ return matrix.clone()
+ ..setTranslation(Vector3(
+ unidirectionalCorrectedTotalTranslation.dx,
+ unidirectionalCorrectedTotalTranslation.dy,
+ 0.0,
+ ));
+ }
+
+ // Return a new matrix representing the given matrix after applying the given
+ // scale.
+ Matrix4 _matrixScale(Matrix4 matrix, double scale) {
+ if (scale == 1.0) {
+ return matrix.clone();
+ }
+ assert(scale != 0.0);
+
+ // Don't allow a scale that results in an overall scale beyond min/max
+ // scale.
+ final double currentScale =
+ _transformationController!.value.getMaxScaleOnAxis();
+ final double totalScale = math.max(
+ currentScale * scale,
+ // Ensure that the scale cannot make the child so big that it can't fit
+ // inside the boundaries (in either direction).
+ math.max(
+ _viewport.width / _boundaryRect.width,
+ _viewport.height / _boundaryRect.height,
+ ),
+ );
+ final double clampedTotalScale = clampDouble(
+ totalScale,
+ widget.minScale,
+ widget.maxScale,
+ );
+ final double clampedScale = clampedTotalScale / currentScale;
+ return matrix.clone()..scale(clampedScale);
+ }
+
+ // Return a new matrix representing the given matrix after applying the given
+ // rotation.
+ Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) {
+ if (rotation == 0) {
+ return matrix.clone();
+ }
+ final Offset focalPointScene = _transformationController!.toScene(
+ focalPoint,
+ );
+ return matrix.clone()
+ ..translate(focalPointScene.dx, focalPointScene.dy)
+ ..rotateZ(-rotation)
+ ..translate(-focalPointScene.dx, -focalPointScene.dy);
+ }
+
+ // Returns true iff the given _GestureType is enabled.
+ bool _gestureIsSupported(_GestureType? gestureType) {
+ return switch (gestureType) {
+ _GestureType.rotate => _rotateEnabled,
+ _GestureType.scale => widget.scaleEnabled,
+ _GestureType.pan || null => widget.panEnabled,
+ };
+ }
+
+ // Decide which type of gesture this is by comparing the amount of scale
+ // and rotation in the gesture, if any. Scale starts at 1 and rotation
+ // starts at 0. Pan will have no scale and no rotation because it uses only one
+ // finger.
+ _GestureType _getGestureType(ScaleUpdateDetails details) {
+ final double scale = !widget.scaleEnabled ? 1.0 : details.scale;
+ final double rotation = !_rotateEnabled ? 0.0 : details.rotation;
+ if ((scale - 1).abs() > rotation.abs()) {
+ return _GestureType.scale;
+ } else if (rotation != 0.0) {
+ return _GestureType.rotate;
+ } else {
+ return _GestureType.pan;
+ }
+ }
+
+ // Handle the start of a gesture. All of pan, scale, and rotate are handled
+ // with GestureDetector's scale gesture.
+ void _onScaleStart(ScaleStartDetails details) {
+ if (details.pointerCount < 2 &&
+ _transformationController?.value.row0.x == 1.0) {
+ widget.onPanStart?.call(details);
+ return;
+ }
+
+ widget.onInteractionStart?.call(details);
+
+ if (_controller.isAnimating) {
+ _controller.stop();
+ _controller.reset();
+ _animation?.removeListener(_onAnimate);
+ _animation = null;
+ }
+ if (_scaleController.isAnimating) {
+ _scaleController.stop();
+ _scaleController.reset();
+ _scaleAnimation?.removeListener(_onScaleAnimate);
+ _scaleAnimation = null;
+ }
+
+ _gestureType = null;
+ _currentAxis = null;
+ _scaleStart = _transformationController!.value.getMaxScaleOnAxis();
+ _referenceFocalPoint = _transformationController!.toScene(
+ details.localFocalPoint,
+ );
+ _rotationStart = _currentRotation;
+ }
+
+ // Handle an update to an ongoing gesture. All of pan, scale, and rotate are
+ // handled with GestureDetector's scale gesture.
+ void _onScaleUpdate(ScaleUpdateDetails details) {
+ if (details.pointerCount < 2 &&
+ _transformationController?.value.row0.x == 1.0) {
+ widget.onPanUpdate?.call(details);
+ return;
+ }
+
+ final double scale = _transformationController!.value.getMaxScaleOnAxis();
+ _scaleAnimationFocalPoint = details.localFocalPoint;
+ final Offset focalPointScene = _transformationController!.toScene(
+ details.localFocalPoint,
+ );
+
+ if (_gestureType == _GestureType.pan) {
+ // When a gesture first starts, it sometimes has no change in scale and
+ // rotation despite being a two-finger gesture. Here the gesture is
+ // allowed to be reinterpreted as its correct type after originally
+ // being marked as a pan.
+ _gestureType = _getGestureType(details);
+ } else {
+ _gestureType ??= _getGestureType(details);
+ }
+ if (!_gestureIsSupported(_gestureType)) {
+ widget.onInteractionUpdate?.call(details);
+ return;
+ }
+
+ switch (_gestureType!) {
+ case _GestureType.scale:
+ assert(_scaleStart != null);
+ // details.scale gives us the amount to change the scale as of the
+ // start of this gesture, so calculate the amount to scale as of the
+ // previous call to _onScaleUpdate.
+ final double desiredScale = _scaleStart! * details.scale;
+ final double scaleChange = desiredScale / scale;
+ _transformationController!.value = _matrixScale(
+ _transformationController!.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(
+ details.localFocalPoint,
+ );
+ _transformationController!.value = _matrixTranslate(
+ _transformationController!.value,
+ focalPointSceneScaled - _referenceFocalPoint!,
+ );
+
+ // details.localFocalPoint should now be at the same location as the
+ // original _referenceFocalPoint point. If it's not, that's because
+ // the translate came in contact with a boundary. In that case, update
+ // _referenceFocalPoint so subsequent updates happen in relation to
+ // the new effective focal point.
+ final Offset focalPointSceneCheck = _transformationController!.toScene(
+ details.localFocalPoint,
+ );
+ if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
+ _referenceFocalPoint = focalPointSceneCheck;
+ }
+
+ case _GestureType.rotate:
+ if (details.rotation == 0.0) {
+ widget.onInteractionUpdate?.call(details);
+ return;
+ }
+ final double desiredRotation = _rotationStart! + details.rotation;
+ _transformationController!.value = _matrixRotate(
+ _transformationController!.value,
+ _currentRotation - desiredRotation,
+ details.localFocalPoint,
+ );
+ _currentRotation = desiredRotation;
+
+ case _GestureType.pan:
+ assert(_referenceFocalPoint != null);
+ // details may have a change in scale here when scaleEnabled is false.
+ // In an effort to keep the behavior similar whether or not scaleEnabled
+ // is true, these gestures are thrown away.
+ if (details.scale != 1.0) {
+ widget.onInteractionUpdate?.call(details);
+ return;
+ }
+ _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene);
+ // Translate so that the same point in the scene is underneath the
+ // focal point before and after the movement.
+ final Offset translationChange =
+ focalPointScene - _referenceFocalPoint!;
+ _transformationController!.value = _matrixTranslate(
+ _transformationController!.value,
+ translationChange,
+ );
+ _referenceFocalPoint = _transformationController!.toScene(
+ details.localFocalPoint,
+ );
+ }
+ widget.onInteractionUpdate?.call(details);
+ }
+
+ // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
+ // are handled with GestureDetector's scale gesture.
+ void _onScaleEnd(ScaleEndDetails details) {
+ if (details.pointerCount < 2 &&
+ _transformationController?.value.row0.x == 1.0) {
+ widget.onPanEnd?.call(details);
+ return;
+ }
+
+ widget.onInteractionEnd?.call(details);
+ _scaleStart = null;
+ _rotationStart = null;
+ _referenceFocalPoint = null;
+
+ _animation?.removeListener(_onAnimate);
+ _scaleAnimation?.removeListener(_onScaleAnimate);
+ _controller.reset();
+ _scaleController.reset();
+
+ if (!_gestureIsSupported(_gestureType)) {
+ _currentAxis = null;
+ return;
+ }
+
+ switch (_gestureType) {
+ case _GestureType.pan:
+ if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
+ _currentAxis = null;
+ return;
+ }
+ final Vector3 translationVector =
+ _transformationController!.value.getTranslation();
+ final Offset translation =
+ Offset(translationVector.x, translationVector.y);
+ final FrictionSimulation frictionSimulationX = FrictionSimulation(
+ widget.interactionEndFrictionCoefficient,
+ translation.dx,
+ details.velocity.pixelsPerSecond.dx,
+ );
+ final FrictionSimulation frictionSimulationY = FrictionSimulation(
+ widget.interactionEndFrictionCoefficient,
+ translation.dy,
+ details.velocity.pixelsPerSecond.dy,
+ );
+ final double tFinal = _getFinalTime(
+ details.velocity.pixelsPerSecond.distance,
+ widget.interactionEndFrictionCoefficient,
+ );
+ _animation = Tween(
+ begin: translation,
+ end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
+ ).animate(CurvedAnimation(
+ parent: _controller,
+ curve: Curves.decelerate,
+ ));
+ _controller.duration = Duration(milliseconds: (tFinal * 1000).round());
+ _animation!.addListener(_onAnimate);
+ _controller.forward();
+ case _GestureType.scale:
+ if (details.scaleVelocity.abs() < 0.1) {
+ _currentAxis = null;
+ return;
+ }
+ final double scale =
+ _transformationController!.value.getMaxScaleOnAxis();
+ final FrictionSimulation frictionSimulation = FrictionSimulation(
+ widget.interactionEndFrictionCoefficient * widget.scaleFactor,
+ scale,
+ details.scaleVelocity / 10);
+ final double tFinal = _getFinalTime(details.scaleVelocity.abs(),
+ widget.interactionEndFrictionCoefficient,
+ effectivelyMotionless: 0.1);
+ _scaleAnimation =
+ Tween(begin: scale, end: frictionSimulation.x(tFinal))
+ .animate(CurvedAnimation(
+ parent: _scaleController, curve: Curves.decelerate));
+ _scaleController.duration =
+ Duration(milliseconds: (tFinal * 1000).round());
+ _scaleAnimation!.addListener(_onScaleAnimate);
+ _scaleController.forward();
+ case _GestureType.rotate || null:
+ break;
+ }
+ }
+
+ // Handle mousewheel and web trackpad scroll events.
+ void _receivedPointerSignal(PointerSignalEvent event) {
+ 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,
+ ),
+ );
+
+ final Offset localDelta = PointerEvent.transformDeltaViaPositions(
+ untransformedEndPosition: event.position + event.scrollDelta,
+ untransformedDelta: event.scrollDelta,
+ transform: event.transform,
+ );
+
+ if (!_gestureIsSupported(_GestureType.pan)) {
+ widget.onInteractionUpdate?.call(ScaleUpdateDetails(
+ focalPoint: event.position - event.scrollDelta,
+ localFocalPoint: event.localPosition - event.scrollDelta,
+ focalPointDelta: -localDelta,
+ ));
+ widget.onInteractionEnd?.call(ScaleEndDetails());
+ return;
+ }
+
+ final Offset focalPointScene = _transformationController!.toScene(
+ event.localPosition,
+ );
+
+ final Offset newFocalPointScene = _transformationController!.toScene(
+ event.localPosition - localDelta,
+ );
+
+ _transformationController!.value = _matrixTranslate(
+ _transformationController!.value,
+ newFocalPointScene - focalPointScene);
+
+ widget.onInteractionUpdate?.call(ScaleUpdateDetails(
+ focalPoint: event.position - event.scrollDelta,
+ localFocalPoint: event.localPosition - localDelta,
+ focalPointDelta: -localDelta));
+ widget.onInteractionEnd?.call(ScaleEndDetails());
+ return;
+ }
+ // Ignore left and right mouse wheel scroll.
+ if (event.scrollDelta.dy == 0.0) {
+ return;
+ }
+ scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor);
+ } else if (event is PointerScaleEvent) {
+ scaleChange = event.scale;
+ } else {
+ return;
+ }
+ widget.onInteractionStart?.call(
+ ScaleStartDetails(
+ focalPoint: event.position,
+ localFocalPoint: event.localPosition,
+ ),
+ );
+
+ if (!_gestureIsSupported(_GestureType.scale)) {
+ widget.onInteractionUpdate?.call(ScaleUpdateDetails(
+ focalPoint: event.position,
+ localFocalPoint: event.localPosition,
+ scale: scaleChange,
+ ));
+ widget.onInteractionEnd?.call(ScaleEndDetails());
+ return;
+ }
+
+ final Offset focalPointScene = _transformationController!.toScene(
+ event.localPosition,
+ );
+
+ _transformationController!.value = _matrixScale(
+ _transformationController!.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,
+ focalPointSceneScaled - focalPointScene,
+ );
+
+ widget.onInteractionUpdate?.call(ScaleUpdateDetails(
+ focalPoint: event.position,
+ localFocalPoint: event.localPosition,
+ scale: scaleChange,
+ ));
+ widget.onInteractionEnd?.call(ScaleEndDetails());
+ }
+
+ // Handle inertia drag animation.
+ void _onAnimate() {
+ if (!_controller.isAnimating) {
+ _currentAxis = null;
+ _animation?.removeListener(_onAnimate);
+ _animation = null;
+ _controller.reset();
+ return;
+ }
+ // Translate such that the resulting translation is _animation.value.
+ final Vector3 translationVector =
+ _transformationController!.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,
+ );
+ }
+
+ // Handle inertia scale animation.
+ void _onScaleAnimate() {
+ if (!_scaleController.isAnimating) {
+ _currentAxis = null;
+ _scaleAnimation?.removeListener(_onScaleAnimate);
+ _scaleAnimation = null;
+ _scaleController.reset();
+ return;
+ }
+ final double desiredScale = _scaleAnimation!.value;
+ final double scaleChange =
+ desiredScale / _transformationController!.value.getMaxScaleOnAxis();
+ final Offset referenceFocalPoint = _transformationController!.toScene(
+ _scaleAnimationFocalPoint,
+ );
+ _transformationController!.value = _matrixScale(
+ _transformationController!.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(
+ _scaleAnimationFocalPoint,
+ );
+ _transformationController!.value = _matrixTranslate(
+ _transformationController!.value,
+ focalPointSceneScaled - referenceFocalPoint,
+ );
+ }
+
+ void _onTransformationControllerChange() {
+ // A change to the TransformationController's value is a change to the
+ // state.
+ setState(() {});
+ }
+
+ @override
+ void initState() {
+ super.initState();
+
+ _transformationController =
+ widget.transformationController ?? TransformationController();
+ _transformationController!.addListener(_onTransformationControllerChange);
+ _controller = AnimationController(
+ vsync: this,
+ );
+ _scaleController = AnimationController(vsync: this);
+ }
+
+ @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);
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ _scaleController.dispose();
+ _transformationController!
+ .removeListener(_onTransformationControllerChange);
+ if (widget.transformationController == null) {
+ _transformationController!.dispose();
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ Widget child;
+ if (widget.child != null) {
+ child = _InteractiveViewerBuilt(
+ childKey: _childKey,
+ clipBehavior: widget.clipBehavior,
+ constrained: widget.constrained,
+ matrix: _transformationController!.value,
+ alignment: widget.alignment,
+ child: widget.child!,
+ );
+ } else {
+ // When using InteractiveViewer.builder, then constrained is false and the
+ // viewport is the size of the constraints.
+ assert(widget.builder != null);
+ assert(!widget.constrained);
+ child = LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ final Matrix4 matrix = _transformationController!.value;
+ return _InteractiveViewerBuilt(
+ childKey: _childKey,
+ clipBehavior: widget.clipBehavior,
+ constrained: widget.constrained,
+ alignment: widget.alignment,
+ matrix: matrix,
+ child: widget.builder!(
+ context,
+ _transformViewport(matrix, Offset.zero & constraints.biggest),
+ ),
+ );
+ },
+ );
+ }
+
+ return Listener(
+ key: _parentKey,
+ onPointerSignal: _receivedPointerSignal,
+ child: GestureDetector(
+ behavior:
+ HitTestBehavior.translucent, // Necessary when panning off screen.
+ onScaleEnd: _onScaleEnd,
+ onScaleStart: _onScaleStart,
+ onScaleUpdate: _onScaleUpdate,
+ trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
+ trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor),
+ child: child,
+ ),
+ );
+ }
+}
+
+// This widget allows us to easily swap in and out the LayoutBuilder in
+// InteractiveViewer's depending on if it's using a builder or a child.
+class _InteractiveViewerBuilt extends StatelessWidget {
+ const _InteractiveViewerBuilt({
+ required this.child,
+ required this.childKey,
+ required this.clipBehavior,
+ required this.constrained,
+ required this.matrix,
+ required this.alignment,
+ });
+
+ final Widget child;
+ final GlobalKey childKey;
+ final Clip clipBehavior;
+ final bool constrained;
+ final Matrix4 matrix;
+ final Alignment? alignment;
+
+ @override
+ Widget build(BuildContext context) {
+ Widget child = Transform(
+ transform: matrix,
+ alignment: alignment,
+ child: KeyedSubtree(
+ key: childKey,
+ child: this.child,
+ ),
+ );
+
+ if (!constrained) {
+ child = OverflowBox(
+ alignment: Alignment.topLeft,
+ minWidth: 0.0,
+ minHeight: 0.0,
+ maxWidth: double.infinity,
+ maxHeight: double.infinity,
+ child: child,
+ );
+ }
+
+ 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);
+ }
+}
+
+// A classification of relevant user gestures. Each contiguous user gesture is
+// represented by exactly one _GestureType.
+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.
+double _getFinalTime(double velocity, double drag,
+ {double effectivelyMotionless = 10}) {
+ return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
+}
+
+// Return the translation from the given Matrix4 as an Offset.
+Offset _getMatrixTranslation(Matrix4 matrix) {
+ final Vector3 nextTranslation = matrix.getTranslation();
+ return Offset(nextTranslation.x, nextTranslation.y);
+}
+
+// Transform the four corners of the viewport by the inverse of the given
+// matrix. This gives the viewport after the child has been transformed by the
+// given matrix. The viewport transforms as the inverse of the child (i.e.
+// moving the child left is equivalent to moving the viewport right).
+Quad _transformViewport(Matrix4 matrix, Rect viewport) {
+ final Matrix4 inverseMatrix = matrix.clone()..invert();
+ return Quad.points(
+ inverseMatrix.transform3(Vector3(
+ viewport.topLeft.dx,
+ viewport.topLeft.dy,
+ 0.0,
+ )),
+ inverseMatrix.transform3(Vector3(
+ viewport.topRight.dx,
+ viewport.topRight.dy,
+ 0.0,
+ )),
+ inverseMatrix.transform3(Vector3(
+ viewport.bottomRight.dx,
+ viewport.bottomRight.dy,
+ 0.0,
+ )),
+ inverseMatrix.transform3(Vector3(
+ viewport.bottomLeft.dx,
+ viewport.bottomLeft.dy,
+ 0.0,
+ )),
+ );
+}
+
+// Find the axis aligned bounding box for the rect rotated about its center by
+// the given amount.
+Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) {
+ final Matrix4 rotationMatrix = Matrix4.identity()
+ ..translate(rect.size.width / 2, rect.size.height / 2)
+ ..rotateZ(rotation)
+ ..translate(-rect.size.width / 2, -rect.size.height / 2);
+ final Quad boundariesRotated = Quad.points(
+ rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)),
+ rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)),
+ rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)),
+ rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)),
+ );
+ return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated);
+}
+
+// Return the amount that viewport lies outside of boundary. If the viewport
+// is completely contained within the boundary (inclusively), then returns
+// Offset.zero.
+Offset _exceedsBy(Quad boundary, Quad viewport) {
+ final List viewportPoints = [
+ viewport.point0,
+ viewport.point1,
+ viewport.point2,
+ viewport.point3,
+ ];
+ Offset largestExcess = Offset.zero;
+ for (final Vector3 point in viewportPoints) {
+ final Vector3 pointInside =
+ InteractiveViewer.getNearestPointInside(point, boundary);
+ final Offset excess = Offset(
+ pointInside.x - point.x,
+ pointInside.y - point.y,
+ );
+ if (excess.dx.abs() > largestExcess.dx.abs()) {
+ largestExcess = Offset(excess.dx, largestExcess.dy);
+ }
+ if (excess.dy.abs() > largestExcess.dy.abs()) {
+ largestExcess = Offset(largestExcess.dx, excess.dy);
+ }
+ }
+
+ return _round(largestExcess);
+}
+
+// Round the output values. This works around a precision problem where
+// values that should have been zero were given as within 10^-10 of zero.
+Offset _round(Offset offset) {
+ return Offset(
+ double.parse(offset.dx.toStringAsFixed(9)),
+ double.parse(offset.dy.toStringAsFixed(9)),
+ );
+}
+
+// Align the given offset to the given axis by allowing movement only in the
+// axis direction.
+Offset _alignAxis(Offset offset, Axis axis) {
+ return switch (axis) {
+ Axis.horizontal => Offset(offset.dx, 0.0),
+ Axis.vertical => Offset(0.0, offset.dy),
+ };
+}
+
+// Given two points, return the axis where the distance between the points is
+// greatest. If they are equal, return null.
+Axis? _getPanAxis(Offset point1, Offset point2) {
+ if (point1 == point2) {
+ return null;
+ }
+ final double x = point2.dx - point1.dx;
+ final double y = point2.dy - point1.dy;
+ return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical;
+}
+
+/// 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
new file mode 100644
index 000000000..4dd591f2e
--- /dev/null
+++ b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart
@@ -0,0 +1,236 @@
+import 'interactive_viewer.dart' as custom;
+import 'package:flutter/material.dart';
+
+/// https://github.com/qq326646683/interactiveviewer_gallery
+
+/// A callback for the [InteractiveViewerBoundary] that is called when the scale
+/// changed.
+typedef ScaleChanged = void Function(double scale);
+
+/// Builds an [InteractiveViewer] and provides callbacks that are called when a
+/// horizontal boundary has been hit.
+///
+/// The callbacks are called when an interaction ends by listening to the
+/// [InteractiveViewer.onInteractionEnd] callback.
+class InteractiveViewerBoundary extends StatefulWidget {
+ const InteractiveViewerBoundary({
+ super.key,
+ required this.child,
+ required this.boundaryWidth,
+ this.controller,
+ this.onScaleChanged,
+ this.onLeftBoundaryHit,
+ this.onRightBoundaryHit,
+ this.onNoBoundaryHit,
+ required this.maxScale,
+ required this.minScale,
+ this.onDismissed,
+ this.dismissThreshold = 0.2,
+ });
+
+ final double dismissThreshold;
+ final VoidCallback? onDismissed;
+
+ final Widget child;
+
+ /// The max width this widget can have.
+ ///
+ /// If the [InteractiveViewer] can take up the entire screen width, this
+ /// should be set to `MediaQuery.of(context).size.width`.
+ final double boundaryWidth;
+
+ /// The [TransformationController] for the [InteractiveViewer].
+ final custom.TransformationController? controller;
+
+ /// Called when the scale changed after an interaction ended.
+ final ScaleChanged? onScaleChanged;
+
+ /// Called when the left boundary has been hit after an interaction ended.
+ final VoidCallback? onLeftBoundaryHit;
+
+ /// Called when the right boundary has been hit after an interaction ended.
+ final VoidCallback? onRightBoundaryHit;
+
+ /// Called when no boundary has been hit after an interaction ended.
+ final VoidCallback? onNoBoundaryHit;
+
+ final double maxScale;
+
+ final double minScale;
+
+ @override
+ InteractiveViewerBoundaryState createState() =>
+ InteractiveViewerBoundaryState();
+}
+
+class InteractiveViewerBoundaryState extends State
+ with SingleTickerProviderStateMixin {
+ custom.TransformationController? _controller;
+
+ double? _scale;
+
+ late AnimationController _animateController;
+ late Animation _slideAnimation;
+ late Animation _scaleAnimation;
+ late Animation _opacityAnimation;
+
+ Offset _offset = Offset.zero;
+ bool _dragging = false;
+
+ bool get _isActive => _dragging || _animateController.isAnimating;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _controller = widget.controller ?? custom.TransformationController();
+
+ _animateController = AnimationController(
+ duration: const Duration(milliseconds: 300),
+ vsync: this,
+ );
+
+ _updateMoveAnimation();
+ }
+
+ @override
+ void dispose() {
+ _controller!.dispose();
+ _animateController.dispose();
+
+ super.dispose();
+ }
+
+ void _updateMoveAnimation() {
+ final double endX = _offset.dx.sign * (_offset.dx.abs() / _offset.dy.abs());
+ final double endY = _offset.dy.sign;
+
+ _slideAnimation = _animateController.drive(
+ Tween(
+ begin: Offset.zero,
+ end: Offset(endX, endY),
+ ),
+ );
+
+ _scaleAnimation = _animateController.drive(
+ Tween(
+ begin: 1,
+ end: 0.25,
+ ),
+ );
+
+ _opacityAnimation = _animateController.drive(
+ DecorationTween(
+ begin: const BoxDecoration(
+ color: Colors.black,
+ ),
+ end: const BoxDecoration(
+ color: Colors.transparent,
+ ),
+ ),
+ );
+ }
+
+ void _handleDragStart(ScaleStartDetails details) {
+ _dragging = true;
+
+ if (_animateController.isAnimating) {
+ _animateController.stop();
+ } else {
+ _offset = Offset.zero;
+ _animateController.value = 0.0;
+ }
+ setState(_updateMoveAnimation);
+ }
+
+ void _handleDragUpdate(ScaleUpdateDetails details) {
+ if (!_isActive || _animateController.isAnimating) {
+ return;
+ }
+
+ _offset += details.focalPointDelta;
+
+ setState(_updateMoveAnimation);
+
+ if (!_animateController.isAnimating) {
+ _animateController.value = _offset.dy.abs() / context.size!.height;
+ }
+ }
+
+ void _handleDragEnd(ScaleEndDetails details) {
+ if (!_isActive || _animateController.isAnimating) {
+ return;
+ }
+
+ _dragging = false;
+
+ if (_animateController.isCompleted) {
+ return;
+ }
+
+ if (!_animateController.isDismissed) {
+ // if the dragged value exceeded the dismissThreshold, call onDismissed
+ // else animate back to initial position.
+ if (_animateController.value > widget.dismissThreshold) {
+ widget.onDismissed?.call();
+ } else {
+ _animateController.reverse();
+ }
+ }
+ }
+
+ void _updateBoundaryDetection() {
+ final double scale = _controller!.value.row0[0];
+
+ if (_scale != scale) {
+ // the scale changed
+ _scale = scale;
+ widget.onScaleChanged?.call(scale);
+ }
+
+ if (scale <= 1.01) {
+ // cant hit any boundaries when the child is not scaled
+ return;
+ }
+
+ final double xOffset = _controller!.value.row0[3];
+ final double boundaryWidth = widget.boundaryWidth;
+ final double boundaryEnd = boundaryWidth * scale;
+ final double xPos = boundaryEnd + xOffset;
+
+ if (boundaryEnd.round() == xPos.round()) {
+ // left boundary hit
+ widget.onLeftBoundaryHit?.call();
+ } else if (boundaryWidth.round() == xPos.round()) {
+ // right boundary hit
+ widget.onRightBoundaryHit?.call();
+ } else {
+ widget.onNoBoundaryHit?.call();
+ }
+ }
+
+ Widget get content => DecoratedBoxTransition(
+ decoration: _opacityAnimation,
+ child: SlideTransition(
+ position: _slideAnimation,
+ child: ScaleTransition(
+ scale: _scaleAnimation,
+ child: widget.child,
+ ),
+ ),
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ return custom.InteractiveViewer(
+ maxScale: widget.maxScale,
+ minScale: widget.minScale,
+ transformationController: _controller,
+ onInteractionEnd: (_) => _updateBoundaryDetection(),
+ onPanStart: _handleDragStart,
+ onPanUpdate: _handleDragUpdate,
+ onPanEnd: _handleDragEnd,
+ child: content,
+ );
+ }
+}
diff --git a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart
new file mode 100644
index 000000000..c7c31e123
--- /dev/null
+++ b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart
@@ -0,0 +1,491 @@
+import 'dart:io';
+
+import 'package:PiliPalaX/utils/download.dart';
+import 'package:PiliPalaX/utils/storage.dart';
+import 'package:PiliPalaX/utils/utils.dart';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:share_plus/share_plus.dart';
+import 'package:status_bar_control/status_bar_control.dart';
+import 'interactive_viewer_boundary.dart';
+import 'interactive_viewer.dart' as custom;
+
+/// https://github.com/qq326646683/interactiveviewer_gallery
+
+/// Builds a carousel controlled by a [PageView] for the tweet media sources.
+///
+/// Used for showing a full screen view of the [TweetMedia] sources.
+///
+/// The sources can be panned and zoomed interactively using an
+/// [InteractiveViewer].
+/// An [InteractiveViewerBoundary] is used to detect when the boundary of the
+/// source is hit after zooming in to disable or enable the swiping gesture of
+/// the [PageView].
+///
+typedef IndexedFocusedWidgetBuilder = Widget Function(
+ BuildContext context, int index, bool isFocus, bool enablePageView);
+
+typedef IndexedTagStringBuilder = String Function(int index);
+
+class InteractiveviewerGallery extends StatefulWidget {
+ const InteractiveviewerGallery({
+ super.key,
+ required this.sources,
+ required this.initIndex,
+ this.itemBuilder,
+ this.maxScale = 8,
+ this.minScale = 1.0,
+ this.onPageChanged,
+ this.onDismissed,
+ });
+
+ /// The sources to show.
+ final List sources;
+
+ /// The index of the first source in [sources] to show.
+ final int initIndex;
+
+ /// The item content
+ final IndexedFocusedWidgetBuilder? itemBuilder;
+
+ final double maxScale;
+
+ final double minScale;
+
+ final ValueChanged? onPageChanged;
+
+ final ValueChanged? onDismissed;
+
+ @override
+ State createState() =>
+ _InteractiveviewerGalleryState();
+}
+
+class _InteractiveviewerGalleryState extends State
+ with SingleTickerProviderStateMixin {
+ PageController? _pageController;
+ custom.TransformationController? _transformationController;
+
+ /// The controller to animate the transformation value of the
+ /// [InteractiveViewer] when it should reset.
+ late AnimationController _animationController;
+ Animation? _animation;
+
+ /// `true` when an source is zoomed in and not at the at a horizontal boundary
+ /// to disable the [PageView].
+ bool _enablePageView = true;
+
+ late Offset _doubleTapLocalPosition;
+
+ int? currentIndex;
+
+ late List _thumbList;
+ late int _quality;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _quality =
+ GStorage.setting.get(SettingBoxKey.previewQuality, defaultValue: 80);
+ _thumbList = List.generate(widget.sources.length, (_) => true);
+
+ _pageController = PageController(initialPage: widget.initIndex);
+
+ _transformationController = custom.TransformationController();
+
+ _animationController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 300),
+ )..addListener(() {
+ _transformationController!.value =
+ _animation?.value ?? Matrix4.identity();
+ });
+
+ currentIndex = widget.initIndex;
+ setStatusBar();
+ }
+
+ setStatusBar() async {
+ if (Platform.isIOS || Platform.isAndroid) {
+ await StatusBarControl.setHidden(
+ true,
+ animation: StatusBarAnimation.FADE,
+ );
+ }
+ }
+
+ @override
+ void dispose() async {
+ _pageController?.dispose();
+ _animationController.removeListener(() {});
+ _animationController.dispose();
+ if (Platform.isIOS || Platform.isAndroid) {
+ StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
+ }
+ super.dispose();
+ }
+
+ /// When the source gets scaled up, the swipe up / down to dismiss gets
+ /// disabled.
+ ///
+ /// When the scale resets, the dismiss and the page view swiping gets enabled.
+ void _onScaleChanged(double scale) {
+ final bool initialScale = scale <= widget.minScale;
+
+ if (initialScale) {
+ if (!_enablePageView) {
+ setState(() {
+ _enablePageView = true;
+ });
+ }
+ } else {
+ if (_enablePageView) {
+ setState(() {
+ _enablePageView = false;
+ });
+ }
+ }
+ }
+
+ /// When the left boundary has been hit after scaling up the source, the page
+ /// view swiping gets enabled if it has a page to swipe to.
+ void _onLeftBoundaryHit() {
+ if (!_enablePageView && _pageController!.page!.floor() > 0) {
+ setState(() {
+ _enablePageView = true;
+ });
+ }
+ }
+
+ /// When the right boundary has been hit after scaling up the source, the page
+ /// view swiping gets enabled if it has a page to swipe to.
+ void _onRightBoundaryHit() {
+ if (!_enablePageView &&
+ _pageController!.page!.floor() < widget.sources.length - 1) {
+ setState(() {
+ _enablePageView = true;
+ });
+ }
+ }
+
+ /// When the source has been scaled up and no horizontal boundary has been hit,
+ /// the page view swiping gets disabled.
+ void _onNoBoundaryHit() {
+ if (_enablePageView) {
+ setState(() {
+ _enablePageView = false;
+ });
+ }
+ }
+
+ /// When the page view changed its page, the source will animate back into the
+ /// original scale if it was scaled up.
+ ///
+ /// Additionally the swipe up / down to dismiss gets enabled.
+ void _onPageChanged(int page) {
+ setState(() {
+ currentIndex = page;
+ });
+ widget.onPageChanged?.call(page);
+ if (_transformationController!.value != Matrix4.identity()) {
+ // animate the reset for the transformation of the interactive viewer
+
+ _animation = Matrix4Tween(
+ begin: _transformationController!.value,
+ end: Matrix4.identity(),
+ ).animate(
+ CurveTween(curve: Curves.easeOut).animate(_animationController),
+ );
+
+ _animationController.forward(from: 0);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ InteractiveViewerBoundary(
+ controller: _transformationController,
+ boundaryWidth: MediaQuery.of(context).size.width,
+ onScaleChanged: _onScaleChanged,
+ onLeftBoundaryHit: _onLeftBoundaryHit,
+ onRightBoundaryHit: _onRightBoundaryHit,
+ onNoBoundaryHit: _onNoBoundaryHit,
+ maxScale: widget.maxScale,
+ minScale: widget.minScale,
+ onDismissed: () {
+ Get.back();
+ widget.onDismissed?.call(_pageController!.page!.floor());
+ },
+ child: PageView.builder(
+ onPageChanged: _onPageChanged,
+ controller: _pageController,
+ physics:
+ _enablePageView ? null : const NeverScrollableScrollPhysics(),
+ itemCount: widget.sources.length,
+ itemBuilder: (BuildContext context, int index) {
+ return GestureDetector(
+ onDoubleTapDown: (TapDownDetails details) {
+ _doubleTapLocalPosition = details.localPosition;
+ },
+ onDoubleTap: onDoubleTap,
+ onLongPress: onLongPress,
+ child: widget.itemBuilder != null
+ ? widget.itemBuilder!(
+ context,
+ index,
+ index == currentIndex,
+ _enablePageView,
+ )
+ : _itemBuilder(widget.sources, index),
+ );
+ },
+ ),
+ ),
+ Positioned(
+ bottom: 0,
+ left: 0,
+ right: 0,
+ child: Container(
+ padding: EdgeInsets.fromLTRB(
+ 12,
+ 8,
+ 20,
+ MediaQuery.of(context).padding.bottom + 8,
+ ),
+ decoration: _enablePageView
+ ? BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.transparent,
+ Colors.black.withOpacity(0.3)
+ ],
+ ),
+ )
+ : null,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.close, color: Colors.white),
+ onPressed: () {
+ Get.back();
+ widget.onDismissed?.call(_pageController!.page!.floor());
+ },
+ ),
+ widget.sources.length > 1
+ ? Text(
+ "${currentIndex! + 1}/${widget.sources.length}",
+ style: const TextStyle(color: Colors.white),
+ )
+ : const SizedBox(),
+ PopupMenuButton(
+ itemBuilder: (context) {
+ return [
+ PopupMenuItem(
+ value: 0,
+ onTap: () => onShareImg(widget.sources[currentIndex!]),
+ child: const Text("分享图片"),
+ ),
+ PopupMenuItem(
+ value: 1,
+ onTap: () {
+ Utils.copyText(widget.sources[currentIndex!]);
+ },
+ child: const Text("复制链接"),
+ ),
+ PopupMenuItem(
+ value: 2,
+ onTap: () {
+ DownloadUtils.downloadImg(
+ context,
+ [widget.sources[currentIndex!]],
+ );
+ },
+ child: const Text("保存图片"),
+ ),
+ if (widget.sources.length > 1)
+ PopupMenuItem(
+ value: 3,
+ onTap: () {
+ DownloadUtils.downloadImg(
+ context,
+ widget.sources[currentIndex!],
+ );
+ },
+ child: const Text("保存全部图片"),
+ ),
+ ];
+ },
+ child: const Icon(Icons.more_horiz, color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ // 图片分享
+ void onShareImg(String imgUrl) async {
+ SmartDialog.showLoading();
+ var response = await Dio()
+ .get(imgUrl, options: Options(responseType: ResponseType.bytes));
+ final temp = await getTemporaryDirectory();
+ SmartDialog.dismiss();
+ String imgName =
+ "plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
+ var path = '${temp.path}/$imgName';
+ File(path).writeAsBytesSync(response.data);
+ Share.shareXFiles([XFile(path)], subject: imgUrl);
+ }
+
+ Widget _itemBuilder(sources, index) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ Get.back();
+ },
+ child: Center(
+ child: Hero(
+ tag: sources[index],
+ child: CachedNetworkImage(
+ fadeInDuration: const Duration(milliseconds: 0),
+ fadeOutDuration: const Duration(milliseconds: 0),
+ imageUrl: _thumbList[index] && _quality != 100
+ ? '${sources[index]}@${_quality}q.webp'
+ : sources[index],
+ fit: BoxFit.contain,
+ progressIndicatorBuilder: (context, url, progress) {
+ return Container(
+ width: 150.0,
+ alignment: Alignment.center,
+ child: LinearProgressIndicator(value: progress.progress ?? 0),
+ );
+ },
+ errorListener: (value) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ setState(() {
+ _thumbList[index] = false;
+ });
+ });
+ },
+ ),
+ ),
+ ),
+ );
+ }
+
+ onDoubleTap() {
+ Matrix4 matrix = _transformationController!.value.clone();
+ double currentScale = matrix.row0.x;
+
+ double targetScale = widget.minScale;
+
+ if (currentScale <= widget.minScale) {
+ targetScale = widget.maxScale * 0.7;
+ }
+
+ double offSetX = targetScale == 1.0
+ ? 0.0
+ : -_doubleTapLocalPosition.dx * (targetScale - 1);
+ double offSetY = targetScale == 1.0
+ ? 0.0
+ : -_doubleTapLocalPosition.dy * (targetScale - 1);
+
+ matrix = Matrix4.fromList([
+ targetScale,
+ matrix.row1.x,
+ matrix.row2.x,
+ matrix.row3.x,
+ matrix.row0.y,
+ targetScale,
+ matrix.row2.y,
+ matrix.row3.y,
+ matrix.row0.z,
+ matrix.row1.z,
+ targetScale,
+ matrix.row3.z,
+ offSetX,
+ offSetY,
+ matrix.row2.w,
+ matrix.row3.w
+ ]);
+
+ _animation = Matrix4Tween(
+ begin: _transformationController!.value,
+ end: matrix,
+ ).animate(
+ CurveTween(curve: Curves.easeOut).animate(_animationController),
+ );
+ _animationController
+ .forward(from: 0)
+ .whenComplete(() => _onScaleChanged(targetScale));
+ }
+
+ onLongPress() {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return AlertDialog(
+ clipBehavior: Clip.hardEdge,
+ contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ListTile(
+ onTap: () {
+ onShareImg(widget.sources[currentIndex!]);
+ Get.back();
+ },
+ dense: true,
+ title: const Text('分享', style: TextStyle(fontSize: 14)),
+ ),
+ ListTile(
+ onTap: () {
+ Get.back();
+ Utils.copyText(widget.sources[currentIndex!]);
+ },
+ dense: true,
+ title: const Text('复制链接', style: TextStyle(fontSize: 14)),
+ ),
+ ListTile(
+ onTap: () {
+ Get.back();
+ DownloadUtils.downloadImg(
+ context,
+ [widget.sources[currentIndex!]],
+ );
+ },
+ dense: true,
+ title: const Text('保存图片', style: TextStyle(fontSize: 14)),
+ ),
+ if (widget.sources.length > 1)
+ ListTile(
+ onTap: () {
+ Get.back();
+ DownloadUtils.downloadImg(
+ context,
+ widget.sources as List,
+ );
+ },
+ dense: true,
+ title: const Text('保存全部图片', style: TextStyle(fontSize: 14)),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart
index 79b7cd682..1ee1839f1 100644
--- a/lib/common/widgets/list_sheet.dart
+++ b/lib/common/widgets/list_sheet.dart
@@ -171,7 +171,7 @@ class _ListSheetContentState extends State
)
: null,
child: LayoutBuilder(
- builder: (_, constraints) => NetworkImgLayer(
+ builder: (context, constraints) => NetworkImgLayer(
radius: 6,
src: episode is video.EpisodeItem
? episode.arc?.pic
@@ -239,7 +239,7 @@ class _ListSheetContentState extends State
),
StreamBuilder(
stream: _favStream?.stream,
- builder: (_, snapshot) => snapshot.hasData
+ builder: (context, snapshot) => snapshot.hasData
? _mediumButton(
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
icon: _seasonFav == 1
@@ -312,7 +312,7 @@ class _ListSheetContentState extends State
StreamBuilder(
stream: _indexStream?.stream,
initialData: 0,
- builder: (_, snapshot) => _mediumButton(
+ builder: (context, snapshot) => _mediumButton(
tooltip: reverse[snapshot.data] ? '正序' : '反序',
icon: !reverse[snapshot.data]
? MdiIcons.sortAscending
@@ -405,7 +405,7 @@ class _ListSheetContentState extends State
);
},
itemScrollController: itemScrollController[i ?? 0],
- separatorBuilder: (_, index) => Divider(
+ separatorBuilder: (context, index) => Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
diff --git a/lib/common/widgets/pull_to_refresh_header.dart b/lib/common/widgets/pull_to_refresh_header.dart
deleted file mode 100644
index 1ae881d23..000000000
--- a/lib/common/widgets/pull_to_refresh_header.dart
+++ /dev/null
@@ -1,133 +0,0 @@
-// ignore_for_file: depend_on_referenced_packages
-
-import 'dart:math';
-import 'dart:ui' as ui show Image;
-
-import 'package:extended_image/extended_image.dart';
-import 'package:flutter/material.dart';
-import 'package:intl/intl.dart';
-import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
-
-double get maxDragOffset => 100;
-double hideHeight = maxDragOffset / 2.3;
-double refreshHeight = maxDragOffset / 1.5;
-
-class PullToRefreshHeader extends StatelessWidget {
- const PullToRefreshHeader(
- this.info,
- this.lastRefreshTime, {
- this.color,
- super.key,
- });
-
- final PullToRefreshScrollNotificationInfo? info;
- final DateTime? lastRefreshTime;
- final Color? color;
-
- @override
- Widget build(BuildContext context) {
- final PullToRefreshScrollNotificationInfo? infos = info;
- if (infos == null) {
- return const SizedBox();
- }
- String text = '';
- if (infos.mode == PullToRefreshIndicatorMode.armed) {
- text = 'Release to refresh';
- } else if (infos.mode == PullToRefreshIndicatorMode.refresh ||
- infos.mode == PullToRefreshIndicatorMode.snap) {
- text = 'Loading...';
- } else if (infos.mode == PullToRefreshIndicatorMode.done) {
- text = 'Refresh completed.';
- } else if (infos.mode == PullToRefreshIndicatorMode.drag) {
- text = 'Pull to refresh';
- } else if (infos.mode == PullToRefreshIndicatorMode.canceled) {
- text = 'Cancel refresh';
- }
-
- final TextStyle ts = const TextStyle(
- color: Colors.grey,
- ).copyWith(fontSize: 14);
-
- final double dragOffset = info?.dragOffset ?? 0.0;
-
- final DateTime time = lastRefreshTime ?? DateTime.now();
- final double top = -hideHeight + dragOffset;
- return Container(
- height: dragOffset,
- color: color ?? Colors.transparent,
- // padding: EdgeInsets.only(top: dragOffset / 3),
- // padding: EdgeInsets.only(bottom: 5.0),
- child: Stack(
- children: [
- Positioned(
- left: 0.0,
- right: 0.0,
- top: top,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Expanded(
- child: Container(
- alignment: Alignment.centerRight,
- margin: const EdgeInsets.only(right: 12.0),
- child: RefreshImage(top: top),
- ),
- ),
- Column(
- children: [
- Text(text, style: ts),
- Text(
- 'Last updated:${DateFormat('yyyy-MM-dd hh:mm').format(time)}',
- style: ts.copyWith(fontSize: 14),
- )
- ],
- ),
- const Spacer(),
- ],
- ),
- )
- ],
- ),
- );
- }
-}
-
-class RefreshImage extends StatelessWidget {
- const RefreshImage({
- super.key,
- required this.top,
- });
-
- final double top;
-
- @override
- Widget build(BuildContext context) {
- const double imageSize = 30;
- return ExtendedImage.asset(
- 'assets/flutterCandies_grey.png',
- width: imageSize,
- height: imageSize,
- afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) {
- final double imageHeight = image.height.toDouble();
- final double imageWidth = image.width.toDouble();
- final Size size = rect.size;
- final double y =
- (1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight;
-
- canvas.drawImageRect(
- image,
- Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y),
- Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height,
- size.width, (imageHeight - y) / imageHeight * size.height),
- Paint()
- ..colorFilter =
- const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn)
- ..isAntiAlias = false
- ..filterQuality = FilterQuality.low,
- );
-
- //canvas.restore();
- },
- );
- }
-}
diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart
index 6dcbdaf8a..8a2a8a3f9 100644
--- a/lib/http/bangumi.dart
+++ b/lib/http/bangumi.dart
@@ -5,7 +5,8 @@ import 'index.dart';
class BangumiHttp {
static Future bangumiList({int? page}) async {
- var res = await Request().get(Api.bangumiList, data: {'page': page});
+ var res =
+ await Request().get(Api.bangumiList, queryParameters: {'page': page});
if (res.data['code'] == 0) {
BangumiListDataModel data =
BangumiListDataModel.fromJson(res.data['data']);
@@ -16,7 +17,8 @@ class BangumiHttp {
}
static Future bangumiFollow({int? mid}) async {
- var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid});
+ var res =
+ await Request().get(Api.bangumiFollow, queryParameters: {'vmid': mid});
if (res.data['code'] == 0) {
BangumiListDataModel data =
BangumiListDataModel.fromJson(res.data['data']);
diff --git a/lib/http/black.dart b/lib/http/black.dart
index 555f2c3b7..45058d46e 100644
--- a/lib/http/black.dart
+++ b/lib/http/black.dart
@@ -5,7 +5,7 @@ import 'index.dart';
class BlackHttp {
static Future blackList({required int pn, int? ps}) async {
- var res = await Request().get(Api.blackLst, data: {
+ var res = await Request().get(Api.blackLst, queryParameters: {
'pn': pn,
'ps': ps ?? 50,
're_version': 0,
diff --git a/lib/http/common.dart b/lib/http/common.dart
index fac65dbf8..3e1b9f71b 100644
--- a/lib/http/common.dart
+++ b/lib/http/common.dart
@@ -2,7 +2,7 @@ import 'index.dart';
class CommonHttp {
static Future unReadDynamic() async {
- var res = await Request().get(Api.getUnreadDynamic, data: {
+ var res = await Request().get(Api.getUnreadDynamic, queryParameters: {
'alltype_offset': 0,
'video_offset': 0,
'article_offset': 0,
diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart
index d98146ee3..eff7e72f3 100644
--- a/lib/http/danmaku.dart
+++ b/lib/http/danmaku.dart
@@ -16,7 +16,7 @@ class DanmakaHttp {
};
var response = await Request().get(
Api.webDanmaku,
- data: params,
+ queryParameters: params,
options: Options(responseType: ResponseType.bytes),
);
if (response.statusCode != 200 || response.data == null) {
diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart
index 26da8148f..4cdca3e05 100644
--- a/lib/http/dynamics.dart
+++ b/lib/http/dynamics.dart
@@ -20,7 +20,7 @@ class DynamicsHttp {
data['host_mid'] = mid;
data.remove('timezone_offset');
}
- var res = await Request().get(Api.followDynamic, data: data);
+ var res = await Request().get(Api.followDynamic, queryParameters: data);
if (res.data['code'] == 0) {
try {
DynamicsDataModel data = DynamicsDataModel.fromJson(res.data['data']);
@@ -80,7 +80,7 @@ class DynamicsHttp {
static Future dynamicDetail({
String? id,
}) async {
- var res = await Request().get(Api.dynamicDetail, data: {
+ var res = await Request().get(Api.dynamicDetail, queryParameters: {
'timezone_offset': -480,
'id': id,
'features': 'itemOpusStyle',
diff --git a/lib/http/fan.dart b/lib/http/fan.dart
index d0c1b44b1..d529a2531 100644
--- a/lib/http/fan.dart
+++ b/lib/http/fan.dart
@@ -6,7 +6,7 @@ import 'index.dart';
class FanHttp {
static Future fans(
{int? vmid, int? pn, int? ps, String? orderType}) async {
- var res = await Request().get(Api.fans, data: {
+ var res = await Request().get(Api.fans, queryParameters: {
'vmid': vmid,
'pn': pn,
'ps': ps,
diff --git a/lib/http/follow.dart b/lib/http/follow.dart
index 316aa95af..11c597d30 100644
--- a/lib/http/follow.dart
+++ b/lib/http/follow.dart
@@ -4,7 +4,7 @@ import 'index.dart';
class FollowHttp {
static Future followings(
{int? vmid, int? pn, int? ps, String? orderType}) async {
- var res = await Request().get(Api.followings, data: {
+ var res = await Request().get(Api.followings, queryParameters: {
'vmid': vmid,
'pn': pn,
'ps': ps,
diff --git a/lib/http/init.dart b/lib/http/init.dart
index 943f1d1e6..154661214 100644
--- a/lib/http/init.dart
+++ b/lib/http/init.dart
@@ -147,7 +147,7 @@ class Request {
// ..httpClientAdapter = Http2Adapter(
// ConnectionManager(
// idleTimeout: const Duration(milliseconds: 10000),
- // onClientCreate: (_, ClientSetting config) =>
+ // onClientCreate: (context, ClientSetting config) =>
// config.onBadCertificate = (_) => true,
// ),
// );
@@ -189,7 +189,8 @@ class Request {
/*
* get请求
*/
- Future get(url, {data, options, cancelToken, extra}) async {
+ Future get(url,
+ {queryParameters, options, cancelToken, extra}) async {
Response response;
if (extra != null) {
if (extra['ua'] != null) {
@@ -202,7 +203,7 @@ class Request {
try {
response = await dio.get(
url,
- queryParameters: data,
+ queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
diff --git a/lib/http/live.dart b/lib/http/live.dart
index bcfb836c6..a1043c468 100644
--- a/lib/http/live.dart
+++ b/lib/http/live.dart
@@ -12,7 +12,7 @@ class LiveHttp {
static Future liveList(
{int? vmid, int? pn, int? ps, String? orderType}) async {
var res = await Request().get(Api.liveList,
- data: {'page': pn, 'page_size': 30, 'platform': 'web'});
+ queryParameters: {'page': pn, 'page_size': 30, 'platform': 'web'});
if (res.data['code'] == 0) {
List list = res.data['data']['list']
.map((e) => LiveItemModel.fromJson(e))
@@ -67,7 +67,7 @@ class LiveHttp {
}
static Future liveRoomInfo({roomId, qn}) async {
- var res = await Request().get(Api.liveRoomInfo, data: {
+ var res = await Request().get(Api.liveRoomInfo, queryParameters: {
'room_id': roomId,
'protocol': '0, 1',
'format': '0, 1, 2',
@@ -90,7 +90,7 @@ class LiveHttp {
}
static Future liveRoomInfoH5({roomId, qn}) async {
- var res = await Request().get(Api.liveRoomInfoH5, data: {
+ var res = await Request().get(Api.liveRoomInfoH5, queryParameters: {
'room_id': roomId,
});
if (res.data['code'] == 0) {
@@ -108,7 +108,7 @@ class LiveHttp {
}
static Future liveRoomDanmaPrefetch({roomId}) async {
- var res = await Request().get(Api.liveRoomDmPrefetch, data: {
+ var res = await Request().get(Api.liveRoomDmPrefetch, queryParameters: {
'roomid': roomId,
});
if (res.data['code'] == 0) {
@@ -123,7 +123,7 @@ class LiveHttp {
}
static Future liveRoomGetDanmakuToken({roomId}) async {
- var res = await Request().get(Api.liveRoomDmToken, data: {
+ var res = await Request().get(Api.liveRoomDmToken, queryParameters: {
'id': roomId,
});
if (res.data['code'] == 0) {
diff --git a/lib/http/login.dart b/lib/http/login.dart
index ccea4d3a6..91191ca6c 100644
--- a/lib/http/login.dart
+++ b/lib/http/login.dart
@@ -349,7 +349,7 @@ class LoginHttp {
static Future safeCenterGetInfo({
required String tmpCode,
}) async {
- var res = await Request().get(Api.safeCenterGetInfo, data: {
+ var res = await Request().get(Api.safeCenterGetInfo, queryParameters: {
'tmp_code': tmpCode,
});
if (res.data['code'] == 0) {
diff --git a/lib/http/member.dart b/lib/http/member.dart
index e3a493527..640ed1f82 100644
--- a/lib/http/member.dart
+++ b/lib/http/member.dart
@@ -91,7 +91,7 @@ class MemberHttp {
int? _mid = GStorage.userInfo.get('userInfoCache')?.mid;
dynamic res = await Request().get(
Api.spaceArticle,
- data: data,
+ queryParameters: data,
options: Options(
headers: {
'env': 'prod',
@@ -136,7 +136,7 @@ class MemberHttp {
int? _mid = GStorage.userInfo.get('userInfoCache')?.mid;
dynamic res = await Request().get(
Api.spaceFav,
- data: data,
+ queryParameters: data,
options: Options(
headers: {
'env': 'prod',
@@ -206,7 +206,7 @@ class MemberHttp {
: type == ContributeType.series
? Api.spaceSeries
: Api.spaceBangumi,
- data: data,
+ queryParameters: data,
options: Options(
headers: {
'env': 'prod',
@@ -251,7 +251,7 @@ class MemberHttp {
int? _mid = GStorage.userInfo.get('userInfoCache')?.mid;
dynamic res = await Request().get(
Api.space,
- data: data,
+ queryParameters: data,
options: Options(
headers: {
'env': 'prod',
@@ -284,7 +284,7 @@ class MemberHttp {
});
var res = await Request().get(
Api.memberInfo,
- data: params,
+ queryParameters: params,
extra: {'ua': 'pc'},
);
if (res.data['code'] == 0) {
@@ -302,7 +302,7 @@ class MemberHttp {
}
static Future memberStat({int? mid}) async {
- var res = await Request().get(Api.userStat, data: {'vmid': mid});
+ var res = await Request().get(Api.userStat, queryParameters: {'vmid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -316,7 +316,7 @@ class MemberHttp {
static Future memberCardInfo({int? mid}) async {
var res = await Request()
- .get(Api.memberCardInfo, data: {'mid': mid, 'photo': true});
+ .get(Api.memberCardInfo, queryParameters: {'mid': mid, 'photo': true});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -358,7 +358,7 @@ class MemberHttp {
});
var res = await Request().get(
Api.memberArchive,
- data: params,
+ queryParameters: params,
extra: {'ua': 'Mozilla/5.0'},
);
if (res.data['code'] == 0) {
@@ -395,7 +395,7 @@ class MemberHttp {
'x-bili-device-req-json': jsonEncode({"platform": "web", "device": "pc"}),
'x-bili-web-req-json': jsonEncode({"spm_id": "333.999"}),
});
- var res = await Request().get(Api.memberDynamic, data: params);
+ var res = await Request().get(Api.memberDynamic, queryParameters: params);
if (res.data['code'] == 0) {
return LoadingState.success(DynamicsDataModel.fromJson(res.data['data']));
} else {
@@ -414,7 +414,7 @@ class MemberHttp {
int? mid,
required String keyword,
}) async {
- var res = await Request().get(Api.memberDynamicSearch, data: {
+ var res = await Request().get(Api.memberDynamicSearch, queryParameters: {
'keyword': keyword,
'mid': mid,
'pn': pn,
@@ -510,7 +510,7 @@ class MemberHttp {
int? pn,
int? ps,
) async {
- var res = await Request().get(Api.followUpGroup, data: {
+ var res = await Request().get(Api.followUpGroup, queryParameters: {
'mid': mid,
'tagid': tagid,
'pn': pn,
@@ -554,7 +554,7 @@ class MemberHttp {
// 获取uo专栏
static Future getMemberSeasons(int? mid, int? pn, int? ps) async {
- var res = await Request().get(Api.getMemberSeasonsApi, data: {
+ var res = await Request().get(Api.getMemberSeasonsApi, queryParameters: {
'mid': mid,
'page_num': pn,
'page_size': ps,
@@ -582,7 +582,7 @@ class MemberHttp {
});
var res = await Request().get(
Api.getRecentCoinVideoApi,
- data: {
+ queryParameters: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
@@ -615,7 +615,7 @@ class MemberHttp {
});
var res = await Request().get(
Api.getRecentLikeVideoApi,
- data: {
+ queryParameters: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
@@ -647,7 +647,7 @@ class MemberHttp {
}) async {
var res = await Request().get(
Api.getSeasonDetailApi,
- data: {
+ queryParameters: {
'mid': mid,
'season_id': seasonId,
'sort_reverse': sortReverse,
@@ -675,7 +675,8 @@ class MemberHttp {
// 获取up播放数、点赞数
static Future memberView({required int mid}) async {
- var res = await Request().get(Api.getMemberViewApi, data: {'mid': mid});
+ var res = await Request()
+ .get(Api.getMemberViewApi, queryParameters: {'mid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -705,7 +706,7 @@ class MemberHttp {
'web_location': 333.999,
};
Map params = await WbiSign().makSign(data);
- var res = await Request().get(Api.followSearch, data: {
+ var res = await Request().get(Api.followSearch, queryParameters: {
...data,
'w_rid': params['w_rid'],
'wts': params['wts'],
diff --git a/lib/http/msg.dart b/lib/http/msg.dart
index f2d9be6e6..a3309b7a5 100644
--- a/lib/http/msg.dart
+++ b/lib/http/msg.dart
@@ -12,7 +12,7 @@ import 'init.dart';
class MsgHttp {
static Future msgFeedReplyMe({int cursor = -1, int cursorTime = -1}) async {
- var res = await Request().get(Api.msgFeedReply, data: {
+ var res = await Request().get(Api.msgFeedReply, queryParameters: {
'id': cursor == -1 ? null : cursor,
'reply_time': cursorTime == -1 ? null : cursorTime,
});
@@ -31,7 +31,7 @@ class MsgHttp {
}
static Future msgFeedAtMe({int cursor = -1, int cursorTime = -1}) async {
- var res = await Request().get(Api.msgFeedAt, data: {
+ var res = await Request().get(Api.msgFeedAt, queryParameters: {
'id': cursor == -1 ? null : cursor,
'at_time': cursorTime == -1 ? null : cursorTime,
});
@@ -50,7 +50,7 @@ class MsgHttp {
}
static Future msgFeedLikeMe({int cursor = -1, int cursorTime = -1}) async {
- var res = await Request().get(Api.msgFeedLike, data: {
+ var res = await Request().get(Api.msgFeedLike, queryParameters: {
'id': cursor == -1 ? null : cursor,
'like_time': cursorTime == -1 ? null : cursorTime,
});
@@ -70,7 +70,7 @@ class MsgHttp {
static Future msgFeedSysUserNotify() async {
String csrf = await Request.getCsrf();
- var res = await Request().get(Api.msgSysUserNotify, data: {
+ var res = await Request().get(Api.msgSysUserNotify, queryParameters: {
'csrf': csrf,
'page_size': 20,
});
@@ -90,7 +90,7 @@ class MsgHttp {
static Future msgFeedSysUnifiedNotify() async {
String csrf = await Request.getCsrf();
- var res = await Request().get(Api.msgSysUnifiedNotify, data: {
+ var res = await Request().get(Api.msgSysUnifiedNotify, queryParameters: {
'csrf': csrf,
'page_size': 10,
});
@@ -110,7 +110,7 @@ class MsgHttp {
static Future msgSysUpdateCursor(int cursor) async {
String csrf = await Request.getCsrf();
- var res = await Request().get(Api.msgSysUpdateCursor, data: {
+ var res = await Request().get(Api.msgSysUpdateCursor, queryParameters: {
'csrf': csrf,
'cursor': cursor,
});
@@ -400,7 +400,7 @@ class MsgHttp {
}
Map signParams = await WbiSign().makSign(params);
- var res = await Request().get(Api.sessionList, data: signParams);
+ var res = await Request().get(Api.sessionList, queryParameters: signParams);
if (res.data['code'] == 0) {
try {
return {
@@ -424,7 +424,7 @@ class MsgHttp {
}
static Future accountList(uids) async {
- var res = await Request().get(Api.sessionAccountList, data: {
+ var res = await Request().get(Api.sessionAccountList, queryParameters: {
'uids': uids,
'build': 0,
'mobi_app': 'web',
@@ -460,7 +460,7 @@ class MsgHttp {
'build': 0,
'mobi_app': 'web',
});
- var res = await Request().get(Api.sessionMsg, data: params);
+ var res = await Request().get(Api.sessionMsg, queryParameters: params);
if (res.data['code'] == 0) {
try {
return {
@@ -494,7 +494,7 @@ class MsgHttp {
'csrf_token': csrf,
'csrf': csrf
});
- var res = await Request().get(Api.ackSessionMsg, data: params);
+ var res = await Request().get(Api.ackSessionMsg, queryParameters: params);
if (res.data['code'] == 0) {
return {
'status': true,
diff --git a/lib/http/reply.dart b/lib/http/reply.dart
index bbbac34f8..5868c3d8b 100644
--- a/lib/http/reply.dart
+++ b/lib/http/reply.dart
@@ -28,7 +28,7 @@ class ReplyHttp {
var res = !isLogin
? await Request().get(
'${HttpString.apiBaseUrl}${Api.replyList}/main',
- data: {
+ queryParameters: {
'oid': oid,
'type': type,
'pagination_str':
@@ -39,7 +39,7 @@ class ReplyHttp {
)
: await Request().get(
'${HttpString.apiBaseUrl}${Api.replyList}',
- data: {
+ queryParameters: {
'oid': oid,
'type': type,
'sort': sort,
@@ -82,7 +82,7 @@ class ReplyHttp {
: null;
var res = await Request().get(
'${HttpString.apiBaseUrl}${Api.replyReplyList}',
- data: {
+ queryParameters: {
'oid': oid,
'root': root,
'pn': pageNum,
@@ -200,7 +200,7 @@ class ReplyHttp {
}
static Future getEmoteList({String? business}) async {
- var res = await Request().get(Api.myEmote, data: {
+ var res = await Request().get(Api.myEmote, queryParameters: {
'business': business ?? 'reply',
'web_location': '333.1245',
});
diff --git a/lib/http/search.dart b/lib/http/search.dart
index 76d56229c..988c37eb4 100644
--- a/lib/http/search.dart
+++ b/lib/http/search.dart
@@ -40,7 +40,7 @@ class SearchHttp {
// 获取搜索建议
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.searchSuggest,
- data: {'term': term, 'main_ver': 'v1', 'highlight': term});
+ queryParameters: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data is String) {
Map resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
@@ -98,7 +98,7 @@ class SearchHttp {
if (pubBegin != null) 'pubtime_begin_s': pubBegin,
if (pubEnd != null) 'pubtime_end_s': pubEnd,
};
- var res = await Request().get(Api.searchByType, data: reqData);
+ var res = await Request().get(Api.searchByType, queryParameters: reqData);
if (res.data['code'] == 0) {
dynamic data;
try {
@@ -146,8 +146,8 @@ class SearchHttp {
} else if (bvid != null) {
data['bvid'] = bvid;
}
- final dynamic res =
- await Request().get(Api.ab2c, data: {...data});
+ final dynamic res = await Request()
+ .get(Api.ab2c, queryParameters: {...data});
if (res.data['code'] == 0) {
return res.data['data'].first['cid'];
} else {
@@ -159,7 +159,7 @@ class SearchHttp {
static Future bangumiInfoNew({int? seasonId, int? epId}) async {
final dynamic res = await Request().get(
Api.bangumiInfo,
- data: {
+ queryParameters: {
if (seasonId != null) 'season_id': seasonId,
if (epId != null) 'ep_id': epId,
},
@@ -182,8 +182,8 @@ class SearchHttp {
} else if (epId != null) {
data['ep_id'] = epId;
}
- final dynamic res =
- await Request().get(Api.bangumiInfo, data: {...data});
+ final dynamic res = await Request()
+ .get(Api.bangumiInfo, queryParameters: {...data});
if (res.data['code'] == 0) {
return {
'status': true,
diff --git a/lib/http/user.dart b/lib/http/user.dart
index eed599eb3..1d0207589 100644
--- a/lib/http/user.dart
+++ b/lib/http/user.dart
@@ -15,7 +15,7 @@ import 'init.dart';
class UserHttp {
static Future userStat({required int mid}) async {
- var res = await Request().get(Api.userStat, data: {'vmid': mid});
+ var res = await Request().get(Api.userStat, queryParameters: {'vmid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -49,7 +49,7 @@ class UserHttp {
required int ps,
required int mid,
}) async {
- var res = await Request().get(Api.userFavFolder, data: {
+ var res = await Request().get(Api.userFavFolder, queryParameters: {
'pn': pn,
'ps': ps,
'up_mid': mid,
@@ -113,7 +113,7 @@ class UserHttp {
static Future folderInfo({
dynamic mediaId,
}) async {
- var res = await Request().get(Api.folderInfo, data: {
+ var res = await Request().get(Api.folderInfo, queryParameters: {
'media_id': mediaId,
});
if (res.data['code'] == 0) {
@@ -130,7 +130,7 @@ class UserHttp {
String keyword = '',
String order = 'mtime',
int type = 0}) async {
- var res = await Request().get(Api.userFavFolderDetail, data: {
+ var res = await Request().get(Api.userFavFolderDetail, queryParameters: {
'media_id': mediaId,
'pn': pn,
'ps': ps,
@@ -172,7 +172,7 @@ class UserHttp {
int? max,
int? viewAt,
}) async {
- var res = await Request().get(Api.historyList, data: {
+ var res = await Request().get(Api.historyList, queryParameters: {
'type': 'all',
'ps': 20,
'max': max ?? 0,
@@ -265,7 +265,7 @@ class UserHttp {
static Future thirdLogin() async {
var res = await Request().get(
'https://passport.bilibili.com/login/app/third',
- data: {
+ queryParameters: {
'appkey': Constants.appKey,
'api': Constants.thirdApi,
'sign': Constants.thirdSign,
@@ -319,7 +319,7 @@ class UserHttp {
static Future hasFollow(int mid) async {
var res = await Request().get(
Api.hasFollow,
- data: {
+ queryParameters: {
'fid': mid,
},
);
@@ -359,7 +359,7 @@ class UserHttp {
{required int pn, required String keyword}) async {
var res = await Request().get(
Api.searchHistory,
- data: {
+ queryParameters: {
'pn': pn,
'keyword': keyword,
'business': 'all',
@@ -378,7 +378,7 @@ class UserHttp {
required int pn,
required int ps,
}) async {
- var res = await Request().get(Api.userSubFolder, data: {
+ var res = await Request().get(Api.userSubFolder, queryParameters: {
'up_mid': mid,
'ps': ps,
'pn': pn,
@@ -399,7 +399,7 @@ class UserHttp {
required int pn,
required int ps,
}) async {
- var res = await Request().get(Api.favSeasonList, data: {
+ var res = await Request().get(Api.favSeasonList, queryParameters: {
'season_id': id,
'ps': ps,
'pn': pn,
@@ -419,7 +419,7 @@ class UserHttp {
required int pn,
required int ps,
}) async {
- var res = await Request().get(Api.favResourceList, data: {
+ var res = await Request().get(Api.favResourceList, queryParameters: {
'media_id': id,
'ps': ps,
'pn': pn,
@@ -463,7 +463,8 @@ class UserHttp {
}
static videoTags({required String bvid}) async {
- var res = await Request().get(Api.videoTags, data: {'bvid': bvid});
+ var res =
+ await Request().get(Api.videoTags, queryParameters: {'bvid': bvid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
diff --git a/lib/http/video.dart b/lib/http/video.dart
index 4be804144..68f3508bf 100644
--- a/lib/http/video.dart
+++ b/lib/http/video.dart
@@ -41,7 +41,7 @@ class VideoHttp {
{required int ps, required int freshIdx}) async {
var res = await Request().get(
Api.recommendListWeb,
- data: {
+ queryParameters: {
'version': 1,
'feed_version': 'V8',
'homepage_ver': 1,
@@ -119,7 +119,7 @@ class VideoHttp {
var res = await Request().get(
Api.recommendListApp,
- data: data,
+ queryParameters: data,
options: Options(headers: {
'Host': 'app.bilibili.com',
'buvid': LoginHttp.buvid,
@@ -165,7 +165,7 @@ class VideoHttp {
{required int pn, required int ps}) async {
var res = await Request().get(
Api.hotList,
- data: {'pn': pn, 'ps': ps},
+ queryParameters: {'pn': pn, 'ps': ps},
);
if (res.data['code'] == 0) {
List list = [];
@@ -229,7 +229,7 @@ class VideoHttp {
});
try {
- var res = await Request().get(Api.videoUrl, data: params);
+ var res = await Request().get(Api.videoUrl, queryParameters: params);
if (res.data['code'] == 0) {
return {
'status': true,
@@ -250,7 +250,8 @@ class VideoHttp {
// 视频信息 标题、简介
static Future videoIntro({required String bvid}) async {
- var res = await Request().get(Api.videoIntro, data: {'bvid': bvid});
+ var res =
+ await Request().get(Api.videoIntro, queryParameters: {'bvid': bvid});
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
if (result.code == 0) {
return {
@@ -305,7 +306,7 @@ class VideoHttp {
static Future videoRelation({required dynamic bvid}) async {
var res = await Request().get(
Api.videoRelation,
- data: {
+ queryParameters: {
'aid': IdUtils.bv2av(bvid),
'bvid': bvid,
},
@@ -325,7 +326,8 @@ class VideoHttp {
// 相关视频
static Future relatedVideoList({required String bvid}) async {
- var res = await Request().get(Api.relatedList, data: {'bvid': bvid});
+ var res =
+ await Request().get(Api.relatedList, queryParameters: {'bvid': bvid});
if (res.data['code'] == 0) {
List list = [];
for (var i in res.data['data']) {
@@ -344,7 +346,7 @@ class VideoHttp {
static Future bangumiLikeCoinFav({dynamic epId}) async {
var res = await Request().get(
Api.bangumiLikeCoinFav,
- data: {'ep_id': epId},
+ queryParameters: {'ep_id': epId},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
@@ -355,7 +357,8 @@ class VideoHttp {
// 获取点赞状态
static Future hasLikeVideo({required String bvid}) async {
- var res = await Request().get(Api.hasLikeVideo, data: {'bvid': bvid});
+ var res =
+ await Request().get(Api.hasLikeVideo, queryParameters: {'bvid': bvid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -365,7 +368,8 @@ class VideoHttp {
// 获取投币状态
static Future hasCoinVideo({required String bvid}) async {
- var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid});
+ var res =
+ await Request().get(Api.hasCoinVideo, queryParameters: {'bvid': bvid});
debugPrint('res: $res');
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
@@ -397,7 +401,8 @@ class VideoHttp {
// 获取收藏状态
static Future hasFavVideo({required int aid}) async {
- var res = await Request().get(Api.hasFavVideo, data: {'aid': aid});
+ var res =
+ await Request().get(Api.hasFavVideo, queryParameters: {'aid': aid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -507,7 +512,7 @@ class VideoHttp {
return {'status': false, 'msg': "请退出账号后重新登录"};
}
assert((reasonId != null) ^ (feedbackId != null));
- var res = await Request().get(Api.feedDislike, data: {
+ var res = await Request().get(Api.feedDislike, queryParameters: {
'goto': goto,
'id': id,
// 'mid': mid,
@@ -537,7 +542,7 @@ class VideoHttp {
return {'status': false, 'msg': "请退出账号后重新登录"};
}
// assert ((reasonId != null) ^ (feedbackId != null));
- var res = await Request().get(Api.feedDislikeCancel, data: {
+ var res = await Request().get(Api.feedDislikeCancel, queryParameters: {
'goto': goto,
'id': id,
// 'mid': mid,
@@ -637,7 +642,7 @@ class VideoHttp {
}) async {
var res = await Request().get(
Api.videoInFolder,
- data: {
+ queryParameters: {
'up_mid': mid,
'rid': rid,
if (type != null) 'type': type,
@@ -712,7 +717,7 @@ class VideoHttp {
// 查询是否关注up
static Future hasFollow({required int mid}) async {
- var res = await Request().get(Api.hasFollow, data: {'fid': mid});
+ var res = await Request().get(Api.hasFollow, queryParameters: {'fid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -818,7 +823,7 @@ class VideoHttp {
// 查看视频同时在看人数
static Future onlineTotal({int? aid, String? bvid, int? cid}) async {
- var res = await Request().get(Api.onlineTotal, data: {
+ var res = await Request().get(Api.onlineTotal, queryParameters: {
'aid': aid,
'bvid': bvid,
'cid': cid,
@@ -840,7 +845,7 @@ class VideoHttp {
'cid': cid,
'up_mid': upMid,
});
- var res = await Request().get(Api.aiConclusion, data: params);
+ var res = await Request().get(Api.aiConclusion, queryParameters: params);
if (res.data['code'] == 0 && res.data['data']['code'] == 0) {
return {
'status': true,
@@ -856,7 +861,7 @@ class VideoHttp {
assert(aid != null || bvid != null);
var res = await Request().get(
Api.subtitleUrl,
- data: {
+ queryParameters: {
if (aid != null) 'aid': aid,
if (bvid != null) 'bvid': bvid,
'cid': cid,
diff --git a/lib/main.dart b/lib/main.dart
index 9e7893999..5fe7734cb 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -54,8 +54,8 @@ void main() async {
Request();
await Request.setCookie();
RecommendFilter();
- SmartDialog.config.toast =
- SmartConfigToast(displayType: SmartToastType.normal);
+ SmartDialog.config.loading =
+ SmartConfigLoading(backType: SmartBackType.normal);
// 异常捕获 logo记录
final Catcher2Options debugConfig = Catcher2Options(
SilentReportMode(),
diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart
index 713e948d5..374c364f4 100644
--- a/lib/pages/bangumi/introduction/view.dart
+++ b/lib/pages/bangumi/introduction/view.dart
@@ -292,7 +292,7 @@ class _BangumiInfoState extends State
.isFollowed.value) {
showDialog(
context: context,
- builder: (_) =>
+ builder: (context) =>
_followDialog(),
);
} else {
diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart
index e969b3ff6..b98970c3c 100644
--- a/lib/pages/dynamics/view.dart
+++ b/lib/pages/dynamics/view.dart
@@ -63,7 +63,7 @@ class _DynamicsPageState extends State
context: context,
useSafeArea: true,
isScrollControlled: true,
- builder: (_) => const CreatePanel(),
+ builder: (context) => const CreatePanel(),
);
}
},
@@ -356,7 +356,7 @@ class _CreatePanelState extends State {
StreamBuilder(
initialData: false,
stream: _isEnableStream.stream,
- builder: (_, snapshot) => FilledButton.tonal(
+ builder: (context, snapshot) => FilledButton.tonal(
onPressed: snapshot.data == true ? _onCreate : null,
style: FilledButton.styleFrom(
padding:
@@ -536,7 +536,7 @@ class _CreatePanelState extends State {
StreamBuilder(
initialData: const [],
stream: _pathStream.stream,
- builder: (_, snapshot) => SizedBox(
+ builder: (context, snapshot) => SizedBox(
height: 75,
child: ListView.separated(
scrollDirection: Axis.horizontal,
@@ -619,7 +619,7 @@ class _CreatePanelState extends State {
);
}
},
- separatorBuilder: (_, index) => const SizedBox(width: 10),
+ separatorBuilder: (context, index) => const SizedBox(width: 10),
),
),
),
diff --git a/lib/pages/dynamics/widgets/action_panel.dart b/lib/pages/dynamics/widgets/action_panel.dart
index 49a645dbe..1063a39a3 100644
--- a/lib/pages/dynamics/widgets/action_panel.dart
+++ b/lib/pages/dynamics/widgets/action_panel.dart
@@ -88,7 +88,7 @@ class _ActionPanelState extends State {
context: context,
isScrollControlled: true,
useSafeArea: true,
- builder: (_) => RepostPanel(
+ builder: (context) => RepostPanel(
item: widget.item,
callback: () {
int count = int.tryParse(
diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart
index 82a3187a8..d11665106 100644
--- a/lib/pages/dynamics/widgets/author_panel.dart
+++ b/lib/pages/dynamics/widgets/author_panel.dart
@@ -240,7 +240,7 @@ class MorePanel extends StatelessWidget {
Get.back();
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
title: const Text('确定删除该动态?'),
actions: [
TextButton(
diff --git a/lib/pages/dynamics/widgets/author_panel_grpc.dart b/lib/pages/dynamics/widgets/author_panel_grpc.dart
index 9bd3f6975..2441ca63f 100644
--- a/lib/pages/dynamics/widgets/author_panel_grpc.dart
+++ b/lib/pages/dynamics/widgets/author_panel_grpc.dart
@@ -241,7 +241,7 @@ class MorePanel extends StatelessWidget {
Get.back();
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
title: const Text('确定删除该动态?'),
actions: [
TextButton(
diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart
index 8c36f0774..e14720dce 100644
--- a/lib/pages/dynamics/widgets/content_panel.dart
+++ b/lib/pages/dynamics/widgets/content_panel.dart
@@ -17,7 +17,7 @@ class Content extends StatelessWidget {
InlineSpan picsNodes() {
return WidgetSpan(
child: LayoutBuilder(
- builder: (_, constraints) => image(
+ builder: (context, constraints) => imageview(
constraints.maxWidth,
(item.modules.moduleDynamic.major.opus.pics as List)
.map(
diff --git a/lib/pages/dynamics/widgets/content_panel_grpc.dart b/lib/pages/dynamics/widgets/content_panel_grpc.dart
index 34a5d9c8b..0e6e5da78 100644
--- a/lib/pages/dynamics/widgets/content_panel_grpc.dart
+++ b/lib/pages/dynamics/widgets/content_panel_grpc.dart
@@ -18,7 +18,7 @@ class ContentGrpc extends StatelessWidget {
InlineSpan picsNodes() {
return WidgetSpan(
child: LayoutBuilder(
- builder: (_, constraints) => image(
+ builder: (context, constraints) => imageview(
constraints.maxWidth,
item.modules.first.moduleDynamic.dynDraw.items
.map(
diff --git a/lib/pages/dynamics/widgets/forward_panel.dart b/lib/pages/dynamics/widgets/forward_panel.dart
index 65e3fd3fb..a08e7ca7b 100644
--- a/lib/pages/dynamics/widgets/forward_panel.dart
+++ b/lib/pages/dynamics/widgets/forward_panel.dart
@@ -17,7 +17,7 @@ import 'video_panel.dart';
InlineSpan picsNodes(List pics) {
return WidgetSpan(
child: LayoutBuilder(
- builder: (_, constraints) => image(
+ builder: (context, constraints) => imageview(
constraints.maxWidth,
pics
.map(
diff --git a/lib/pages/dynamics/widgets/pic_panel.dart b/lib/pages/dynamics/widgets/pic_panel.dart
index 0652d0544..76702b84e 100644
--- a/lib/pages/dynamics/widgets/pic_panel.dart
+++ b/lib/pages/dynamics/widgets/pic_panel.dart
@@ -9,7 +9,7 @@ Widget picWidget(item, context) {
return const SizedBox();
}
return LayoutBuilder(
- builder: (_, constraints) => image(
+ builder: (context, constraints) => imageview(
constraints.maxWidth,
(item.modules.moduleDynamic.major.draw.items as List)
.map(
diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart
index 65d994354..69ff9feba 100644
--- a/lib/pages/fav_detail/view.dart
+++ b/lib/pages/fav_detail/view.dart
@@ -352,7 +352,8 @@ class _FavDetailPageState extends State {
bottom: 0,
child: IgnorePointer(
child: LayoutBuilder(
- builder: (_, constraints) => AnimatedOpacity(
+ builder: (context, constraints) =>
+ AnimatedOpacity(
opacity: loadingState.response[index].checked
? 1
: 0,
diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart
index afdbe3e54..44ce47689 100644
--- a/lib/pages/home/controller.dart
+++ b/lib/pages/home/controller.dart
@@ -110,7 +110,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
showDialog(
context: context,
useSafeArea: true,
- builder: (_) => const Dialog(
+ builder: (context) => const Dialog(
child: MinePage(),
));
}
diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart
index 34359dd70..8aea9b51b 100644
--- a/lib/pages/home/widgets/app_bar.dart
+++ b/lib/pages/home/widgets/app_bar.dart
@@ -59,7 +59,7 @@ class HomeAppBar extends StatelessWidget {
GestureDetector(
onTap: () => showModalBottomSheet(
context: context,
- builder: (_) => const SizedBox(
+ builder: (context) => const SizedBox(
height: 450,
child: MinePage(),
),
@@ -80,7 +80,7 @@ class HomeAppBar extends StatelessWidget {
tooltip: '登录',
onPressed: () => showModalBottomSheet(
context: context,
- builder: (_) => const SizedBox(
+ builder: (context) => const SizedBox(
height: 450,
child: MinePage(),
),
diff --git a/lib/pages/html/view.dart b/lib/pages/html/view.dart
index d327e4b52..1e9127b10 100644
--- a/lib/pages/html/view.dart
+++ b/lib/pages/html/view.dart
@@ -525,7 +525,7 @@ class _HtmlRenderPageState extends State
)
: SliverToBoxAdapter(
child: LayoutBuilder(
- builder: (_, constraints) => htmlRender(
+ builder: (context, constraints) => htmlRender(
context: context,
htmlContent: _htmlRenderCtr.response['content'],
constrainedWidth: constraints.maxWidth,
diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart
index 7f884ce8b..ca23f038b 100644
--- a/lib/pages/later/view.dart
+++ b/lib/pages/later/view.dart
@@ -168,7 +168,7 @@ class _LaterPageState extends State {
bottom: 0,
child: IgnorePointer(
child: LayoutBuilder(
- builder: (_, constraints) => AnimatedOpacity(
+ builder: (context, constraints) => AnimatedOpacity(
opacity: videoItem.checked ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Container(
diff --git a/lib/pages/live_room/widgets/chat.dart b/lib/pages/live_room/widgets/chat.dart
index 56411f419..a3f815e6c 100644
--- a/lib/pages/live_room/widgets/chat.dart
+++ b/lib/pages/live_room/widgets/chat.dart
@@ -30,7 +30,7 @@ class _LiveRoomChatState extends State {
() => ListView.separated(
padding: const EdgeInsets.all(0),
controller: widget.liveRoomController.scrollController,
- separatorBuilder: (_, index) => const SizedBox(height: 6),
+ separatorBuilder: (context, index) => const SizedBox(height: 6),
itemCount: widget.liveRoomController.messages.length,
itemBuilder: (context, index) {
return Container(
diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart
index 651e8b91e..006ad3872 100644
--- a/lib/pages/main/view.dart
+++ b/lib/pages/main/view.dart
@@ -143,7 +143,7 @@ class _MainAppState extends State
}
},
child: LayoutBuilder(
- builder: (_, constriants) {
+ builder: (context, constriants) {
bool isPortait = constriants.maxHeight > constriants.maxWidth;
return Scaffold(
diff --git a/lib/pages/member/new/content/member_contribute/content/article/member_article.dart b/lib/pages/member/new/content/member_contribute/content/article/member_article.dart
index 263f8f2ea..3f131fb4a 100644
--- a/lib/pages/member/new/content/member_contribute/content/article/member_article.dart
+++ b/lib/pages/member/new/content/member_contribute/content/article/member_article.dart
@@ -52,7 +52,7 @@ class _MemberArticleState extends State
},
child: ListView.separated(
itemCount: loadingState.response.length,
- itemBuilder: (_, index) {
+ itemBuilder: (context, index) {
if (index == loadingState.response.length - 1) {
_controller.onLoadMore();
}
@@ -66,7 +66,7 @@ class _MemberArticleState extends State
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
child: LayoutBuilder(
- builder: (_, constraints) {
+ builder: (context, constraints) {
return NetworkImgLayer(
radius: 6,
src: item.originImageUrls!.first,
@@ -97,7 +97,7 @@ class _MemberArticleState extends State
: null,
);
},
- separatorBuilder: (_, index) => Divider(height: 1),
+ separatorBuilder: (context, index) => Divider(height: 1),
),
),
)
diff --git a/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite.dart b/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite.dart
index bff1c4d0c..9a6319b67 100644
--- a/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite.dart
+++ b/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite.dart
@@ -168,7 +168,7 @@ class _MemberFavoriteState extends State
leading: Container(
margin: const EdgeInsets.symmetric(vertical: 6),
child: LayoutBuilder(
- builder: (_, constraints) {
+ builder: (context, constraints) {
return Stack(
children: [
NetworkImgLayer(
diff --git a/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite_ctr.dart b/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite_ctr.dart
index 7518b58bd..26da276eb 100644
--- a/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite_ctr.dart
+++ b/lib/pages/member/new/content/member_contribute/content/favorite/member_favorite_ctr.dart
@@ -57,7 +57,7 @@ class MemberFavoriteCtr extends CommonController {
}
Future userfavFolder() async {
- var res = await Request().get(Api.userFavFolder, data: {
+ var res = await Request().get(Api.userFavFolder, queryParameters: {
'pn': page,
'ps': 20,
'up_mid': mid,
@@ -81,7 +81,7 @@ class MemberFavoriteCtr extends CommonController {
}
Future userSubFolder() async {
- var res = await Request().get(Api.userSubFolder, data: {
+ var res = await Request().get(Api.userSubFolder, queryParameters: {
'up_mid': mid,
'ps': 20,
'pn': page1,
diff --git a/lib/pages/member/new/content/member_dynamic/member_dynamic.dart b/lib/pages/member/new/content/member_dynamic/member_dynamic.dart
index adbfd557f..483b3cdae 100644
--- a/lib/pages/member/new/content/member_dynamic/member_dynamic.dart
+++ b/lib/pages/member/new/content/member_dynamic/member_dynamic.dart
@@ -41,7 +41,7 @@ class _MemberDynamicState extends State
},
child: ListView.separated(
itemCount: loadingState.response.length,
- itemBuilder: (_, index) {
+ itemBuilder: (context, index) {
if (index == loadingState.response.length - 1) {
_controller.onLoadMore();
}
@@ -49,7 +49,8 @@ class _MemberDynamicState extends State
item: loadingState.response[index],
);
},
- separatorBuilder: (_, index) => const SizedBox(height: 10),
+ separatorBuilder: (context, index) =>
+ const SizedBox(height: 10),
),
)
: errorWidget(
diff --git a/lib/pages/member/new/content/member_home/member_home.dart b/lib/pages/member/new/content/member_home/member_home.dart
index ed05ff1a6..d2a120d04 100644
--- a/lib/pages/member/new/content/member_home/member_home.dart
+++ b/lib/pages/member/new/content/member_home/member_home.dart
@@ -128,7 +128,7 @@ class _MemberHomeState extends State
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
child: LayoutBuilder(
- builder: (_, constraints) {
+ builder: (context, constraints) {
return NetworkImgLayer(
radius: 6,
src: loadingState.response.article.item
diff --git a/lib/pages/member/new/member_page.dart b/lib/pages/member/new/member_page.dart
index eb6aa8ed5..8cffc79b4 100644
--- a/lib/pages/member/new/member_page.dart
+++ b/lib/pages/member/new/member_page.dart
@@ -54,7 +54,7 @@ class _MemberPageNewState extends State
body: Obx(
() => _userController.loadingState.value is Success
? LayoutBuilder(
- builder: (_, constraints) {
+ builder: (context, constraints) {
// if (constraints.maxHeight > constraints.maxWidth) {
return ExtendedNestedScrollView(
key: _key,
@@ -251,7 +251,7 @@ class _MemberPageNewState extends State
onTap: () {
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
diff --git a/lib/pages/member/new/widget/edit_profile_page.dart b/lib/pages/member/new/widget/edit_profile_page.dart
index 8b7293376..eef997a81 100644
--- a/lib/pages/member/new/widget/edit_profile_page.dart
+++ b/lib/pages/member/new/widget/edit_profile_page.dart
@@ -75,7 +75,7 @@ class _EditProfilePageState extends State {
Request()
.get(
'${HttpString.appBaseUrl}/x/v2/account/myinfo',
- data: data,
+ queryParameters: data,
)
.then((data) {
setState(() {
@@ -145,7 +145,8 @@ class _EditProfilePageState extends State {
onTap: () {
showDialog(
context: context,
- builder: (_) => _sexDialog(loadingState.response['sex']),
+ builder: (context_) =>
+ _sexDialog(loadingState.response['sex']),
);
},
),
diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart
index ae22db1bf..bda0f5c99 100644
--- a/lib/pages/member/view.dart
+++ b/lib/pages/member/view.dart
@@ -152,7 +152,7 @@ class _MemberPageState extends State
onTap: () {
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
diff --git a/lib/pages/member_search/search_dynamic.dart b/lib/pages/member_search/search_dynamic.dart
index 76c736ea7..23e749c48 100644
--- a/lib/pages/member_search/search_dynamic.dart
+++ b/lib/pages/member_search/search_dynamic.dart
@@ -277,7 +277,7 @@ class SearchDynamic extends StatelessWidget {
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
child: LayoutBuilder(
- builder: (_, constraints) {
+ builder: (context, constraints) {
return NetworkImgLayer(
radius: 6,
src: json['pic'],
diff --git a/lib/pages/msg_feed_top/at_me/view.dart b/lib/pages/msg_feed_top/at_me/view.dart
index 3b91722ab..a8304a4e9 100644
--- a/lib/pages/msg_feed_top/at_me/view.dart
+++ b/lib/pages/msg_feed_top/at_me/view.dart
@@ -67,7 +67,7 @@ class _AtMePageState extends State {
itemCount: _atMeController.msgFeedAtMeList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- itemBuilder: (_, int i) {
+ itemBuilder: (context, int i) {
return ListTile(
onTap: () {
String? nativeUri =
diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart
index 8b98bb388..38cf253ae 100644
--- a/lib/pages/msg_feed_top/like_me/view.dart
+++ b/lib/pages/msg_feed_top/like_me/view.dart
@@ -117,7 +117,7 @@ class LikeMeList extends StatelessWidget {
itemCount: msgFeedLikeMeList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- itemBuilder: (_, int i) {
+ itemBuilder: (context, int i) {
return ListTile(
onTap: () {
String? nativeUri = msgFeedLikeMeList[i].item?.nativeUri;
diff --git a/lib/pages/msg_feed_top/reply_me/view.dart b/lib/pages/msg_feed_top/reply_me/view.dart
index 8bb5134f0..97fe30828 100644
--- a/lib/pages/msg_feed_top/reply_me/view.dart
+++ b/lib/pages/msg_feed_top/reply_me/view.dart
@@ -66,7 +66,7 @@ class _ReplyMePageState extends State {
itemCount: _replyMeController.msgFeedReplyMeList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- itemBuilder: (_, int i) {
+ itemBuilder: (context, int i) {
return ListTile(
onTap: () {
String? nativeUri = _replyMeController
diff --git a/lib/pages/msg_feed_top/sys_msg/view.dart b/lib/pages/msg_feed_top/sys_msg/view.dart
index b722b2169..575416999 100644
--- a/lib/pages/msg_feed_top/sys_msg/view.dart
+++ b/lib/pages/msg_feed_top/sys_msg/view.dart
@@ -72,7 +72,7 @@ class _SysMsgPageState extends State {
itemCount: _sysMsgController.msgFeedSysMsgList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- itemBuilder: (_, int i) {
+ itemBuilder: (context, int i) {
String? content =
_sysMsgController.msgFeedSysMsgList[i].content;
if (content != null) {
@@ -88,7 +88,7 @@ class _SysMsgPageState extends State {
onLongPress: () {
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
title: const Text('确定删除该通知?'),
actions: [
TextButton(
diff --git a/lib/pages/preview/controller.dart b/lib/pages/preview/controller.dart
deleted file mode 100644
index 527152e3a..000000000
--- a/lib/pages/preview/controller.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-import 'dart:io';
-
-import 'package:device_info_plus/device_info_plus.dart';
-import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
-import 'package:get/get.dart';
-import 'package:dio/dio.dart';
-import 'package:path_provider/path_provider.dart';
-import 'package:permission_handler/permission_handler.dart';
-import 'package:share_plus/share_plus.dart';
-
-class PreviewController extends GetxController {
- DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
- RxInt initialPage = 0.obs;
- RxInt currentPage = 1.obs;
- RxList imgList = [].obs;
- bool storage = true;
- bool videos = true;
- bool photos = true;
- String currentImgUrl = '';
-
- requestPermission() async {
- Map statuses = await [
- Permission.storage,
- // Permission.photos
- ].request();
-
- statuses[Permission.storage].toString();
- // final photosInfo = statuses[Permission.photos].toString();
- }
-
- // 图片分享
- void onShareImg() async {
- SmartDialog.showLoading();
- var response = await Dio().get(imgList[initialPage.value],
- options: Options(responseType: ResponseType.bytes));
- final temp = await getTemporaryDirectory();
- SmartDialog.dismiss();
- String imgName =
- "PiliPalaX_pic_${DateTime.now().toString().replaceAll(' ', '_').replaceAll(':', '-').split('.').first}.jpg";
- var path = '${temp.path}/$imgName';
- File(path).writeAsBytesSync(response.data);
- Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]);
- }
-
- void onChange(int index) {
- initialPage.value = index;
- currentPage.value = index + 1;
- currentImgUrl = imgList[index];
- }
-}
diff --git a/lib/pages/preview/index.dart b/lib/pages/preview/index.dart
deleted file mode 100644
index 9fb82e8dd..000000000
--- a/lib/pages/preview/index.dart
+++ /dev/null
@@ -1,4 +0,0 @@
-library preview;
-
-export './controller.dart';
-export './view.dart';
diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart
deleted file mode 100644
index f89692b2a..000000000
--- a/lib/pages/preview/view.dart
+++ /dev/null
@@ -1,328 +0,0 @@
-// ignore_for_file: library_private_types_in_public_api
-
-import 'dart:io';
-
-import 'package:PiliPalaX/utils/extension.dart';
-import 'package:PiliPalaX/utils/storage.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
-import 'package:get/get.dart';
-import 'package:flutter/material.dart';
-import 'package:extended_image/extended_image.dart';
-import 'package:PiliPalaX/utils/download.dart';
-import 'controller.dart';
-import 'package:status_bar_control/status_bar_control.dart';
-
-typedef DoubleClickAnimationListener = void Function();
-
-class ImagePreview extends StatefulWidget {
- final int? initialPage;
- final List? imgList;
- const ImagePreview({
- super.key,
- this.initialPage,
- this.imgList,
- });
-
- @override
- _ImagePreviewState createState() => _ImagePreviewState();
-}
-
-class _ImagePreviewState extends State
- with TickerProviderStateMixin {
- final PreviewController _previewController = Get.put(PreviewController());
- // late AnimationController animationController;
- late AnimationController _doubleClickAnimationController;
- Animation? _doubleClickAnimation;
- late DoubleClickAnimationListener _doubleClickAnimationListener;
- List doubleTapScales = [1.0, 2.0];
- late List _imgList;
- int _quality = 80;
- late List _thumbList;
-
- @override
- void initState() {
- super.initState();
-
- _imgList = widget.imgList?.map((url) => url.http2https).toList() ??
- (Get.arguments['imgList'] as List)
- .map((url) => url.http2https)
- .toList();
- _thumbList = List.generate(_imgList.length, (_) => true);
-
- _quality =
- GStorage.setting.get(SettingBoxKey.previewQuality, defaultValue: 80);
-
- _previewController.initialPage.value =
- widget.initialPage ?? Get.arguments['initialPage'];
- _previewController.currentPage.value =
- (widget.initialPage ?? Get.arguments['initialPage']) + 1;
- _previewController.imgList.value = _imgList;
- _previewController.currentImgUrl =
- _imgList[widget.initialPage ?? Get.arguments['initialPage']];
- // animationController = AnimationController(
- // vsync: this, duration: const Duration(milliseconds: 400));
- setStatusBar();
- _doubleClickAnimationController = AnimationController(
- duration: const Duration(milliseconds: 250), vsync: this);
- }
-
- onOpenMenu() {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return AlertDialog(
- clipBehavior: Clip.hardEdge,
- contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- ListTile(
- onTap: () {
- _previewController.onShareImg();
- Get.back();
- },
- dense: true,
- title: const Text('分享', style: TextStyle(fontSize: 14)),
- ),
- ListTile(
- onTap: () {
- Clipboard.setData(
- ClipboardData(text: _previewController.currentImgUrl))
- .then((value) {
- Get.back();
- SmartDialog.showToast('已复制到粘贴板');
- }).catchError((err) {
- SmartDialog.showNotify(
- msg: err.toString(),
- notifyType: NotifyType.error,
- );
- });
- },
- dense: true,
- title: const Text('复制链接', style: TextStyle(fontSize: 14)),
- ),
- ListTile(
- onTap: () {
- Get.back();
- DownloadUtils.downloadImg(
- context,
- [_previewController.currentImgUrl],
- );
- },
- dense: true,
- title: const Text('保存到手机', style: TextStyle(fontSize: 14)),
- ),
- if (_imgList.length > 1)
- ListTile(
- onTap: () {
- Get.back();
- DownloadUtils.downloadImg(
- context,
- _imgList,
- );
- },
- dense: true,
- title: const Text('保存全部到手机', style: TextStyle(fontSize: 14)),
- ),
- ],
- ),
- );
- },
- );
- }
-
- // 隐藏状态栏,避免遮挡图片内容
- setStatusBar() async {
- if (Platform.isIOS || Platform.isAndroid) {
- await StatusBarControl.setHidden(true,
- animation: StatusBarAnimation.SLIDE);
- }
- }
-
- @override
- void dispose() {
- // animationController.dispose();
- try {
- StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE);
- } catch (_) {}
- _doubleClickAnimationController.dispose();
- clearGestureDetailsCache();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- backgroundColor: Colors.black,
- primary: false,
- extendBody: true,
- appBar: AppBar(
- primary: false,
- toolbarHeight: 0,
- backgroundColor: Colors.black,
- systemOverlayStyle: SystemUiOverlayStyle.dark,
- ),
- body: Stack(
- children: [
- Semantics(
- label: '长按保存',
- child: GestureDetector(
- onLongPress: () => onOpenMenu(),
- child: ExtendedImageGesturePageView.builder(
- controller: ExtendedPageController(
- initialPage: _previewController.initialPage.value,
- pageSpacing: 0,
- ),
- onPageChanged: (int index) =>
- _previewController.onChange(index),
- canScrollPage: (GestureDetails? gestureDetails) =>
- gestureDetails?.totalScale == null ||
- gestureDetails!.totalScale! <= 1.0,
- itemCount: _imgList.length,
- itemBuilder: (BuildContext context, int index) {
- return ExtendedImage.network(
- '${_imgList[index]}${_thumbList[index] && _quality != 100 ? '@${_quality}q.webp' : ''}',
- fit: BoxFit.contain,
- mode: ExtendedImageMode.gesture,
- handleLoadingProgress: true,
- clearMemoryCacheWhenDispose: true,
- onDoubleTap: (ExtendedImageGestureState state) {
- final Offset? pointerDownPosition =
- state.pointerDownPosition;
- final double? begin = state.gestureDetails!.totalScale;
- double end;
-
- //remove old
- _doubleClickAnimation
- ?.removeListener(_doubleClickAnimationListener);
-
- //stop pre
- _doubleClickAnimationController.stop();
-
- //reset to use
- _doubleClickAnimationController.reset();
-
- if (begin == doubleTapScales[0]) {
- setState(() {});
- end = doubleTapScales[1];
- } else {
- setState(() {});
- end = doubleTapScales[0];
- }
-
- _doubleClickAnimationListener = () {
- state.handleDoubleTap(
- scale: _doubleClickAnimation!.value,
- doubleTapPosition: pointerDownPosition);
- };
- _doubleClickAnimation = _doubleClickAnimationController
- .drive(Tween(begin: begin, end: end));
-
- _doubleClickAnimation!
- .addListener(_doubleClickAnimationListener);
-
- _doubleClickAnimationController.forward();
- },
- // ignore: body_might_complete_normally_nullable
- loadStateChanged: (ExtendedImageState state) {
- if (state.extendedImageLoadState == LoadState.loading) {
- final ImageChunkEvent? loadingProgress =
- state.loadingProgress;
- final double? progress =
- loadingProgress?.expectedTotalBytes != null
- ? loadingProgress!.cumulativeBytesLoaded /
- loadingProgress.expectedTotalBytes!
- : null;
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- SizedBox(
- width: 150.0,
- child: LinearProgressIndicator(
- value: progress ?? 0),
- ),
- // const SizedBox(height: 10.0),
- // Text('${((progress ?? 0.0) * 100).toInt()}%',),
- ],
- ),
- );
- } else if (state.extendedImageLoadState ==
- LoadState.failed) {
- WidgetsBinding.instance
- .addPostFrameCallback((timeStamp) {
- setState(() {
- _thumbList[index] = false;
- });
- });
- }
- },
- initGestureConfigHandler: (ExtendedImageState state) {
- return GestureConfig(
- inPageView: true,
- initialScale: 1.0,
- maxScale: 5.0,
- animationMaxScale: 6.0,
- initialAlignment: InitialAlignment.center,
- );
- },
- );
- },
- ),
- ),
- ),
- Positioned(
- left: 0,
- right: 0,
- bottom: 0,
- child: Container(
- padding: EdgeInsets.only(
- left: 20,
- right: 20,
- bottom: MediaQuery.of(context).padding.bottom + 30),
- // decoration: const BoxDecoration(
- // gradient: LinearGradient(
- // begin: Alignment.topCenter,
- // end: Alignment.bottomCenter,
- // colors: [
- // Colors.transparent,
- // Colors.black87,
- // ],
- // tileMode: TileMode.mirror,
- // ),
- // ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- _imgList.length > 1
- ? Obx(
- () => Text.rich(
- textAlign: TextAlign.center,
- TextSpan(
- style: const TextStyle(
- color: Colors.white, fontSize: 16),
- children: [
- TextSpan(
- text: _previewController.currentPage
- .toString()),
- const TextSpan(text: ' / '),
- TextSpan(text: _imgList.length.toString()),
- ]),
- ),
- )
- : const SizedBox(),
- IconButton(
- onPressed: () => Get.back(),
- icon: const Icon(Icons.close, color: Colors.white),
- tooltip: '关闭',
- ),
- ],
- )),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart
index a86ea71db..268401e42 100644
--- a/lib/pages/search/view.dart
+++ b/lib/pages/search/view.dart
@@ -217,7 +217,7 @@ class _SearchPageState extends State with RouteAware {
return switch (loadingState) {
Success() => (loadingState.response as List?)?.isNotEmpty == true
? LayoutBuilder(
- builder: (_, constraints) => HotKeyword(
+ builder: (context, constraints) => HotKeyword(
width: constraints.maxWidth,
hotSearchList: loadingState.response,
onClick: _searchController.onClickKeyword,
diff --git a/lib/pages/search_panel/widgets/article_panel.dart b/lib/pages/search_panel/widgets/article_panel.dart
index c6f743abb..fd49d97b5 100644
--- a/lib/pages/search_panel/widgets/article_panel.dart
+++ b/lib/pages/search_panel/widgets/article_panel.dart
@@ -255,7 +255,7 @@ class ArticlePanelController extends GetxController {
showModalBottomSheet(
context: context,
isScrollControlled: true,
- builder: (_) => SingleChildScrollView(
+ builder: (context) => SingleChildScrollView(
child: Container(
width: double.infinity,
padding: EdgeInsets.only(
diff --git a/lib/pages/search_panel/widgets/user_panel.dart b/lib/pages/search_panel/widgets/user_panel.dart
index 1d7df53c2..4618a4e65 100644
--- a/lib/pages/search_panel/widgets/user_panel.dart
+++ b/lib/pages/search_panel/widgets/user_panel.dart
@@ -188,7 +188,7 @@ class UserPanelController extends GetxController {
showModalBottomSheet(
context: context,
isScrollControlled: true,
- builder: (_) => SingleChildScrollView(
+ builder: (context) => SingleChildScrollView(
child: Container(
width: double.infinity,
padding: EdgeInsets.only(
diff --git a/lib/pages/search_panel/widgets/video_panel.dart b/lib/pages/search_panel/widgets/video_panel.dart
index ed3b286a2..1a433778a 100644
--- a/lib/pages/search_panel/widgets/video_panel.dart
+++ b/lib/pages/search_panel/widgets/video_panel.dart
@@ -256,7 +256,7 @@ class VideoPanelController extends GetxController {
showModalBottomSheet(
context: context,
isScrollControlled: true,
- builder: (_) => StatefulBuilder(
+ builder: (context) => StatefulBuilder(
builder: (context, setState) {
Widget dateWidget([bool isFirst = true]) {
return SearchText(
diff --git a/lib/pages/setting/pages/display_mode.dart b/lib/pages/setting/pages/display_mode.dart
index 7cb1951f2..95fd99f00 100644
--- a/lib/pages/setting/pages/display_mode.dart
+++ b/lib/pages/setting/pages/display_mode.dart
@@ -89,7 +89,7 @@ class _SetDisplayModeState extends State {
Expanded(
child: ListView.builder(
itemCount: modes.length,
- itemBuilder: (_, int i) {
+ itemBuilder: (context, int i) {
final DisplayMode mode = modes[i];
return RadioListTile(
value: mode,
diff --git a/lib/pages/setting/sponsor_block_page.dart b/lib/pages/setting/sponsor_block_page.dart
index 0ab54802b..13aa0b065 100644
--- a/lib/pages/setting/sponsor_block_page.dart
+++ b/lib/pages/setting/sponsor_block_page.dart
@@ -390,13 +390,13 @@ class _SponsorBlockPageState extends State {
_dividerL,
SliverList.separated(
itemCount: _blockSettings.length,
- itemBuilder: (_, index) => ListTile(
+ itemBuilder: (context, index) => ListTile(
dense: true,
enabled: _blockSettings[index].second != SkipType.disable,
onTap: () {
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
title: Text.rich(
@@ -527,7 +527,7 @@ class _SponsorBlockPageState extends State {
),
),
),
- separatorBuilder: (_, index) => Divider(
+ separatorBuilder: (context, index) => Divider(
height: 1,
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
),
diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart
index 39d028ec8..551fc65dd 100644
--- a/lib/pages/video/detail/controller.dart
+++ b/lib/pages/video/detail/controller.dart
@@ -216,6 +216,16 @@ class VideoDetailController extends GetxController
PersistentBottomSheetController? bsController;
+ bool imageStatus = false;
+
+ void onViewImage() {
+ imageStatus = true;
+ }
+
+ void onDismissed(value) {
+ imageStatus = false;
+ }
+
@override
void onInit() {
super.onInit();
@@ -311,7 +321,7 @@ class VideoDetailController extends GetxController
void _showCategoryDialog(BuildContext context, SegmentModel segment) {
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
content: SingleChildScrollView(
@@ -376,7 +386,7 @@ class VideoDetailController extends GetxController
void _showVoteDialog(BuildContext context, SegmentModel segment) {
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
content: SingleChildScrollView(
@@ -426,7 +436,7 @@ class VideoDetailController extends GetxController
void showSBDetail(BuildContext context) {
showDialog(
context: context,
- builder: (_) => AlertDialog(
+ builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
content: SingleChildScrollView(
@@ -532,7 +542,6 @@ class VideoDetailController extends GetxController
void _showBlockToast(String msg) {
SmartDialog.showToast(
msg,
- displayType: SmartToastType.normal,
alignment:
plPlayerController.isFullScreen.value ? Alignment(-0.9, 0.5) : null,
);
@@ -553,7 +562,7 @@ class VideoDetailController extends GetxController
Future _querySponsorBlock() async {
dynamic result = await Request().get(
'${GStorage.blockServer}/api/skipSegments',
- data: {
+ queryParameters: {
'videoID': bvid,
'cid': cid.value,
},
@@ -1112,7 +1121,7 @@ class VideoDetailController extends GetxController
onPressed: () {
showDialog(
context: context,
- builder: (_) {
+ builder: (context) {
String initV = value;
return AlertDialog(
content: TextFormField(
diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart
index 324cafffa..e19bb6745 100644
--- a/lib/pages/video/detail/introduction/controller.dart
+++ b/lib/pages/video/detail/introduction/controller.dart
@@ -849,7 +849,7 @@ class _PayCoinsPageState extends State
@override
Widget build(BuildContext context) {
- return LayoutBuilder(builder: (_, constraints) {
+ return LayoutBuilder(builder: (context, constraints) {
return _buildBody(constraints.maxHeight > constraints.maxWidth);
});
}
diff --git a/lib/pages/video/detail/introduction/widgets/create_fav_page.dart b/lib/pages/video/detail/introduction/widgets/create_fav_page.dart
index fb720991f..10837016b 100644
--- a/lib/pages/video/detail/introduction/widgets/create_fav_page.dart
+++ b/lib/pages/video/detail/introduction/widgets/create_fav_page.dart
@@ -230,7 +230,7 @@ class _CreateFavPageState extends State {
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: LayoutBuilder(
- builder: (_, constraints) {
+ builder: (context, constraints) {
return ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart
index ca79728da..763bc2d5d 100644
--- a/lib/pages/video/detail/reply/controller.dart
+++ b/lib/pages/video/detail/reply/controller.dart
@@ -3,8 +3,10 @@ import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/models/common/reply_type.dart';
import 'package:PiliPalaX/pages/common/reply_controller.dart';
import 'package:PiliPalaX/http/reply.dart';
+import 'package:PiliPalaX/pages/video/detail/controller.dart';
import 'package:PiliPalaX/utils/global_data.dart';
import 'package:fixnum/fixnum.dart' as $fixnum;
+import 'package:get/get.dart';
class VideoReplyController extends ReplyController {
VideoReplyController(
diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart
index c3d577464..2749f9648 100644
--- a/lib/pages/video/detail/reply/view.dart
+++ b/lib/pages/video/detail/reply/view.dart
@@ -21,15 +21,19 @@ class VideoReplyPanel extends StatefulWidget {
final String? replyLevel;
final String heroTag;
final Function replyReply;
+ final VoidCallback? onViewImage;
+ final ValueChanged? onDismissed;
const VideoReplyPanel({
+ super.key,
this.bvid,
this.oid,
this.rpid = 0,
this.replyLevel,
required this.heroTag,
required this.replyReply,
- super.key,
+ this.onViewImage,
+ this.onDismissed,
});
@override
@@ -248,6 +252,8 @@ class _VideoReplyPanelState extends State
isTop: _videoReplyController.hasUpTop && index == 0,
upMid: loadingState.response.subjectControl.upMid,
getTag: () => heroTag,
+ onViewImage: widget.onViewImage,
+ onDismissed: widget.onDismissed,
)
: ReplyItem(
replyItem: loadingState.response.replies[index],
@@ -263,9 +269,8 @@ class _VideoReplyPanelState extends State
);
},
onDelete: _videoReplyController.onMDelete,
- // isTop: _videoReplyController.hasUpTop && index == 0,
- // upMid: loadingState.response.subjectControl.upMid,
- // getTag: () => heroTag,
+ onViewImage: widget.onViewImage,
+ onDismissed: widget.onDismissed,
);
}
},
diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart
index d98493b50..2b35d54f7 100644
--- a/lib/pages/video/detail/reply/widgets/reply_item.dart
+++ b/lib/pages/video/detail/reply/widgets/reply_item.dart
@@ -32,6 +32,8 @@ class ReplyItem extends StatelessWidget {
this.needDivider = true,
this.onReply,
this.onDelete,
+ this.onViewImage,
+ this.onDismissed,
});
final ReplyItemModel? replyItem;
final String? replyLevel;
@@ -41,6 +43,8 @@ class ReplyItem extends StatelessWidget {
final bool needDivider;
final Function()? onReply;
final Function(dynamic rpid, dynamic frpid)? onDelete;
+ final VoidCallback? onViewImage;
+ final ValueChanged? onDismissed;
@override
Widget build(BuildContext context) {
@@ -310,7 +314,7 @@ class ReplyItem extends StatelessWidget {
showReplyRow!) ...[
Padding(
padding: const EdgeInsets.only(top: 5, bottom: 12),
- child: ReplyItemRow(
+ child: replyItemRow(
replies: replyItem!.replies,
replyControl: replyItem!.replyControl,
// f_rpid: replyItem!.rpid,
@@ -386,28 +390,15 @@ class ReplyItem extends StatelessWidget {
],
);
}
-}
-// ignore: must_be_immutable
-class ReplyItemRow extends StatelessWidget {
- ReplyItemRow({
- super.key,
- this.replies,
- this.replyControl,
- // this.f_rpid,
- this.replyItem,
- this.replyReply,
- this.onDelete,
- });
- final List? replies;
- ReplyControl? replyControl;
- // int? f_rpid;
- ReplyItemModel? replyItem;
- Function? replyReply;
- final Function(dynamic rpid)? onDelete;
-
- @override
- Widget build(BuildContext context) {
+ Widget replyItemRow({
+ context,
+ replies,
+ replyControl,
+ replyItem,
+ replyReply,
+ onDelete,
+ }) {
final int extraRow = replyControl?.isShow == true ||
(replyControl?.entryText != null && replies!.isEmpty)
? 1
@@ -564,294 +555,127 @@ class ReplyItemRow extends StatelessWidget {
),
);
}
-}
-InlineSpan buildContent(
- BuildContext context,
- replyItem,
- replyReply,
- fReplyItem,
- textPainter,
- didExceedMaxLines,
-) {
- final String routePath = Get.currentRoute;
- bool isVideoPage = routePath.startsWith('/video');
+ InlineSpan buildContent(
+ BuildContext context,
+ replyItem,
+ replyReply,
+ fReplyItem,
+ textPainter,
+ didExceedMaxLines,
+ ) {
+ final String routePath = Get.currentRoute;
+ bool isVideoPage = routePath.startsWith('/video');
- // replyItem 当前回复内容
- // replyReply 查看二楼回复(回复详情)回调
- // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
- final content = replyItem.content;
- String message = content.message ?? '';
- final List spanChildren = [];
+ // replyItem 当前回复内容
+ // replyReply 查看二楼回复(回复详情)回调
+ // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
+ final content = replyItem.content;
+ String message = content.message ?? '';
+ final List spanChildren = [];
- if (didExceedMaxLines == true) {
- final textSize = textPainter.size;
- var position = textPainter.getPositionForOffset(
- Offset(
- textSize.width,
- textSize.height,
- ),
- );
- final endOffset = textPainter.getOffsetBefore(position.offset);
- message = message.substring(0, endOffset);
- }
-
- // 投票
- if (content.vote.isNotEmpty) {
- message.splitMapJoin(RegExp(r"\{vote:\d+?\}"), onMatch: (Match match) {
- // String matchStr = match[0]!;
- spanChildren.add(
- TextSpan(
- text: '投票: ${content.vote['title']}',
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- ),
- recognizer: TapGestureRecognizer()
- ..onTap = () => Get.toNamed(
- '/webviewnew',
- parameters: {
- 'url': content.vote['url'],
- 'type': 'vote',
- 'pageTitle': content.vote['title'],
- },
- ),
+ if (didExceedMaxLines == true) {
+ final textSize = textPainter.size;
+ var position = textPainter.getPositionForOffset(
+ Offset(
+ textSize.width,
+ textSize.height,
),
);
- return '';
- }, onNonMatch: (String str) {
- return str;
- });
- message = message.replaceAll(RegExp(r"\{vote:\d+?\}"), "");
- }
- message = message
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"')
- .replaceAll(''', "'")
- .replaceAll(' ', ' ');
- // 构建正则表达式
- final List specialTokens = [
- ...content.emote.keys,
- ...content.topicsMeta?.keys?.map((e) => '#$e#') ?? [],
- ...content.atNameToMid.keys.map((e) => '@$e'),
- ];
- List jumpUrlKeysList = content.jumpUrl.keys.map((String e) {
- return e.replaceAllMapped(
- RegExp(r'[?+*]'), (match) => '\\${match.group(0)}');
- }).toList();
- specialTokens.sort((a, b) => b.length.compareTo(a.length));
- String patternStr = specialTokens.map(RegExp.escape).join('|');
- if (patternStr.isNotEmpty) {
- patternStr += "|";
- }
- patternStr += r'(\b(?:\d+[::])?[0-5]?[0-9][::][0-5]?[0-9]\b)';
- if (jumpUrlKeysList.isNotEmpty) {
- patternStr += '|${jumpUrlKeysList.map(RegExp.escape).join('|')}';
- }
- final RegExp pattern = RegExp(patternStr);
- List matchedStrs = [];
- void addPlainTextSpan(str) {
- spanChildren.add(TextSpan(
- text: str,
- ));
- // TextSpan(
- //
- // text: str,
- // recognizer: TapGestureRecognizer()
- // ..onTap = () => replyReply
- // ?.call(replyItem.root == 0 ? replyItem : fReplyItem)))));
- }
+ final endOffset = textPainter.getOffsetBefore(position.offset);
+ message = message.substring(0, endOffset);
+ }
- // 分割文本并处理每个部分
- message.splitMapJoin(
- pattern,
- onMatch: (Match match) {
- String matchStr = match[0]!;
- if (content.emote.containsKey(matchStr)) {
- // 处理表情
- final int size = content.emote[matchStr]['meta']['size'];
- spanChildren.add(WidgetSpan(
- child: ExcludeSemantics(
- child: NetworkImgLayer(
- src: content.emote[matchStr]['url'],
- type: 'emote',
- width: size * 20,
- height: size * 20,
- semanticsLabel: matchStr,
- )),
- ));
- } else if (matchStr.startsWith("@") &&
- content.atNameToMid.containsKey(matchStr.substring(1))) {
- // 处理@用户
- final String userName = matchStr.substring(1);
- final int userId = content.atNameToMid[userName];
+ // 投票
+ if (content.vote.isNotEmpty) {
+ message.splitMapJoin(RegExp(r"\{vote:\d+?\}"), onMatch: (Match match) {
+ // String matchStr = match[0]!;
spanChildren.add(
TextSpan(
- text: matchStr,
+ text: '投票: ${content.vote['title']}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
- ..onTap = () {
- final String heroTag = Utils.makeHeroTag(userId);
- Get.toNamed(
- '/member?mid=$userId',
- arguments: {'face': '', 'heroTag': heroTag},
- );
- },
- ),
- );
- } else if (RegExp(r'^\b(?:\d+[::])?[0-5]?[0-9][::][0-5]?[0-9]\b$')
- .hasMatch(matchStr)) {
- matchStr = matchStr.replaceAll(':', ':');
- spanChildren.add(
- TextSpan(
- text: ' $matchStr ',
- style: isVideoPage
- ? TextStyle(
- color: Theme.of(context).colorScheme.primary,
- )
- : null,
- recognizer: TapGestureRecognizer()
- ..onTap = () {
- // 跳转到指定位置
- if (isVideoPage) {
- try {
- SmartDialog.showToast('跳转至:$matchStr');
- Get.find(
- tag: Get.arguments['heroTag'])
- .plPlayerController
- .seekTo(Duration(seconds: Utils.duration(matchStr)),
- type: 'slider');
- } catch (e) {
- SmartDialog.showToast('跳转失败: $e');
- }
- }
- },
- ),
- );
- } else {
- String appUrlSchema = '';
- final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
- defaultValue: false) as bool;
- if (content.jumpUrl[matchStr] != null &&
- !matchedStrs.contains(matchStr)) {
- appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
- if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
- addPlainTextSpan(matchStr);
- return "";
- }
- spanChildren.addAll(
- [
- if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[
- WidgetSpan(
- child: Image.network(
- content.jumpUrl[matchStr]['prefix_icon'],
- height: 19,
- color: Theme.of(context).colorScheme.primary,
+ ..onTap = () => Get.toNamed(
+ '/webviewnew',
+ parameters: {
+ 'url': content.vote['url'],
+ 'type': 'vote',
+ 'pageTitle': content.vote['title'],
+ },
),
- )
- ],
- TextSpan(
- text: content.jumpUrl[matchStr]['title'],
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- ),
- recognizer: TapGestureRecognizer()
- ..onTap = () async {
- final String title = content.jumpUrl[matchStr]['title'];
- if (appUrlSchema == '') {
- if (matchStr.startsWith('BV')) {
- UrlUtils.matchUrlPush(
- matchStr,
- title,
- '',
- );
- } else if (RegExp(r'^[Cc][Vv][0-9]+$')
- .hasMatch(matchStr)) {
- Get.toNamed('/htmlRender', parameters: {
- 'url': 'https://www.bilibili.com/read/$matchStr',
- 'title': title,
- 'id': matchStr,
- 'dynamicType': 'read'
- });
- } else {
- final String redirectUrl =
- await UrlUtils.parseRedirectUrl(matchStr);
- // if (redirectUrl == matchStr) {
- // Clipboard.setData(ClipboardData(text: matchStr));
- // SmartDialog.showToast('地址可能有误');
- // return;
- // }
- Uri uri = Uri.parse(redirectUrl);
- PiliScheme.routePush(uri);
- // final String pathSegment = Uri.parse(redirectUrl).path;
- // final String lastPathSegment =
- // pathSegment.split('/').last;
- // if (lastPathSegment.startsWith('BV')) {
- // UrlUtils.matchUrlPush(
- // lastPathSegment,
- // title,
- // redirectUrl,
- // );
- // } else {
- // Get.toNamed(
- // '/webviewnew',
- // parameters: {
- // 'url': redirectUrl,
- // 'type': 'url',
- // 'pageTitle': title
- // },
- // );
- // }
- }
- } else {
- if (appUrlSchema.startsWith('bilibili://search')) {
- Get.toNamed('/searchResult',
- parameters: {'keyword': title});
- } else if (matchStr.startsWith('https://b23.tv')) {
- final String redirectUrl =
- await UrlUtils.parseRedirectUrl(matchStr);
- final String pathSegment = Uri.parse(redirectUrl).path;
- final String lastPathSegment =
- pathSegment.split('/').last;
- if (lastPathSegment.startsWith('BV')) {
- UrlUtils.matchUrlPush(
- lastPathSegment,
- title,
- redirectUrl,
- );
- } else {
- Get.toNamed(
- '/webviewnew',
- parameters: {
- 'url': redirectUrl,
- 'type': 'url',
- 'pageTitle': title
- },
- );
- }
- } else {
- Get.toNamed(
- '/webviewnew',
- parameters: {
- 'url': matchStr,
- 'type': 'url',
- 'pageTitle': title
- },
- );
- }
- }
- },
- )
- ],
- );
- // 只显示一次
- matchedStrs.add(matchStr);
- } else if (matchStr.length > 1 &&
- content.topicsMeta[matchStr.substring(1, matchStr.length - 1)] !=
- null) {
+ ),
+ );
+ return '';
+ }, onNonMatch: (String str) {
+ return str;
+ });
+ message = message.replaceAll(RegExp(r"\{vote:\d+?\}"), "");
+ }
+ message = message
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll(''', "'")
+ .replaceAll(' ', ' ');
+ // 构建正则表达式
+ final List specialTokens = [
+ ...content.emote.keys,
+ ...content.topicsMeta?.keys?.map((e) => '#$e#') ?? [],
+ ...content.atNameToMid.keys.map((e) => '@$e'),
+ ];
+ List jumpUrlKeysList = content.jumpUrl.keys.map((String e) {
+ return e.replaceAllMapped(
+ RegExp(r'[?+*]'), (match) => '\\${match.group(0)}');
+ }).toList();
+ specialTokens.sort((a, b) => b.length.compareTo(a.length));
+ String patternStr = specialTokens.map(RegExp.escape).join('|');
+ if (patternStr.isNotEmpty) {
+ patternStr += "|";
+ }
+ patternStr += r'(\b(?:\d+[::])?[0-5]?[0-9][::][0-5]?[0-9]\b)';
+ if (jumpUrlKeysList.isNotEmpty) {
+ patternStr += '|${jumpUrlKeysList.map(RegExp.escape).join('|')}';
+ }
+ final RegExp pattern = RegExp(patternStr);
+ List matchedStrs = [];
+ void addPlainTextSpan(str) {
+ spanChildren.add(TextSpan(
+ text: str,
+ ));
+ // TextSpan(
+ //
+ // text: str,
+ // recognizer: TapGestureRecognizer()
+ // ..onTap = () => replyReply
+ // ?.call(replyItem.root == 0 ? replyItem : fReplyItem)))));
+ }
+
+ // 分割文本并处理每个部分
+ message.splitMapJoin(
+ pattern,
+ onMatch: (Match match) {
+ String matchStr = match[0]!;
+ if (content.emote.containsKey(matchStr)) {
+ // 处理表情
+ final int size = content.emote[matchStr]['meta']['size'];
+ spanChildren.add(WidgetSpan(
+ child: ExcludeSemantics(
+ child: NetworkImgLayer(
+ src: content.emote[matchStr]['url'],
+ type: 'emote',
+ width: size * 20,
+ height: size * 20,
+ semanticsLabel: matchStr,
+ )),
+ ));
+ } else if (matchStr.startsWith("@") &&
+ content.atNameToMid.containsKey(matchStr.substring(1))) {
+ // 处理@用户
+ final String userName = matchStr.substring(1);
+ final int userId = content.atNameToMid[userName];
spanChildren.add(
TextSpan(
text: matchStr,
@@ -860,110 +684,281 @@ InlineSpan buildContent(
),
recognizer: TapGestureRecognizer()
..onTap = () {
- final String topic =
- matchStr.substring(1, matchStr.length - 1);
- Get.toNamed('/searchResult', parameters: {'keyword': topic});
+ final String heroTag = Utils.makeHeroTag(userId);
+ Get.toNamed(
+ '/member?mid=$userId',
+ arguments: {'face': '', 'heroTag': heroTag},
+ );
+ },
+ ),
+ );
+ } else if (RegExp(r'^\b(?:\d+[::])?[0-5]?[0-9][::][0-5]?[0-9]\b$')
+ .hasMatch(matchStr)) {
+ matchStr = matchStr.replaceAll(':', ':');
+ spanChildren.add(
+ TextSpan(
+ text: ' $matchStr ',
+ style: isVideoPage
+ ? TextStyle(
+ color: Theme.of(context).colorScheme.primary,
+ )
+ : null,
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ // 跳转到指定位置
+ if (isVideoPage) {
+ try {
+ SmartDialog.showToast('跳转至:$matchStr');
+ Get.find(
+ tag: Get.arguments['heroTag'])
+ .plPlayerController
+ .seekTo(Duration(seconds: Utils.duration(matchStr)),
+ type: 'slider');
+ } catch (e) {
+ SmartDialog.showToast('跳转失败: $e');
+ }
+ }
},
),
);
} else {
- addPlainTextSpan(matchStr);
- }
- }
- return '';
- },
- onNonMatch: (String nonMatchStr) {
- addPlainTextSpan(nonMatchStr);
- return nonMatchStr;
- },
- );
-
- if (content.jumpUrl.keys.isNotEmpty) {
- List unmatchedItems = content.jumpUrl.keys
- .toList()
- .where((item) => !content.message.contains(item))
- .toList();
- if (unmatchedItems.isNotEmpty) {
- for (int i = 0; i < unmatchedItems.length; i++) {
- String patternStr = unmatchedItems[i];
- spanChildren.addAll(
- [
- if (content.jumpUrl[patternStr]?['prefix_icon'] != null) ...[
- WidgetSpan(
- child: Image.network(
- content.jumpUrl[patternStr]['prefix_icon'],
- height: 19,
+ String appUrlSchema = '';
+ final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
+ defaultValue: false) as bool;
+ if (content.jumpUrl[matchStr] != null &&
+ !matchedStrs.contains(matchStr)) {
+ appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
+ if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
+ addPlainTextSpan(matchStr);
+ return "";
+ }
+ spanChildren.addAll(
+ [
+ if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[
+ WidgetSpan(
+ child: Image.network(
+ content.jumpUrl[matchStr]['prefix_icon'],
+ height: 19,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ )
+ ],
+ TextSpan(
+ text: content.jumpUrl[matchStr]['title'],
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () async {
+ final String title = content.jumpUrl[matchStr]['title'];
+ if (appUrlSchema == '') {
+ if (matchStr.startsWith('BV')) {
+ UrlUtils.matchUrlPush(
+ matchStr,
+ title,
+ '',
+ );
+ } else if (RegExp(r'^[Cc][Vv][0-9]+$')
+ .hasMatch(matchStr)) {
+ Get.toNamed('/htmlRender', parameters: {
+ 'url': 'https://www.bilibili.com/read/$matchStr',
+ 'title': title,
+ 'id': matchStr,
+ 'dynamicType': 'read'
+ });
+ } else {
+ final String redirectUrl =
+ await UrlUtils.parseRedirectUrl(matchStr);
+ // if (redirectUrl == matchStr) {
+ // Clipboard.setData(ClipboardData(text: matchStr));
+ // SmartDialog.showToast('地址可能有误');
+ // return;
+ // }
+ Uri uri = Uri.parse(redirectUrl);
+ PiliScheme.routePush(uri);
+ // final String pathSegment = Uri.parse(redirectUrl).path;
+ // final String lastPathSegment =
+ // pathSegment.split('/').last;
+ // if (lastPathSegment.startsWith('BV')) {
+ // UrlUtils.matchUrlPush(
+ // lastPathSegment,
+ // title,
+ // redirectUrl,
+ // );
+ // } else {
+ // Get.toNamed(
+ // '/webviewnew',
+ // parameters: {
+ // 'url': redirectUrl,
+ // 'type': 'url',
+ // 'pageTitle': title
+ // },
+ // );
+ // }
+ }
+ } else {
+ if (appUrlSchema.startsWith('bilibili://search')) {
+ Get.toNamed('/searchResult',
+ parameters: {'keyword': title});
+ } else if (matchStr.startsWith('https://b23.tv')) {
+ final String redirectUrl =
+ await UrlUtils.parseRedirectUrl(matchStr);
+ final String pathSegment =
+ Uri.parse(redirectUrl).path;
+ final String lastPathSegment =
+ pathSegment.split('/').last;
+ if (lastPathSegment.startsWith('BV')) {
+ UrlUtils.matchUrlPush(
+ lastPathSegment,
+ title,
+ redirectUrl,
+ );
+ } else {
+ Get.toNamed(
+ '/webviewnew',
+ parameters: {
+ 'url': redirectUrl,
+ 'type': 'url',
+ 'pageTitle': title
+ },
+ );
+ }
+ } else {
+ Get.toNamed(
+ '/webviewnew',
+ parameters: {
+ 'url': matchStr,
+ 'type': 'url',
+ 'pageTitle': title
+ },
+ );
+ }
+ }
+ },
+ )
+ ],
+ );
+ // 只显示一次
+ matchedStrs.add(matchStr);
+ } else if (matchStr.length > 1 &&
+ content.topicsMeta[matchStr.substring(1, matchStr.length - 1)] !=
+ null) {
+ spanChildren.add(
+ TextSpan(
+ text: matchStr,
+ style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
- )
- ],
- TextSpan(
- text: content.jumpUrl[patternStr]['title'],
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ final String topic =
+ matchStr.substring(1, matchStr.length - 1);
+ Get.toNamed('/searchResult',
+ parameters: {'keyword': topic});
+ },
),
- recognizer: TapGestureRecognizer()
- ..onTap = () {
- Get.toNamed(
- '/webviewnew',
- parameters: {
- 'url': patternStr,
- 'type': 'url',
- 'pageTitle': content.jumpUrl[patternStr]['title']
- },
- );
- },
- )
- ],
- );
- }
- }
- }
- // 图片渲染
- if (content.pictures.isNotEmpty) {
- spanChildren.add(const TextSpan(text: '\n'));
- spanChildren.add(
- WidgetSpan(
- child: LayoutBuilder(
- builder: (_, constraints) => image(
- constraints.maxWidth,
- (content.pictures as List)
- .map(
- (item) => ImageModel(
- width: item['img_width'],
- height: item['img_height'],
- url: item['img_src'],
+ );
+ } else {
+ addPlainTextSpan(matchStr);
+ }
+ }
+ return '';
+ },
+ onNonMatch: (String nonMatchStr) {
+ addPlainTextSpan(nonMatchStr);
+ return nonMatchStr;
+ },
+ );
+
+ if (content.jumpUrl.keys.isNotEmpty) {
+ List unmatchedItems = content.jumpUrl.keys
+ .toList()
+ .where((item) => !content.message.contains(item))
+ .toList();
+ if (unmatchedItems.isNotEmpty) {
+ for (int i = 0; i < unmatchedItems.length; i++) {
+ String patternStr = unmatchedItems[i];
+ spanChildren.addAll(
+ [
+ if (content.jumpUrl[patternStr]?['prefix_icon'] != null) ...[
+ WidgetSpan(
+ child: Image.network(
+ content.jumpUrl[patternStr]['prefix_icon'],
+ height: 19,
+ color: Theme.of(context).colorScheme.primary,
),
)
- .toList(),
+ ],
+ TextSpan(
+ text: content.jumpUrl[patternStr]['title'],
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ Get.toNamed(
+ '/webviewnew',
+ parameters: {
+ 'url': patternStr,
+ 'type': 'url',
+ 'pageTitle': content.jumpUrl[patternStr]['title']
+ },
+ );
+ },
+ )
+ ],
+ );
+ }
+ }
+ }
+ // 图片渲染
+ if (content.pictures.isNotEmpty) {
+ spanChildren.add(const TextSpan(text: '\n'));
+ spanChildren.add(
+ WidgetSpan(
+ child: LayoutBuilder(
+ builder: (context, constraints) => imageview(
+ constraints.maxWidth,
+ (content.pictures as List)
+ .map(
+ (item) => ImageModel(
+ width: item['img_width'],
+ height: item['img_height'],
+ url: item['img_src'],
+ ),
+ )
+ .toList(),
+ onViewImage,
+ onDismissed,
+ ),
),
),
- ),
- );
- }
+ );
+ }
- // 笔记链接
- if (content.richText.isNotEmpty) {
- spanChildren.add(
- TextSpan(
- text: ' 笔记',
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
+ // 笔记链接
+ if (content.richText.isNotEmpty) {
+ spanChildren.add(
+ TextSpan(
+ text: ' 笔记',
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () => Get.toNamed(
+ '/webviewnew',
+ parameters: {
+ 'url': content.richText['note']['click_url'],
+ 'type': 'note',
+ 'pageTitle': '笔记预览'
+ },
+ ),
),
- recognizer: TapGestureRecognizer()
- ..onTap = () => Get.toNamed(
- '/webviewnew',
- parameters: {
- 'url': content.richText['note']['click_url'],
- 'type': 'note',
- 'pageTitle': '笔记预览'
- },
- ),
- ),
- );
+ );
+ }
+ // spanChildren.add(TextSpan(text: matchMember));
+ return TextSpan(children: spanChildren);
}
- // spanChildren.add(TextSpan(text: matchMember));
- return TextSpan(children: spanChildren);
}
class MorePanel extends StatelessWidget {
diff --git a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart
index 31a0c269f..6958635f7 100644
--- a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart
+++ b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart
@@ -35,6 +35,8 @@ class ReplyItemGrpc extends StatelessWidget {
this.isTop = false,
this.showDialogue,
this.getTag,
+ this.onViewImage,
+ this.onDismissed,
});
final ReplyInfo replyItem;
final String? replyLevel;
@@ -48,6 +50,8 @@ class ReplyItemGrpc extends StatelessWidget {
final bool isTop;
final VoidCallback? showDialogue;
final Function? getTag;
+ final VoidCallback? onViewImage;
+ final ValueChanged? onDismissed;
@override
Widget build(BuildContext context) {
@@ -63,7 +67,7 @@ class ReplyItemGrpc extends StatelessWidget {
feedBack();
// showDialog(
// context: Get.context!,
- // builder: (_) => AlertDialog(
+ // builder: (context) => AlertDialog(
// content: SelectableText(jsonEncode(replyItem.toProto3Json())),
// ),
// );
@@ -993,7 +997,7 @@ class ReplyItemGrpc extends StatelessWidget {
spanChildren.add(
WidgetSpan(
child: LayoutBuilder(
- builder: (_, constraints) => image(
+ builder: (context, constraints) => imageview(
constraints.maxWidth,
content.pictures
.map(
@@ -1004,6 +1008,8 @@ class ReplyItemGrpc extends StatelessWidget {
),
)
.toList(),
+ onViewImage,
+ onDismissed,
),
),
),
diff --git a/lib/pages/video/detail/reply_new/reply_page.dart b/lib/pages/video/detail/reply_new/reply_page.dart
index eef7874ba..2a7a18908 100644
--- a/lib/pages/video/detail/reply_new/reply_page.dart
+++ b/lib/pages/video/detail/reply_new/reply_page.dart
@@ -127,7 +127,7 @@ class _ReplyPageState extends State
child: GestureDetector(
onTap: Get.back,
child: LayoutBuilder(
- builder: (_, constraints) {
+ builder: (context, constraints) {
bool isH = constraints.maxWidth > constraints.maxHeight;
late double padding = constraints.maxWidth * 0.12;
return Padding(
@@ -234,7 +234,7 @@ class _ReplyPageState extends State
image: FileImage(File(_pathList[index])),
),
),
- separatorBuilder: (_, index) => const SizedBox(width: 10),
+ separatorBuilder: (context, index) => const SizedBox(width: 10),
),
);
} else {
@@ -317,7 +317,7 @@ class _ReplyPageState extends State
StreamBuilder(
initialData: true,
stream: _keyboardStream.stream,
- builder: (_, snapshot) => ToolbarIconButton(
+ builder: (context, snapshot) => ToolbarIconButton(
tooltip: '输入',
onPressed: () {
if (!_selectKeyboard) {
@@ -334,7 +334,7 @@ class _ReplyPageState extends State
StreamBuilder(
initialData: true,
stream: _keyboardStream.stream,
- builder: (_, snapshot) => ToolbarIconButton(
+ builder: (context, snapshot) => ToolbarIconButton(
tooltip: '表情',
onPressed: () {
if (_selectKeyboard) {
@@ -391,7 +391,7 @@ class _ReplyPageState extends State
StreamBuilder(
initialData: _enablePublish,
stream: _publishStream.stream,
- builder: (_, snapshot) => FilledButton.tonal(
+ builder: (context, snapshot) => FilledButton.tonal(
onPressed: snapshot.data == true ? submitReplyAdd : null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart
index 3b5e6969a..4b62f19c2 100644
--- a/lib/pages/video/detail/reply_new/view.dart
+++ b/lib/pages/video/detail/reply_new/view.dart
@@ -236,7 +236,7 @@ class _VideoReplyNewDialogState extends State
StreamBuilder(
initialData: false,
stream: _publishStream.stream,
- builder: (_, snapshot) => FilledButton.tonal(
+ builder: (context, snapshot) => FilledButton.tonal(
onPressed: snapshot.data == true ? submitReplyAdd : null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart
index d0bc05b72..87ab781d2 100644
--- a/lib/pages/video/detail/reply_reply/view.dart
+++ b/lib/pages/video/detail/reply_reply/view.dart
@@ -20,6 +20,7 @@ import 'controller.dart';
class VideoReplyReplyPanel extends StatefulWidget {
const VideoReplyReplyPanel({
+ super.key,
this.id,
this.oid,
this.rpid,
@@ -29,7 +30,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.replyType,
this.isDialogue = false,
this.isTop = false,
- super.key,
+ this.onViewImage,
+ this.onDismissed,
});
final int? id;
final int? oid;
@@ -40,6 +42,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
final ReplyType? replyType;
final bool isDialogue;
final bool isTop;
+ final VoidCallback? onViewImage;
+ final ValueChanged? onDismissed;
@override
State createState() => _VideoReplyReplyPanelState();
@@ -149,7 +153,7 @@ class _VideoReplyReplyPanelState extends State {
itemScrollController:
_videoReplyReplyController.itemScrollCtr,
physics: const AlwaysScrollableScrollPhysics(),
- itemBuilder: (_, index) {
+ itemBuilder: (context, index) {
if (widget.isDialogue) {
return _buildBody(
_videoReplyReplyController.loadingState.value,
@@ -168,6 +172,8 @@ class _VideoReplyReplyPanelState extends State {
},
upMid: _videoReplyReplyController.upMid,
isTop: widget.isTop,
+ onViewImage: widget.onViewImage,
+ onDismissed: widget.onDismissed,
)
: ReplyItem(
replyItem: firstFloor,
@@ -178,6 +184,8 @@ class _VideoReplyReplyPanelState extends State {
onReply: () {
_onReply(firstFloor, -1);
},
+ onViewImage: widget.onViewImage,
+ onDismissed: widget.onDismissed,
);
} else if (index == 1) {
return Divider(
@@ -418,6 +426,8 @@ class _VideoReplyReplyPanelState extends State {
),
);
},
+ onViewImage: widget.onViewImage,
+ onDismissed: widget.onDismissed,
)
: ReplyItem(
replyItem: replyItem,
@@ -436,6 +446,8 @@ class _VideoReplyReplyPanelState extends State {
_videoReplyReplyController.loadingState.value =
LoadingState.success(list);
},
+ onViewImage: widget.onViewImage,
+ onDismissed: widget.onDismissed,
);
}
diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart
index 03efd501e..ff538d960 100644
--- a/lib/pages/video/detail/view.dart
+++ b/lib/pages/video/detail/view.dart
@@ -346,6 +346,9 @@ class _VideoDetailPageState extends State
// 离开当前页面时
void didPushNext() async {
// _bufferedListener?.cancel();
+ if (videoDetailController.imageStatus) {
+ return;
+ }
ScreenBrightness().resetApplicationScreenBrightness();
@@ -376,6 +379,10 @@ class _VideoDetailPageState extends State
@override
// 返回当前页面时
void didPopNext() async {
+ if (videoDetailController.imageStatus) {
+ return;
+ }
+
isShowing = true;
PlPlayerController.setPlayCallBack(playCallBack);
videoIntroController.startTimer();
@@ -1286,6 +1293,8 @@ class _VideoDetailPageState extends State
oid: videoDetailController.oid.value,
heroTag: heroTag,
replyReply: replyReply,
+ onViewImage: videoDetailController.onViewImage,
+ onDismissed: videoDetailController.onDismissed,
),
);
@@ -1303,6 +1312,8 @@ class _VideoDetailPageState extends State
replyType: ReplyType.video,
source: 'videoDetail',
isTop: isTop ?? false,
+ onViewImage: videoDetailController.onViewImage,
+ onDismissed: videoDetailController.onDismissed,
),
);
});
@@ -1474,7 +1485,8 @@ class _VideoDetailPageState extends State
)
: null,
child: LayoutBuilder(
- builder: (_, constraints) => NetworkImgLayer(
+ builder: (context, constraints) =>
+ NetworkImgLayer(
radius: 6,
src: segment.url,
width: constraints.maxHeight *
diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart
index db2aaf3dd..85d844412 100644
--- a/lib/pages/video/detail/widgets/header_control.dart
+++ b/lib/pages/video/detail/widgets/header_control.dart
@@ -122,7 +122,7 @@ class _HeaderControlState extends State {
elevation: 0,
context: context,
backgroundColor: Colors.transparent,
- builder: (_) {
+ builder: (context) {
return Container(
width: double.infinity,
height: 500,
diff --git a/lib/pages/webview/webview_page.dart b/lib/pages/webview/webview_page.dart
index adb6e1cee..71be98950 100644
--- a/lib/pages/webview/webview_page.dart
+++ b/lib/pages/webview/webview_page.dart
@@ -50,7 +50,7 @@ class _WebviewPageNewState extends State {
title: StreamBuilder(
initialData: null,
stream: _titleStream.stream,
- builder: (_, snapshot) => Text(
+ builder: (context, snapshot) => Text(
maxLines: 1,
snapshot.hasData ? snapshot.data! : _url,
overflow: TextOverflow.ellipsis,
@@ -61,7 +61,7 @@ class _WebviewPageNewState extends State {
child: StreamBuilder(
initialData: 0.0,
stream: _progressStream.stream,
- builder: (_, snapshot) => snapshot.data as double < 1
+ builder: (context, snapshot) => snapshot.data as double < 1
? LinearProgressIndicator(
value: snapshot.data as double,
)
diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart
index 9fa93090f..882fd3781 100644
--- a/lib/pages/whisper/view.dart
+++ b/lib/pages/whisper/view.dart
@@ -160,7 +160,7 @@ class _WhisperPageState extends State {
itemCount: sessionList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- itemBuilder: (_, int i) {
+ itemBuilder: (context, int i) {
dynamic content =
sessionList[i].lastMsg.content;
if (content == null || content == "") {
diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart
index da6f9d813..15ff7535a 100644
--- a/lib/pages/whisper_detail/view.dart
+++ b/lib/pages/whisper_detail/view.dart
@@ -159,7 +159,7 @@ class _WhisperDetailPageState extends State {
shrinkWrap: true,
reverse: true,
itemCount: messageList.length,
- itemBuilder: (_, int i) {
+ itemBuilder: (context, int i) {
return ChatItem(
item: messageList[i],
eInfos: _whisperDetailController.eInfos,
diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart
index 00f1cae3a..cb49697c4 100644
--- a/lib/router/app_pages.dart
+++ b/lib/router/app_pages.dart
@@ -72,14 +72,6 @@ class Routes {
CustomGetPage(name: '/hot', page: () => const HotPage()),
// 视频详情
CustomGetPage(name: '/video', page: () => const VideoDetailPage()),
- // 图片预览
- // GetPage(
- // name: '/preview',
- // page: () => const ImagePreview(),
- // transition: Transition.fade,
- // transitionDuration: const Duration(milliseconds: 300),
- // showCupertinoParallax: false,
- // ),
//
CustomGetPage(name: '/webview', page: () => const WebviewPage()),
CustomGetPage(name: '/webviewnew', page: () => const WebviewPageNew()),
diff --git a/lib/utils/download.dart b/lib/utils/download.dart
index fbe356f82..26924acaf 100644
--- a/lib/utils/download.dart
+++ b/lib/utils/download.dart
@@ -1,5 +1,6 @@
import 'dart:typed_data';
+import 'package:PiliPalaX/http/init.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
@@ -92,17 +93,14 @@ class DownloadUtils {
if (!await checkPermissionDependOnSdkInt(context)) {
return;
}
- Dio dio = Dio()
- ..options = BaseOptions(
- connectTimeout: const Duration(milliseconds: 10000),
- receiveTimeout: const Duration(milliseconds: 10000),
- );
for (int i = 0; i < imgList.length; i++) {
SmartDialog.showLoading(
msg:
'正在下载原图${imgList.length > 1 ? '${i + 1}/${imgList.length}' : ''}');
- var response = await dio.get(imgList[i],
- options: Options(responseType: ResponseType.bytes));
+ var response = await Request().get(
+ imgList[i],
+ options: Options(responseType: ResponseType.bytes),
+ );
String picName =
"${imgType}_${DateTime.now().toString().replaceAll(' ', '_').replaceAll(':', '-').split('.').first}";
final SaveResult result = await SaverGallery.saveImage(
diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart
index 4ad2cd42b..ae2b05927 100644
--- a/lib/utils/extension.dart
+++ b/lib/utils/extension.dart
@@ -1,7 +1,7 @@
-import 'package:PiliPalaX/pages/preview/view.dart';
+import 'package:PiliPalaX/common/widgets/interactiveviewer_gallery/hero_dialog_route.dart';
+import 'package:PiliPalaX/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
-import 'package:get/get_navigation/src/dialog/dialog_route.dart';
extension ImageExtension on num {
int cacheSize(BuildContext context) {
@@ -67,24 +67,16 @@ extension BuildContextExt on BuildContext {
void imageView({
int? initialPage,
required List imgList,
+ ValueChanged? onDismissed,
}) {
Navigator.of(this).push(
- GetDialogRoute(
- pageBuilder: (buildContext, animation, secondaryAnimation) {
- return ImagePreview(
- initialPage: initialPage ?? 0,
- imgList: imgList,
- );
- },
- transitionDuration: const Duration(milliseconds: 300),
- transitionBuilder: (context, animation, secondaryAnimation, child) {
- var tween = Tween(begin: 0.0, end: 1.0)
- .chain(CurveTween(curve: Curves.linear));
- return FadeTransition(
- opacity: animation.drive(tween),
- child: child,
- );
- },
+ HeroDialogRoute(
+ builder: (context) => InteractiveviewerGallery(
+ sources: imgList,
+ initIndex: initialPage ?? 0,
+ onPageChanged: (int pageIndex) {},
+ onDismissed: onDismissed,
+ ),
),
);
}
diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart
index 4f281e26d..2c0070b7f 100644
--- a/lib/utils/utils.dart
+++ b/lib/utils/utils.dart
@@ -88,7 +88,7 @@ class Utils {
Map followStatus = result['data'];
showDialog(
context: context,
- builder: (_) {
+ builder: (context) {
bool isSpecialFollowed = followStatus['special'] == 1;
String text = isSpecialFollowed ? '移除特别关注' : '加入特别关注';
return AlertDialog(
diff --git a/pubspec.lock b/pubspec.lock
index e477322fa..ff3da9b39 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -488,22 +488,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.1"
- extended_image:
- dependency: "direct main"
- description:
- name: extended_image
- sha256: "613875dc319f17546ea07499b5f0774755709a19a36dfde812e5eda9eb7a5c8c"
- url: "https://pub.dev"
- source: hosted
- version: "9.0.7"
- extended_image_library:
- dependency: transitive
- description:
- name: extended_image_library
- sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7"
- url: "https://pub.dev"
- source: hosted
- version: "4.0.5"
extended_list:
dependency: transitive
description:
@@ -933,14 +917,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
- http_client_helper:
- dependency: transitive
- description:
- name: http_client_helper
- sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
- url: "https://pub.dev"
- source: hosted
- version: "3.0.0"
http_multi_server:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index c845ba94f..aabdb2fec 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -49,7 +49,7 @@ dependencies:
# 图片
cached_network_image: ^3.4.1
- extended_image: ^9.0.7
+ # extended_image: ^9.0.7
saver_gallery: ^4.0.0
# QRCode