From 89a077be5c9952d85e809fa90af43ba44cd44d1b Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Sun, 25 May 2025 19:59:56 +0800 Subject: [PATCH] feat: cross row select (#867) --- lib/pages/article/view.dart | 216 +++++++++-------- lib/pages/article/widgets/html_render.dart | 24 +- lib/pages/article/widgets/opus_content.dart | 252 ++++++++++---------- lib/pages/article/widgets/read_opus.dart | 7 +- 4 files changed, 249 insertions(+), 250 deletions(-) diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index e843b9bb..63a92e8d 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -268,25 +268,29 @@ class _ArticlePageState extends State return LayoutBuilder(builder: (context, constraints) { final maxWidth = constraints.maxWidth - 2 * padding - 24; return Padding( - padding: EdgeInsets.symmetric(horizontal: padding), - child: CustomScrollView( - controller: _articleCtr.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - _buildContent(theme, maxWidth), - SliverToBoxAdapter( - child: Divider( - thickness: 8, - color: - theme.dividerColor.withValues(alpha: 0.05), - ), + padding: EdgeInsets.symmetric(horizontal: padding), + child: SelectionArea( + child: CustomScrollView( + controller: _articleCtr.scrollController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + _buildContent(theme, maxWidth), + SelectionContainer.disabled( + child: SliverToBoxAdapter( + child: Divider( + 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 { return Row( @@ -298,7 +302,8 @@ class _ArticlePageState extends State builder: (context, constraints) { final maxWidth = constraints.maxWidth - padding / 4 - 24; - return CustomScrollView( + return SelectionArea( + child: CustomScrollView( controller: _articleCtr.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ @@ -312,7 +317,7 @@ class _ArticlePageState extends State sliver: _buildContent(theme, maxWidth), ), ], - ); + )); }, ), ), @@ -421,90 +426,89 @@ class _ArticlePageState extends State ?.pics?.isNotEmpty == true) SliverToBoxAdapter( - child: Builder( - builder: (context) { - final pics = _articleCtr.opusData!.modules.moduleTop! - .display!.album!.pics!; - final length = pics.length; - final first = pics.first; - double height; - double paddingRight; - if (first.height != null && first.width != null) { - final ratio = first.height! / first.width!; - height = min(maxWidth * ratio, Get.height * 0.55); - paddingRight = (maxWidth - height / ratio) / 2 + 12; - } else { - height = Get.height * 0.55; - paddingRight = 12; - } - return Stack( - clipBehavior: Clip.none, - children: [ - Container( - height: height, - width: maxWidth, - margin: const EdgeInsets.only(bottom: 10), - child: PageView.builder( - physics: const ClampingScrollPhysics(), - onPageChanged: (value) { - _articleCtr.topIndex.value = value; - }, - itemCount: length, - itemBuilder: (context, index) { - final pic = pics[index]; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => context.imageView( - imgList: pics - .map( - (e) => SourceModel(url: e.url!)) - .toList(), - initialPage: index, - ), - child: Hero( - tag: pic.url!, - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Positioned.fill( - child: CachedNetworkImage( - fit: pic.isLongPic == true - ? BoxFit.cover - : null, - imageUrl: Utils.thumbnailImgUrl( - pic.url, 60), - ), + child: SelectionContainer.disabled(child: Builder( + builder: (context) { + final pics = _articleCtr + .opusData!.modules.moduleTop!.display!.album!.pics!; + final length = pics.length; + final first = pics.first; + double height; + double paddingRight; + if (first.height != null && first.width != null) { + final ratio = first.height! / first.width!; + height = min(maxWidth * ratio, Get.height * 0.55); + paddingRight = (maxWidth - height / ratio) / 2 + 12; + } else { + height = Get.height * 0.55; + paddingRight = 12; + } + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: height, + width: maxWidth, + margin: const EdgeInsets.only(bottom: 10), + child: PageView.builder( + physics: const ClampingScrollPhysics(), + onPageChanged: (value) { + _articleCtr.topIndex.value = value; + }, + itemCount: length, + itemBuilder: (context, index) { + final pic = pics[index]; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context.imageView( + imgList: pics + .map((e) => SourceModel(url: e.url!)) + .toList(), + initialPage: index, + ), + child: Hero( + tag: pic.url!, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Positioned.fill( + child: CachedNetworkImage( + fit: pic.isLongPic == true + ? BoxFit.cover + : null, + imageUrl: Utils.thumbnailImgUrl( + pic.url, 60), ), - if (pic.isLongPic == true) - PBadge( - text: '长图', - type: PBadgeType.primary, - right: paddingRight, - bottom: 12, - ), - ], - ), + ), + if (pic.isLongPic == true) + PBadge( + text: '长图', + type: PBadgeType.primary, + right: paddingRight, + bottom: 12, + ), + ], ), - ); - }, - ), + ), + ); + }, ), - Obx( - () => PBadge( - top: 12, - right: paddingRight, - type: PBadgeType.gray, - text: - '${_articleCtr.topIndex.value + 1}/$length'), - ), - ], - ); - }, - ), - ), + ), + Obx( + () => PBadge( + top: 12, + right: paddingRight, + type: PBadgeType.gray, + text: + '${_articleCtr.topIndex.value + 1}/$length'), + ), + ], + ); + }, + ))), if (_articleCtr.summary.title != null) - SliverToBoxAdapter( + SelectionContainer.disabled( + child: SliverToBoxAdapter( child: Text( _articleCtr.summary.title!, style: const TextStyle( @@ -512,8 +516,9 @@ class _ArticlePageState extends State fontWeight: FontWeight.bold, ), ), - ), - SliverToBoxAdapter( + )), + SelectionContainer.disabled( + child: SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: GestureDetector( @@ -554,15 +559,16 @@ class _ArticlePageState extends State ), ), ), - ), + )), if (_articleCtr.type != 'read' && _articleCtr.opusData?.modules.moduleCollection != null) - SliverToBoxAdapter( + SelectionContainer.disabled( + child: SliverToBoxAdapter( child: opusCollection( theme, _articleCtr.opusData!.modules.moduleCollection!, ), - ), + )), content, ], ); diff --git a/lib/pages/article/widgets/html_render.dart b/lib/pages/article/widgets/html_render.dart index be9a9020..7ad1bb7b 100644 --- a/lib/pages/article/widgets/html_render.dart +++ b/lib/pages/article/widgets/html_render.dart @@ -126,17 +126,15 @@ Widget htmlRender({ margin: Margins.zero, ), }; - return SelectionArea( - child: element != null - ? Html.fromElement( - documentElement: element, - extensions: extensions, - style: style, - ) - : Html( - data: html, - extensions: extensions, - style: style, - ), - ); + return element != null + ? Html.fromElement( + documentElement: element, + extensions: extensions, + style: style, + ) + : Html( + data: html, + extensions: extensions, + style: style, + ); } diff --git a/lib/pages/article/widgets/opus_content.dart b/lib/pages/article/widgets/opus_content.dart index 103c4274..eb1a72f7 100644 --- a/lib/pages/article/widgets/opus_content.dart +++ b/lib/pages/article/widgets/opus_content.dart @@ -69,78 +69,76 @@ class OpusContent extends StatelessWidget { switch (element.paraType) { case 1 || 4: final isQuote = element.paraType == 4; - Widget widget = SelectionArea( - child: Text.rich( - textAlign: element.align == 1 ? TextAlign.center : null, - TextSpan( - children: element.text?.nodes?.map((item) { - switch (item.type) { - case 'TEXT_NODE_TYPE_RICH' when (item.rich != null): - Rich rich = item.rich!; - switch (rich.type) { - case 'RICH_TEXT_NODE_TYPE_EMOJI': - Emoji emoji = rich.emoji!; - final size = 20.0 * emoji.size; - return WidgetSpan( - child: NetworkImgLayer( - width: size, - height: size, - src: emoji.url, - 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(), - ), + Widget widget = Text.rich( + textAlign: element.align == 1 ? TextAlign.center : null, + TextSpan( + children: element.text?.nodes?.map((item) { + switch (item.type) { + case 'TEXT_NODE_TYPE_RICH' when (item.rich != null): + Rich rich = item.rich!; + switch (rich.type) { + case 'RICH_TEXT_NODE_TYPE_EMOJI': + Emoji emoji = rich.emoji!; + final size = 20.0 * emoji.size; + return WidgetSpan( + child: NetworkImgLayer( + width: size, + height: size, + src: emoji.url, + type: ImageType.emote, ), - ], - ); - default: - return _getSpan( - item.word, - isQuote ? colorScheme.onSurfaceVariant : null, - ); - } - }).toList()), - ), + ); + 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: + return _getSpan( + item.word, + isQuote ? colorScheme.onSurfaceVariant : null, + ); + } + }).toList()), ); if (isQuote) { widget = Container( @@ -167,7 +165,8 @@ class OpusContent extends StatelessWidget { final height = width == null || pic.height == null ? null : width * pic.height! / pic.width!; - return Hero( + return SelectionContainer.disabled( + child: Hero( tag: pic.url!, child: GestureDetector( onTap: () { @@ -192,42 +191,42 @@ class OpusContent extends StatelessWidget { ), ), ), - ); + )); } else { - return imageView( - maxWidth, - element.pic!.pics! - .map( - (e) => ImageModel(width: 1, height: 1, url: e.url!)) - .toList()); + return SelectionContainer.disabled( + child: imageView( + maxWidth, + element.pic!.pics! + .map((e) => + ImageModel(width: 1, height: 1, url: e.url!)) + .toList())); } case 3 when (element.line != null): - return CachedNetworkImage( + return SelectionContainer.disabled( + child: CachedNetworkImage( width: maxWidth, fit: BoxFit.contain, height: element.line!.pic!.height?.toDouble(), imageUrl: Utils.thumbnailImgUrl(element.line!.pic!.url!), - ); + )); case 5 when (element.list != null): - return SelectionArea( - child: Text.rich( - TextSpan( - children: element.list!.items?.indexed.map((entry) { - return TextSpan( - children: [ - const WidgetSpan( - child: Icon(MdiIcons.circleMedium), - alignment: PlaceholderAlignment.middle, - ), - ...entry.$2.nodes!.map((item) { - return _getSpan(item.word); - }), - if (entry.$1 < element.list!.items!.length - 1) - const TextSpan(text: '\n'), - ], - ); - }).toList(), - ), + return Text.rich( + TextSpan( + children: element.list!.items?.indexed.map((entry) { + return TextSpan( + children: [ + const WidgetSpan( + child: Icon(MdiIcons.circleMedium), + alignment: PlaceholderAlignment.middle, + ), + ...entry.$2.nodes!.map((item) { + return _getSpan(item.word); + }), + if (entry.$1 < element.list!.items!.length - 1) + const TextSpan(text: '\n'), + ], + ); + }).toList(), ), ); case 6: @@ -528,7 +527,7 @@ class OpusContent extends StatelessWidget { ), ); case 7 when (element.code != null): - final Highlight highlight = Highlight() + final highlight = Highlight() ..registerLanguages(builtinAllLanguages); final HighlightResult result = highlight.highlightAuto( element.code!.content!, @@ -539,7 +538,7 @@ class OpusContent extends StatelessWidget { .replaceAll('language-', '') .replaceAll('like', ''), ]); - final TextSpanRenderer renderer = TextSpanRenderer( + final renderer = TextSpanRenderer( const TextStyle(), builtinAllThemes['github']!); result.render(renderer); return Container( @@ -549,40 +548,34 @@ class OpusContent extends StatelessWidget { color: colorScheme.onInverseSurface, ), width: double.infinity, - child: SelectionArea(child: Text.rich(renderer.span!)), + child: Text.rich(renderer.span!), ); default: debugPrint('unknown type ${element.paraType}'); if (element.text?.nodes?.isNotEmpty == true) { - return SelectionArea( - child: Text.rich( - textAlign: element.align == 1 ? TextAlign.center : null, - TextSpan( - children: element.text!.nodes! - .map((item) => _getSpan(item.word)) - .toList()), - ), + return Text.rich( + textAlign: element.align == 1 ? TextAlign.center : null, + TextSpan( + children: element.text!.nodes! + .map((item) => _getSpan(item.word)) + .toList()), ); } - return SelectionArea( - child: Text( - '不支持的类型 (${element.paraType})', - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.red, - ), + return Text( + '不支持的类型 (${element.paraType})', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, ), ); } } catch (e) { - return SelectionArea( - child: Text( - '错误的类型 $e', - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.red, - ), + return Text( + '错误的类型 $e', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, ), ); } @@ -597,7 +590,7 @@ Widget moduleBlockedItem( BoxDecoration? bgImg() { return moduleBlocked.bgImg == null ? null - : BoxDecoration( + : (BoxDecoration( image: DecorationImage( fit: BoxFit.fill, image: CachedNetworkImageProvider( @@ -608,7 +601,7 @@ Widget moduleBlockedItem( ), ), ), - ); + )); } Widget icon(double width) { @@ -690,7 +683,8 @@ Widget moduleBlockedItem( ), ); } - return Container( + return SelectionContainer.disabled( + child: Container( decoration: bgImg(), padding: const EdgeInsets.all(12), child: Row( @@ -726,7 +720,7 @@ Widget moduleBlockedItem( ), ], ), - ); + )); } Widget opusCollection(ThemeData theme, ModuleCollection item) { diff --git a/lib/pages/article/widgets/read_opus.dart b/lib/pages/article/widgets/read_opus.dart index 4fb67e27..0109a0a7 100644 --- a/lib/pages/article/widgets/read_opus.dart +++ b/lib/pages/article/widgets/read_opus.dart @@ -25,13 +25,14 @@ class ReadOpus extends StatelessWidget { try { final item = ops![index]; if (item.insert is String) { - return SelectableText(item.insert); + return Text(item.insert); } if (item.insert is Insert) { InsertCard card = item.insert.card; if (card.url?.isNotEmpty == true) { - return GestureDetector( + return SelectionContainer.disabled( + child: GestureDetector( onTap: () { switch (item.attributes?.clazz) { case 'article-card card': @@ -60,7 +61,7 @@ class ReadOpus extends StatelessWidget { imageUrl: Utils.thumbnailImgUrl(card.url, 60), ), ), - ); + )); } }