mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-13 12:50:10 +08:00
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user