Add the option to block panning in a certain axis in Flutter interactive viewer

Use case
When you don't want the users to make y-axis or x-axis panning.
Proposal
You can add a new properties to the InteractiveViewer, an Axis? called panningDirection, if it's null we don't do nothing, if it's Axis.vertical or Axis.horizontal, then we filter the Axis when executing the _matrixTranslate method.
If you wan't I can provide a solution in code working and make a PR with the proper documentation.

// 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/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3, Matrix4;

import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'layout_builder.dart';
import 'ticker_provider.dart';

/// 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.
///
/// The [child] must not be null.
///
/// See also:
///   * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/master/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.
  ///
  /// The [child] parameter must not be null.
  InteractiveViewer({
    super.key,
    this.clipBehavior = Clip.hardEdge,
    this.blockAxis,
    this.alignPanAxis = false,
    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.onInteractionEnd,
    this.onInteractionStart,
    this.onInteractionUpdate,
    this.panEnabled = true,
    this.scaleEnabled = true,
    this.scaleFactor = 200.0,
    this.transformationController,
    required Widget this.child,
  })  : assert(alignPanAxis != null),
        assert(child != null),
        assert(constrained != null),
        assert(minScale != null),
        assert(minScale > 0),
        assert(minScale.isFinite),
        assert(maxScale != null),
        assert(maxScale > 0),
        assert(!maxScale.isNaN),
        assert(maxScale >= minScale),
        assert(panEnabled != null),
        assert(scaleEnabled != null),
        // 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.
  ///
  /// The [builder] parameter must not be null. See its docs for an example of
  /// using it to optimize a large child.
  InteractiveViewer.builder({
    super.key,
    this.clipBehavior = Clip.hardEdge,
    this.blockAxis,
    this.alignPanAxis = false,
    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.onInteractionEnd,
    this.onInteractionStart,
    this.onInteractionUpdate,
    this.panEnabled = true,
    this.scaleEnabled = true,
    this.scaleFactor = 200.0,
    this.transformationController,
    required InteractiveViewerWidgetBuilder this.builder,
  })  : assert(alignPanAxis != null),
        assert(builder != null),
        assert(minScale != null),
        assert(minScale > 0),
        assert(minScale.isFinite),
        assert(maxScale != null),
        assert(maxScale > 0),
        assert(!maxScale.isNaN),
        assert(maxScale >= minScale),
        assert(panEnabled != null),
        assert(scaleEnabled != null),
        // 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;

  /// 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;

  /// The axis in which the pan gesture should be aligned.
  ///
  /// If different to null it will lock the pan gesture to that axis. For example
  /// if set to [Axis.vertical] the pan gesture will only be allowed to pan in the
  /// vertical direction.
  final Axis? blockAxis;

  /// If true, panning is only allowed in the direction of the horizontal axis
  /// or the vertical axis.
  ///
  /// In other words, when this is true, diagonal panning is not allowed. A
  /// single gesture begun along one axis cannot also cause panning along the
  /// other axis without stopping and beginning a new gesture. This is a common
  /// pattern in tables where data is displayed in columns and rows.
  ///
  /// See also:
  ///  * [constrained], which has an example of creating a table that uses
  ///    alignPanAxis.
  final bool alignPanAxis;

  /// 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;

  /// Determines the amount of scale to be performed per pointer scroll.
  ///
  /// Defaults to 200.0, which was arbitrarily chosen to feel natural for most
  /// trackpads and mousewheels on all supported platforms.
  ///
  /// 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.
  ///
  /// Cannot be null, and 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.
  ///
  /// Cannot be null, and must be a finite number greater than zero and less
  /// than maxScale.
  final double minScale;

  /// 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;

  /// 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 = (l1P.dot(l1L2) / lengthSquared).clamp(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<Vector3> closestPoints = <Vector3>[
      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<InteractiveViewer> createState() => _InteractiveViewerState();
}

class _InteractiveViewerState extends State<InteractiveViewer> with TickerProviderStateMixin {
  TransformationController? _transformationController;

  final GlobalKey _childKey = GlobalKey();
  final GlobalKey _parentKey = GlobalKey();
  Animation<Offset>? _animation;
  late AnimationController _controller;
  Axis? _panAxis; // Used with alignPanAxis.
  Offset? _referenceFocalPoint; // Point where the current gesture began.
  double? _scaleStart; // Scale value at start of scaling gesture.
  double? _rotationStart = 0; // Rotation at start of rotation gesture.
  double _currentRotation = 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;

  // 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;

