Compare commits

..

11 Commits

Author SHA1 Message Date
dom
bce73d9f16 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 15:46:06 +08:00
dom
6f30d2e331 opt reply
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 12:37:21 +08:00
dom
556bda0d68 opt video intro
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 11:46:47 +08:00
dom
9d5eb55e26 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 11:46:42 +08:00
dom
110469961d opt video scheme
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-27 11:40:00 +08:00
dom
fa348db7c5 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-27 11:19:49 +08:00
dom
3eac565b5e fix slide
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:52:49 +08:00
dom
af40e489bc opt ao
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:38:48 +08:00
dom
361eb4c614 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:15:47 +08:00
dom
7ace981f24 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:15:47 +08:00
My-Responsitories
bfb2becb2d opt: ao (#1811)
* opt: ao

* multi select

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-26 14:11:48 +08:00
28 changed files with 2757 additions and 429 deletions

View File

@@ -1,6 +1,3 @@
import 'dart:math' as math;
import 'dart:ui' show clampDouble;
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'
@@ -10,18 +7,14 @@ import 'package:flutter/rendering.dart'
MultiChildLayoutParentData;
import 'package:flutter/widgets.dart';
enum TooltipType { top, right }
class CustomTooltip extends StatefulWidget {
const CustomTooltip({
super.key,
this.type = TooltipType.top,
required this.overlayWidget,
required this.child,
required this.indicator,
});
final TooltipType type;
final Widget child;
final ValueGetter<Widget> overlayWidget;
final ValueGetter<Widget> indicator;
@@ -51,27 +44,20 @@ class _CustomTooltipState extends State<CustomTooltip> {
longPressRecognizer.addPointer(event);
}
Widget _buildCustomTooltipOverlay(BuildContext context) {
final OverlayState overlayState = Overlay.of(
context,
debugRequiredFor: widget,
Widget _buildCustomTooltipOverlay(
BuildContext context,
OverlayChildLayoutInfo layoutInfo,
) {
final target = MatrixUtils.transformPoint(
layoutInfo.childPaintTransform,
layoutInfo.childSize.topCenter(Offset.zero),
);
final RenderBox box = this.context.findRenderObject()! as RenderBox;
final Offset target = box.localToGlobal(
box.size.center(Offset.zero),
ancestor: overlayState.context.findRenderObject(),
);
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
verticalOffset: box.size.height / 2,
horizontalOffset: box.size.width / 2,
type: widget.type,
target: target,
onDismiss: _scheduleDismissTooltip,
overlayWidget: widget.overlayWidget,
indicator: widget.indicator,
);
return SelectionContainer.maybeOf(context) == null
? overlayChild
: SelectionContainer.disabled(child: overlayChild);
@@ -105,7 +91,7 @@ class _CustomTooltipState extends State<CustomTooltip> {
child: widget.child,
);
}
return OverlayPortal(
return OverlayPortal.overlayChildLayoutBuilder(
controller: _overlayController,
overlayChildBuilder: _buildCustomTooltipOverlay,
child: result,
@@ -113,22 +99,14 @@ class _CustomTooltipState extends State<CustomTooltip> {
}
}
enum _ChildType { overlay, indicator }
class _CustomTooltipOverlay extends StatelessWidget {
const _CustomTooltipOverlay({
required this.verticalOffset,
required this.horizontalOffset,
required this.type,
required this.target,
required this.onDismiss,
required this.overlayWidget,
required this.indicator,
});
final double verticalOffset;
final double horizontalOffset;
final TooltipType type;
final Offset target;
final VoidCallback onDismiss;
final ValueGetter<Widget> overlayWidget;
@@ -137,21 +115,12 @@ class _CustomTooltipOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _ToolTip(
type: type,
target: target,
verticalOffset: verticalOffset,
horizontalOffset: horizontalOffset,
preferBelow: false,
onTap: PlatformUtils.isMobile ? onDismiss : null,
children: [
LayoutId(
id: _ChildType.indicator,
child: indicator(),
),
LayoutId(
id: _ChildType.overlay,
child: overlayWidget(),
),
indicator(),
overlayWidget(),
],
);
}
@@ -161,28 +130,19 @@ class _ToolTip extends MultiChildRenderObjectWidget {
const _ToolTip({
super.children,
this.onTap,
required this.type,
required this.target,
required this.verticalOffset,
required this.horizontalOffset,
required this.preferBelow,
});
final VoidCallback? onTap;
final TooltipType type;
final Offset target;
final double verticalOffset;
final double horizontalOffset;
final bool preferBelow;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderToolTip(
onTap: onTap,
type: type,
target: target,
verticalOffset: verticalOffset,
horizontalOffset: horizontalOffset,
preferBelow: preferBelow,
);
}
@@ -192,8 +152,6 @@ class _ToolTip extends MultiChildRenderObjectWidget {
renderObject
..onTap = onTap
..target = target
..verticalOffset = verticalOffset
..horizontalOffset = horizontalOffset
..preferBelow = preferBelow;
}
}
@@ -204,15 +162,9 @@ class _RenderToolTip extends RenderBox
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
_RenderToolTip({
VoidCallback? onTap,
required TooltipType type,
required Offset target,
required double verticalOffset,
required double horizontalOffset,
required bool preferBelow,
}) : _type = type,
_target = target,
_verticalOffset = verticalOffset,
_horizontalOffset = horizontalOffset,
}) : _target = target,
_preferBelow = preferBelow,
_hitTestSelf = onTap != null {
if (onTap != null) {
@@ -246,8 +198,6 @@ class _RenderToolTip extends RenderBox
}
}
final TooltipType _type;
Offset _target;
Offset get target => _target;
set target(Offset value) {
@@ -256,22 +206,6 @@ class _RenderToolTip extends RenderBox
markNeedsPaint();
}
double _verticalOffset;
double get verticalOffset => _verticalOffset;
set verticalOffset(double value) {
if (_verticalOffset == value) return;
_verticalOffset = value;
markNeedsPaint();
}
double _horizontalOffset;
double get horizontalOffset => _horizontalOffset;
set horizontalOffset(double value) {
if (_horizontalOffset == value) return;
_horizontalOffset = value;
markNeedsPaint();
}
bool _preferBelow;
bool get preferBelow => _preferBelow;
set preferBelow(bool value) {
@@ -302,40 +236,18 @@ class _RenderToolTip extends RenderBox
indicator.parentData as MultiChildLayoutParentData;
final overlayParentData = overlay.parentData as MultiChildLayoutParentData;
switch (_type) {
case TooltipType.top:
Offset offset = _positionDependentBox(
type: _type,
size: size,
childSize: overlaySize,
target: target,
verticalOffset: verticalOffset,
horizontalOffset: horizontalOffset,
preferBelow: preferBelow,
);
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
overlayParentData.offset = offset;
indicatorParentData.offset = Offset(
target.dx - indicatorSize.width / 2,
offset.dy + overlaySize.height - 1,
);
case TooltipType.right:
Offset offset = _positionDependentBox(
type: _type,
size: size,
childSize: overlaySize,
target: target,
verticalOffset: verticalOffset,
horizontalOffset: horizontalOffset,
preferBelow: preferBelow,
);
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
overlayParentData.offset = offset;
Offset(
offset.dx - indicatorSize.width + 1,
target.dy - indicatorSize.height / 2,
);
}
Offset offset = positionDependentBox(
size: size,
childSize: overlaySize,
target: target,
preferBelow: preferBelow,
);
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
overlayParentData.offset = offset;
indicatorParentData.offset = Offset(
target.dx - indicatorSize.width / 2,
offset.dy + overlaySize.height - 1,
);
}
@override
@@ -357,19 +269,16 @@ class Triangle extends LeafRenderObjectWidget {
super.key,
required this.color,
required this.size,
this.type = .top,
});
final Color color;
final Size size;
final TooltipType type;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderTriangle(
color: color,
preferredSize: size,
type: type,
);
}
@@ -388,10 +297,8 @@ class RenderTriangle extends RenderBox {
RenderTriangle({
required Color color,
required Size preferredSize,
required TooltipType type,
}) : _color = color,
_preferredSize = preferredSize,
_type = type;
_preferredSize = preferredSize;
Color _color;
Color get color => _color;
@@ -408,8 +315,6 @@ class RenderTriangle extends RenderBox {
markNeedsLayout();
}
final TooltipType _type;
@override
void performLayout() {
size = constraints.constrain(_preferredSize);
@@ -422,21 +327,11 @@ class RenderTriangle extends RenderBox {
..color = color
..style = PaintingStyle.fill;
Path path;
switch (_type) {
case TooltipType.top:
path = Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width / 2, size.height)
..close();
case TooltipType.right:
path = Path()
..moveTo(0, size.height / 2)
..lineTo(size.width, 0)
..lineTo(size.width, size.height)
..close();
}
final path = Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width / 2, size.height)
..close();
context.canvas.drawPath(path, paint);
}
@@ -444,50 +339,3 @@ class RenderTriangle extends RenderBox {
@override
bool get isRepaintBoundary => true;
}
Offset _positionDependentBox({
required TooltipType type,
required Size size,
required Size childSize,
required Offset target,
required bool preferBelow,
double verticalOffset = 0.0,
double horizontalOffset = 0.0,
double margin = 10.0,
}) {
switch (type) {
case TooltipType.top:
// VERTICAL DIRECTION
final bool fitsBelow =
target.dy + verticalOffset + childSize.height <= size.height - margin;
final bool fitsAbove =
target.dy - verticalOffset - childSize.height >= margin;
final bool tooltipBelow = fitsAbove == fitsBelow
? preferBelow
: fitsBelow;
final double y;
if (tooltipBelow) {
y = math.min(target.dy + verticalOffset, size.height - margin);
} else {
y = math.max(target.dy - verticalOffset - childSize.height, margin);
} // HORIZONTAL DIRECTION
final double flexibleSpace = size.width - childSize.width;
final double x = flexibleSpace <= 2 * margin
// If there's not enough horizontal space for margin + child, center the
// child.
? flexibleSpace / 2.0
: clampDouble(
target.dx - childSize.width / 2,
margin,
flexibleSpace - margin,
);
return Offset(x, y);
case TooltipType.right:
final double dy = math.max(margin, target.dy - childSize.height / 2);
final double dx = math.min(
target.dx + horizontalOffset,
size.width - childSize.width - margin,
);
return Offset(dx, dy);
}
}

