mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
opt show more text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -6,6 +6,10 @@ class StyleString {
|
|||||||
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
|
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
|
||||||
static const Radius imgRadius = Radius.circular(10);
|
static const Radius imgRadius = Radius.circular(10);
|
||||||
static const double aspectRatio = 16 / 10;
|
static const double aspectRatio = 16 / 10;
|
||||||
|
static const bottomSheetRadius = BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(18),
|
||||||
|
topRight: Radius.circular(18),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
|
|||||||
3604
lib/common/widgets/text/paragraph.dart
Normal file
3604
lib/common/widgets/text/paragraph.dart
Normal file
File diff suppressed because it is too large
Load Diff
314
lib/common/widgets/text/rich_text.dart
Normal file
314
lib/common/widgets/text/rich_text.dart
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/widgets/text/paragraph.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||||
|
|
||||||
|
/// A paragraph of rich text.
|
||||||
|
///
|
||||||
|
/// {@youtube 560 315 https://www.youtube.com/watch?v=rykDVh-QFfw}
|
||||||
|
///
|
||||||
|
/// The [RichText] widget displays text that uses multiple different styles. The
|
||||||
|
/// text to display is described using a tree of [TextSpan] objects, each of
|
||||||
|
/// which has an associated style that is used for that subtree. The text might
|
||||||
|
/// break across multiple lines or might all be displayed on the same line
|
||||||
|
/// depending on the layout constraints.
|
||||||
|
///
|
||||||
|
/// Text displayed in a [RichText] widget must be explicitly styled. When
|
||||||
|
/// picking which style to use, consider using [DefaultTextStyle.of] the current
|
||||||
|
/// [BuildContext] to provide defaults. For more details on how to style text in
|
||||||
|
/// a [RichText] widget, see the documentation for [TextStyle].
|
||||||
|
///
|
||||||
|
/// Consider using the [Text] widget to integrate with the [DefaultTextStyle]
|
||||||
|
/// automatically. When all the text uses the same style, the default constructor
|
||||||
|
/// is less verbose. The [Text.rich] constructor allows you to style multiple
|
||||||
|
/// spans with the default text style while still allowing specified styles per
|
||||||
|
/// span.
|
||||||
|
///
|
||||||
|
/// {@tool snippet}
|
||||||
|
///
|
||||||
|
/// This sample demonstrates how to mix and match text with different text
|
||||||
|
/// styles using the [RichText] Widget. It displays the text "Hello bold world,"
|
||||||
|
/// emphasizing the word "bold" using a bold font weight.
|
||||||
|
///
|
||||||
|
/// 
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// RichText(
|
||||||
|
/// text: TextSpan(
|
||||||
|
/// text: 'Hello ',
|
||||||
|
/// style: DefaultTextStyle.of(context).style,
|
||||||
|
/// children: const <TextSpan>[
|
||||||
|
/// TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
/// TextSpan(text: ' world!'),
|
||||||
|
/// ],
|
||||||
|
/// ),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// ## Selections
|
||||||
|
///
|
||||||
|
/// To make this [RichText] Selectable, the [RichText] needs to be in the
|
||||||
|
/// subtree of a [SelectionArea] or [SelectableRegion] and a
|
||||||
|
/// [SelectionRegistrar] needs to be assigned to the
|
||||||
|
/// [RichText.selectionRegistrar]. One can use
|
||||||
|
/// [SelectionContainer.maybeOf] to get the [SelectionRegistrar] from a
|
||||||
|
/// context. This enables users to select the text in [RichText]s with mice or
|
||||||
|
/// touch events.
|
||||||
|
///
|
||||||
|
/// The [selectionColor] also needs to be set if the selection is enabled to
|
||||||
|
/// draw the selection highlights.
|
||||||
|
///
|
||||||
|
/// {@tool snippet}
|
||||||
|
///
|
||||||
|
/// This sample demonstrates how to assign a [SelectionRegistrar] for RichTexts
|
||||||
|
/// in the SelectionArea subtree.
|
||||||
|
///
|
||||||
|
/// 
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// RichText(
|
||||||
|
/// text: const TextSpan(text: 'Hello'),
|
||||||
|
/// selectionRegistrar: SelectionContainer.maybeOf(context),
|
||||||
|
/// selectionColor: const Color(0xAF6694e8),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [TextStyle], which discusses how to style text.
|
||||||
|
/// * [TextSpan], which is used to describe the text in a paragraph.
|
||||||
|
/// * [Text], which automatically applies the ambient styles described by a
|
||||||
|
/// [DefaultTextStyle] to a single string.
|
||||||
|
/// * [Text.rich], a const text widget that provides similar functionality
|
||||||
|
/// as [RichText]. [Text.rich] will inherit [TextStyle] from [DefaultTextStyle].
|
||||||
|
/// * [SelectableRegion], which provides an overview of the selection system.
|
||||||
|
class RichText extends MultiChildRenderObjectWidget {
|
||||||
|
/// Creates a paragraph of rich text.
|
||||||
|
///
|
||||||
|
/// The [maxLines] property may be null (and indeed defaults to null), but if
|
||||||
|
/// it is not null, it must be greater than zero.
|
||||||
|
///
|
||||||
|
/// The [textDirection], if null, defaults to the ambient [Directionality],
|
||||||
|
/// which in that case must not be null.
|
||||||
|
RichText({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
this.textAlign = TextAlign.start,
|
||||||
|
this.textDirection,
|
||||||
|
this.softWrap = true,
|
||||||
|
this.overflow = TextOverflow.clip,
|
||||||
|
@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.',
|
||||||
|
)
|
||||||
|
double textScaleFactor = 1.0,
|
||||||
|
TextScaler textScaler = TextScaler.noScaling,
|
||||||
|
this.maxLines,
|
||||||
|
this.locale,
|
||||||
|
this.strutStyle,
|
||||||
|
this.textWidthBasis = TextWidthBasis.parent,
|
||||||
|
this.textHeightBehavior,
|
||||||
|
this.selectionRegistrar,
|
||||||
|
this.selectionColor,
|
||||||
|
}) : assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(selectionRegistrar == null || selectionColor != null),
|
||||||
|
assert(
|
||||||
|
textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling),
|
||||||
|
'Use textScaler instead.',
|
||||||
|
),
|
||||||
|
textScaler = _effectiveTextScalerFrom(textScaler, textScaleFactor),
|
||||||
|
super(
|
||||||
|
children: WidgetSpan.extractFromInlineSpan(
|
||||||
|
text,
|
||||||
|
_effectiveTextScalerFrom(textScaler, textScaleFactor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static TextScaler _effectiveTextScalerFrom(
|
||||||
|
TextScaler textScaler, double textScaleFactor) {
|
||||||
|
return switch ((textScaler, textScaleFactor)) {
|
||||||
|
(final TextScaler scaler, 1.0) => scaler,
|
||||||
|
(TextScaler.noScaling, final double textScaleFactor) =>
|
||||||
|
TextScaler.linear(textScaleFactor),
|
||||||
|
(final TextScaler scaler, _) => scaler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The text to display in this widget.
|
||||||
|
final InlineSpan text;
|
||||||
|
|
||||||
|
/// How the text should be aligned horizontally.
|
||||||
|
final TextAlign textAlign;
|
||||||
|
|
||||||
|
/// The directionality of the text.
|
||||||
|
///
|
||||||
|
/// This decides how [textAlign] values like [TextAlign.start] and
|
||||||
|
/// [TextAlign.end] are interpreted.
|
||||||
|
///
|
||||||
|
/// This is also used to disambiguate how to render bidirectional text. For
|
||||||
|
/// example, if the [text] is an English phrase followed by a Hebrew phrase,
|
||||||
|
/// in a [TextDirection.ltr] context the English phrase will be on the left
|
||||||
|
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
|
||||||
|
/// context, the English phrase will be on the right and the Hebrew phrase on
|
||||||
|
/// its left.
|
||||||
|
///
|
||||||
|
/// Defaults to the ambient [Directionality], if any. If there is no ambient
|
||||||
|
/// [Directionality], then this must not be null.
|
||||||
|
final TextDirection? textDirection;
|
||||||
|
|
||||||
|
/// Whether the text should break at soft line breaks.
|
||||||
|
///
|
||||||
|
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
|
||||||
|
final bool softWrap;
|
||||||
|
|
||||||
|
/// How visual overflow should be handled.
|
||||||
|
final TextOverflow overflow;
|
||||||
|
|
||||||
|
/// Deprecated. Will be removed in a future version of Flutter. Use
|
||||||
|
/// [textScaler] instead.
|
||||||
|
///
|
||||||
|
/// The number of font pixels for each logical pixel.
|
||||||
|
///
|
||||||
|
/// For example, if the text scale factor is 1.5, text will be 50% larger than
|
||||||
|
/// the specified font size.
|
||||||
|
@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.',
|
||||||
|
)
|
||||||
|
double get textScaleFactor => textScaler.textScaleFactor;
|
||||||
|
|
||||||
|
/// {@macro flutter.painting.textPainter.textScaler}
|
||||||
|
final TextScaler textScaler;
|
||||||
|
|
||||||
|
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||||
|
/// If the text exceeds the given number of lines, it will be truncated according
|
||||||
|
/// to [overflow].
|
||||||
|
///
|
||||||
|
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
|
||||||
|
/// edge of the box.
|
||||||
|
final int? maxLines;
|
||||||
|
|
||||||
|
/// Used to select a font when the same Unicode character can
|
||||||
|
/// be rendered differently, depending on the locale.
|
||||||
|
///
|
||||||
|
/// It's rarely necessary to set this property. By default its value
|
||||||
|
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
|
||||||
|
///
|
||||||
|
/// See [RenderParagraph.locale] for more information.
|
||||||
|
final Locale? locale;
|
||||||
|
|
||||||
|
/// {@macro flutter.painting.textPainter.strutStyle}
|
||||||
|
final StrutStyle? strutStyle;
|
||||||
|
|
||||||
|
/// {@macro flutter.painting.textPainter.textWidthBasis}
|
||||||
|
final TextWidthBasis textWidthBasis;
|
||||||
|
|
||||||
|
/// {@macro dart.ui.textHeightBehavior}
|
||||||
|
final ui.TextHeightBehavior? textHeightBehavior;
|
||||||
|
|
||||||
|
/// The [SelectionRegistrar] this rich text is subscribed to.
|
||||||
|
///
|
||||||
|
/// If this is set, [selectionColor] must be non-null.
|
||||||
|
final SelectionRegistrar? selectionRegistrar;
|
||||||
|
|
||||||
|
/// The color to use when painting the selection.
|
||||||
|
///
|
||||||
|
/// This is ignored if [selectionRegistrar] is null.
|
||||||
|
///
|
||||||
|
/// See the section on selections in the [RichText] top-level API
|
||||||
|
/// documentation for more details on enabling selection in [RichText]
|
||||||
|
/// widgets.
|
||||||
|
final Color? selectionColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderParagraph createRenderObject(BuildContext context) {
|
||||||
|
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||||
|
return RenderParagraph(
|
||||||
|
text,
|
||||||
|
textAlign: textAlign,
|
||||||
|
textDirection: textDirection ?? Directionality.of(context),
|
||||||
|
softWrap: softWrap,
|
||||||
|
overflow: overflow,
|
||||||
|
textScaler: textScaler,
|
||||||
|
maxLines: maxLines,
|
||||||
|
strutStyle: strutStyle,
|
||||||
|
textWidthBasis: textWidthBasis,
|
||||||
|
textHeightBehavior: textHeightBehavior,
|
||||||
|
locale: locale ?? Localizations.maybeLocaleOf(context),
|
||||||
|
registrar: selectionRegistrar,
|
||||||
|
selectionColor: selectionColor,
|
||||||
|
primary: Theme.of(context).colorScheme.primary,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
|
||||||
|
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||||
|
renderObject
|
||||||
|
..text = text
|
||||||
|
..textAlign = textAlign
|
||||||
|
..textDirection = textDirection ?? Directionality.of(context)
|
||||||
|
..softWrap = softWrap
|
||||||
|
..overflow = overflow
|
||||||
|
..textScaler = textScaler
|
||||||
|
..maxLines = maxLines
|
||||||
|
..strutStyle = strutStyle
|
||||||
|
..textWidthBasis = textWidthBasis
|
||||||
|
..textHeightBehavior = textHeightBehavior
|
||||||
|
..locale = locale ?? Localizations.maybeLocaleOf(context)
|
||||||
|
..registrar = selectionRegistrar
|
||||||
|
..selectionColor = selectionColor
|
||||||
|
..primary = Theme.of(context).colorScheme.primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(EnumProperty<TextAlign>('textAlign', textAlign,
|
||||||
|
defaultValue: TextAlign.start));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
FlagProperty(
|
||||||
|
'softWrap',
|
||||||
|
value: softWrap,
|
||||||
|
ifTrue: 'wrapping at box width',
|
||||||
|
ifFalse: 'no wrapping except at line break characters',
|
||||||
|
showName: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
EnumProperty<TextOverflow>('overflow', overflow,
|
||||||
|
defaultValue: TextOverflow.clip),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<TextScaler>('textScaler', textScaler,
|
||||||
|
defaultValue: TextScaler.noScaling),
|
||||||
|
);
|
||||||
|
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||||
|
properties.add(
|
||||||
|
EnumProperty<TextWidthBasis>(
|
||||||
|
'textWidthBasis',
|
||||||
|
textWidthBasis,
|
||||||
|
defaultValue: TextWidthBasis.parent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
properties.add(StringProperty('text', text.toPlainText()));
|
||||||
|
properties
|
||||||
|
.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<StrutStyle>('strutStyle', strutStyle,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<TextHeightBehavior>(
|
||||||
|
'textHeightBehavior',
|
||||||
|
textHeightBehavior,
|
||||||
|
defaultValue: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1180
lib/common/widgets/text/text.dart
Normal file
1180
lib/common/widgets/text/text.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -495,10 +495,11 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
|||||||
|
|
||||||
void onChanged(String value) {
|
void onChanged(String value) {
|
||||||
bool isEmpty = value.trim().isEmpty;
|
bool isEmpty = value.trim().isEmpty;
|
||||||
if (!isEmpty && !enablePublish.value) {
|
if (isEmpty) {
|
||||||
enablePublish.value = true;
|
|
||||||
} else if (isEmpty && enablePublish.value) {
|
|
||||||
enablePublish.value = false;
|
enablePublish.value = false;
|
||||||
|
mentions?.clear();
|
||||||
|
} else {
|
||||||
|
enablePublish.value = true;
|
||||||
}
|
}
|
||||||
widget.onSave?.call((text: value, mentions: mentions));
|
widget.onSave?.call((text: value, mentions: mentions));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/dialog/report.dart';
|
import 'package:PiliPlus/common/widgets/dialog/report.dart';
|
||||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||||
import 'package:PiliPlus/http/constants.dart';
|
import 'package:PiliPlus/http/constants.dart';
|
||||||
@@ -243,10 +244,7 @@ class AuthorPanel extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: Get.back,
|
onTap: Get.back,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: StyleString.bottomSheetRadius,
|
||||||
topLeft: Radius.circular(18),
|
|
||||||
topRight: Radius.circular(18),
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 35,
|
height: 35,
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// 内容
|
// 内容
|
||||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/text/text.dart' as custom_text;
|
||||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||||
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
|
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -68,13 +69,12 @@ Widget content(
|
|||||||
? const TextStyle(fontSize: 15)
|
? const TextStyle(fontSize: 15)
|
||||||
: const TextStyle(fontSize: 16),
|
: const TextStyle(fontSize: 16),
|
||||||
)
|
)
|
||||||
: Text.rich(
|
: custom_text.Text.rich(
|
||||||
style: floor == 1
|
style: floor == 1
|
||||||
? const TextStyle(fontSize: 15)
|
? const TextStyle(fontSize: 15)
|
||||||
: const TextStyle(fontSize: 14),
|
: const TextStyle(fontSize: 14),
|
||||||
richNodes,
|
richNodes,
|
||||||
maxLines: isSave ? null : 6,
|
maxLines: isSave ? null : 6,
|
||||||
overflow: isSave ? null : TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
if (item.modules.moduleDynamic?.major?.opus?.pics?.isNotEmpty == true)
|
if (item.modules.moduleDynamic?.major?.opus?.pics?.isNotEmpty == true)
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
|
|||||||
@@ -205,11 +205,12 @@ List<SettingsModel> get extraSettings => [
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Get.back();
|
Get.back();
|
||||||
|
int length = int.tryParse(replyLengthLimit) ?? 6;
|
||||||
ReplyItemGrpc.replyLengthLimit =
|
ReplyItemGrpc.replyLengthLimit =
|
||||||
int.tryParse(replyLengthLimit) ?? 6;
|
length == 0 ? null : length;
|
||||||
await GStorage.setting.put(
|
await GStorage.setting.put(
|
||||||
SettingBoxKey.replyLengthLimit,
|
SettingBoxKey.replyLengthLimit,
|
||||||
ReplyItemGrpc.replyLengthLimit,
|
length,
|
||||||
);
|
);
|
||||||
setState();
|
setState();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:PiliPlus/common/widgets/dialog/report.dart';
|
|||||||
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/text/text.dart' as custom_text;
|
||||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||||
show ReplyInfo, ReplyControl, Content;
|
show ReplyInfo, ReplyControl, Content;
|
||||||
import 'package:PiliPlus/http/init.dart';
|
import 'package:PiliPlus/http/init.dart';
|
||||||
@@ -13,7 +14,6 @@ import 'package:PiliPlus/http/video.dart';
|
|||||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||||
import 'package:PiliPlus/models/common/image_type.dart';
|
import 'package:PiliPlus/models/common/image_type.dart';
|
||||||
import 'package:PiliPlus/pages/dynamics/widgets/vote.dart';
|
import 'package:PiliPlus/pages/dynamics/widgets/vote.dart';
|
||||||
import 'package:PiliPlus/pages/save_panel/view.dart';
|
|
||||||
import 'package:PiliPlus/pages/video/controller.dart';
|
import 'package:PiliPlus/pages/video/controller.dart';
|
||||||
import 'package:PiliPlus/pages/video/reply/widgets/zan_grpc.dart';
|
import 'package:PiliPlus/pages/video/reply/widgets/zan_grpc.dart';
|
||||||
import 'package:PiliPlus/utils/accounts.dart';
|
import 'package:PiliPlus/utils/accounts.dart';
|
||||||
@@ -68,10 +68,10 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
final ValueChanged<ReplyInfo>? onCheckReply;
|
final ValueChanged<ReplyInfo>? onCheckReply;
|
||||||
final ValueChanged<ReplyInfo>? onToggleTop;
|
final ValueChanged<ReplyInfo>? onToggleTop;
|
||||||
|
|
||||||
static final _voteRegExp = RegExp(r"\{vote:\d+?\}");
|
static final _voteRegExp = RegExp(r"^\{vote:\d+?\}$");
|
||||||
static final _timeRegExp = RegExp(r'^\b(?:\d+[::])?\d+[::]\d+\b$');
|
static final _timeRegExp = RegExp(r'^\b(?:\d+[::])?\d+[::]\d+\b$');
|
||||||
static bool enableWordRe = Pref.enableWordRe;
|
static bool enableWordRe = Pref.enableWordRe;
|
||||||
static int replyLengthLimit = Pref.replyLengthLimit;
|
static int? replyLengthLimit = Pref.replyLengthLimit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -181,6 +181,10 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget content(BuildContext context, ThemeData theme) {
|
Widget content(BuildContext context, ThemeData theme) {
|
||||||
|
final padding = EdgeInsets.only(
|
||||||
|
left: replyLevel == 0 ? 6 : 45,
|
||||||
|
right: 6,
|
||||||
|
);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -218,7 +222,6 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
Image.asset(
|
Image.asset(
|
||||||
'assets/images/lv/lv${replyItem.member.isSeniorMember == 1 ? '6_s' : replyItem.member.level}.png',
|
'assets/images/lv/lv${replyItem.member.isSeniorMember == 1 ? '6_s' : replyItem.member.level}.png',
|
||||||
height: 11,
|
height: 11,
|
||||||
semanticLabel: "等级:${replyItem.member.level}",
|
|
||||||
),
|
),
|
||||||
if (replyItem.mid == upMid)
|
if (replyItem.mid == upMid)
|
||||||
const PBadge(
|
const PBadge(
|
||||||
@@ -255,35 +258,15 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// title
|
const SizedBox(height: 10),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: padding,
|
||||||
top: 10,
|
child: custom_text.Text.rich(
|
||||||
left: replyLevel == 0 ? 6 : 45,
|
style: TextStyle(
|
||||||
right: 6,
|
|
||||||
bottom: 4,
|
|
||||||
),
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
String text = replyItem.content.message;
|
|
||||||
TextStyle style = TextStyle(
|
|
||||||
height: 1.75,
|
height: 1.75,
|
||||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||||
);
|
),
|
||||||
TextPainter? textPainter;
|
maxLines: replyLevel == 1 ? replyLengthLimit : null,
|
||||||
bool? didExceedMaxLines;
|
|
||||||
if (replyLevel == 1 && replyLengthLimit != 0) {
|
|
||||||
textPainter = TextPainter(
|
|
||||||
text: TextSpan(text: text, style: style),
|
|
||||||
maxLines: replyLengthLimit,
|
|
||||||
textDirection: Directionality.of(context),
|
|
||||||
)..layout(maxWidth: constraints.maxWidth);
|
|
||||||
didExceedMaxLines = textPainter.didExceedMaxLines;
|
|
||||||
}
|
|
||||||
return Semantics(
|
|
||||||
label: text,
|
|
||||||
child: Text.rich(
|
|
||||||
style: style,
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
if (replyItem.replyControl.isUpTop) ...[
|
if (replyItem.replyControl.isUpTop) ...[
|
||||||
@@ -305,19 +288,39 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
theme,
|
theme,
|
||||||
replyItem,
|
replyItem,
|
||||||
null,
|
null,
|
||||||
textPainter,
|
|
||||||
didExceedMaxLines,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
if (replyItem.content.pictures.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) => imageView(
|
||||||
|
constraints.maxWidth,
|
||||||
|
replyItem.content.pictures
|
||||||
|
.map(
|
||||||
|
(item) => ImageModel(
|
||||||
|
width: item.imgWidth,
|
||||||
|
height: item.imgHeight,
|
||||||
|
url: item.imgSrc,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onViewImage: onViewImage,
|
||||||
|
onDismissed: onDismissed,
|
||||||
|
callback: callback,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
// 操作区域
|
// 操作区域
|
||||||
if (replyLevel != 0)
|
if (replyLevel != 0) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
buttonAction(context, theme, replyItem.replyControl),
|
buttonAction(context, theme, replyItem.replyControl),
|
||||||
|
],
|
||||||
// 一楼的评论
|
// 一楼的评论
|
||||||
if (replyLevel == 1 && replyItem.count > Int64.ZERO) ...[
|
if (replyLevel == 1 && replyItem.count > Int64.ZERO) ...[
|
||||||
Padding(
|
Padding(
|
||||||
@@ -472,10 +475,6 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Semantics(
|
|
||||||
label:
|
|
||||||
'${childReply.member.name} ${childReply.content.message}',
|
|
||||||
excludeSemantics: true,
|
|
||||||
child: Text.rich(
|
child: Text.rich(
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||||
@@ -525,14 +524,11 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
theme,
|
theme,
|
||||||
childReply,
|
childReply,
|
||||||
replyItem,
|
replyItem,
|
||||||
null,
|
|
||||||
null,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
if (extraRow)
|
if (extraRow)
|
||||||
@@ -580,81 +576,58 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
ThemeData theme,
|
ThemeData theme,
|
||||||
ReplyInfo replyItem,
|
ReplyInfo replyItem,
|
||||||
ReplyInfo? fReplyItem,
|
ReplyInfo? fReplyItem,
|
||||||
TextPainter? textPainter,
|
|
||||||
bool? didExceedMaxLines,
|
|
||||||
) {
|
) {
|
||||||
final String routePath = Get.currentRoute;
|
|
||||||
bool isVideoPage = routePath.startsWith('/video');
|
|
||||||
|
|
||||||
// replyItem 当前回复内容
|
// replyItem 当前回复内容
|
||||||
// replyReply 查看二楼回复(回复详情)回调
|
// replyReply 查看二楼回复(回复详情)回调
|
||||||
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
|
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
|
||||||
final Content content = replyItem.content;
|
final Content content = replyItem.content;
|
||||||
String message = content.message;
|
|
||||||
final List<InlineSpan> spanChildren = <InlineSpan>[];
|
final List<InlineSpan> spanChildren = <InlineSpan>[];
|
||||||
|
|
||||||
if (didExceedMaxLines == true) {
|
if (content.hasRichText()) {
|
||||||
final textSize = textPainter!.size;
|
|
||||||
final double maxHeight = textPainter.preferredLineHeight * 6;
|
|
||||||
var position = textPainter.getPositionForOffset(
|
|
||||||
Offset(textSize.width, maxHeight),
|
|
||||||
);
|
|
||||||
message = message.substring(0, position.offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 投票
|
|
||||||
if (content.hasVote()) {
|
|
||||||
message = message.replaceAllMapped(_voteRegExp, (Match match) {
|
|
||||||
spanChildren.add(
|
spanChildren.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '投票: ${content.vote.title}',
|
text: '[笔记] ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () => showVoteDialog(context, content.vote.id.toInt()),
|
..onTap =
|
||||||
|
() => PageUtils.handleWebview(content.richText.note.clickUrl),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建正则表达式
|
// 构建正则表达式
|
||||||
final List<String> specialTokens = [
|
final List<String> specialTokens = [
|
||||||
...content.emotes.keys,
|
...content.emotes.keys,
|
||||||
...content.topics.keys.map((e) => '#$e#'),
|
...content.topics.keys.map((e) => '#$e#'),
|
||||||
...content.atNameToMid.keys.map((e) => '@$e'),
|
...content.atNameToMid.keys.map((e) => '@$e'),
|
||||||
|
...content.urls.keys,
|
||||||
];
|
];
|
||||||
List<String> jumpUrlKeysList = content.urls.keys.map<String>((String e) {
|
String patternStr = [
|
||||||
return e;
|
...specialTokens.map(RegExp.escape),
|
||||||
}).toList();
|
r'(\b(?:\d+[::])?\d+[::]\d+\b)',
|
||||||
specialTokens.sort((a, b) => b.length.compareTo(a.length));
|
r'(\{vote:\d+?\})',
|
||||||
String patternStr = specialTokens.map(RegExp.escape).join('|');
|
Constants.urlPattern,
|
||||||
if (patternStr.isNotEmpty) {
|
].join('|');
|
||||||
patternStr += "|";
|
|
||||||
}
|
|
||||||
patternStr += r'(\b(?:\d+[::])?\d+[::]\d+\b)';
|
|
||||||
if (jumpUrlKeysList.isNotEmpty) {
|
|
||||||
patternStr += '|${jumpUrlKeysList.map(RegExp.escape).join('|')}';
|
|
||||||
}
|
|
||||||
patternStr += '|${Constants.urlPattern}';
|
|
||||||
final RegExp pattern = RegExp(patternStr);
|
final RegExp pattern = RegExp(patternStr);
|
||||||
List<String> matchedStrs = [];
|
|
||||||
|
late List<String> matchedStrs = [];
|
||||||
|
|
||||||
void addPlainTextSpan(str) {
|
void addPlainTextSpan(str) {
|
||||||
spanChildren.add(TextSpan(
|
spanChildren.add(TextSpan(text: str));
|
||||||
text: str,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分割文本并处理每个部分
|
// 分割文本并处理每个部分
|
||||||
message.splitMapJoin(
|
content.message.splitMapJoin(
|
||||||
pattern,
|
pattern,
|
||||||
onMatch: (Match match) {
|
onMatch: (Match match) {
|
||||||
String matchStr = match[0]!;
|
String matchStr = match[0]!;
|
||||||
if (content.emotes.containsKey(matchStr)) {
|
if (content.emotes.containsKey(matchStr)) {
|
||||||
// 处理表情
|
// 处理表情
|
||||||
final int size = content.emotes[matchStr]!.size.toInt();
|
final int size = content.emotes[matchStr]!.size.toInt();
|
||||||
spanChildren.add(WidgetSpan(
|
spanChildren.add(
|
||||||
child: ExcludeSemantics(
|
WidgetSpan(
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
src: content.emotes[matchStr]?.hasGifUrl() == true
|
src: content.emotes[matchStr]?.hasGifUrl() == true
|
||||||
? content.emotes[matchStr]?.gifUrl
|
? content.emotes[matchStr]?.gifUrl
|
||||||
@@ -662,9 +635,9 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
type: ImageType.emote,
|
type: ImageType.emote,
|
||||||
width: size * 20,
|
width: size * 20,
|
||||||
height: size * 20,
|
height: size * 20,
|
||||||
semanticsLabel: matchStr,
|
),
|
||||||
)),
|
),
|
||||||
));
|
);
|
||||||
} else if (matchStr.startsWith("@") &&
|
} else if (matchStr.startsWith("@") &&
|
||||||
content.atNameToMid.containsKey(matchStr.substring(1))) {
|
content.atNameToMid.containsKey(matchStr.substring(1))) {
|
||||||
// 处理@用户
|
// 处理@用户
|
||||||
@@ -680,6 +653,18 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
..onTap = () => Get.toNamed('/member?mid=$userId'),
|
..onTap = () => Get.toNamed('/member?mid=$userId'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else if (_voteRegExp.hasMatch(matchStr)) {
|
||||||
|
spanChildren.add(
|
||||||
|
TextSpan(
|
||||||
|
text: '投票: ${content.vote.title}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap =
|
||||||
|
() => showVoteDialog(context, content.vote.id.toInt()),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (_timeRegExp.hasMatch(matchStr)) {
|
} else if (_timeRegExp.hasMatch(matchStr)) {
|
||||||
matchStr = matchStr.replaceAll(':', ':');
|
matchStr = matchStr.replaceAll(':', ':');
|
||||||
bool isValid = false;
|
bool isValid = false;
|
||||||
@@ -702,6 +687,7 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (kDebugMode) debugPrint('failed to validate: $e');
|
if (kDebugMode) debugPrint('failed to validate: $e');
|
||||||
}
|
}
|
||||||
|
bool isVideoPage = Get.currentRoute.startsWith('/video');
|
||||||
spanChildren.add(
|
spanChildren.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: isValid ? ' $matchStr ' : matchStr,
|
text: isValid ? ' $matchStr ' : matchStr,
|
||||||
@@ -905,58 +891,6 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (didExceedMaxLines == true) {
|
|
||||||
spanChildren.add(
|
|
||||||
TextSpan(
|
|
||||||
text: '\n查看更多',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图片渲染
|
|
||||||
if (content.pictures.isNotEmpty) {
|
|
||||||
spanChildren
|
|
||||||
..add(const TextSpan(text: '\n'))
|
|
||||||
..add(
|
|
||||||
WidgetSpan(
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) => imageView(
|
|
||||||
constraints.maxWidth,
|
|
||||||
content.pictures
|
|
||||||
.map(
|
|
||||||
(item) => ImageModel(
|
|
||||||
width: item.imgWidth,
|
|
||||||
height: item.imgHeight,
|
|
||||||
url: item.imgSrc,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onViewImage: onViewImage,
|
|
||||||
onDismissed: onDismissed,
|
|
||||||
callback: callback,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 笔记链接
|
|
||||||
if (content.hasRichText()) {
|
|
||||||
spanChildren.add(
|
|
||||||
TextSpan(
|
|
||||||
text: ' 笔记',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap =
|
|
||||||
() => PageUtils.handleWebview(content.richText.note.clickUrl),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return TextSpan(children: spanChildren);
|
return TextSpan(children: spanChildren);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,61 +900,40 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
required VoidCallback onDelete,
|
required VoidCallback onDelete,
|
||||||
required bool isSubReply,
|
required bool isSubReply,
|
||||||
}) {
|
}) {
|
||||||
final ownerMid = Int64(Accounts.main.mid);
|
|
||||||
Future<void> menuActionHandler(String type) async {
|
|
||||||
late String message = item.content.message;
|
late String message = item.content.message;
|
||||||
switch (type) {
|
final ownerMid = Int64(Accounts.main.mid);
|
||||||
case 'report':
|
final theme = Theme.of(context);
|
||||||
Get.back();
|
final errorColor = theme.colorScheme.error;
|
||||||
autoWrapReportDialog(
|
final style = theme.textTheme.titleSmall;
|
||||||
context,
|
|
||||||
ReportOptions.commentReport,
|
return Padding(
|
||||||
(reasonType, reasonDesc, banUid) async {
|
padding: EdgeInsets.only(
|
||||||
final res = await Request().post(
|
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||||
'/x/v2/reply/report',
|
|
||||||
data: {
|
|
||||||
'add_blacklist': banUid,
|
|
||||||
'csrf': Accounts.main.csrf,
|
|
||||||
'gaia_source': 'main_h5',
|
|
||||||
'oid': item.oid,
|
|
||||||
'platform': 'android',
|
|
||||||
'reason': reasonType,
|
|
||||||
'rpid': item.id,
|
|
||||||
'scene': 'main',
|
|
||||||
'type': 1,
|
|
||||||
if (reasonType == 0) 'content': reasonDesc!
|
|
||||||
},
|
|
||||||
options:
|
|
||||||
Options(contentType: Headers.formUrlEncodedContentType),
|
|
||||||
);
|
|
||||||
if (res.data['code'] == 0) {
|
|
||||||
onDelete();
|
|
||||||
}
|
|
||||||
return res.data as Map;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'copyAll':
|
|
||||||
Get.back();
|
|
||||||
Utils.copyText(message);
|
|
||||||
break;
|
|
||||||
case 'copyFreedom':
|
|
||||||
Get.back();
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return Dialog(
|
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
||||||
child: SelectableText(message),
|
|
||||||
),
|
),
|
||||||
);
|
child: Column(
|
||||||
},
|
mainAxisSize: MainAxisSize.min,
|
||||||
);
|
children: [
|
||||||
break;
|
InkWell(
|
||||||
case 'delete':
|
onTap: Get.back,
|
||||||
Get.back();
|
borderRadius: StyleString.bottomSheetRadius,
|
||||||
|
child: Container(
|
||||||
|
height: 35,
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 32,
|
||||||
|
height: 3,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (ownerMid == upMid || ownerMid == item.member.mid)
|
||||||
|
ListTile(
|
||||||
|
onTap: () async {
|
||||||
bool? isDelete = await showDialog<bool>(
|
bool? isDelete = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -1078,70 +991,53 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
SmartDialog.showToast('删除失败, ${result["msg"]}');
|
SmartDialog.showToast('删除失败, ${result["msg"]}');
|
||||||
}
|
}
|
||||||
break;
|
},
|
||||||
case 'checkReply':
|
|
||||||
Get.back();
|
|
||||||
onCheckReply?.call(item);
|
|
||||||
break;
|
|
||||||
case 'top':
|
|
||||||
Get.back();
|
|
||||||
onToggleTop?.call(item);
|
|
||||||
break;
|
|
||||||
case 'saveReply':
|
|
||||||
Get.back();
|
|
||||||
SavePanel.toSavePanel(upMid: upMid, item: item);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final errorColor = theme.colorScheme.error;
|
|
||||||
final style = theme.textTheme.titleSmall;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
InkWell(
|
|
||||||
onTap: Get.back,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(18),
|
|
||||||
topRight: Radius.circular(18),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
height: 35,
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
width: 32,
|
|
||||||
height: 3,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.outline,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(3))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (ownerMid == upMid || ownerMid == item.member.mid)
|
|
||||||
ListTile(
|
|
||||||
onTap: () => menuActionHandler('delete'),
|
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
leading: Icon(Icons.delete_outlined, color: errorColor, size: 19),
|
leading: Icon(Icons.delete_outlined, color: errorColor, size: 19),
|
||||||
title: Text('删除', style: style!.copyWith(color: errorColor)),
|
title: Text('删除', style: style!.copyWith(color: errorColor)),
|
||||||
),
|
),
|
||||||
if (ownerMid != Int64.ZERO)
|
if (ownerMid != Int64.ZERO)
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => menuActionHandler('report'),
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
autoWrapReportDialog(
|
||||||
|
context,
|
||||||
|
ReportOptions.commentReport,
|
||||||
|
(reasonType, reasonDesc, banUid) async {
|
||||||
|
final res = await Request().post(
|
||||||
|
'/x/v2/reply/report',
|
||||||
|
data: {
|
||||||
|
'add_blacklist': banUid,
|
||||||
|
'csrf': Accounts.main.csrf,
|
||||||
|
'gaia_source': 'main_h5',
|
||||||
|
'oid': item.oid,
|
||||||
|
'platform': 'android',
|
||||||
|
'reason': reasonType,
|
||||||
|
'rpid': item.id,
|
||||||
|
'scene': 'main',
|
||||||
|
'type': 1,
|
||||||
|
if (reasonType == 0) 'content': reasonDesc!
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType),
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
return res.data as Map;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
leading: Icon(Icons.error_outline, color: errorColor, size: 19),
|
leading: Icon(Icons.error_outline, color: errorColor, size: 19),
|
||||||
title: Text('举报', style: style!.copyWith(color: errorColor)),
|
title: Text('举报', style: style!.copyWith(color: errorColor)),
|
||||||
),
|
),
|
||||||
if (replyLevel == 1 && !isSubReply && ownerMid == upMid)
|
if (replyLevel == 1 && !isSubReply && ownerMid == upMid)
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => menuActionHandler('top'),
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
onToggleTop?.call(item);
|
||||||
|
},
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
leading: const Icon(Icons.vertical_align_top, size: 19),
|
leading: const Icon(Icons.vertical_align_top, size: 19),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -1150,26 +1046,104 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => menuActionHandler('copyAll'),
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
Utils.copyText(message);
|
||||||
|
},
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
leading: const Icon(Icons.copy_all_outlined, size: 19),
|
leading: const Icon(Icons.copy_all_outlined, size: 19),
|
||||||
title: Text('复制全部', style: style),
|
title: Text('复制全部', style: style),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => menuActionHandler('copyFreedom'),
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return Dialog(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20, vertical: 16),
|
||||||
|
child: SelectableText(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
leading: const Icon(Icons.copy_outlined, size: 19),
|
leading: const Icon(Icons.copy_outlined, size: 19),
|
||||||
title: Text('自由复制', style: style),
|
title: Text('自由复制', style: style),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => menuActionHandler('saveReply'),
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
bool? isDelete = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('删除评论'),
|
||||||
|
content: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: '确定删除这条评论吗?\n\n'),
|
||||||
|
if (ownerMid != item.member.mid.toInt()) ...[
|
||||||
|
TextSpan(
|
||||||
|
text: '@${item.member.name}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: ':\n'),
|
||||||
|
],
|
||||||
|
TextSpan(text: message),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(result: false),
|
||||||
|
child: Text(
|
||||||
|
'取消',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(result: true),
|
||||||
|
child: const Text('确定'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (isDelete == true) {
|
||||||
|
SmartDialog.showLoading(msg: '删除中...');
|
||||||
|
var result = await VideoHttp.replyDel(
|
||||||
|
type: item.type.toInt(),
|
||||||
|
oid: item.oid.toInt(),
|
||||||
|
rpid: item.id.toInt(),
|
||||||
|
);
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
if (result['status']) {
|
||||||
|
SmartDialog.showToast('删除成功');
|
||||||
|
onDelete();
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast('删除失败, ${result["msg"]}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
leading: const Icon(Icons.save_alt, size: 19),
|
leading: const Icon(Icons.save_alt, size: 19),
|
||||||
title: Text('保存评论', style: style),
|
title: Text('保存评论', style: style),
|
||||||
),
|
),
|
||||||
if (item.mid == ownerMid)
|
if (item.mid == ownerMid)
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => menuActionHandler('checkReply'),
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
onCheckReply?.call(item);
|
||||||
|
},
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
leading: const Stack(
|
leading: const Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
|
|||||||
@@ -305,8 +305,13 @@ class Pref {
|
|||||||
static bool get horizontalMemberPage =>
|
static bool get horizontalMemberPage =>
|
||||||
_setting.get(SettingBoxKey.horizontalMemberPage, defaultValue: false);
|
_setting.get(SettingBoxKey.horizontalMemberPage, defaultValue: false);
|
||||||
|
|
||||||
static int get replyLengthLimit =>
|
static int? get replyLengthLimit {
|
||||||
_setting.get(SettingBoxKey.replyLengthLimit, defaultValue: 6);
|
int length = _setting.get(SettingBoxKey.replyLengthLimit, defaultValue: 6);
|
||||||
|
if (length <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
static int get defaultPicQa =>
|
static int get defaultPicQa =>
|
||||||
_setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
|
_setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/main.dart';
|
import 'package:PiliPlus/main.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
@@ -91,10 +92,7 @@ class ThemeUtils {
|
|||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
backgroundColor: colorScheme.surface,
|
backgroundColor: colorScheme.surface,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: StyleString.bottomSheetRadius,
|
||||||
topLeft: Radius.circular(18),
|
|
||||||
topRight: Radius.circular(18),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
|
|||||||
Reference in New Issue
Block a user