  // 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();
    }
    late final Offset alignedTranslation;

    if (widget.blockAxis != null && _panAxis != null) {
      alignedTranslation = _alignAxis(translation, widget.blockAxis!);
    } else if (widget.alignPanAxis && _panAxis != null) {
      alignedTranslation = _alignAxis(translation, _panAxis!);
    } 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,
        ),
      );

    // 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,
        ),
      );
  }

  // 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 = totalScale.clamp(
      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) {
    switch (gestureType) {
      case _GestureType.rotate:
        return _rotateEnabled;

      case _GestureType.scale:
        return widget.scaleEnabled;

      case _GestureType.pan:
      case null:
        return 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) {
    widget.onInteractionStart?.call(details);

    if (_controller.isAnimating) {
      _controller
        ..stop()
        ..reset();
      _animation?.removeListener(_onAnimate);
      _animation = null;
    }

    _gestureType = null;
    _panAxis = 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) {
    final double scale = _transformationController!.value.getMaxScaleOnAxis();
    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;
        }
        break;

      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;
        break;

      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;
        }
        _panAxis ??= _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,
        );
        break;
    }
    widget.onInteractionUpdate?.call(details);
  }

  // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
  // are handled with GestureDetector's scale gesture.
  void _onScaleEnd(ScaleEndDetails details) {
    widget.onInteractionEnd?.call(details);
    _scaleStart = null;
    _rotationStart = null;
    _referenceFocalPoint = null;

    _animation?.removeListener(_onAnimate);
    _controller.reset();

    if (!_gestureIsSupported(_gestureType)) {
      _panAxis = null;
      return;
    }

    // If the scale ended with enough velocity, animate inertial movement.
    if (_gestureType != _GestureType.pan || details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
      _panAxis = null;
      return;
    }

    final Vector3 translationVector = _transformationController!.value.getTranslation();
    final Offset translation = Offset(translationVector.x, translationVector.y);
    final FrictionSimulation frictionSimulationX = FrictionSimulation(
      _kDrag,
      translation.dx,
      details.velocity.pixelsPerSecond.dx,
    );
    final FrictionSimulation frictionSimulationY = FrictionSimulation(
      _kDrag,
      translation.dy,
      details.velocity.pixelsPerSecond.dy,
    );
    final double tFinal = _getFinalTime(
      details.velocity.pixelsPerSecond.distance,
      _kDrag,
    );
    _animation = Tween<Offset>(
      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();
  }

  // Handle mousewheel scroll events.
  void _receivedPointerSignal(PointerSignalEvent event) {
    if (event is PointerScrollEvent) {
      // Ignore left and right scroll.
      if (event.scrollDelta.dy == 0.0) {
        return;
      }
      widget.onInteractionStart?.call(
        ScaleStartDetails(
          focalPoint: event.position,
          localFocalPoint: event.localPosition,
        ),
      );
      final double scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor);

      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) {
      _panAxis = 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,
    );
  }

  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,
    );
  }

  @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();
    _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,
        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,
            matrix: matrix,
            child: widget.builder!(
              context,
              _transformViewport(matrix, Offset.zero & constraints.biggest),
            ),
          );
        },
      );
    }

    return Listener(
      key: _parentKey,
      onPointerSignal: _receivedPointerSignal,
      child: GestureDetector(
        behavior: HitTestBehavior.opaque, // Necessary when panning off screen.
        onScaleEnd: _onScaleEnd,
        onScaleStart: _onScaleStart,
        onScaleUpdate: _onScaleUpdate,
        child: child,
      ),
    );
  }
}