View File

@@ -0,0 +1,901 @@
// 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:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'package:PiliPlus/common/widgets/flutter/text_intro/text_selection.dart';
import 'package:flutter/cupertino.dart'
hide TextSelectionGestureDetectorBuilder;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TextSelectionGestureDetectorBuilder;
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
class _TextSpanEditingController extends TextEditingController {
_TextSpanEditingController({required TextSpan textSpan})
: _textSpan = textSpan,
super(text: textSpan.toPlainText(includeSemanticsLabels: false));
final TextSpan _textSpan;
@override
TextSpan buildTextSpan({
required BuildContext context,
TextStyle? style,
required bool withComposing,
}) {
// This does not care about composing.
return TextSpan(style: style, children: <TextSpan>[_textSpan]);
}
@override
set text(String? newText) {
// This should never be reached.
throw UnimplementedError();
}
}
class _SelectableTextSelectionGestureDetectorBuilder
extends CustomTextSelectionGestureDetectorBuilder {
_SelectableTextSelectionGestureDetectorBuilder({
required _SelectableTextState state,
}) : _state = state,
super(delegate: state);
final _SelectableTextState _state;
@override
void onSingleTapUp(TapDragUpDetails details) {
if (!delegate.selectionEnabled) {
return;
}
super.onSingleTapUp(details);
_state.widget.onTap?.call();
}
}
/// A run of selectable text with a single style.
///
/// Consider using [SelectionArea] or [SelectableRegion] instead, which enable
/// selection on a widget subtree, including but not limited to [Text] widgets.
///
/// The [SelectableText] widget displays a string of text with a single style.
/// The string might break across multiple lines or might all be displayed on
/// the same line depending on the layout constraints.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
///
/// The [style] argument is optional. When omitted, the text will use the style
/// from the closest enclosing [DefaultTextStyle]. If the given style's
/// [TextStyle.inherit] property is true (the default), the given style will
/// be merged with the closest enclosing [DefaultTextStyle]. This merging
/// behavior is useful, for example, to make the text bold while using the
/// default font family and size.
///
/// {@macro flutter.material.textfield.wantKeepAlive}
///
/// {@tool snippet}
///
/// ```dart
/// const SelectableText(
/// 'Hello! How are you?',
/// textAlign: TextAlign.center,
/// style: TextStyle(fontWeight: FontWeight.bold),
/// )
/// ```
/// {@end-tool}
///
/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can
/// display a paragraph with differently styled [TextSpan]s. The sample
/// that follows displays "Hello beautiful world" with different styles
/// for each word.
///
/// {@tool snippet}
///
/// ```dart
/// const SelectableText.rich(
/// TextSpan(
/// text: 'Hello', // default text style
/// children: <TextSpan>[
/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
/// ],
/// ),
/// )
/// ```
/// {@end-tool}
///
/// ## Interactivity
///
/// To make [SelectableText] react to touch events, use callback [onTap] to achieve
/// the desired behavior.
///
/// ## Scrolling Considerations
///
/// If this [SelectableText] is not a descendant of [Scaffold] and is being used
/// within a [Scrollable] or nested [Scrollable]s, consider placing a
/// [ScrollNotificationObserver] above the root [Scrollable] that contains this
/// [SelectableText] to ensure proper scroll coordination for [SelectableText]
/// and its components like [TextSelectionOverlay].
///
/// See also:
///
/// * [Text], which is the non selectable version of this widget.
/// * [TextField], which is the editable version of this widget.
/// * [SelectionArea], which enables the selection of multiple [Text] widgets
/// and of other widgets.
class SelectableText extends StatefulWidget {
/// Creates a selectable text widget.
///
/// If the [style] argument is null, the text will use the style from the
/// closest enclosing [DefaultTextStyle].
///
/// If the [showCursor], [autofocus], [dragStartBehavior],
/// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are
/// specified, the [maxLines] argument must be greater than zero.
const SelectableText(
String this.data, {
super.key,
this.focusNode,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
this.textScaleFactor,
this.textScaler,
this.showCursor = false,
this.autofocus = false,
@Deprecated(
'Use `contextMenuBuilder` instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
this.toolbarOptions,
this.minLines,
this.maxLines,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionColor,
this.selectionHeightStyle,
this.selectionWidthStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollPhysics,
this.scrollBehavior,
this.semanticsLabel,
this.textHeightBehavior,
this.textWidthBasis,
this.onSelectionChanged,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
}) : assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(
textScaler == null || textScaleFactor == null,
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
),
textSpan = null;
/// Creates a selectable text widget with a [TextSpan].
///
/// The [TextSpan.children] attribute of the [textSpan] parameter must only
/// contain [TextSpan]s. Other types of [InlineSpan] are not allowed.
const SelectableText.rich(
TextSpan this.textSpan, {
super.key,
this.focusNode,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
this.textScaleFactor,
this.textScaler,
this.showCursor = false,
this.autofocus = false,
@Deprecated(
'Use `contextMenuBuilder` instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
this.toolbarOptions,
this.minLines,
this.maxLines,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionColor,
this.selectionHeightStyle,
this.selectionWidthStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollPhysics,
this.scrollBehavior,
this.semanticsLabel,
this.textHeightBehavior,
this.textWidthBasis,
this.onSelectionChanged,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
}) : assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(
textScaler == null || textScaleFactor == null,
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
),
data = null;
/// The text to display.
///
/// This will be null if a [textSpan] is provided instead.
final String? data;
/// The text to display as a [TextSpan].
///
/// This will be null if [data] is provided instead.
final TextSpan? textSpan;
/// Defines the focus for this widget.
///
/// Text is only selectable when widget is focused.
///
/// The [focusNode] is a long-lived object that's typically managed by a
/// [StatefulWidget] parent. See [FocusNode] for more information.
///
/// To give the focus to this widget, provide a [focusNode] and then
/// use the current [FocusScope] to request the focus:
///
/// ```dart
/// FocusScope.of(context).requestFocus(myFocusNode);
/// ```
///
/// This happens automatically when the widget is tapped.
///
/// To be notified when the widget gains or loses the focus, add a listener
/// to the [focusNode]:
///
/// ```dart
/// myFocusNode.addListener(() { print(myFocusNode.hasFocus); });
/// ```
///
/// If null, this widget will create its own [FocusNode] with
/// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
/// to be skipped over during focus traversal.
final FocusNode? focusNode;
/// The style to use for the text.
///
/// If null, defaults [DefaultTextStyle] of context.
final TextStyle? style;
/// {@macro flutter.widgets.editableText.strutStyle}
final StrutStyle? strutStyle;
/// {@macro flutter.widgets.editableText.textAlign}
final TextAlign? textAlign;
/// {@macro flutter.widgets.editableText.textDirection}
final TextDirection? textDirection;
/// {@macro flutter.widgets.editableText.textScaleFactor}
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
final double? textScaleFactor;
/// {@macro flutter.painting.textPainter.textScaler}
final TextScaler? textScaler;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autofocus;
/// {@macro flutter.widgets.editableText.minLines}
final int? minLines;
/// {@macro flutter.widgets.editableText.maxLines}
final int? maxLines;
/// {@macro flutter.widgets.editableText.showCursor}
final bool showCursor;
/// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth;
/// {@macro flutter.widgets.editableText.cursorHeight}
final double? cursorHeight;
/// {@macro flutter.widgets.editableText.cursorRadius}
final Radius? cursorRadius;
/// The color of the cursor.
///
/// The cursor indicates the current text insertion point.
///
/// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also
/// null and [ThemeData.platform] is [TargetPlatform.iOS] or
/// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used.
/// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used.
final Color? cursorColor;
/// The color to use when painting the selection.
///
/// If this property is null, this widget gets the selection color from the
/// inherited [DefaultSelectionStyle] (if any); if none, the selection
/// color is derived from the [CupertinoThemeData.primaryColor] on
/// Apple platforms and [ColorScheme.primary] of [ThemeData.colorScheme] on
/// other platforms.
final Color? selectionColor;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
final ui.BoxHeightStyle? selectionHeightStyle;
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
final ui.BoxWidthStyle? selectionWidthStyle;
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
final bool enableInteractiveSelection;
/// {@macro flutter.widgets.editableText.selectionControls}
final TextSelectionControls? selectionControls;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// Configuration of toolbar options.
///
/// Paste and cut will be disabled regardless.
///
/// If not set, select all and copy will be enabled by default.
@Deprecated(
'Use `contextMenuBuilder` instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
final ToolbarOptions? toolbarOptions;
/// {@macro flutter.widgets.editableText.selectionEnabled}
bool get selectionEnabled => enableInteractiveSelection;
/// Called when the user taps on this selectable text.
///
/// The selectable text builds a [GestureDetector] to handle input events like tap,
/// to trigger focus requests, to move the caret, adjust the selection, etc.
/// Handling some of those events by wrapping the selectable text with a competing
/// GestureDetector is problematic.
///
/// To unconditionally handle taps, without interfering with the selectable text's
/// internal gesture detector, provide this callback.
///
/// To be notified when the text field gains or loses the focus, provide a
/// [focusNode] and add a listener to that.
///
/// To listen to arbitrary pointer events without competing with the
/// selectable text's internal gesture detector, use a [Listener].
final GestureTapCallback? onTap;
/// {@macro flutter.widgets.editableText.scrollPhysics}
final ScrollPhysics? scrollPhysics;
/// {@macro flutter.widgets.editableText.scrollBehavior}
final ScrollBehavior? scrollBehavior;
/// {@macro flutter.widgets.Text.semanticsLabel}
final String? semanticsLabel;
/// {@macro dart.ui.textHeightBehavior}
final TextHeightBehavior? textHeightBehavior;
/// {@macro flutter.painting.textPainter.textWidthBasis}
final TextWidthBasis? textWidthBasis;
/// {@macro flutter.widgets.editableText.onSelectionChanged}
final SelectionChangedCallback? onSelectionChanged;
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
final EditableTextContextMenuBuilder? contextMenuBuilder;
static Widget _defaultContextMenuBuilder(
BuildContext context,
EditableTextState editableTextState,
) {
return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState,
);
}
/// The configuration for the magnifier used when the text is selected.
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
/// on Android, and builds nothing on all other platforms. To suppress the
/// magnifier, consider passing [TextMagnifierConfiguration.disabled].
///
/// {@macro flutter.widgets.magnifier.intro}
final TextMagnifierConfiguration? magnifierConfiguration;
@override
State<SelectableText> createState() => _SelectableTextState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(
DiagnosticsProperty<String>('data', data, defaultValue: null),
)
..add(
DiagnosticsProperty<String>(
'semanticsLabel',
semanticsLabel,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<FocusNode>(
'focusNode',
focusNode,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null),
)
..add(
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false),
)
..add(
DiagnosticsProperty<bool>(
'showCursor',
showCursor,
defaultValue: false,
),
)
..add(IntProperty('minLines', minLines, defaultValue: null))
..add(IntProperty('maxLines', maxLines, defaultValue: null))
..add(
EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null),
)
..add(
EnumProperty<TextDirection>(
'textDirection',
textDirection,
defaultValue: null,
),
)
..add(
DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null),
)
..add(
DiagnosticsProperty<TextScaler>(
'textScaler',
textScaler,
defaultValue: null,
),
)
..add(
DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0),
)
..add(
DoubleProperty('cursorHeight', cursorHeight, defaultValue: null),
)
..add(
DiagnosticsProperty<Radius>(
'cursorRadius',
cursorRadius,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<Color>(
'cursorColor',
cursorColor,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<Color>(
'selectionColor',
selectionColor,
defaultValue: null,
),
)
..add(
FlagProperty(
'selectionEnabled',
value: selectionEnabled,
defaultValue: true,
ifFalse: 'selection disabled',
),
)
..add(
DiagnosticsProperty<TextSelectionControls>(
'selectionControls',
selectionControls,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<ScrollPhysics>(
'scrollPhysics',
scrollPhysics,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<ScrollBehavior>(
'scrollBehavior',
scrollBehavior,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<TextHeightBehavior>(
'textHeightBehavior',
textHeightBehavior,
defaultValue: null,
),
);
}
}
class _SelectableTextState extends State<SelectableText>
implements TextSelectionGestureDetectorBuilderDelegate {
EditableTextState? get _editableText => editableTextKey.currentState;
late _TextSpanEditingController _controller;
FocusNode? _focusNode;
FocusNode get _effectiveFocusNode =>
widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true));
bool _showSelectionHandles = false;
late _SelectableTextSelectionGestureDetectorBuilder
_selectionGestureDetectorBuilder;
// API for TextSelectionGestureDetectorBuilderDelegate.
@override
late bool forcePressEnabled;
@override
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
@override
bool get selectionEnabled => widget.selectionEnabled;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
@override
void initState() {
super.initState();
_selectionGestureDetectorBuilder =
_SelectableTextSelectionGestureDetectorBuilder(state: this);
_controller = _TextSpanEditingController(
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
);
_controller.addListener(_onControllerChanged);
_effectiveFocusNode.addListener(_handleFocusChanged);
}
@override
void didUpdateWidget(SelectableText oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data ||
widget.textSpan != oldWidget.textSpan) {
_controller.removeListener(_onControllerChanged);
_controller.dispose();
_controller = _TextSpanEditingController(
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
);
_controller.addListener(_onControllerChanged);
}
if (widget.focusNode != oldWidget.focusNode) {
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
(widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
}
if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
_showSelectionHandles = false;
} else {
_showSelectionHandles = true;
}
}
@override
void dispose() {
_effectiveFocusNode.removeListener(_handleFocusChanged);
_focusNode?.dispose();
_controller.dispose();
super.dispose();
}
void _onControllerChanged() {
final bool showSelectionHandles =
!_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed;
if (showSelectionHandles == _showSelectionHandles) {
return;
}
setState(() {
_showSelectionHandles = showSelectionHandles;
});
}
void _handleFocusChanged() {
if (!_effectiveFocusNode.hasFocus &&
SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) {
// We should only clear the selection when this SelectableText loses
// focus while the application is currently running. It is possible
// that the application is not currently running, for example on desktop
// platforms, clicking on a different window switches the focus to
// the new window causing the Flutter application to go inactive. In this
// case we want to retain the selection so it remains when we return to
// the Flutter application.
_controller.value = TextEditingValue(text: _controller.value.text);
}
}
void _handleSelectionChanged(
TextSelection selection,
SelectionChangedCause? cause,
) {
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
if (willShowSelectionHandles != _showSelectionHandles) {
setState(() {
_showSelectionHandles = willShowSelectionHandles;
});
}
widget.onSelectionChanged?.call(selection, cause);
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
if (cause == SelectionChangedCause.longPress) {
_editableText?.bringIntoView(selection.base);
}
return;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Do nothing.
}
}
/// Toggle the toolbar when a selection handle is tapped.
void _handleSelectionHandleTapped() {
if (_controller.selection.isCollapsed) {
_editableText!.toggleToolbar();
}
}
bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
return false;
}
if (_controller.selection.isCollapsed) {
return false;
}
if (cause == SelectionChangedCause.keyboard) {
return false;
}
if (cause == SelectionChangedCause.longPress) {
return true;
}
if (_controller.text.isNotEmpty) {
return true;
}
return false;
}
@override
Widget build(BuildContext context) {
// TODO(garyq): Assert to block WidgetSpans from being used here are removed,
// but we still do not yet have nice handling of things like carets, clipboard,
// and other features. We should add proper support. Currently, caret handling
// is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010
// should be landed in SkParagraph after the switch is complete.
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasDirectionality(context));
assert(
!(widget.style != null &&
!widget.style!.inherit &&
(widget.style!.fontSize == null ||
widget.style!.textBaseline == null)),
'inherit false style must supply fontSize and textBaseline',
);
final ThemeData theme = Theme.of(context);
final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(
context,
);
final FocusNode focusNode = _effectiveFocusNode;
TextSelectionControls? textSelectionControls = widget.selectionControls;
final bool paintCursorAboveText;
final bool cursorOpacityAnimates;
Offset? cursorOffset;
final Color cursorColor;
final Color selectionColor;
Radius? cursorRadius = widget.cursorRadius;
switch (theme.platform) {
case TargetPlatform.iOS:
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
forcePressEnabled = true;
textSelectionControls ??= cupertinoTextSelectionHandleControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
cupertinoTheme.primaryColor;
selectionColor =
selectionStyle.selectionColor ??
cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
0,
);
case TargetPlatform.macOS:
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
forcePressEnabled = false;
textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
cupertinoTheme.primaryColor;
selectionColor =
selectionStyle.selectionColor ??
cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
0,
);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
forcePressEnabled = false;
textSelectionControls ??= materialTextSelectionHandleControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
theme.colorScheme.primary;
selectionColor =
selectionStyle.selectionColor ??
theme.colorScheme.primary.withOpacity(0.40);
case TargetPlatform.linux:
case TargetPlatform.windows:
forcePressEnabled = false;
textSelectionControls ??= desktopTextSelectionHandleControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
theme.colorScheme.primary;
selectionColor =
selectionStyle.selectionColor ??
theme.colorScheme.primary.withOpacity(0.40);
}
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
TextStyle? effectiveTextStyle = widget.style;
if (effectiveTextStyle == null || effectiveTextStyle.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(
widget.style ?? _controller._textSpan.style,
);
}
final TextScaler? effectiveScaler =
widget.textScaler ??
switch (widget.textScaleFactor) {
null => null,
final double textScaleFactor => TextScaler.linear(textScaleFactor),
};
final Widget child = RepaintBoundary(
child: EditableText(
key: editableTextKey,
style: effectiveTextStyle,
readOnly: true,
toolbarOptions: widget.toolbarOptions,
textWidthBasis:
widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
textHeightBehavior:
widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
showSelectionHandles: _showSelectionHandles,
showCursor: widget.showCursor,
controller: _controller,
focusNode: focusNode,
strutStyle: widget.strutStyle ?? const StrutStyle(),
textAlign:
widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
textDirection: widget.textDirection,
textScaler: effectiveScaler,
autofocus: widget.autofocus,
forceLine: false,
minLines: widget.minLines,
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
selectionColor: widget.selectionColor ?? selectionColor,
selectionControls: widget.selectionEnabled
? textSelectionControls
: null,
onSelectionChanged: _handleSelectionChanged,
onSelectionHandleTapped: _handleSelectionHandleTapped,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
cursorOpacityAnimates: cursorOpacityAnimates,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,
backgroundCursorColor: CupertinoColors.inactiveGray,
enableInteractiveSelection: widget.enableInteractiveSelection,
magnifierConfiguration:
widget.magnifierConfiguration ??
TextMagnifier.adaptiveMagnifierConfiguration,
dragStartBehavior: widget.dragStartBehavior,
scrollPhysics: widget.scrollPhysics,
scrollBehavior: widget.scrollBehavior,
autofillHints: null,
contextMenuBuilder: widget.contextMenuBuilder,
),
);
return Semantics(
label: widget.semanticsLabel,
excludeSemantics: widget.semanticsLabel != null,
onLongPress: () {
_effectiveFocusNode.requestFocus();
},
child: _selectionGestureDetectorBuilder.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: child,
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
import 'package:PiliPlus/common/widgets/flutter/text_intro/selectable_text.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/material.dart' hide SelectableText;
Widget selectableRichText(
TextSpan textSpan, {
TextStyle? style,
}) {
if (PlatformUtils.isDesktop) {
return SelectionArea(
child: Text.rich(
style: style,
textSpan,
),
);
}
return SelectableText.rich(
style: style,
textSpan,
scrollPhysics: const NeverScrollableScrollPhysics(),
);
}

View File

@@ -0,0 +1,419 @@
// 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:PiliPlus/common/widgets/flutter/text_intro/tap_and_drag.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'
hide BaseTapAndDragGestureRecognizer, TapAndHorizontalDragGestureRecognizer;
import 'package:flutter/material.dart';
class CustomTextSelectionGestureDetectorBuilder
extends TextSelectionGestureDetectorBuilder {
CustomTextSelectionGestureDetectorBuilder({required super.delegate});
@override
Widget buildGestureDetector({
Key? key,
HitTestBehavior? behavior,
required Widget child,
}) {
return TextSelectionGestureDetector(
key: key,
onTapTrackStart: onTapTrackStart,
onTapTrackReset: onTapTrackReset,
onTapDown: onTapDown,
onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
onSingleTapUp: onSingleTapUp,
onSingleTapCancel: onSingleTapCancel,
onUserTap: onUserTap,
onSingleLongTapStart: onSingleLongTapStart,
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd,
onSingleLongTapCancel: onSingleLongTapCancel,
onDoubleTapDown: onDoubleTapDown,
onTripleTapDown: onTripleTapDown,
onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd,
onUserTapAlwaysCalled: onUserTapAlwaysCalled,
behavior: behavior,
child: child,
);
}
}
/// A gesture detector to respond to non-exclusive event chains for a text field.
///
/// An ordinary [GestureDetector] configured to handle events like tap and
/// double tap will only recognize one or the other. This widget detects both:
/// the first tap and then any subsequent taps that occurs within a time limit
/// after the first.
///
/// See also:
///
/// * [TextField], a Material text field which uses this gesture detector.
/// * [CupertinoTextField], a Cupertino text field which uses this gesture
/// detector.
class TextSelectionGestureDetector extends StatefulWidget {
/// Create a [TextSelectionGestureDetector].
///
/// Multiple callbacks can be called for one sequence of input gesture.
const TextSelectionGestureDetector({
super.key,
this.onTapTrackStart,
this.onTapTrackReset,
this.onTapDown,
this.onForcePressStart,
this.onForcePressEnd,
this.onSecondaryTap,
this.onSecondaryTapDown,
this.onSingleTapUp,
this.onSingleTapCancel,
this.onUserTap,
this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.onSingleLongTapCancel,
this.onDoubleTapDown,
this.onTripleTapDown,
this.onDragSelectionStart,
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
this.onUserTapAlwaysCalled = false,
this.behavior,
required this.child,
});
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart}
/// Callback used to indicate that a tap tracking has started upon
/// a [PointerDownEvent].
/// {@endtemplate}
final VoidCallback? onTapTrackStart;
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset}
/// Callback used to indicate that a tap tracking has been reset which
/// happens on the next [PointerDownEvent] after the timer between two taps
/// elapses, the recognizer loses the arena, the gesture is cancelled or
/// the recognizer is disposed of.
/// {@endtemplate}
final VoidCallback? onTapTrackReset;
/// Called for every tap down including every tap down that's part of a
/// double click or a long press, except touches that include enough movement
/// to not qualify as taps (e.g. pans and flings).
final GestureTapDragDownCallback? onTapDown;
/// Called when a pointer has tapped down and the force of the pointer has
/// just become greater than [ForcePressGestureRecognizer.startPressure].
final GestureForcePressStartCallback? onForcePressStart;
/// Called when a pointer that had previously triggered [onForcePressStart] is
/// lifted off the screen.
final GestureForcePressEndCallback? onForcePressEnd;
/// Called for a tap event with the secondary mouse button.
final GestureTapCallback? onSecondaryTap;
/// Called for a tap down event with the secondary mouse button.
final GestureTapDownCallback? onSecondaryTapDown;
/// Called for the first tap in a series of taps, consecutive taps do not call
/// this method.
///
/// For example, if the detector was configured with [onTapDown] and
/// [onDoubleTapDown], three quick taps would be recognized as a single tap
/// down, followed by a tap up, then a double tap down, followed by a single tap down.
final GestureTapDragUpCallback? onSingleTapUp;
/// Called for each touch that becomes recognized as a gesture that is not a
/// short tap, such as a long tap or drag. It is called at the moment when
/// another gesture from the touch is recognized.
final GestureCancelCallback? onSingleTapCancel;
/// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
/// disabled, which is the default behavior.
///
/// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
/// including consecutive taps.
final GestureTapCallback? onUserTap;
/// Called for a single long tap that's sustained for longer than
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
/// double-tap-hold, which calls [onDoubleTapDown] instead.
final GestureLongPressStartCallback? onSingleLongTapStart;
/// Called after [onSingleLongTapStart] when the pointer is dragged.
final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate;
/// Called after [onSingleLongTapStart] when the pointer is lifted.
final GestureLongPressEndCallback? onSingleLongTapEnd;
/// Called after [onSingleLongTapStart] when the pointer is canceled.
final GestureLongPressCancelCallback? onSingleLongTapCancel;
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous short tap.
final GestureTapDragDownCallback? onDoubleTapDown;
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous double-tap.
final GestureTapDragDownCallback? onTripleTapDown;
/// Called when a mouse starts dragging to select text.
final GestureTapDragStartCallback? onDragSelectionStart;
/// Called repeatedly as a mouse moves while dragging.
final GestureTapDragUpdateCallback? onDragSelectionUpdate;
/// Called when a mouse that was previously dragging is released.
final GestureTapDragEndCallback? onDragSelectionEnd;
/// Whether [onUserTap] will be called for all taps including consecutive taps.
///
/// Defaults to false, so [onUserTap] is only called for each distinct tap.
final bool onUserTapAlwaysCalled;
/// How this gesture detector should behave during hit testing.
///
/// This defaults to [HitTestBehavior.deferToChild].
final HitTestBehavior? behavior;
/// Child below this widget.
final Widget child;
@override
State<StatefulWidget> createState() => _TextSelectionGestureDetectorState();
}
class _TextSelectionGestureDetectorState
extends State<TextSelectionGestureDetector> {
// Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
// which can grow to be infinitely large, to a value between 1 and 3. The value
// that the raw count is converted to is based on the default observed behavior
// on the native platforms.
//
// This method should be used in all instances when details.consecutiveTapCount
// would be used.
static int _getEffectiveConsecutiveTapCount(int rawCount) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
// From observation, these platform's reset their tap count to 0 when
// the number of consecutive taps exceeds 3. For example on Debian Linux
// with GTK, when going past a triple click, on the fourth click the
// selection is moved to the precise click position, on the fifth click
// the word at the position is selected, and on the sixth click the
// paragraph at the position is selected.
return rawCount <= 3
? rawCount
: (rawCount % 3 == 0 ? 3 : rawCount % 3);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// From observation, these platform's either hold their tap count at 3.
// For example on macOS, when going past a triple click, the selection
// should be retained at the paragraph that was first selected on triple
// click.
return math.min(rawCount, 3);
case TargetPlatform.windows:
// From observation, this platform's consecutive tap actions alternate
// between double click and triple click actions. For example, after a
// triple click has selected a paragraph, on the next click the word at
// the clicked position will be selected, and on the next click the
// paragraph at the position is selected.
return rawCount < 2 ? rawCount : 2 + rawCount % 2;
}
}
void _handleTapTrackStart() {
widget.onTapTrackStart?.call();
}
void _handleTapTrackReset() {
widget.onTapTrackReset?.call();
}
// The down handler is force-run on success of a single tap and optimistically
// run before a long press success.
void _handleTapDown(TapDragDownDetails details) {
widget.onTapDown?.call(details);
// This isn't detected as a double tap gesture in the gesture recognizer
// because it's 2 single taps, each of which may do different things depending
// on whether it's a single tap, the first tap of a double tap, the second
// tap held down, a clean double tap etc.
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
return widget.onDoubleTapDown?.call(details);
}
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) {
return widget.onTripleTapDown?.call(details);
}
}
void _handleTapUp(TapDragUpDetails details) {
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
widget.onSingleTapUp?.call(details);
widget.onUserTap?.call();
} else if (widget.onUserTapAlwaysCalled) {
widget.onUserTap?.call();
}
}
void _handleTapCancel() {
widget.onSingleTapCancel?.call();
}
void _handleDragStart(TapDragStartDetails details) {
widget.onDragSelectionStart?.call(details);
}
void _handleDragUpdate(TapDragUpdateDetails details) {
widget.onDragSelectionUpdate?.call(details);
}
void _handleDragEnd(TapDragEndDetails details) {
widget.onDragSelectionEnd?.call(details);
}
void _forcePressStarted(ForcePressDetails details) {
widget.onForcePressStart?.call(details);
}
void _forcePressEnded(ForcePressDetails details) {
widget.onForcePressEnd?.call(details);
}
void _handleLongPressStart(LongPressStartDetails details) {
widget.onSingleLongTapStart?.call(details);
}
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
widget.onSingleLongTapMoveUpdate?.call(details);
}
void _handleLongPressEnd(LongPressEndDetails details) {
widget.onSingleLongTapEnd?.call(details);
}
void _handleLongPressCancel() {
widget.onSingleLongTapCancel?.call();
}
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures =
<Type, GestureRecognizerFactory>{};
gestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onSecondaryTap = widget.onSecondaryTap
..onSecondaryTapDown = widget.onSecondaryTapDown;
},
);
if (widget.onSingleLongTapStart != null ||
widget.onSingleLongTapMoveUpdate != null ||
widget.onSingleLongTapEnd != null ||
widget.onSingleLongTapCancel != null) {
gestures[LongPressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
debugOwner: this,
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.touch},
),
(LongPressGestureRecognizer instance) {
instance
..onLongPressStart = _handleLongPressStart
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
..onLongPressEnd = _handleLongPressEnd
..onLongPressCancel = _handleLongPressCancel;
},
);
}
if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
gestures[TapAndHorizontalDragGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<
TapAndHorizontalDragGestureRecognizer
>(
() => TapAndHorizontalDragGestureRecognizer(debugOwner: this),
(TapAndHorizontalDragGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..eagerVictoryOnDrag =
defaultTargetPlatform != TargetPlatform.iOS
..onTapTrackStart = _handleTapTrackStart
..onTapTrackReset = _handleTapTrackReset
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
gestures[TapAndPanGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
() => TapAndPanGestureRecognizer(debugOwner: this),
(TapAndPanGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..onTapTrackStart = _handleTapTrackStart
..onTapTrackReset = _handleTapTrackReset
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
}
}
if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
gestures[ForcePressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) {
instance
..onStart = widget.onForcePressStart != null
? _forcePressStarted
: null
..onEnd = widget.onForcePressEnd != null
? _forcePressEnded
: null;
},
);
}
return RawGestureDetector(
gestures: gestures,
excludeFromSemantics: true,
behavior: widget.behavior,
child: widget.child,
);
}
}

