feat: cross row select (#867)

This commit is contained in:
My-Responsitories
2025-05-25 19:59:56 +08:00
committed by GitHub
parent 76a5b6221d
commit 89a077be5c
4 changed files with 249 additions and 250 deletions

View File

@@ -268,25 +268,29 @@ class _ArticlePageState extends State<ArticlePage>
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
final maxWidth = constraints.maxWidth - 2 * padding - 24; final maxWidth = constraints.maxWidth - 2 * padding - 24;
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: padding), padding: EdgeInsets.symmetric(horizontal: padding),
child: CustomScrollView( child: SelectionArea(
controller: _articleCtr.scrollController, child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(), controller: _articleCtr.scrollController,
slivers: [ physics: const AlwaysScrollableScrollPhysics(),
_buildContent(theme, maxWidth), slivers: [
SliverToBoxAdapter( _buildContent(theme, maxWidth),
child: Divider( SelectionContainer.disabled(
thickness: 8, child: SliverToBoxAdapter(
color: child: Divider(
theme.dividerColor.withValues(alpha: 0.05), thickness: 8,
), color: theme.dividerColor
.withValues(alpha: 0.05),
),
)),
SelectionContainer.disabled(
child: _buildReplyHeader(theme)),
SelectionContainer.disabled(
child: Obx(() => _buildReplyList(theme,
_articleCtr.loadingState.value))),
],
), ),
_buildReplyHeader(theme), ));
Obx(() => _buildReplyList(
theme, _articleCtr.loadingState.value)),
],
),
);
}); });
} else { } else {
return Row( return Row(
@@ -298,7 +302,8 @@ class _ArticlePageState extends State<ArticlePage>
builder: (context, constraints) { builder: (context, constraints) {
final maxWidth = final maxWidth =
constraints.maxWidth - padding / 4 - 24; constraints.maxWidth - padding / 4 - 24;
return CustomScrollView( return SelectionArea(
child: CustomScrollView(
controller: _articleCtr.scrollController, controller: _articleCtr.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
@@ -312,7 +317,7 @@ class _ArticlePageState extends State<ArticlePage>
sliver: _buildContent(theme, maxWidth), sliver: _buildContent(theme, maxWidth),
), ),
], ],
); ));
}, },
), ),
), ),
@@ -421,90 +426,89 @@ class _ArticlePageState extends State<ArticlePage>
?.pics?.isNotEmpty == ?.pics?.isNotEmpty ==
true) true)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Builder( child: SelectionContainer.disabled(child: Builder(
builder: (context) { builder: (context) {
final pics = _articleCtr.opusData!.modules.moduleTop! final pics = _articleCtr
.display!.album!.pics!; .opusData!.modules.moduleTop!.display!.album!.pics!;
final length = pics.length; final length = pics.length;
final first = pics.first; final first = pics.first;
double height; double height;
double paddingRight; double paddingRight;
if (first.height != null && first.width != null) { if (first.height != null && first.width != null) {
final ratio = first.height! / first.width!; final ratio = first.height! / first.width!;
height = min(maxWidth * ratio, Get.height * 0.55); height = min(maxWidth * ratio, Get.height * 0.55);
paddingRight = (maxWidth - height / ratio) / 2 + 12; paddingRight = (maxWidth - height / ratio) / 2 + 12;
} else { } else {
height = Get.height * 0.55; height = Get.height * 0.55;
paddingRight = 12; paddingRight = 12;
} }
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Container( Container(
height: height, height: height,
width: maxWidth, width: maxWidth,
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
child: PageView.builder( child: PageView.builder(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
onPageChanged: (value) { onPageChanged: (value) {
_articleCtr.topIndex.value = value; _articleCtr.topIndex.value = value;
}, },
itemCount: length, itemCount: length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final pic = pics[index]; final pic = pics[index];
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => context.imageView( onTap: () => context.imageView(
imgList: pics imgList: pics
.map( .map((e) => SourceModel(url: e.url!))
(e) => SourceModel(url: e.url!)) .toList(),
.toList(), initialPage: index,
initialPage: index, ),
), child: Hero(
child: Hero( tag: pic.url!,
tag: pic.url!, child: Stack(
child: Stack( clipBehavior: Clip.none,
clipBehavior: Clip.none, alignment: Alignment.center,
alignment: Alignment.center, children: [
children: [ Positioned.fill(
Positioned.fill( child: CachedNetworkImage(
child: CachedNetworkImage( fit: pic.isLongPic == true
fit: pic.isLongPic == true ? BoxFit.cover
? BoxFit.cover : null,
: null, imageUrl: Utils.thumbnailImgUrl(
imageUrl: Utils.thumbnailImgUrl( pic.url, 60),
pic.url, 60),
),
), ),
if (pic.isLongPic == true) ),
PBadge( if (pic.isLongPic == true)
text: '长图', PBadge(
type: PBadgeType.primary, text: '长图',
right: paddingRight, type: PBadgeType.primary,
bottom: 12, right: paddingRight,
), bottom: 12,
], ),
), ],
), ),
); ),
}, );
), },
), ),
Obx( ),
() => PBadge( Obx(
top: 12, () => PBadge(
right: paddingRight, top: 12,
type: PBadgeType.gray, right: paddingRight,
text: type: PBadgeType.gray,
'${_articleCtr.topIndex.value + 1}/$length'), text:
), '${_articleCtr.topIndex.value + 1}/$length'),
], ),
); ],
}, );
), },
), ))),
if (_articleCtr.summary.title != null) if (_articleCtr.summary.title != null)
SliverToBoxAdapter( SelectionContainer.disabled(
child: SliverToBoxAdapter(
child: Text( child: Text(
_articleCtr.summary.title!, _articleCtr.summary.title!,
style: const TextStyle( style: const TextStyle(
@@ -512,8 +516,9 @@ class _ArticlePageState extends State<ArticlePage>
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), )),
SliverToBoxAdapter( SelectionContainer.disabled(
child: SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
child: GestureDetector( child: GestureDetector(
@@ -554,15 +559,16 @@ class _ArticlePageState extends State<ArticlePage>
), ),
), ),
), ),
), )),
if (_articleCtr.type != 'read' && if (_articleCtr.type != 'read' &&
_articleCtr.opusData?.modules.moduleCollection != null) _articleCtr.opusData?.modules.moduleCollection != null)
SliverToBoxAdapter( SelectionContainer.disabled(
child: SliverToBoxAdapter(
child: opusCollection( child: opusCollection(
theme, theme,
_articleCtr.opusData!.modules.moduleCollection!, _articleCtr.opusData!.modules.moduleCollection!,
), ),
), )),
content, content,
], ],
); );