// This widget simply 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({
    super.key,
    required this.child,
    required this.childKey,
    required this.clipBehavior,
    required this.constrained,
    required this.matrix,
  });

  final Widget child;
  final GlobalKey childKey;
  final Clip clipBehavior;
  final bool constrained;
  final Matrix4 matrix;

  @override
  Widget build(BuildContext context) {
    Widget child = Transform(
      transform: matrix,
      child: KeyedSubtree(
        key: childKey,
        child: this.child,
      ),
    );

    if (!constrained) {
      child = OverflowBox(
        alignment: Alignment.topLeft,
        minWidth: 0,
        minHeight: 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<Matrix4> {
  /// 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
  /// void 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) {
  const 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,
      ),
    ),
    inverseMatrix.transform3(
      Vector3(
        viewport.topRight.dx,
        viewport.topRight.dy,
        0,
      ),
    ),
    inverseMatrix.transform3(
      Vector3(
        viewport.bottomRight.dx,
        viewport.bottomRight.dy,
        0,
      ),
    ),
    inverseMatrix.transform3(
      Vector3(
        viewport.bottomLeft.dx,
        viewport.bottomLeft.dy,
        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)),
    rotationMatrix.transform3(Vector3(rect.right, rect.top, 0)),
    rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0)),
    rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 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<Vector3> viewportPoints = <Vector3>[
    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) {
  switch (axis) {
    case Axis.horizontal:
      return Offset(offset.dx, 0);
    case Axis.vertical:
      return Offset(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;
}

Great!! Looking forward to it!

pls add this!

I really need this!

excellent idea!

will be a great contribution to flutter

Thanks!! I've been looking for this!

it would be great to add this to flutter

Would love to have this feature ASAP!

It would come really handy!


Add support for varargs command arguments

Given the nature of command arguments, it would be great if the API supported varargs.
Currently I'm supplementing the API with:
fun Command.args(vararg args: String) = args(args.toList())

Ideally rather than doing this:
Command("ping")
     .args(listOf("-c", "5", "localhost"))

One should be able to do this:
Command("ping")
     .args("-c", "5", "localhost")

The advantage of varargs is that it allows to also do fancy things with spread, such as:
Command("ping")
     .args(getPingCount()*, "localhost")

fun getPingCount(): Array<String> =
    if(config.pingCount >= 1) arrayOf("-c", config.pingCount.toString()) else emptyArray()

It will be added in subsequent versions, within one month

Now supports


ArrayIndexOutOfBoundsError in Bus

For a while now our app has been having crashes within the otto Bus file:
       at java.util.HashMap.addNewEntry(HashMap.java:476)
       at java.util.HashMap.put(HashMap.java:408)
       at com.squareup.otto.Bus.post(SourceFile:320)
       at com.example.myapp.asyncTask.doWakefulWork(SourceFile:56)
       at com.commonsware.cwac.wakeful.WakefulIntentService.onHandleIntent(SourceFile:106)
       at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loop(Looper.java:137)
       at android.os.HandlerThread.run(HandlerThread.java:60)```

My guess is that this is something to do with `Set<Class<?>> flattenHierarchy(Class<?> concreteClass)` as it is the only function I can see that would be being called that is modifying a `HashMap`

Looks like a thread-safety issue to me. The flattenHierarchyCache in Otto.Bus is not threadsafe (just a hashmap with no synchronization), so if you call Bus.post() from multiple threads there is potential for corruption internally. I think that's arguably a bug, because the javadocs imply that you are allowed to relax the main thread restrictions if you want. It doesn't explicitly say that Otto.Bus is threadsafe though, and if you use your bus instance from a single thread (main, or other) it should work fine.
IntentService.onHandleIntent() is called from a worker thread, not the main thread and calls to Bus.post() would typically be called from the main thread in simpler use cases.
When you create your Otto instance you might consider keeping the ThreadEnforcer.MAIN policy and fixing up your code to use the main thread for Otto interactions. It's closer to what most people will do, although the docs aren't 100% clear on the threadsafety of the various methods.

FYI, see PR #154. I think multithreaded access to Otto is discouraged, but since the APIs allow it it seems like a safe fix with little to no real-world performance degradation.


Release 24

This is a PR for Release 24.
Status:

[x] In development
[ ] In testing at NRK
[ ] Finalize and merge

Release 24 overall status: https://github.com/nrkno/Sofie-TV-automation/issues/20

Codecov Report

Merging #299 into master will increase coverage by 3.96%.
The diff coverage is 76.20%.

@@            Coverage Diff             @@
##           master     #299      +/-   ##
==========================================
+ Coverage   63.98%   67.94%   +3.96%
==========================================
  Files         162      178      +16
  Lines       11089    12158    +1069
  Branches     2442     2810     +368
==========================================
+ Hits         7095     8261    +1166
+ Misses       3943     3839     -104
- Partials       51       58       +7

Impacted Files
Coverage Δ

meteor/lib/Rundown.ts
70.73% <ø> (-0.70%)
:arrow_down:

meteor/lib/Settings.ts
100.00% <ø> (ø)

meteor/lib/api/notes.ts
100.00% <ø> (ø)

meteor/lib/api/peripheralDevice.ts
95.61% <ø> (+0.87%)
:arrow_up:

meteor/lib/api/rundown.ts
61.29% <ø> (ø)

meteor/lib/api/rundownNotifications.ts
100.00% <ø> (ø)

meteor/lib/api/systemStatus.ts
100.00% <ø> (ø)

meteor/lib/api/userActions.ts
100.00% <ø> (ø)

meteor/lib/collections/AdLibActions.ts
100.00% <ø> (ø)

meteor/lib/collections/Evaluations.ts
75.00% <ø> (ø)

... and 136 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e5b4d60...f486491. Read the comment docs.


stakira.OpenUTAU version 0.1.149

[ ] Have you signed the Contributor License Agreement?
[ ] Have you checked that there aren't other open pull requests for the same manifest update/change?
[ ] This PR only modifies one (1) manifest
[ ] Have you validated your manifest locally with winget validate --manifest <path>?
[ ] Have you tested your manifest locally with winget install --manifest <path>?
[ ] Does your manifest conform to the 1.4 schema?

Note: <path> is the name of the directory containing the manifest you're submitting.

Microsoft Reviewers: codeflow:open?pullrequest=https://github.com/microsoft/winget-pkgs/pull/110019&drop=dogfoodAlpha

May just be a false positive on my end, but I'll wait until @stephengillie validates this PR

May just be a false positive on my end, but I'll wait until @stephengillie can validate this PR as well

Seeing same here:

May just be a false positive on my end, but I'll wait until @stephengillie can validate this PR as well

Seeing same here:
cc @stephengillie

Maybe it got fixed overnight? I'm not seeing the same:


"no acceptable grep could be found" when building GNU Make (and others)

When building GNU Make, the configure scripts displays the following error:
checking for grep that handles long lines and -e... configure: error: no acceptable grep could be found in /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin:/usr/xpg4/bin
Many other configure scripts show the exact same error, I'm guessing this is a standard autotools test that toybox grep is failing.

Acknowledged, I'll try to take a look this weekend. (Please poke me on monday if I forget.)

autoconf has gotten extra-annoying because when I insert toybox grep into the start of the $PATH, the test finds /bin/grep later in the $PATH anyway and uses that without comment. How this is supposed to be sane behavior I don't know, but it means I need to rewrite logwrap to be able to use a second path variable instead of just traversing $PATH like ccache and friends.
Working on it, need to adjust test environment to lie to autoconf harder...

No, it's that in my local test setup when toybox grep doesn't pass whatever test it ran, it silently falls back to calling /bin/grep anyway. Just saying I need to rework my test setup to stop autoconf from trying to be "clever"...

Note that if you just put "GNU" into the output of grep --version, autoconf skips the check and Make has no trouble building afaict

Sed also has this problem because autotools. ("If you are into pain, get the autotools book.. Read it awhile, throw it in a box and start sacrificing to Cthulu.")
Custom --version output will fix it (e.g. "This is not GNU grep 9.0") but that also means that grep will also have
to manually help.
These lines somewhere in the main function, TOYFLAG_NOHELP in the option flags, and (help)(version) in the option string:
  // Lie to autoconf when it asks stupid questions, so configure regexes
  // that look for "GNU sed version %f" greater than some old buggy number
  // don't fail us for not matching their narrow expectations.
  if (FLAG(version)) {
    xprintf("This is not GNU grep version 9.0\n");
    return;
  }

  // Handling our own --version means we handle our own --help too.
  if (FLAG(help)) return show_help(stdout, 0);

Patch is in waiting approval

Note that if you just put "GNU" into the output of
grep --version, autoconf skips the check and Make
has no trouble building afaict

Yeah, I did the same thing for sed ages ago. The problem is if autoconf is traversing the REST of the $PATH to find broken commands and use those instead when the first one wasn't happy, then I may need to redesign my logpath wrapper so the remaining commands aren't in $PATH but are in some other variable entirely. (That's my pending $TODO item here.)

Try commit d0e44b3a8cbd


[BACKLOG-21937] Fixed standard deviation calculations within Group by…

… and Memory Group by steps in kettle

Build Completed
:fire: This pull request has some issues. It would be preferable to fix them in order for it to be just perfect. See below for more details. Some links are also available below for further assistance in addressing those issues.
Build Commands
mvn -B -fn -f 'pom.xml' -pl 'engine' -P '!assemblies' -Dsurefire.runOrder=alphabetical -Daudit -amd clean install

Cleanup Commands
mvn -B -f 'pom.xml' -pl 'engine' -P '!assemblies' -amd build-helper:remove-project-artifact

Changed files
  engine/src/main/java/org/pentaho/di/trans/steps/groupby/GroupBy.java
  engine/src/main/java/org/pentaho/di/trans/steps/memgroupby/MemoryGroupBy.java
  engine/src/test/java/org/pentaho/di/trans/steps/groupby/GroupByNewAggregateTest.java
  engine/src/test/java/org/pentaho/di/trans/steps/groupby/GroupByNullInputTest.java
  engine/src/test/java/org/pentaho/di/trans/steps/groupby/GroupByTest.java
  engine/src/test/java/org/pentaho/di/trans/steps/memgroupby/MemoryGroupByNewAggregateTest.java
  engine/src/test/java/org/pentaho/di/trans/steps/memgroupby/MemoryGroupByTest.java

Unit Test Coverage
These statistics help you identify how your changes have affected the coverage of the following files. If a file is not in this list, then its coverage was not affected by your changes. To get some help interpreting these metrics, please refer to Jacoco's documentation.

:warning: Coverage Changes: +0.1% (click to expand)
org.pentaho.di.trans.step.StepMeta

Branch Change: -0.7% :small_red_triangle_down:
Complexity Change: -0.6% :small_red_triangle_down:
Instruction Change: -0.2% :small_red_triangle_down:
Line Change: -0.3% :small_red_triangle_down:

org.pentaho.di.trans.steps.constant.ConstantMeta

Instruction Change: -0.1% :small_red_triangle_down:

org.pentaho.di.trans.steps.dimensionlookup.DimensionLookupMeta

Method Change: -1% :small_red_triangle_down:

org.pentaho.di.trans.steps.ldapoutput.LDAPOutputMeta

Instruction Change: -0.1% :small_red_triangle_down:

org.pentaho.di.trans.steps.sortedmerge.SortedMergeMeta

Instruction Change: -0.2% :small_red_triangle_down:

org.pentaho.di.trans.steps.systemdata.SystemDataMeta

Branch Change: -2% :small_red_triangle_down:
Complexity Change: -2% :small_red_triangle_down:
Instruction Change: -0.3% :small_red_triangle_down:
Line Change: -0.8% :small_red_triangle_down:

org.pentaho.di.trans.steps.webservices.WebServiceMeta

Branch Change: -1.7% :small_red_triangle_down:
Complexity Change: -1.2% :small_red_triangle_down:
Instruction Change: -0.1% :small_red_triangle_down:


version of nodejs on clone git repo

if it compiles just for a version it should be enough to clone just the tag and the single-branch with depth of 1 - works better on RPI :)

Please remove changes to .gitignore and package-lock.json. And i'll try this PR while releasing binaries next time.

done


[1/6] Deployments Table - Deployment struct updates

Note that this work is based off a feature branch (deployments-table).
This PR adds the affectedServices field to Deployment. This field is populated by DCOSStore when it receives deployments data from MarathonStore. If it is not populated and something attempts to access it, it will error rather than show bad data.
Once this is merged, the whole services table can be populated using only the output from DCOSStore.deploymentsList.

Reviewer: @MatApple

Nice @philipnrmn!

Thanks for the speedy review! Addressed all comments.

Incidentally, integration tests are failing because something's up with Velocity - they are certified to be Working On My Machine™

🚢  great stuff!


Reconnect to remote devbox agent when connection is dropped

As we discussed over hangouts, here are the main pieces of feedback:

Let's move flushOutstandingMsgs from the ScheduledThreadPoolExecutor into the start of the main thread's while(continue()) loop. This will allow us to enforce that flushing existing messages and sending new messages never happen concurrently, ruling out a whole class of potential bugs

pendingQueue can be a Queue (or BlockingQueue or ConcurrentLinkedQueue or something) rather than a ConcurrentHashMap. This will allow us to preserve ordering, such that when we replay the messages. Since both the main Syncer logic and remote DevboxAgentMain are both single threaded, and exchanging messages of TCP (which enforces in-order delivery) we can be confident that any Acks that get received will be for the entries at the head of the queue.

We should preserve the behavior of the draining(client){...} blocks, since that is what allows us to ensure things have fully synced before showing notifications to the user. We can do this by waiting until the pendingQueue has emptied out before returning, OR the agent has died in which case you bail out early and make another go around the big while(continue()) loop

This was not discussed, but I suspect we should also move the logic for re-creating the agent (agent.start(), client.resetInt, client.resetOut) to the start of the big while(continue())loop. This will ensure that if the agent dies halfway through the main thread syncing something, it stays dead until the main thread comes around again, reducing the potential for bugs in the following case: the agent dies halfway through the big loop, the main thread sends some messages that go intopendingQueue, the agent restarts, and the main thread continues sending new messages while there are still old messages sitting in pendingQueue`, meaning the messages will arrive out of order.

As discussed, we should have a standalone unit test for this recovery behavior. You can run the existing tests via mill devbox.test devbox.DevboxTests.edge.0, mill devbox.test devbox.DevboxTests.edge.restart, mill devbox.test devbox.DevboxTests.scalatags.0, etc.. We already have handy mock Agents for testing, e.g. InMemoryAgent or a local subprocess syncing between two folders (see def instantiateSyncer), and it shouldn't be too hard to inject some failures at the Agent-level that we expect Syncer.scala to be able to recover from.

These are the high level pieces of feedback; nitpicks can come later. Thanks for doing this!

Thanks @lihaoyi-databricks

This is hard because the drainning thread is not always running (only runs in synchronizeRepo) Therefore, if connection died when draining thread is running, main thread should be stuck at drainer.join and won't be able to execute flushOutstandingMsgs. My current solution is that when monitor detects that the connection is dropped and attemp to reconnect, it set a state in client shouldFlush. In the draining thread, it checks if shouldFlush and flushes outstanding messages. This is safe because draining thread will only finish when all outstanding messages are acked.
Done
Done
This is no longer the case because we ensure that draining is a checkpoint. Therefore, if agent dies halfway and restarts, main thread will flush all outstanding messages first, join the draining thread and send new messages.

This is hard because the drainning thread is not always running (only runs in synchronizeRepo) Therefore, if connection died when draining thread is running, main thread should be stuck at drainer.join and won't be able to execute flushOutstandingMsgs. My current solution is that when monitor detects that the connection is dropped and attemp to reconnect, it set a state in client shouldFlush. In the draining thread, it checks if shouldFlush and flushes outstanding messages. This is safe because draining thread will only finish when all outstanding messages are acked.

This seems reasonable to me: so if the connection dies, the main thread's messages will get ignored by the dead subprocess, and the thread will get "stuck" at the conclusion of draining{...}. The main thread will thus stay there until the connection is re-established, at which point the drainer thread will notice it shouldFlush, and will flushOutstandingMsgs before terminating and letting the main thread proceed.
Do I understand that correctly?

This is hard because the drainning thread is not always running (only runs in synchronizeRepo) Therefore, if connection died when draining thread is running, main thread should be stuck at drainer.join and won't be able to execute flushOutstandingMsgs. My current solution is that when monitor detects that the connection is dropped and attemp to reconnect, it set a state in client shouldFlush. In the draining thread, it checks if shouldFlush and flushes outstanding messages. This is safe because draining thread will only finish when all outstanding messages are acked.

This seems reasonable to me: so if the connection dies, the main thread's messages will get ignored by the dead subprocess, and the thread will get "stuck" at the conclusion of draining{...}. The main thread will thus stay there until the connection is re-established, at which point the drainer thread will notice it shouldFlush, and will flushOutstandingMsgs before terminating and letting the main thread proceed.
Do I understand that correctly?

Exactly. This is a bit messy to spread state all around different component and makes it very hard to maintain (I will put more comments in the PR). But I found it the simplest way to keep draining semantics while not refactoring a huge chunk of code.

@codeyq I put some time on your calendar to go over this PR together, but we could also do it now if you're free/online

Shall we consider replace ssh with mosh (https://mosh.org/)? That should solve the auto reconnect problem when the client switches to another network. It also has other perks like local echo to improve interactivity when the network latency is high.

@haogang I think this PR tends to make sure the underlying connection is stable no matter what kind of connection because devbox only cares about stdout, stdin and stderr interface with the remote agent.
Currently, we run devbox.jar which takes a ssh command as a parameter. If we want to use mosh, we can just switch this ssh one to mosh.
java -jar devbox.jar \
     --log-file ../log.txt \
     --healthcheck-interval 3 \
     --repo /Users/qianyu/devbox ssh -p 2222 root@localhost java -jar /root/agent.jar

@haogang this is for the syncer thread, not the SSH thread, so MOSH's local echo wouldn't be useful. If you want to use MOSH on the devbox interactively, you can install it and connect manually (or add it to our setup_devbox.sh script so it's always installed by default).
I'm not sure how well MOSH does with Devbox's command connection, since it's sending over large blobs of bytes rather than the interactive terminal sessions that MOSH is designed to optimize for, but its' worth trying as qian mentioned above

This generally looks good to me. Last comments:

Let's go make the DevboxAgentMain RPC handlers idempotent. We should make it idempotent when deployed, but turn off the idempotency in devbox's own test suite so the tests can be more aggressive at finding bugs

Clean up the various .sh scripts you were using for testing

Maybe revert https://github.com/databricks/devbox/pull/1/files#diff-b77033c1e4cce42c184d0d0cc278cdd9L11 if possible


Implement recovery codes for 2 factor authentication

not for now... make it simple...

Fixed... https://github.com/lazaronixon/authentication-zero/commit/b8777a02565df21ec76c67bf874aee0bae229117


JupyterHub with containers

Can we create a JupyterHub instance on SRC that uses custom container images to instantiate Lab sessions?

Maybe a first attempt could be based on using the pang notebook image: https://github.com/pangeo-data/pangeo-docker-images

JupyterHub Docker Spawner: https://jupyterhub-dockerspawner.readthedocs.io/en/latest/

We should maybe start by having a look at the JupyterHub catalog items on SRC. Do any of these already provide an implementation that can work with containers? Some information could maybe be found on the SRC docs: https://servicedesk.surf.nl/wiki/display/WIKI/SURF+Research+Cloud

According to doc, there is a section "Workspace parameters" to set container info. However, it is not clear which catalog items have this section. This section is not available for JupyterHub or Ubuntu.
I also found documentation on creating a Docker Compose catalog item on SRC.

To use JupyterHub Docker Spawne, see valid docker images at how-to-pick-a-docker-image, examples are here. Example jupyter_config file is here.

[ ] test pangeo image locally
[ ] test config file on SRC locally
[ ] check if container can be paused and not deleted
[ ] create catalog item on SRC, (in a different repo)


Markdown preview - show tooltip on links

Would be great to see a tooltip on links, showing the target. Or popping it up in the status bar or elsewhere, like Chrome.

@roblourens : Could you please explain the preview functionality bit more with screenshots of Chrome?
I am new to open source community and this project.
I use vscode often and would love to work on this if someone can guide me a little.

Lower left corner of the Chrome window when I hover the link to your name:

But I think a normal tooltip would be easier.

Ah yes. I understand what you are talking about now. To verify, where the link is leading to.
Seems like a beginner friendly feature, would pick it up this weekend if anyone can nudge me towards the right direction. :)

Sorry for the delayed response.
I looked into this. It seems that tooltips are show if the element has a title attribute, but not for links that only have a href. I wasn't able to find if there is an electron setting to enable tooltips for links without titles. I think we may have to implement this ourselves.
To get started, take a look at this PR that added a similar feature to another electron app: https://github.com/brave/browser-laptop/pull/625
Here's where our webview code lives: https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/parts/html/browser/webview.ts I believe you should be able to attach a update-target-url handler to the webview handler.
Please let me know if you have any questions about the code or want to discuss potential designs

I m happy to receive help! :)
I was facing issues while trying to build and fiddle around with the source code.
Will definitely look into this PR and the code you pointed out this weekend.
Thanks for taking your time to post this info.
I ll post any questions or designs which I have.

Hi @mjbvz , I will create a PR to add title attribute to every <a>.

For verification: this was implemented as tooltip when you hover over links in a webview


label_image.py  Example:  Show ONLY results Higher than 75 % ?

Hello,
can Sombody please explain how to change label_image.py from the Example Codes
so its only Shows the Result (Name,Score) if the Score is Higher than 75 % , else  print ".... "
Would be very Helpfull.

Thank you for your post. We noticed you have not filled out the following field in the issue template. Could you update them if they are relevant in your case, or leave them as N/A? Thanks.
Have I written custom code
OS Platform and Distribution
TensorFlow installed from
TensorFlow version
Bazel version
CUDA/cuDNN version
GPU model and memory
Exact command to reproduce

It has been 14 days with no activity and the awaiting response label was assigned. Is this still an issue?

It has been 16 days with no activity and the awaiting response label was assigned. Is this still an issue?

It has been 14 days with no activity and the awaiting response label was assigned. Is this still an issue?

We are closing this issue for now due to lack of activity. Please comment if this is still an issue for you. Thanks!


fix(app): include config_path in yaml -> DB migration

What type of PR is this? (check all applicable)

[ ] Refactor
[ ] Feature
[x] Bug Fix
[ ] Optimization
[ ] Documentation Update
[ ] Community Node Submission

Have you discussed this change with the InvokeAI team?

[x] Yes
[ ] No, because:

Have you updated all relevant documentation?

[ ] Yes
[ ] No

Description
If yaml entry has a config_path, make sure that is migrated into DB as absolute path

We should probably add a test for the yaml migration, given it's a sensitive thing and we basically get one chance at it...


Test calling other contracts

Issue: related to #70
Add test to the calling_other_contracts example

LGTM, it's all good!


moving from mocha to lab

Moved tests to lab, consistent with other stores.
This does not includes any refactoring of the tests them selves.

@colmharte I just tested this PR and also chatted with @paolochiodi about it.
I can only get the tests to run on Node 0.10.x (tested on 0.10.40) and Seneca 0.5.x (tested on 0.5.21).
Other combinations of Node 0.12.7 and 4.0.0 and Seneca 0.6.x and 0.7.x did not work. I think that this is intended for this PR.
Changes I'd recommend to the package.json file:

I think that there should be an engines section: "engines" : {    "node" : ">=0.10.0 <=0.10.40"  }
We should move seneca to a devDependency and specify a hard version number instead of plugin.
Bump the package.json version value
Change seneca-store-test to a published version.

@colmharte @guyellis I confirm this PR was only intended to move test framework to Lab.
Another PR is in progress for further rework, including support for new seneca and node versions.
We can add seneca and engines restrictions directly in this one if everyone agrees.

My feeling is that each accepted PR should run clean on npm test (once the DB is setup). So I would be biased towards setting those restrictions to make that happen. We should obviously also be striving to get up to the latest in the next PR that you have pending which I think is what you're doing.

@guyellis Agreed, @paolochiodi can you add the necessary restrictions to this PR ensure tests run? Then we can create a new PR to enable compatibility with latest versions of node / seneca etc

Sure.
Should I also bump the package (minor) version?

I suppose that doesn't need to be changed until we publish right?

@guyellis Updated dependencies as agreed. Unfortunately I wasn't able to move seneca-store-test to a published version because I'm making use of some of the test that aren't in the published version.
We should ask @rjrodger to review the change and publish it in the near future

LGTM. Clean npm i after pulling the latest and using node 0.10.40 runs clean with npm test.


Add type check support for Object.create and Object.defineProperties

This issue was imported from Closure Compiler's previous home at http://closure-compiler.googlecode.com
The link to the original issue is:
https://code.google.com/p/closure-compiler/issues/detail?id=455

+1 :smirk:

Is there any news regarding type-checking for objects created using Object.create()?
I would find this extremely helpful, for situations like this:
var a = {};

/** @param {number} x */
a.myFunction = function (x) {
    window.console.log(x);
};

var b = Object.create(a);

b.myFunction('not a number');  // Should raise TypeValidator.TYPE_MISMATCH_WARNING

+1. This is not type-checking for me:
/* @flow */

Object.defineProperties({}, {
	foo: {
		get: function() {
			return '';
		}
	}
});

I get this error:
$ flow
Account.js:3
  3: Object.defineProperties({}, {
     ^ .foo of Object.defineProperties. Property not found in
  4: 	foo: {
     	     ^ object literal

Found 1 error

@mbrowne This seems like an error log from Flow :) .

Oops, you're right - please disregard.

This Isn't fixed!

We mass-closed all issues tagged NTI a few months ago. (https://github.com/google/closure-compiler/wiki/[OBSOLETE]-Using-NTI-(new-type-inference))
I'll reopen this issue since it wasn't actually specific to NTI.

Created Google internal issue b/120557713.


Update jsxstyle-loader readme

I think It would be helpful to specify the loaders' order in code example although it's already detailed in https://github.com/smyte/jsxstyle/tree/master/packages/jsxstyle-loader#its-not-working-

I heartily agree. Thanks!

I added a note right at the top of the README as well: https://github.com/smyte/jsxstyle/commit/a24e17db0f49bf7326f6920d3d1cd01d9ab86f53#diff-43592ca7fb9dcaa6528fda73dbe90044R11


Add config to allow commenting empty lines (useful for Golang's godoc)

The motivation here is how Golang's godoc works:

godoc processes Go source files to extract documentation about the contents of the package. Comments that appear before top-level declarations, with no intervening newlines, are extracted along with the declaration to serve as explanatory text for the item.
https://golang.org/doc/effective_go.html#commentary
http://godoc.org/golang.org/x/tools/cmd/godoc

E.g. before this PR
// This whole block is commented
// in two separate blocks,

// because there's an empty line
// between the first and second block,
// and if you select until the empty line below,
// it's not commented either

func doSome() {
}

For godoc to capture the whole comment block, and associate it with the following function in the generated documentation, we need the whole block to be commented until the last line before the function declaration.
E.g. after this PR, with NERDCommentEmptyLines=1
// This whole block is commented
// as one single comment block
//
// despite of empty lines between blocks,
// and if you select until the empty line below,
// it'll be commented as well
//
func doSome() {
}

I kept the original behavior by setting NERDCommentEmptyLines=0 by default, so it can be overridden in vimrc with:
au FileType go let NERDCommentEmptyLines=1

Fork notice: With the upstream project sitting stale after nearly 8 months since the last activity, 71 outstanding pull requests, no comments from the original developer, and dozens of developers duplicating efforts while scratching their own itches, I've decided to step in and run a fork for a while. Hopefully the original developer will eventually consider picking this plugin up again or adding more maintainers so that the user base doesn't end up fragmented, but for now I would invite anyone interested in the latest updates to consider installing from and contributing pull requests to my fork.
About this PR: I have merged this PR as is. Please consider installing the plugin from alerque/nerdcommenter for this and (so far) over two dozen other updates.

Change of plans. Thanks to scrooloose who has added me as a maintainer to this repo, I'll be going through and taking care of business here instead of in a fork. This PR has been merged directly ustream.
Thanks for your contribution @wcamarao.

Thank you for taking this over.
Sent from my iPhone
On May 23, 2016, at 12:35 PM, Caleb Maclennan<EMAIL_ADDRESS>wrote:
Change of plans. Thanks to scrooloose who has added me as a maintainer to this repo, I'll be going through and taking care of business here instead of in a fork. This PR has been merged directly ustream.
Thanks for your contribution @wcamarao.
—
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub


expose APIs to set target window size

There is no API to change the target window size to be different from SETTINGS_INITIAL_WINDOW_SIZE.  An endpoint may wish to this on either a connection or individual stream.
Clients, servers, and their streams should probably have window_target(&self) -> u32 and set_window_target(&mut self, sz: u32) functions.

So, several things do currently have methods like release_capacity, which will eventually lead to a WINDOW_UPDATE sent to the peer. It currently asserts that the amount "released" was previously received, but if that assertion were not in place, the release_capacity(100_000) could essentially send an update giving the peer that much window space...
Perhaps it is best to keep them separate, so as to be more explicit when someone is staying with the INITIAL_WINDOW_SIZE, and when someone is granting more than that...

I don't think it would be exactly correct as release_capacity(100_000) would break the how the connection manages assigning connection level capacity (I think). Either way, I believe that the right way is to set target windows per stream and connection.


I like it.

Might be confusing though if you create the machines through some sort of automation or cloning that sets the same hostname on all hosts.

Might be confusing though if you create the machines through some sort of automation or cloning that sets the same hostname on all hosts.

Hostnames need to be unique, see: https://github.com/kontena/pharos-cluster/issues/307

Merged to resolve conflicts. This is approved, do we include this in 1.3?


Invitation token not recognized after updating to 1.0.19

Error: PowInvitation.Phoenix.InvitationController halted in :load_user_from_invitation_token/2
I read the changelog

[PowInvitation.Plug] Added PowInvitation.Plug.sign_invitation_token/2 to sign the invitation_token
[PowInvitation.Plug] Added PowInvitation.Plug.load_invited_user_by_token/2 to verify the signed invitation_token to prevent timing attacks

and I tried to find any custom code I use that affects the changes, but can't seem to find any. Could you give me some clues on where to start looking?
Edit: I checked on a fresh install of Phoenix and Pow reproducing the same error.

@joepstender you have to sign the token before you send it out, so you do anything custom you might want to find where you generate the invitation token and then sign it with ‘PowInvitation.Plug.sign_invitation_token(conn, user)’ if you are doing it in a liveview you want to make sure it has a secret_key_base from your endpoint. You could retrieve that with something like ‘MyAppWeb.Endpoint.config(:secret_key_base)’

Something like that was my first thought, but I could replicate the problem on a default Phoenix + Pow installation.

Hmm, my guess would also be that the token was not signed right.
The URL gets generated with the signed token here:
  defp invitation_url(conn, user) do
    token = Plug.sign_invitation_token(conn, user)

    routes(conn).url_for(conn, __MODULE__, :edit, [token])
  end

And then it loads from the signed token here:
  defp load_user_from_invitation_token(%{params: %{"id" => token}} = conn, _opts) do
    case Plug.load_invited_user_by_token(conn, token) do
      {:error, conn}  ->
        conn
        |> put_flash(:error, extension_messages(conn).invalid_invitation(conn))
        |> redirect(to: routes(conn).path_for(conn, SessionController, :new))
        |> halt()

      {:ok, conn} ->
        conn
    end
  end

It's concerning it doesn't work out-of-the-box for you. Let me try it out locally to see if I can replicate it.

It works local here with a default setup. You don't have any custom logic for the invitation controller?

Nope, I used a new Phoenix, with nothing else as Pow 1.0.19. And I followed
the guides step by step.
Op di 14 apr. 2020 om 17:49 schreef Dan Schultzer<EMAIL_ADDRESS>
It works local here with a default setup. You don't have any custom logic
for the invitation controller?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/danschultzer/pow/issues/491#issuecomment-613522071,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAEMAJLFNIO5V53LRGGGYSLRMSAXLANCNFSM4MHQOGPA
.

--
Joep Stender
Roptawei 11
9123 JA  Metslawier
+31 6 10913970

Tested with a complete clean install and it works as expected.

Do you see an error flash with The invitation doesn't exist.?
What do you use for mailer? Is it just the one from the readme that logs it?
From where do you copy the generated invitation URL?

Yeah I see that flash message, the token in the url is the same as the one in the database.

I use Bamboo with Sendgrid on a live server, where I first noticed the issue. Then started testing locally with the same result.

From the send email, and locally from the server logs.

the token in the url is the same as the one in the database

This is what's wrong. It should've been signed so it look like this format:
SFMyNTY.NWJhMmEzMjgtYTcyMy00MzI2LTg3ZTktNzVlMTE4ZTA4MmU3.GEGqxOiMKi2LhdFoLypXZrkK0ppx7RJCx3BUEFQJ4Bc
I'm not sure what could be wrong if this isn't some custom logic that overrides the normal URL generation in PowInvitation.Phoenix.InvitationController.

It uses Elixir 1.10.2 (compiled with Erlang/OTP 22).

Here is the code: https://github.com/joepstender/testpow

Tested it and it works for me. I'm creating a new user then going to http://localhost:4000/invitations/new to create the invitation. After that I sign out, and then copy and open the link from the e-mail debug log (in this case http://localhost:4000/invitations/SFMyNTY.ZGI4MmViMzctNzVhOS00M2JiLTk2OWQtZDBhMTc4ZGFjYjM5.kpnGXKcdezQufKefgxT6j-PP6xmjz9WV7j3CvTGYnQo/edit).

I'm creating a new user then going to http://localhost:4000/invitations/new to create the invitation.

This is weird, when I try now it creates the correct signed token. How is this possible?

Just to sign in so I can invite another user.

This is weird, when I try now it creates the correct signed token. How is this possible?

Hmm, I've no idea, I haven't been able to replicate this issue.

I'm gonna close this assuming that I made an error somewhere. If I find it I'll post it. Thanks for the swift replies!


AHOYAPPS-489 Update override behavior

Description
These changes address the betterments to the --override behavior described in #14.
Breakdown

Specify a serviceSid if re-deploying an existing project
Specify asset SIDs when re-deploying

Validation
Added 3 new tests to validate the intended behavior.

I added some unit tests for the deploy function.


Fix useEffect web component warnings

We've had useEffect dependency warnings on the web component for some time. As we're now looking to update and expand the web component functionality it'd be best to address these before additional work is started.
It's worth:

Reviewing the dependency array and seeing if they're needed
Add them to the array or refactor into multiple useEffect hooks
Test to ensure over rendering or infinite loops aren't introduced

Take a look at this PR, possible fix for useEffect https://github.com/RaspberryPiFoundation/editor-ui/pull/595/files


DOC: special: Add an example to the airy docstring.

A token contribution to the docathon. 😃

Great, thanks @WarrenWeckesser.


CI add manually build

TODO

[ ] Format tar verdicts as ${sha1}-${job-id}

Build log
Preview build with this PR (commit)

more info:cloud: Storage Free space   4.68 GiB    31.18%:open_file_folder: 185.84 MiB  CI_WORK/GitHubActions/roki-web


AWS: ingress not working be default

Worker pool and NLB target group configuration doesn't match in a default Lokomotive setup on AWS and ingress doesn't work out of the box.
There are two problems:

The NLB target groups for port 80 and 443 are empty after installation. (<clustername>-workers-http/<clustername>-workers-https). The worker pool target groups (<poolname>-workers-http/<poolname>-workers-https) on the other hand are not configured with the NLB.

The NLB target group's health check is configured to check target nodes for health on port :10254/healthz, but that port is not actually used / listend on anymore.

Manually adding the worker nodes to the NLB target groups and changing the health check configuration to theck port 80/443 (the target ports) instead is a workaround.

Fixed in https://github.com/kinvolk/lokomotive/pull/367.


Bug report - Adds as basic zigbee instead of power strip

I am in the U.S. and have a few WiFi/Zigbee Tuya power strips. Even though the plugs are different in this country from what you have in the Tuya app, I was able to add a 4 plug power strip with no problems to my Homey Pro. When I tried to add a second one using the same process, I get an error with unknown Zigbee device. It still adds the device, but only has option for on/off. It doesn’t give options for each individual plug. I restarted the Tuya Zigbee app, but that didn’t help.
**To Reproduce*I have your Tuya Zigbee app installed on my Homey Pro.
Steps to reproduce the behavior:

Go to Devices and tap + New device.
Click on Tuya Zigbee app
Scroll down to Wall Plug
Select Power strip socket 1/4
Tap Connect then on Zigbee page tap Continue
Put power strip into pairing mode

Expected behavior
I was expecting it to add a device with 4 sockets and a single USB control. Instead it added a basic single Zigbee device without the individual socket options. As you can see in the screenshot below, the 1st power strip was added successfully. Then get error on second try as described.
App and Homey version
What Homey version are you running? Homey Pro (Early 2023)
What version of Homey firmware do you use? 10.2.1
What version of the Tuya Zigbee App are you using? 0.1.45
Homey app version <IP_ADDRESS>8
Screenshots

Additional context
I realize since the sockets are different being in different countries, but since the first one worked perfectly, I figured the others added would act the same way.

For a device to work with tuya zigbee app the model and manufacturer id must match a device in the list of devices that this app support. Even if you have two devices that even looks identical one could work and the other one not just because they have different ids. This is by design. If you look here https://tools.developer.homey.app/tools/zigbee you can see you device id for each device.

@Netwitz , Did you try the TEST version of the app? Install from here: https://homey.app/nl-nl/app/com.tuya.zigbee/Tuya-Zigbee/test/ From your reported version (v 0.1.45) I think you did not. After installing it, your device might just work. If not (and still being (un)recognized as an unknow zigbee device), you should open a New Device issue and mark it as such. See guidelines. And then close this issue. Your 'problem' is not a bug, but merely a new device request.

Thank you for your response. I'll check the newer app.
 On Thursday, February 8, 2024 at 06:29:10 PM EST, Boechie<EMAIL_ADDRESS>wrote:

@Netwitz , Did you try the TEST version of the app? Install from here: https://homey.app/nl-nl/app/com.tuya.zigbee/Tuya-Zigbee/test/ From your reported version (v 0.1.45) I think you did not. After installing it, your device might just work. If not (and still being (un)recognized as an unknow zigbee device), you should open a New Device issue and mark it as such. See guidelines. And then close this issue. Your 'problem' is not a bug, but merely a new device request.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: @.***>


openlane package is broken

https://github.com/hdl/conda-eda/actions/runs/5944971903/job/16125504708

related to https://github.com/The-OpenROAD-Project/OpenLane/issues/1952, possible workaround is to disable ir drop reporting by default (as it is currently documented)


Create prototype iterator functionality

The code proposed came out of the prototype work done with the yank command defined in Utopia, with improved class hierarchy.
Key features:

Kerberus class: is the filter guardian wrapping up anything to do with filtering to be used with the iterator functions
CoreFilter class hierarchy: consists of the base class: CoreFilter, RegexFilter and FlobFilter
FilterDriver: is a wrapper around the CoreFilter and consists of the base class FilterDriver, UnaryFilter designed for single filter scenarios and CompoundFilter for mult-filter scenarios, such as filtering on leaf/child nodes
For compound filters, we need to be able apply mutiple filters with all or any semantics. This is achieved with the CompoundHandler class hierarchy, constisting of AllCompoundHandler and AnyCompoundHandler classes
FilterNode represents the directory presented to Invoke-TraverseDirectory client, ie the client side script block/context. Contains extra information about the directory and a subject that further contains information about the directory segments
FilterStrategy defines the semantics behind the filter scope, eg what is a child node, and what is a leaf node

The CoreFilter can be applied to any part of the directory path and this is denoted by its scope value.

The commit: 6482cea fixes this issue, the commit references the wrong issue.