View File

@@ -1,4 +1,6 @@
enum SkipType {
import 'package:PiliPlus/models/common/enum_with_label.dart';
enum SkipType implements EnumWithLabel {
alwaysSkip('总是跳过'),
skipOnce('跳过一次'),
skipManually('手动跳过'),
@@ -6,6 +8,7 @@ enum SkipType {
disable('禁用')
;
final String title;
const SkipType(this.title);
@override
final String label;
const SkipType(this.label);
}

View File

@@ -40,12 +40,12 @@ mixin CommonSlideMixin<T extends CommonSlidePage> on State<T>, TickerProvider {
final isRTL = dx >= _maxWidth - offset;
if (isLTR || isRTL) {
_isRTL = isRTL;
_downDx = dx;
return true;
}
return false;
},
)
..onStart = _onDragStart
..onUpdate = _onDragUpdate
..onEnd = _onDragEnd
..onCancel = _onDragEnd;
@@ -91,6 +91,7 @@ mixin CommonSlideMixin<T extends CommonSlidePage> on State<T>, TickerProvider {
Widget buildList(ThemeData theme) => throw UnimplementedError();
void _onDragEnd([_]) {
if (_downDx == null) return;
final dx = _downDx!;
if (_animController.value * _maxWidth + (_isRTL ? (_maxWidth - dx) : dx) >=
100) {
@@ -101,6 +102,10 @@ mixin CommonSlideMixin<T extends CommonSlidePage> on State<T>, TickerProvider {
_downDx = null;
}
void _onDragStart(DragStartDetails details) {
_downDx = details.localPosition.dx;
}
void _onDragUpdate(DragUpdateDetails details) {
final from = _downDx!;
final to = details.localPosition.dx;

View File

@@ -134,54 +134,12 @@ List<SettingsModel> get extraSettings => [
],
),
),
NormalModel(
leading: const Icon(MdiIcons.debugStepOver),
getPopupMenuModel(
title: '番剧片头/片尾跳过类型',
getTrailing: () => Builder(
builder: (context) {
final pgcSkipType = Pref.pgcSkipType;
final colorScheme = ColorScheme.of(context);
final color = pgcSkipType == SkipType.disable
? colorScheme.outline
: colorScheme.secondary;
return PopupMenuButton<SkipType>(
initialValue: pgcSkipType,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text.rich(
style: TextStyle(fontSize: 14, height: 1, color: color),
strutStyle: const StrutStyle(
leading: 0,
height: 1,
fontSize: 14,
),
TextSpan(
children: [
TextSpan(text: pgcSkipType.title),
WidgetSpan(
alignment: .middle,
child: Icon(
MdiIcons.unfoldMoreHorizontal,
size: 14,
color: color,
),
),
],
),
),
),
onSelected: (value) async {
await GStorage.setting.put(SettingBoxKey.pgcSkipType, value.index);
if (context.mounted) {
(context as Element).markNeedsBuild();
}
},
itemBuilder: (context) => SkipType.values
.map((e) => PopupMenuItem(value: e, child: Text(e.title)))
.toList(),
);
},
),
leading: const Icon(MdiIcons.debugStepOver),
key: SettingBoxKey.pgcSkipType,
values: SkipType.values,
defaultIndex: SkipType.skipOnce.index,
),
SwitchModel(
title: '检查未读动态',

View File

@@ -1,4 +1,5 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/models/common/enum_with_label.dart';
import 'package:PiliPlus/pages/setting/widgets/normal_item.dart';
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/switch_item.dart';
@@ -7,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
@immutable
sealed class SettingsModel {
@@ -258,3 +260,61 @@ SettingsModel getVideoFilterSelectModel({
},
);
}
SettingsModel getPopupMenuModel({
required String title,
Widget? leading,
String? subtitle,
required String key,
required List<EnumWithLabel> values,
int defaultIndex = 0,
}) {
// final globalKey = GlobalKey<PopupMenuButtonState<EnumWithLabel>>();
return NormalModel(
title: title,
subtitle: subtitle,
leading: leading,
// onTap: (context, setState) => globalKey.currentState?.showButtonMenu(),
getTrailing: () => Builder(
builder: (context) {
final color = ColorScheme.of(context).secondary;
final v = values[GStorage.setting.get(key, defaultValue: defaultIndex)];
return PopupMenuButton(
// key: globalKey,
padding: .zero,
initialValue: v,
onSelected: (value) async {
await GStorage.setting.put(key, value.index);
if (context.mounted) {
(context as Element).markNeedsBuild();
}
},
itemBuilder: (context) => values
.map((i) => PopupMenuItem(value: i, child: Text(i.label)))
.toList(),
child: Padding(
padding: const .symmetric(vertical: 8),
child: Text.rich(
style: TextStyle(fontSize: 14, height: 1, color: color),
strutStyle: const StrutStyle(leading: 0, height: 1, fontSize: 14),
TextSpan(
children: [
TextSpan(text: v.label),
WidgetSpan(
alignment: .middle,
child: Icon(
size: 14,
MdiIcons.unfoldMoreHorizontal,
color: color,
),
),
],
style: TextStyle(color: color),
),
),
),
);
},
),
);
}

View File

@@ -8,11 +8,13 @@ import 'package:PiliPlus/models/common/video/video_quality.dart';
import 'package:PiliPlus/pages/setting/models/model.dart';
import 'package:PiliPlus/pages/setting/widgets/ordered_multi_select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/plugin/pl_player/models/audio_output_type.dart';
import 'package:PiliPlus/plugin/pl_player/models/hwdec_type.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/video_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
@@ -317,13 +319,32 @@ List<SettingsModel> get videoSettings => [
}
},
),
if (Platform.isAndroid)
const SwitchModel(
title: '优先使用 OpenSL ES 输出音频',
leading: Icon(Icons.speaker_outlined),
subtitle: '关闭则优先使用AAudio输出音频此项即mpv的--ao若遇系统音效丢失、无声、音画不同步等问题请尝试打开。',
setKey: SettingBoxKey.useOpenSLES,
defaultVal: false,
if (kDebugMode || Platform.isAndroid)
NormalModel(
title: '音频输出设备',
leading: const Icon(Icons.speaker_outlined),
getSubtitle: () => '当前:${Pref.audioOutput}',
onTap: (context, setState) async {
final result = await showDialog<List<String>>(
context: context,
builder: (context) {
return OrderedMultiSelectDialog<String>(
title: '音频输出设备',
initValues: Pref.audioOutput.split(','),
values: {
for (final e in AudioOutput.values) e.name: e.label,
},
);
},
);
if (result != null && result.isNotEmpty) {
await GStorage.setting.put(
SettingBoxKey.audioOutput,
result.join(','),
);
setState();
}
},
),
const SwitchModel(
title: '扩大缓冲区',

View File

@@ -594,7 +594,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
.map(
(item) => PopupMenuItem<SkipType>(
value: item,
child: Text(item.title),
child: Text(item.label),
),
)
.toList(),
@@ -617,7 +617,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
),
TextSpan(
children: [
TextSpan(text: item.second.title),
TextSpan(text: item.second.label),
WidgetSpan(
alignment: .middle,
child: Icon(

View File

@@ -132,6 +132,10 @@ class VideoDetailController extends GetxController
String? audioUrl;
Duration? defaultST;
Duration? playedTime;
String get playedTimePos {
final pos = playedTime?.inMilliseconds;
return pos == null || pos == 0 ? '' : '?t=${pos / 1000}';
}
// 亮度
double? brightness;
@@ -659,7 +663,7 @@ class VideoDetailController extends GetxController
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.skipType.title,
item.skipType.label,
style: const TextStyle(fontSize: 13),
),
if (item.segment.second != 0)

View File

@@ -143,7 +143,8 @@ class PgcIntroController extends CommonIntroController {
showDialog(
context: context,
builder: (_) {
String videoUrl = '${HttpString.baseUrl}/bangumi/play/ep$epId';
String videoUrl =
'${HttpString.baseUrl}/bangumi/play/ep$epId${videoDetailCtr.playedTimePos}';
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),

View File

@@ -302,7 +302,8 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
context: context,
builder: (_) {
final videoDetail = this.videoDetail.value;
String videoUrl = '${HttpString.baseUrl}/video/$bvid';
String videoUrl =
'${HttpString.baseUrl}/video/$bvid${videoDetailCtr.playedTimePos}';
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/flutter/text_intro/text.dart';
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
@@ -19,7 +20,6 @@ import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/page.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/season.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/selectable_text.dart';
import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/date_utils.dart';
import 'package:PiliPlus/utils/duration_utils.dart';

View File

@@ -19,22 +19,3 @@ Widget selectableText(
scrollPhysics: const NeverScrollableScrollPhysics(),
);
}
Widget selectableRichText(
TextSpan textSpan, {
TextStyle? style,
}) {
if (PlatformUtils.isDesktop) {
return SelectionArea(
child: Text.rich(
style: style,
textSpan,
),
);
}
return SelectableText.rich(
style: style,
textSpan,
scrollPhysics: const NeverScrollableScrollPhysics(),
);
}

View File

@@ -78,7 +78,6 @@ class ReplyItemGrpc extends StatelessWidget {
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final isMobile = PlatformUtils.isMobile;
void showMore() => showModalBottomSheet(
context: context,
useSafeArea: true,
@@ -101,63 +100,63 @@ class ReplyItemGrpc extends StatelessWidget {
child: InkWell(
onTap: () => replyReply?.call(replyItem, null),
onLongPress: showMore,
onSecondaryTap: isMobile ? null : showMore,
onSecondaryTap: PlatformUtils.isMobile ? null : showMore,
child: _buildContent(context, theme),
),
);
}
Widget _buildAuthorPanel(BuildContext context, ThemeData theme) => Padding(
padding: const EdgeInsets.fromLTRB(12, 14, 8, 5),
child: content(context, theme),
);
Widget _buildContent(BuildContext context, ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (PendantAvatar.showDynDecorate &&
replyItem.member.hasGarbCardImage())
Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: 8,
right: 12,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.centerRight,
children: [
CachedNetworkImage(
height: 38,
memCacheHeight: 38.cacheSize(context),
imageUrl: ImageUtils.safeThumbnailUrl(
replyItem.member.garbCardImage,
),
placeholder: (_, _) => const SizedBox.shrink(),
),
if (replyItem.member.hasGarbCardNumber())
Text(
'NO.\n${replyItem.member.garbCardNumber}',
style: TextStyle(
fontSize: 8,
fontFamily: 'digital_id_num',
color:
replyItem.member.garbCardFanColor.startsWith('#')
? Utils.parseColor(
replyItem.member.garbCardFanColor,
)
: null,
),
),
],
Widget child = Padding(
padding: const .fromLTRB(12, 14, 8, 5),
child: content(context, theme),
);
const double top = 8.0;
const double right = 12.0;
const double height = 38.0;
if (PendantAvatar.showDynDecorate && replyItem.member.hasGarbCardImage()) {
child = Stack(
clipBehavior: .none,
children: [
child,
Positioned(
top: top,
right: right,
height: height,
child: CachedNetworkImage(
height: height,
memCacheHeight: height.cacheSize(context),
imageUrl: ImageUtils.safeThumbnailUrl(
replyItem.member.garbCardImage,
),
placeholder: (_, _) => const SizedBox.shrink(),
),
),
if (replyItem.member.hasGarbCardNumber())
Positioned(
top: top,
right: right,
height: height,
child: Center(
child: Text(
'NO.\n${replyItem.member.garbCardNumber}',
style: TextStyle(
fontSize: 8,
fontFamily: 'digital_id_num',
color: replyItem.member.garbCardFanColor.startsWith('#')
? Utils.parseColor(replyItem.member.garbCardFanColor)
: null,
),
),
),
_buildAuthorPanel(context, theme),
],
)
else
_buildAuthorPanel(context, theme),
),
],
);
}
return Column(
crossAxisAlignment: .stretch,
children: [
child,
if (needDivider)
Divider(
indent: 55,
@@ -169,25 +168,11 @@ class ReplyItemGrpc extends StatelessWidget {
);
}
Widget _buildAvatar() => PendantAvatar(
avatar: replyItem.member.face,
size: 34,
badgeSize: 14,
isVip: replyItem.member.vipStatus > 0,
officialType: replyItem.member.officialVerifyType.toInt(),
garbPendantImage: replyItem.member.hasGarbPendantImage()
? replyItem.member.garbPendantImage
: null,
);
Widget content(BuildContext context, ThemeData theme) {
final padding = EdgeInsets.only(
left: replyLevel == 0 ? 6 : 45,
right: 6,
);
final padding = EdgeInsets.only(left: replyLevel == 0 ? 6 : 45, right: 6);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
@@ -196,11 +181,20 @@ class ReplyItemGrpc extends StatelessWidget {
Get.toNamed('/member?mid=${replyItem.mid}');
},
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: .min,
crossAxisAlignment: .center,
spacing: 12,
children: [
_buildAvatar(),
PendantAvatar(
avatar: replyItem.member.face,
size: 34,
badgeSize: 14,
isVip: replyItem.member.vipStatus > 0,
officialType: replyItem.member.officialVerifyType.toInt(),
garbPendantImage: replyItem.member.hasGarbPendantImage()
? replyItem.member.garbPendantImage
: null,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,

View File

@@ -1344,13 +1344,13 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
required double height,
bool isPipMode = false,
}) => PopScope(
key: videoDetailController.videoPlayerKey,
canPop:
!isFullScreen &&
!videoDetailController.plPlayerController.isDesktopPip &&
(videoDetailController.horizontalScreen || isPortrait),
onPopInvokedWithResult: _onPopInvokedWithResult,
child: Obx(
key: videoDetailController.videoPlayerKey,
() =>
videoDetailController.videoState.value is! Success ||
!videoDetailController.autoPlay.value ||

View File

@@ -796,8 +796,7 @@ class PlPlayerController {
await pp.setProperty("af", "scaletempo2=max-speed=8");
if (Platform.isAndroid) {
await pp.setProperty("volume-max", "100");
final ao = Pref.useOpenSLES ? "opensles,aaudio" : "aaudio,opensles";
await pp.setProperty("ao", ao);
await pp.setProperty("ao", Pref.audioOutput);
}
// video-sync=display-resample
await pp.setProperty("video-sync", Pref.videoSync);

View File

@@ -0,0 +1,14 @@
import 'package:PiliPlus/models/common/enum_with_label.dart';
enum AudioOutput implements EnumWithLabel {
opensles('OpenSL ES'),
aaudio('AAudio'),
audiotrack('AudioTrack')
;
static final defaultValue = values.map((e) => e.name).join(',');
@override
final String label;
const AudioOutput(this.label);
}

View File

@@ -41,6 +41,18 @@ abstract final class PiliScheme {
listener = appLinks.uriLinkStream.listen(routePush);
}
static int? _videoProgress(Map<String, String> queryParameters) {
if ((queryParameters['start_progress'] ?? queryParameters['dm_progress'])
case final p?) {
return int.tryParse(p);
} else if (queryParameters['t'] case final t0?) {
if (double.tryParse(t0) case final t1?) {
return (t1 * 1000).toInt();
}
}
return null;
}
static Future<bool> routePushFromUrl(
String url, {
bool selfHandle = false,
@@ -99,7 +111,7 @@ abstract final class PiliScheme {
PageUtils.viewPgc(
seasonId: isEp ? null : id,
epId: isEp ? id : null,
progress: uri.queryParameters['start_progress'],
progress: _videoProgress(uri.queryParameters),
);
return true;
}
@@ -121,7 +133,6 @@ abstract final class PiliScheme {
// bilibili://video/{aid}/?comment_root_id=***&comment_secondary_id=***
final queryParameters = uri.queryParameters;
if (queryParameters['comment_root_id'] != null) {
// to check
// to video reply
String? oid = uriDigitRegExp.firstMatch(path)?.group(1);
int? rpid = int.tryParse(queryParameters['comment_root_id']!);
@@ -146,11 +157,10 @@ abstract final class PiliScheme {
final cid = queryParameters['cid'];
if (cid != null) {
bvid ??= IdUtils.av2bv(int.parse(aid!));
final progress = queryParameters['dm_progress'];
PageUtils.toVideoPage(
bvid: bvid,
cid: int.parse(cid),
progress: progress == null ? null : int.parse(progress),
progress: _videoProgress(queryParameters),
off: off,
);
} else {
@@ -158,7 +168,7 @@ abstract final class PiliScheme {
aid != null ? int.parse(aid) : null,
bvid,
off: off,
progress: queryParameters['dm_progress'],
progress: _videoProgress(queryParameters),
);
}
return true;
@@ -629,12 +639,9 @@ abstract final class PiliScheme {
case 'bangumi':
// www.bilibili.com/bangumi/play/ep{eid}?start_progress={offset}&thumb_up_dm_id={dmid}
// if (kDebugMode) debugPrint('番剧');
final queryParameters = uri.queryParameters;
bool hasMatch = PageUtils.viewPgcFromUri(
path,
progress:
queryParameters['start_progress'] ??
queryParameters['dm_progress'],
progress: _videoProgress(uri.queryParameters),
);
if (hasMatch) {
return true;
@@ -662,7 +669,7 @@ abstract final class PiliScheme {
res.av,
res.bv,
off: off,
progress: queryParameters['dm_progress'],
progress: _videoProgress(queryParameters),
part: part,
);
return true;
@@ -850,7 +857,7 @@ abstract final class PiliScheme {
String? bvid, {
bool showDialog = true,
bool off = false,
String? progress,
int? progress, // milliseconds
String? part,
}) async {
try {
@@ -872,7 +879,7 @@ abstract final class PiliScheme {
aid: aid,
bvid: bvid,
cid: cid,
progress: progress == null ? null : int.parse(progress),
progress: progress,
off: off,
);
}

View File

@@ -736,7 +736,7 @@ abstract final class PageUtils {
int? pgcType,
String? cover,
String? title,
int? progress,
int? progress, // milliseconds
Map? extraArguments,
bool off = false,
}) {
@@ -773,7 +773,7 @@ abstract final class PageUtils {
static bool viewPgcFromUri(
String uri, {
bool isPgc = true,
String? progress,
int? progress, // milliseconds
int? aid,
}) {
RegExpMatch? match = _pgcRegex.firstMatch(uri);
@@ -817,7 +817,7 @@ abstract final class PageUtils {
static Future<void> viewPgc({
dynamic seasonId,
dynamic epId,
String? progress,
int? progress, // milliseconds
}) async {
try {
SmartDialog.showLoading(msg: '资源获取中');
@@ -837,7 +837,7 @@ abstract final class PageUtils {
seasonId: response.seasonId,
epId: episode.epId,
cover: episode.cover,
progress: progress == null ? null : int.tryParse(progress),
progress: progress,
extraArguments: {
'pgcApi': true,
'pgcItem': response,
@@ -886,7 +886,7 @@ abstract final class PageUtils {
epId: episode.epId,
pgcType: response.type,
cover: episode.cover,
progress: progress == null ? null : int.tryParse(progress),
progress: progress,
extraArguments: {
'pgcItem': response,
},

View File

@@ -98,46 +98,47 @@ abstract final class ReplyUtils {
await Future.delayed(const Duration(seconds: 8));
}
void showReplyCheckResult(String message, {bool isBan = false}) {
final actions = [
if (isBan)
TextButton(
onPressed: () {
Get.back();
String? uri;
switch (type) {
case 1:
uri = IdUtils.av2bv(oid);
case 17:
uri = 'https://www.bilibili.com/opus/$oid';
}
if (uri != null) {
Utils.copyText(uri);
}
Get.toNamed(
'/webview',
parameters: {
'url':
'https://www.bilibili.com/h5/comment/appeal?${Utils.themeUrl(Get.isDarkMode)}',
},
);
},
child: const Text('申诉'),
),
if (!isManual)
TextButton(
onPressed: Get.back,
child: Text(
'关闭',
style: TextStyle(color: Get.theme.colorScheme.outline),
),
),
];
showDialog(
context: Get.context!,
barrierDismissible: isManual,
builder: (context) => AlertDialog(
title: const Text('评论检查结果'),
content: SelectableText(message),
actions: [
if (isBan)
TextButton(
onPressed: () {
Get.back();
String? uri;
switch (type) {
case 1:
uri = IdUtils.av2bv(oid);
case 17:
uri = 'https://www.bilibili.com/opus/$oid';
}
if (uri != null) {
Utils.copyText(uri);
}
Get.toNamed(
'/webview',
parameters: {
'url':
'https://www.bilibili.com/h5/comment/appeal?${Utils.themeUrl(Get.isDarkMode)}',
},
);
},
child: const Text('申诉'),
),
if (!isManual)
TextButton(
onPressed: Get.back,
child: Text(
'关闭',
style: TextStyle(color: Get.theme.colorScheme.outline),
),
),
],
actions: actions.isEmpty ? null : actions,
),
);
}

View File

@@ -316,6 +316,31 @@ abstract final class RequestUtils {
clearCookie: true,
);
final isSuccess = res.isSuccess;
final actions = [
if (!isSuccess)
TextButton(
onPressed: () {
Get.back();
Utils.copyText('https://www.bilibili.com/opus/$id');
Get.toNamed(
'/webview',
parameters: {
'url':
'https://www.bilibili.com/h5/comment/appeal?${Utils.themeUrl(Get.isDarkMode)}',
},
);
},
child: const Text('申诉'),
),
if (!isManual)
TextButton(
onPressed: Get.back,
child: Text(
'关闭',
style: TextStyle(color: Get.theme.colorScheme.outline),
),
),
];
showDialog(
context: Get.context!,
barrierDismissible: isManual,
@@ -324,31 +349,7 @@ abstract final class RequestUtils {
content: SelectableText(
'${isSuccess ? '无账号状态下找到了你的动态,动态正常!' : '你的动态被shadow ban仅自己可见'}${dynText != null ? ' \n\n动态内容: $dynText' : ''}',
),
actions: [
if (!isSuccess)
TextButton(
onPressed: () {
Get.back();
Utils.copyText('https://www.bilibili.com/opus/$id');
Get.toNamed(
'/webview',
parameters: {
'url':
'https://www.bilibili.com/h5/comment/appeal?${Utils.themeUrl(Get.isDarkMode)}',
},
);
},
child: const Text('申诉'),
),
if (!isManual)
TextButton(
onPressed: Get.back,
child: Text(
'关闭',
style: TextStyle(color: Get.theme.colorScheme.outline),
),
),
],
actions: actions.isEmpty ? null : actions,
),
);
}

View File

@@ -13,7 +13,7 @@ abstract final class SettingBoxKey {
defaultToastOp = 'defaultToastOp',
defaultPicQa = 'defaultPicQa',
enableHA = 'enableHA',
useOpenSLES = 'useOpenSLES',
audioOutput = 'audioOutput',
expandBuffer = 'expandBuffer',
hardwareDecoding = 'hardwareDecoding',
videoSync = 'videoSync',

View File

@@ -24,6 +24,7 @@ import 'package:PiliPlus/models/common/video/video_decode_type.dart';
import 'package:PiliPlus/models/common/video/video_quality.dart';
import 'package:PiliPlus/models/user/danmaku_rule.dart';
import 'package:PiliPlus/models/user/info.dart';
import 'package:PiliPlus/plugin/pl_player/models/audio_output_type.dart';
import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart';
import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart';
import 'package:PiliPlus/plugin/pl_player/models/hwdec_type.dart';
@@ -786,8 +787,10 @@ abstract final class Pref {
static bool get expandBuffer =>
_setting.get(SettingBoxKey.expandBuffer, defaultValue: false);
static bool get useOpenSLES =>
_setting.get(SettingBoxKey.useOpenSLES, defaultValue: false);
static String get audioOutput => _setting.get(
SettingBoxKey.audioOutput,
defaultValue: AudioOutput.defaultValue,
);
static bool get enableAi =>
_setting.get(SettingBoxKey.enableAi, defaultValue: false);

View File

@@ -416,10 +416,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25
url: "https://pub.dev"
source: hosted
version: "5.9.0"
version: "5.9.1"
dio_http2_adapter:
dependency: "direct main"
description:
@@ -529,11 +529,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: mod
resolved-ref: e9f51575d0103880f4d0ad64f314bc57d0b49cf1
ref: "v10.3.10"
resolved-ref: fd8afd89260436b63b9d41e150b4eb3c806cc8fd
url: "https://github.com/bggRGjQaUbCoE/flutter_file_picker.git"
source: git
version: "10.3.8"
version: "10.3.10"
file_selector_linux:
dependency: transitive
description:
@@ -945,10 +945,10 @@ packages:
dependency: transitive
description:
name: image_picker_android
sha256: "297e42bd236c4ac4b091d4277292159b3280545e030cae2be3d503f9ecf7e6a1"
sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d"
url: "https://pub.dev"
source: hosted
version: "0.8.13+12"
version: "0.8.13+13"
image_picker_for_web:
dependency: transitive
description:
@@ -1480,10 +1480,10 @@ packages:
dependency: transitive
description:
name: safe_local_storage
sha256: "608354d4cdfdabb29428bdb330c4e54dbc31aab238b09ba59fe73660580553cc"
sha256: "287ea1f667c0b93cdc127dccc707158e2d81ee59fba0459c31a0c7da4d09c755"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.0.3"
saver_gallery:
dependency: "direct main"
description:
@@ -1592,10 +1592,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f
url: "https://pub.dev"
source: hosted
version: "2.4.18"
version: "2.4.20"
shared_preferences_foundation:
dependency: transitive
description:

View File

@@ -47,7 +47,7 @@ dependencies:
ref: version_4.7.2
# 网络
dio: ^5.7.0
dio: ^5.9.1
cookie_jar: ^4.0.8
connectivity_plus: ^7.0.0
dio_http2_adapter: ^2.5.3
@@ -218,7 +218,7 @@ dependencies:
file_picker:
git:
url: https://github.com/bggRGjQaUbCoE/flutter_file_picker.git
ref: mod
ref: v10.3.10
super_sliver_list:
git:
url: https://github.com/bggRGjQaUbCoE/super_sliver_list.git