View File

@@ -126,17 +126,15 @@ Widget htmlRender({
margin: Margins.zero, margin: Margins.zero,
), ),
}; };
return SelectionArea( return element != null
child: element != null ? Html.fromElement(
? Html.fromElement( documentElement: element,
documentElement: element, extensions: extensions,
extensions: extensions, style: style,
style: style, )
) : Html(
: Html( data: html,
data: html, extensions: extensions,
extensions: extensions, style: style,
style: style, );
),
);
} }

View File

@@ -69,78 +69,76 @@ class OpusContent extends StatelessWidget {
switch (element.paraType) { switch (element.paraType) {
case 1 || 4: case 1 || 4:
final isQuote = element.paraType == 4; final isQuote = element.paraType == 4;
Widget widget = SelectionArea( Widget widget = Text.rich(
child: Text.rich( textAlign: element.align == 1 ? TextAlign.center : null,
textAlign: element.align == 1 ? TextAlign.center : null, TextSpan(
TextSpan( children: element.text?.nodes?.map((item) {
children: element.text?.nodes?.map((item) { switch (item.type) {
switch (item.type) { case 'TEXT_NODE_TYPE_RICH' when (item.rich != null):
case 'TEXT_NODE_TYPE_RICH' when (item.rich != null): Rich rich = item.rich!;
Rich rich = item.rich!; switch (rich.type) {
switch (rich.type) { case 'RICH_TEXT_NODE_TYPE_EMOJI':
case 'RICH_TEXT_NODE_TYPE_EMOJI': Emoji emoji = rich.emoji!;
Emoji emoji = rich.emoji!; final size = 20.0 * emoji.size;
final size = 20.0 * emoji.size; return WidgetSpan(
return WidgetSpan( child: NetworkImgLayer(
child: NetworkImgLayer( width: size,
width: size, height: size,
height: size, src: emoji.url,
src: emoji.url, type: ImageType.emote,
type: ImageType.emote,
),
);
default:
return TextSpan(
text:
'${rich.type == 'RICH_TEXT_NODE_TYPE_WEB' ? '\u{1F517}' : ''}${item.rich!.text}',
style: _getStyle(
rich.style,
rich.type == 'RICH_TEXT_NODE_TYPE_TEXT'
? null
: colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
switch (rich.type) {
case 'RICH_TEXT_NODE_TYPE_AT':
Get.toNamed('/member?mid=${rich.rid}');
// case 'RICH_TEXT_NODE_TYPE_TOPIC':
default:
if (rich.jumpUrl != null) {
PiliScheme.routePushFromUrl(
rich.jumpUrl!,
);
}
}
},
);
}
case 'TEXT_NODE_TYPE_FORMULA' when (item.formula != null):
return TextSpan(
children: [
WidgetSpan(
child: CachedNetworkSVGImage(
height: 65,
'https://api.bilibili.com/x/web-frontend/mathjax/tex?formula=${Uri.encodeComponent(item.formula!.latexContent!)}',
colorFilter: ColorFilter.mode(
colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
alignment: Alignment.centerLeft,
placeholderBuilder: (_) =>
const SizedBox.shrink(),
),
), ),
], );
); default:
default: return TextSpan(
return _getSpan( text:
item.word, '${rich.type == 'RICH_TEXT_NODE_TYPE_WEB' ? '\u{1F517}' : ''}${item.rich!.text}',
isQuote ? colorScheme.onSurfaceVariant : null, style: _getStyle(
); rich.style,
} rich.type == 'RICH_TEXT_NODE_TYPE_TEXT'
}).toList()), ? null
), : colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
switch (rich.type) {
case 'RICH_TEXT_NODE_TYPE_AT':
Get.toNamed('/member?mid=${rich.rid}');
// case 'RICH_TEXT_NODE_TYPE_TOPIC':
default:
if (rich.jumpUrl != null) {
PiliScheme.routePushFromUrl(
rich.jumpUrl!,
);
}
}
},
);
}
case 'TEXT_NODE_TYPE_FORMULA' when (item.formula != null):
return TextSpan(
children: [
WidgetSpan(
child: CachedNetworkSVGImage(
height: 65,
'https://api.bilibili.com/x/web-frontend/mathjax/tex?formula=${Uri.encodeComponent(item.formula!.latexContent!)}',
colorFilter: ColorFilter.mode(
colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
alignment: Alignment.centerLeft,
placeholderBuilder: (_) =>
const SizedBox.shrink(),
),
),
],
);
default:
return _getSpan(
item.word,
isQuote ? colorScheme.onSurfaceVariant : null,
);
}
}).toList()),
); );
if (isQuote) { if (isQuote) {
widget = Container( widget = Container(
@@ -167,7 +165,8 @@ class OpusContent extends StatelessWidget {
final height = width == null || pic.height == null final height = width == null || pic.height == null
? null ? null
: width * pic.height! / pic.width!; : width * pic.height! / pic.width!;
return Hero( return SelectionContainer.disabled(
child: Hero(
tag: pic.url!, tag: pic.url!,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
@@ -192,42 +191,42 @@ class OpusContent extends StatelessWidget {
), ),
), ),
), ),
); ));
} else { } else {
return imageView( return SelectionContainer.disabled(
maxWidth, child: imageView(
element.pic!.pics! maxWidth,
.map( element.pic!.pics!
(e) => ImageModel(width: 1, height: 1, url: e.url!)) .map((e) =>
.toList()); ImageModel(width: 1, height: 1, url: e.url!))
.toList()));
} }
case 3 when (element.line != null): case 3 when (element.line != null):
return CachedNetworkImage( return SelectionContainer.disabled(
child: CachedNetworkImage(
width: maxWidth, width: maxWidth,
fit: BoxFit.contain, fit: BoxFit.contain,
height: element.line!.pic!.height?.toDouble(), height: element.line!.pic!.height?.toDouble(),
imageUrl: Utils.thumbnailImgUrl(element.line!.pic!.url!), imageUrl: Utils.thumbnailImgUrl(element.line!.pic!.url!),
); ));
case 5 when (element.list != null): case 5 when (element.list != null):
return SelectionArea( return Text.rich(
child: Text.rich( TextSpan(
TextSpan( children: element.list!.items?.indexed.map((entry) {
children: element.list!.items?.indexed.map((entry) { return TextSpan(
return TextSpan( children: [
children: [ const WidgetSpan(
const WidgetSpan( child: Icon(MdiIcons.circleMedium),
child: Icon(MdiIcons.circleMedium), alignment: PlaceholderAlignment.middle,
alignment: PlaceholderAlignment.middle, ),
), ...entry.$2.nodes!.map((item) {
...entry.$2.nodes!.map((item) { return _getSpan(item.word);
return _getSpan(item.word); }),
}), if (entry.$1 < element.list!.items!.length - 1)
if (entry.$1 < element.list!.items!.length - 1) const TextSpan(text: '\n'),
const TextSpan(text: '\n'), ],
], );
); }).toList(),
}).toList(),
),
), ),
); );
case 6: case 6:
@@ -528,7 +527,7 @@ class OpusContent extends StatelessWidget {
), ),
); );
case 7 when (element.code != null): case 7 when (element.code != null):
final Highlight highlight = Highlight() final highlight = Highlight()
..registerLanguages(builtinAllLanguages); ..registerLanguages(builtinAllLanguages);
final HighlightResult result = highlight.highlightAuto( final HighlightResult result = highlight.highlightAuto(
element.code!.content!, element.code!.content!,
@@ -539,7 +538,7 @@ class OpusContent extends StatelessWidget {
.replaceAll('language-', '') .replaceAll('language-', '')
.replaceAll('like', ''), .replaceAll('like', ''),
]); ]);
final TextSpanRenderer renderer = TextSpanRenderer( final renderer = TextSpanRenderer(
const TextStyle(), builtinAllThemes['github']!); const TextStyle(), builtinAllThemes['github']!);
result.render(renderer); result.render(renderer);
return Container( return Container(
@@ -549,40 +548,34 @@ class OpusContent extends StatelessWidget {
color: colorScheme.onInverseSurface, color: colorScheme.onInverseSurface,
), ),
width: double.infinity, width: double.infinity,
child: SelectionArea(child: Text.rich(renderer.span!)), child: Text.rich(renderer.span!),
); );
default: default:
debugPrint('unknown type ${element.paraType}'); debugPrint('unknown type ${element.paraType}');
if (element.text?.nodes?.isNotEmpty == true) { if (element.text?.nodes?.isNotEmpty == true) {
return SelectionArea( return Text.rich(
child: Text.rich( textAlign: element.align == 1 ? TextAlign.center : null,
textAlign: element.align == 1 ? TextAlign.center : null, TextSpan(
TextSpan( children: element.text!.nodes!
children: element.text!.nodes! .map<TextSpan>((item) => _getSpan(item.word))
.map<TextSpan>((item) => _getSpan(item.word)) .toList()),
.toList()),
),
); );
} }
return SelectionArea( return Text(
child: Text( '不支持的类型 (${element.paraType})',
'不支持的类型 (${element.paraType})', style: const TextStyle(
style: const TextStyle( fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, color: Colors.red,
color: Colors.red,
),
), ),
); );
} }
} catch (e) { } catch (e) {
return SelectionArea( return Text(
child: Text( '错误的类型 $e',
'错误的类型 $e', style: const TextStyle(
style: const TextStyle( fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, color: Colors.red,
color: Colors.red,
),
), ),
); );
} }
@@ -597,7 +590,7 @@ Widget moduleBlockedItem(
BoxDecoration? bgImg() { BoxDecoration? bgImg() {
return moduleBlocked.bgImg == null return moduleBlocked.bgImg == null
? null ? null
: BoxDecoration( : (BoxDecoration(
image: DecorationImage( image: DecorationImage(
fit: BoxFit.fill, fit: BoxFit.fill,
image: CachedNetworkImageProvider( image: CachedNetworkImageProvider(
@@ -608,7 +601,7 @@ Widget moduleBlockedItem(
), ),
), ),
), ),
); ));
} }
Widget icon(double width) { Widget icon(double width) {
@@ -690,7 +683,8 @@ Widget moduleBlockedItem(
), ),
); );
} }
return Container( return SelectionContainer.disabled(
child: Container(
decoration: bgImg(), decoration: bgImg(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Row( child: Row(
@@ -726,7 +720,7 @@ Widget moduleBlockedItem(
), ),
], ],
), ),
); ));
} }
Widget opusCollection(ThemeData theme, ModuleCollection item) { Widget opusCollection(ThemeData theme, ModuleCollection item) {

View File

@@ -25,13 +25,14 @@ class ReadOpus extends StatelessWidget {
try { try {
final item = ops![index]; final item = ops![index];
if (item.insert is String) { if (item.insert is String) {
return SelectableText(item.insert); return Text(item.insert);
} }
if (item.insert is Insert) { if (item.insert is Insert) {
InsertCard card = item.insert.card; InsertCard card = item.insert.card;
if (card.url?.isNotEmpty == true) { if (card.url?.isNotEmpty == true) {
return GestureDetector( return SelectionContainer.disabled(
child: GestureDetector(
onTap: () { onTap: () {
switch (item.attributes?.clazz) { switch (item.attributes?.clazz) {
case 'article-card card': case 'article-card card':
@@ -60,7 +61,7 @@ class ReadOpus extends StatelessWidget {
imageUrl: Utils.thumbnailImgUrl(card.url, 60), imageUrl: Utils.thumbnailImgUrl(card.url, 60),
), ),
), ),
); ));
} }
} }