Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-07 11:54:28 +08:00
parent cae5befa63
commit 1946218df3
21 changed files with 446 additions and 334 deletions

View File

@@ -2,13 +2,14 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart';
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models/dynamics/article_content_model.dart' show Pic;
import 'package:PiliPlus/models/dynamics/result.dart' show DynamicStat;
import 'package:PiliPlus/pages/article/controller.dart';
import 'package:PiliPlus/pages/article/widgets/article_ops.dart';
@@ -26,7 +27,7 @@ import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/share_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image_ce/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide PageView;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@@ -73,7 +74,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
double padding = max(maxWidth / 2 - Grid.smallCardWidth, 0);
if (isPortrait) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
padding: .symmetric(horizontal: padding),
child: SelectionArea(
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
@@ -103,7 +104,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
final flex = controller.ratio[0].toInt();
final flex1 = controller.ratio[1].toInt();
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: .start,
children: [
Expanded(
flex: flex,
@@ -112,7 +113,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(
padding: .only(
left: padding,
bottom: this.padding.bottom + 100,
),
@@ -135,7 +136,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
Expanded(
flex: flex1,
child: Padding(
padding: EdgeInsets.only(right: padding),
padding: .only(right: padding),
child: Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
@@ -157,7 +158,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
}
Widget _buildContent(double maxWidth) => SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const .symmetric(horizontal: 12, vertical: 8),
sliver: Obx(
() {
if (controller.isLoaded.value) {
@@ -203,185 +204,28 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
maxWidth: maxWidth,
);
},
separatorBuilder: (context, index) =>
const SizedBox(height: 10),
separatorBuilder: (_, _) => const SizedBox(height: 10),
);
}
} else {
content = const SliverToBoxAdapter(child: Text('NULL'));
}
final pubTime =
controller.opusData?.modules.moduleAuthor?.pubTs ??
controller.articleData?.publishTime;
return SliverMainAxisGroup(
slivers: [
if (controller.type != 'read')
if (controller.opusData?.modules.moduleTop?.display?.album?.pics
case final pics? when pics.isNotEmpty)
SliverToBoxAdapter(
child: Builder(
builder: (context) {
final length = pics.length;
final first = pics.first;
double height;
if (first.height != null && first.width != null) {
final ratio = first.height! / first.width!;
height = min(maxWidth * ratio, maxHeight * 0.55);
} else {
height = maxHeight * 0.55;
}
return Stack(
clipBehavior: Clip.none,
children: [
Container(
height: height,
width: maxWidth,
margin: const EdgeInsets.only(bottom: 10),
child: PageView.builder(
physics: clampingScrollPhysics,
onPageChanged: (value) =>
controller.topIndex.value = value,
itemCount: length,
itemBuilder: (context, index) {
final pic = pics[index];
int? memCacheWidth, memCacheHeight;
if (pic.isLongPic ?? false) {
memCacheWidth = maxWidth.cacheSize(context);
} else if (pic.width != null &&
pic.height != null) {
if (pic.width! > pic.height!) {
memCacheWidth = maxWidth.cacheSize(
context,
);
} else {
memCacheHeight = height.cacheSize(
context,
);
}
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => PageUtils.imageView(
quality: 60,
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: [
CachedNetworkImage(
height: height,
width: maxWidth,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
fit: pic.isLongPic == true
? BoxFit.cover
: null,
imageUrl: ImageUtils.thumbnailUrl(
pic.url,
60,
),
fadeInDuration: const Duration(
milliseconds: 120,
),
fadeOutDuration: const Duration(
milliseconds: 120,
),
placeholder: (_, _) =>
const SizedBox.shrink(),
),
if (pic.isLongPic == true)
const PBadge(
right: 12,
bottom: 12,
text: '长图',
type: .primary,
),
],
),
),
);
},
),
),
Obx(
() => PBadge(
top: 12,
right: 12,
type: PBadgeType.gray,
text:
'${controller.topIndex.value + 1}/$length',
),
),
],
);
},
),
),
SliverToBoxAdapter(child: _buildImageGallery(pics)),
if (controller.summary.title != null)
SliverToBoxWithVisibilityAdapter(
onVisibilityChanged: (bool visible) =>
controller.showTitle.value = !visible,
onVisibilityChanged: controller.showTitle.call,
child: Text(
controller.summary.title!,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
style: const TextStyle(fontSize: 17, fontWeight: .bold),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: GestureDetector(
onTap: () => Get.toNamed(
'/member?mid=${controller.summary.author?.mid}',
),
child: SelectionContainer.disabled(
child: Row(
children: [
NetworkImgLayer(
width: 40,
height: 40,
type: ImageType.avatar,
src: controller.summary.author?.face,
),
const SizedBox(width: 10),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.summary.author?.name ?? '',
style: TextStyle(
fontSize:
theme.textTheme.titleSmall!.fontSize,
),
),
if (pubTime != null)
Text(
DateFormatUtils.format(pubTime),
style: TextStyle(
color: theme.colorScheme.outline,
fontSize:
theme.textTheme.labelSmall!.fontSize,
),
),
],
),
),
],
),
),
),
),
),
SliverToBoxAdapter(child: _buildAuthor()),
if (controller.type != 'read' &&
controller.opusData?.modules.moduleCollection != null)
SliverToBoxAdapter(
@@ -405,7 +249,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
PreferredSizeWidget _buildAppBar() => AppBar(
title: Obx(() {
if (controller.isLoaded.value && controller.showTitle.value) {
return Text(controller.summary.title ?? '');
return Text(controller.summary.title!);
}
return const SizedBox.shrink();
}),
@@ -423,10 +267,10 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
PopupMenuItem(
onTap: () => ShareUtils.shareText(controller.url),
child: const Row(
mainAxisSize: MainAxisSize.min,
spacing: 10,
mainAxisSize: .min,
children: [
Icon(Icons.share_outlined, size: 19),
SizedBox(width: 10),
Text('分享'),
],
),
@@ -434,10 +278,10 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
PopupMenuItem(
onTap: () => Utils.copyText(controller.url),
child: const Row(
mainAxisSize: MainAxisSize.min,
spacing: 10,
mainAxisSize: .min,
children: [
Icon(Icons.copy_rounded, size: 19),
SizedBox(width: 10),
Text('复制链接'),
],
),
@@ -473,10 +317,10 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
}
},
child: const Row(
mainAxisSize: MainAxisSize.min,
spacing: 10,
mainAxisSize: .min,
children: [
Icon(Icons.forward_to_inbox, size: 19),
SizedBox(width: 10),
Text('分享至消息'),
],
),
@@ -496,7 +340,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
late final outline = theme.colorScheme.outline;
late final btnStyle = TextButton.styleFrom(
tapTargetSize: .padded,
padding: const EdgeInsets.symmetric(horizontal: 15),
padding: const .symmetric(horizontal: 15),
foregroundColor: outline,
);
@@ -530,7 +374,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
final stats = controller.stats.value;
Widget btn = Padding(
padding: EdgeInsets.only(
padding: .only(
right: kFloatingActionButtonMargin,
bottom:
kFloatingActionButtonMargin +
@@ -540,15 +384,12 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
);
if (stats == null) {
return Align(
alignment: Alignment.bottomRight,
child: btn,
);
return Align(alignment: .bottomRight, child: btn);
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: .min,
crossAxisAlignment: .end,
children: [
btn,
Container(
@@ -556,13 +397,11 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
color: theme.colorScheme.surface,
border: Border(
top: BorderSide(
color: theme.colorScheme.outline.withValues(
alpha: 0.08,
),
color: theme.colorScheme.outline.withValues(alpha: 0.08),
),
),
),
padding: EdgeInsets.only(bottom: padding.bottom),
padding: .only(bottom: padding.bottom),
child: Row(
children: [
Expanded(
@@ -643,4 +482,141 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
}),
);
}
Widget? _buildImageGallery(List<Pic> pics) {
final length = pics.length;
final first = pics.first;
double height;
if (first.height != null && first.width != null) {
final ratio = first.height! / first.width!;
height = min(maxWidth * ratio, maxHeight * 0.55);
} else {
height = maxHeight * 0.55;
}
return Stack(
clipBehavior: .none,
children: [
Container(
height: height,
width: maxWidth,
margin: const .only(bottom: 10),
child: PageView<CustomHorizontalDragGestureRecognizer>.builder(
physics: clampingScrollPhysics,
horizontalDragGestureRecognizer:
CustomHorizontalDragGestureRecognizer.new,
onPageChanged: controller.topIndex.call,
itemCount: length,
itemBuilder: (context, index) {
final pic = pics[index];
int? memCacheWidth, memCacheHeight;
if (pic.isLongPic ?? false) {
memCacheWidth = maxWidth.cacheSize(context);
} else if (pic.width != null && pic.height != null) {
if (pic.width! > pic.height!) {
memCacheWidth = maxWidth.cacheSize(
context,
);
} else {
memCacheHeight = height.cacheSize(
context,
);
}
}
return GestureDetector(
behavior: .opaque,
onTap: () => PageUtils.imageView(
quality: 60,
imgList: pics.map((e) => SourceModel(url: e.url!)).toList(),
initialPage: index,
),
child: Hero(
tag: pic.url!,
child: Stack(
clipBehavior: .none,
alignment: Alignment.center,
children: [
CachedNetworkImage(
height: height,
width: maxWidth,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
fit: pic.isLongPic == true ? BoxFit.cover : null,
imageUrl: ImageUtils.thumbnailUrl(pic.url, 60),
fadeInDuration: const Duration(milliseconds: 120),
fadeOutDuration: const Duration(milliseconds: 120),
placeholder: (_, _) => const SizedBox.shrink(),
),
if (pic.isLongPic == true)
const PBadge(
right: 12,
bottom: 12,
text: '长图',
type: .primary,
),
],
),
),
);
},
),
),
Obx(
() => PBadge(
top: 12,
right: 12,
type: .gray,
text: '${controller.topIndex.value + 1}/$length',
),
),
],
);
}
Widget? _buildAuthor() {
final pubTime =
controller.opusData?.modules.moduleAuthor?.pubTs ??
controller.articleData?.publishTime;
return Padding(
padding: const .symmetric(vertical: 10),
child: GestureDetector(
onTap: () => Get.toNamed(
'/member?mid=${controller.summary.author?.mid}',
),
child: SelectionContainer.disabled(
child: Row(
spacing: 10,
children: [
NetworkImgLayer(
width: 40,
height: 40,
type: .avatar,
src: controller.summary.author?.face,
),
Flexible(
child: Column(
crossAxisAlignment: .start,
children: [
Text(
controller.summary.author?.name ?? '',
style: TextStyle(
fontSize: theme.textTheme.titleSmall!.fontSize,
),
),
if (pubTime != null)
Text(
DateFormatUtils.format(pubTime),
style: TextStyle(
color: theme.colorScheme.outline,
fontSize: theme.textTheme.labelSmall!.fontSize,
),
),
],
),
),
],
),
),
),
);
}
}