💄 Optimized post list
This commit is contained in:
parent
a04bfe4cf9
commit
32c33a963a
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -87,28 +88,70 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
|
|
||||||
final scrollProgress =
|
final scrollProgress =
|
||||||
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
|
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
|
||||||
final backgroundColor = Color.lerp(
|
final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0;
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerLow
|
|
||||||
.withOpacity(0),
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerLow
|
|
||||||
.withOpacity(0.9),
|
|
||||||
scrollProgress,
|
|
||||||
);
|
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
backgroundColor: backgroundColor,
|
flexibleSpace: ClipRRect(
|
||||||
flexibleSpace: SizedBox(
|
child: BackdropFilter(
|
||||||
height: 48,
|
filter: ImageFilter.blur(
|
||||||
child: const Row(
|
sigmaX: blurSigma,
|
||||||
children: [
|
sigmaY: blurSigma,
|
||||||
RealmSwitcher(),
|
),
|
||||||
],
|
child: ListView(
|
||||||
).paddingSymmetric(horizontal: 8),
|
padding: EdgeInsets.zero,
|
||||||
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
RealmSwitcher(),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
dividerHeight: 0.3,
|
||||||
|
tabAlignment: TabAlignment.fill,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.feed, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListNews'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.people, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListFriends'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.shuffle_on_outlined,
|
||||||
|
size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListShuffle'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expandedHeight: 96,
|
||||||
snap: true,
|
snap: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
@ -120,43 +163,6 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
dividerHeight: 0.3,
|
|
||||||
tabAlignment: TabAlignment.fill,
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.feed, size: 20),
|
|
||||||
const Gap(8),
|
|
||||||
Text('postListNews'.tr),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.people, size: 20),
|
|
||||||
const Gap(8),
|
|
||||||
Text('postListFriends'.tr),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.shuffle_on_outlined, size: 20),
|
|
||||||
const Gap(8),
|
|
||||||
Text('postListShuffle'.tr),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -180,6 +186,12 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
onRefresh: () => _postController.reloadAllOver(),
|
onRefresh: () => _postController.reloadAllOver(),
|
||||||
child: CustomScrollView(slivers: [
|
child: CustomScrollView(slivers: [
|
||||||
ControlledPostListWidget(
|
ControlledPostListWidget(
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
onUpdate: () => _postController.reloadAllOver(),
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
@ -191,6 +203,9 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
onRefresh: () => _postController.reloadAllOver(),
|
onRefresh: () => _postController.reloadAllOver(),
|
||||||
child: CustomScrollView(slivers: [
|
child: CustomScrollView(slivers: [
|
||||||
ControlledPostListWidget(
|
ControlledPostListWidget(
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(horizontal: 16)
|
||||||
|
: EdgeInsets.zero,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
onUpdate: () => _postController.reloadAllOver(),
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
|
@ -4,6 +4,7 @@ import 'package:solian/exts.dart';
|
|||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/content/posts.dart';
|
import 'package:solian/providers/content/posts.dart';
|
||||||
import 'package:solian/providers/last_read.dart';
|
import 'package:solian/providers/last_read.dart';
|
||||||
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
import 'package:solian/widgets/posts/post_replies.dart';
|
import 'package:solian/widgets/posts/post_replies.dart';
|
||||||
|
|
||||||
@ -67,11 +68,18 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
isFullContent: true,
|
isFullContent: true,
|
||||||
isShowReply: false,
|
isShowReply: false,
|
||||||
isContentSelectable: true,
|
isContentSelectable: true,
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child:
|
child: const Divider(thickness: 0.3, height: 1).paddingOnly(
|
||||||
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4),
|
top: 8,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Align(
|
child: Align(
|
||||||
@ -82,7 +90,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
).paddingOnly(left: 24, right: 24, top: 16),
|
).paddingOnly(left: 24, right: 24, top: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PostReplyList(item: item!),
|
PostReplyList(
|
||||||
|
item: item!,
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||||
),
|
),
|
||||||
|
@ -89,8 +89,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.75,
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
child: Column(
|
child: ListView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
AccountHeadingWidget(
|
AccountHeadingWidget(
|
||||||
avatar: _userinfo!.avatar,
|
avatar: _userinfo!.avatar,
|
||||||
|
@ -155,11 +155,18 @@ class _AttachmentItemImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (showBadge && badge != null)
|
if (showBadge && badge != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 12,
|
right: 8,
|
||||||
bottom: 8,
|
bottom: 4,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Chip(label: Text(badge!)),
|
child: Chip(
|
||||||
|
label: Text(badge!),
|
||||||
|
labelStyle: GoogleFonts.robotoMono(),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showHideButton && item.isMature)
|
if (showHideButton && item.isMature)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/material.dart' hide CarouselController;
|
import 'package:flutter/material.dart' hide CarouselController;
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
@ -23,6 +22,7 @@ class AttachmentList extends StatefulWidget {
|
|||||||
final bool autoload;
|
final bool autoload;
|
||||||
final double columnMaxWidth;
|
final double columnMaxWidth;
|
||||||
|
|
||||||
|
final EdgeInsets? padding;
|
||||||
final double? width;
|
final double? width;
|
||||||
final double? viewport;
|
final double? viewport;
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ class AttachmentList extends StatefulWidget {
|
|||||||
this.isFullWidth = false,
|
this.isFullWidth = false,
|
||||||
this.autoload = false,
|
this.autoload = false,
|
||||||
this.columnMaxWidth = 480,
|
this.columnMaxWidth = 480,
|
||||||
|
this.padding,
|
||||||
this.width,
|
this.width,
|
||||||
this.viewport,
|
this.viewport,
|
||||||
});
|
});
|
||||||
@ -161,9 +162,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
color: _unFocusColor,
|
color: _unFocusColor,
|
||||||
).paddingOnly(right: 5),
|
).paddingOnly(right: 5),
|
||||||
Text(
|
Text(
|
||||||
'attachmentHint'.trParams(
|
'attachmentHint'.trParams({'count': _attachments.toString()}),
|
||||||
{'count': _attachments.toString()},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -179,8 +178,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
final element = _attachments.first;
|
final element = _attachments.first;
|
||||||
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
||||||
return Container(
|
return Container(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: widget.columnMaxWidth,
|
|
||||||
maxHeight: 640,
|
maxHeight: 640,
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
@ -271,26 +270,26 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return Container(
|
||||||
width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth),
|
constraints: BoxConstraints(
|
||||||
child: CarouselSlider.builder(
|
maxHeight: 320,
|
||||||
options: CarouselOptions(
|
),
|
||||||
disableCenter: true,
|
child: ListView.separated(
|
||||||
animateToClosest: true,
|
padding: widget.padding,
|
||||||
aspectRatio: _aspectRatio,
|
scrollDirection: Axis.horizontal,
|
||||||
enlargeCenterPage: true,
|
shrinkWrap: true,
|
||||||
viewportFraction: widget.viewport ?? 0.95,
|
|
||||||
enableInfiniteScroll: false,
|
|
||||||
),
|
|
||||||
itemCount: _attachments.length,
|
itemCount: _attachments.length,
|
||||||
itemBuilder: (context, idx, _) {
|
itemBuilder: (context, idx) {
|
||||||
final element = _attachments[idx];
|
final element = _attachments[idx];
|
||||||
if (element == null) const SizedBox.shrink();
|
if (element == null) const SizedBox.shrink();
|
||||||
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
final ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: widget.columnMaxWidth,
|
maxWidth: math.min(
|
||||||
maxHeight: 640,
|
widget.columnMaxWidth,
|
||||||
|
MediaQuery.of(context).size.width -
|
||||||
|
(widget.padding?.horizontal ?? 0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
@ -310,6 +309,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
separatorBuilder: (context, _) => const Gap(8),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,21 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/link.dart';
|
||||||
import 'package:solian/providers/link_expander.dart';
|
import 'package:solian/providers/link_expander.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class LinkExpansion extends StatelessWidget {
|
class LinkExpansion extends StatefulWidget {
|
||||||
final String content;
|
final String content;
|
||||||
|
|
||||||
const LinkExpansion({super.key, required this.content});
|
const LinkExpansion({super.key, required this.content});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LinkExpansion> createState() => _LinkExpansionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkExpansionState extends State<LinkExpansion> {
|
||||||
Widget _buildImage(String url, {double? width, double? height}) {
|
Widget _buildImage(String url, {double? width, double? height}) {
|
||||||
if (url.endsWith('svg')) {
|
if (url.endsWith('svg')) {
|
||||||
return SvgPicture.network(url, width: width, height: height);
|
return SvgPicture.network(url, width: width, height: height);
|
||||||
@ -22,61 +28,74 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
List<LinkMeta>? _meta;
|
||||||
Widget build(BuildContext context) {
|
|
||||||
|
Future<void> _doExpand() async {
|
||||||
final linkRegex = RegExp(
|
final linkRegex = RegExp(
|
||||||
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
||||||
);
|
);
|
||||||
final matches = linkRegex.allMatches(content);
|
final matches = linkRegex.allMatches(widget.content);
|
||||||
if (matches.isEmpty) {
|
if (matches.isEmpty) return;
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final LinkExpandProvider expandController = Get.find();
|
final LinkExpandProvider expandController = Get.find();
|
||||||
|
|
||||||
|
if (matches.isEmpty) return;
|
||||||
|
|
||||||
|
List<LinkMeta> out = List.empty(growable: true);
|
||||||
|
for (final x in matches) {
|
||||||
|
final result = await expandController.expandLink(x.group(0)!);
|
||||||
|
if (result != null) out.add(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _meta = out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_doExpand();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_meta?.isEmpty ?? true) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
children: matches.map((x) {
|
children: _meta!.map((x) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: matches.length == 1 ? 480 : 340,
|
maxWidth: _meta!.length == 1 ? 480 : 340,
|
||||||
),
|
),
|
||||||
child: FutureBuilder(
|
child: Builder(
|
||||||
future: expandController.expandLink(x.group(0)!),
|
builder: (context) {
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final isRichDescription = [
|
final isRichDescription = [
|
||||||
'solsynth.dev',
|
'solsynth.dev',
|
||||||
].contains(Uri.parse(snapshot.data!.url).host);
|
].contains(Uri.parse(x.url).host);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if ([
|
if ([(x.icon?.isNotEmpty ?? false), x.siteName != null]
|
||||||
(snapshot.data!.icon?.isNotEmpty ?? false),
|
.any((x) => x))
|
||||||
snapshot.data!.siteName != null
|
|
||||||
].any((x) => x))
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (snapshot.data!.icon?.isNotEmpty ?? false)
|
if (x.icon?.isNotEmpty ?? false)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: _buildImage(
|
child: _buildImage(
|
||||||
snapshot.data!.icon!,
|
x.icon!,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
).paddingOnly(right: 8),
|
).paddingOnly(right: 8),
|
||||||
if (snapshot.data!.siteName != null)
|
if (x.siteName != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
snapshot.data!.siteName!,
|
x.siteName!,
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -84,32 +103,27 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
|
bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4,
|
||||||
? 8
|
|
||||||
: 4,
|
|
||||||
),
|
),
|
||||||
if (snapshot.data!.image != null &&
|
if (x.image != null &&
|
||||||
(snapshot.data!.image?.startsWith('http') ?? false))
|
(x.image?.startsWith('http') ?? false))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: _buildImage(
|
child: _buildImage(x.image!),
|
||||||
snapshot.data!.image!,
|
|
||||||
),
|
|
||||||
).paddingOnly(bottom: 8),
|
).paddingOnly(bottom: 8),
|
||||||
Text(
|
Text(
|
||||||
snapshot.data!.title ?? 'No Title',
|
x.title ?? 'No Title',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
if (snapshot.data!.description != null &&
|
if (x.description != null && isRichDescription)
|
||||||
isRichDescription)
|
MarkdownBody(data: x.description!)
|
||||||
MarkdownBody(data: snapshot.data!.description!)
|
else if (x.description != null)
|
||||||
else if (snapshot.data!.description != null)
|
|
||||||
Text(
|
Text(
|
||||||
snapshot.data!.description!,
|
x.description!,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -117,7 +131,7 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
).paddingAll(12),
|
).paddingAll(12),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(x.group(0)!);
|
launchUrlString(x.url);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/content/posts.dart';
|
import 'package:solian/providers/content/posts.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/posts/post_detail.dart';
|
import 'package:solian/screens/posts/post_detail.dart';
|
||||||
import 'package:solian/shells/title_shell.dart';
|
import 'package:solian/shells/title_shell.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
@ -34,7 +35,10 @@ class PostItem extends StatefulWidget {
|
|||||||
final bool isContentSelectable;
|
final bool isContentSelectable;
|
||||||
final bool showFeaturedReply;
|
final bool showFeaturedReply;
|
||||||
final String? attachmentParent;
|
final String? attachmentParent;
|
||||||
|
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
final Function? onComment;
|
final Function? onComment;
|
||||||
|
|
||||||
const PostItem({
|
const PostItem({
|
||||||
@ -51,6 +55,7 @@ class PostItem extends StatefulWidget {
|
|||||||
this.isContentSelectable = false,
|
this.isContentSelectable = false,
|
||||||
this.showFeaturedReply = false,
|
this.showFeaturedReply = false,
|
||||||
this.attachmentParent,
|
this.attachmentParent,
|
||||||
|
this.padding,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.onComment,
|
this.onComment,
|
||||||
});
|
});
|
||||||
@ -126,9 +131,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
LinkExpansion(content: item.body['content']).paddingOnly(
|
LinkExpansion(content: item.body['content']).paddingOnly(
|
||||||
left: 8,
|
left: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
top: 4,
|
|
||||||
),
|
),
|
||||||
_PostFooterWidget(item: item).paddingOnly(left: 12),
|
|
||||||
if (attachments.isNotEmpty)
|
if (attachments.isNotEmpty)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -149,9 +152,8 @@ class _PostItemState extends State<PostItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OpenContainer(
|
return GestureDetector(
|
||||||
tappable: widget.isClickable,
|
child: Column(
|
||||||
closedBuilder: (_, openContainer) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_PostThumbnail(
|
_PostThumbnail(
|
||||||
@ -220,18 +222,20 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_PostFooterWidget(item: item),
|
_PostFooterWidget(item: item),
|
||||||
LinkExpansion(content: item.body['content']).paddingOnly(top: 4),
|
LinkExpansion(content: item.body['content']),
|
||||||
],
|
],
|
||||||
).paddingOnly(
|
).paddingSymmetric(
|
||||||
right: 16,
|
horizontal: (widget.padding?.horizontal ?? 0) + 16,
|
||||||
left: 16,
|
),
|
||||||
|
if (hasAttachment) const Gap(8),
|
||||||
|
_PostAttachmentWidget(
|
||||||
|
item: item,
|
||||||
|
padding: widget.padding,
|
||||||
),
|
),
|
||||||
_PostAttachmentWidget(item: item),
|
|
||||||
if (widget.showFeaturedReply)
|
if (widget.showFeaturedReply)
|
||||||
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
||||||
horizontal: 12,
|
horizontal: (widget.padding?.horizontal ?? 0) + 12,
|
||||||
),
|
),
|
||||||
if (widget.showFeaturedReply) const Gap(8),
|
|
||||||
if (widget.isShowReply || widget.isReactable)
|
if (widget.isShowReply || widget.isReactable)
|
||||||
PostQuickAction(
|
PostQuickAction(
|
||||||
isShowReply: widget.isShowReply,
|
isShowReply: widget.isShowReply,
|
||||||
@ -249,22 +253,23 @@ class _PostItemState extends State<PostItem> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
left: 14,
|
top: 8,
|
||||||
right: 14,
|
left: (widget.padding?.left ?? 0) + 14,
|
||||||
|
right: (widget.padding?.right ?? 0) + 14,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
).paddingOnly(
|
||||||
|
top: widget.padding?.top ?? 0,
|
||||||
|
bottom: widget.padding?.bottom ?? 0,
|
||||||
),
|
),
|
||||||
openBuilder: (_, __) => TitleShell(
|
onTap: () {
|
||||||
title: 'postDetail'.tr,
|
if (widget.isClickable) {
|
||||||
child: PostDetailScreen(
|
AppRouter.instance.pushNamed(
|
||||||
id: item.id.toString(),
|
'postDetail',
|
||||||
post: item,
|
pathParameters: {'id': item.id.toString()},
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
closedElevation: 0,
|
},
|
||||||
openElevation: 0,
|
|
||||||
closedColor: Colors.transparent,
|
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -293,6 +298,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@ -389,8 +395,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class _PostAttachmentWidget extends StatelessWidget {
|
class _PostAttachmentWidget extends StatelessWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const _PostAttachmentWidget({required this.item});
|
const _PostAttachmentWidget({required this.item, required this.padding});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -402,14 +409,22 @@ class _PostAttachmentWidget extends StatelessWidget {
|
|||||||
|
|
||||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
if (attachments.length == 1) {
|
if (attachments.length == 1 && !isLargeScreen) {
|
||||||
return AttachmentList(
|
return AttachmentList(
|
||||||
parentId: item.id.toString(),
|
parentId: item.id.toString(),
|
||||||
attachmentIds: item.preload == null ? attachments : null,
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
attachments: item.preload?.attachments,
|
attachments: item.preload?.attachments,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
isFullWidth: true,
|
isFullWidth: true,
|
||||||
).paddingOnly(top: 4);
|
);
|
||||||
|
} else if (attachments.length == 1) {
|
||||||
|
return AttachmentList(
|
||||||
|
parentId: item.id.toString(),
|
||||||
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
|
attachments: item.preload?.attachments,
|
||||||
|
autoload: false,
|
||||||
|
isColumn: true,
|
||||||
|
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||||
} else if (attachments.length > 1 &&
|
} else if (attachments.length > 1 &&
|
||||||
attachments.length % 3 == 0 &&
|
attachments.length % 3 == 0 &&
|
||||||
!isLargeScreen) {
|
!isLargeScreen) {
|
||||||
@ -419,14 +434,17 @@ class _PostAttachmentWidget extends StatelessWidget {
|
|||||||
attachments: item.preload?.attachments,
|
attachments: item.preload?.attachments,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
isGrid: true,
|
isGrid: true,
|
||||||
).paddingSymmetric(horizontal: 14, vertical: 8);
|
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||||
} else {
|
} else {
|
||||||
return AttachmentList(
|
return AttachmentList(
|
||||||
parentId: item.id.toString(),
|
parentId: item.id.toString(),
|
||||||
attachmentIds: item.preload == null ? attachments : null,
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
attachments: item.preload?.attachments,
|
attachments: item.preload?.attachments,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: (padding?.horizontal ?? 0) + 14,
|
||||||
|
),
|
||||||
autoload: false,
|
autoload: false,
|
||||||
).paddingOnly(bottom: 8, top: 4);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -568,7 +586,7 @@ class _PostFooterWidget extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: widgets,
|
children: widgets,
|
||||||
).paddingOnly(top: 4);
|
).paddingSymmetric(vertical: 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,9 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
final bool isClickable;
|
final bool isClickable;
|
||||||
final bool showFeaturedReply;
|
final bool showFeaturedReply;
|
||||||
final Post item;
|
final Post item;
|
||||||
final Function onUpdate;
|
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final Function onUpdate;
|
||||||
|
|
||||||
const PostListEntryWidget({
|
const PostListEntryWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@ -70,8 +71,9 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
required this.isClickable,
|
required this.isClickable,
|
||||||
required this.showFeaturedReply,
|
required this.showFeaturedReply,
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.onUpdate,
|
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.padding,
|
||||||
|
required this.onUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -83,6 +85,7 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
isShowEmbed: isShowEmbed,
|
isShowEmbed: isShowEmbed,
|
||||||
isClickable: isNestedClickable,
|
isClickable: isNestedClickable,
|
||||||
showFeaturedReply: showFeaturedReply,
|
showFeaturedReply: showFeaturedReply,
|
||||||
|
padding: padding,
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
onComment: () {
|
onComment: () {
|
||||||
AppRouter.instance
|
AppRouter.instance
|
||||||
@ -129,6 +132,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
|||||||
final bool isNestedClickable;
|
final bool isNestedClickable;
|
||||||
final bool isPinned;
|
final bool isPinned;
|
||||||
final PagingController<int, Post> controller;
|
final PagingController<int, Post> controller;
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Function? onUpdate;
|
final Function? onUpdate;
|
||||||
|
|
||||||
const ControlledPostListWidget({
|
const ControlledPostListWidget({
|
||||||
@ -138,6 +142,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
|||||||
this.isClickable = true,
|
this.isClickable = true,
|
||||||
this.isNestedClickable = true,
|
this.isNestedClickable = true,
|
||||||
this.isPinned = true,
|
this.isPinned = true,
|
||||||
|
this.padding,
|
||||||
this.onUpdate,
|
this.onUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,6 +161,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
|||||||
isNestedClickable: isNestedClickable,
|
isNestedClickable: isNestedClickable,
|
||||||
isClickable: isClickable,
|
isClickable: isClickable,
|
||||||
showFeaturedReply: true,
|
showFeaturedReply: true,
|
||||||
|
padding: padding,
|
||||||
item: item,
|
item: item,
|
||||||
onUpdate: onUpdate ?? () {},
|
onUpdate: onUpdate ?? () {},
|
||||||
);
|
);
|
||||||
|
@ -8,11 +8,13 @@ import 'package:solian/widgets/posts/post_list.dart';
|
|||||||
|
|
||||||
class PostReplyList extends StatefulWidget {
|
class PostReplyList extends StatefulWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
const PostReplyList({
|
const PostReplyList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
this.padding,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PostListWidget(
|
return PostListWidget(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
padding: widget.padding,
|
||||||
isShowEmbed: false,
|
isShowEmbed: false,
|
||||||
controller: _pagingController,
|
controller: _pagingController,
|
||||||
backgroundColor: widget.backgroundColor,
|
backgroundColor: widget.backgroundColor,
|
||||||
@ -93,6 +95,7 @@ class PostReplyListPopup extends StatelessWidget {
|
|||||||
slivers: [
|
slivers: [
|
||||||
PostReplyList(
|
PostReplyList(
|
||||||
item: item,
|
item: item,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
),
|
),
|
||||||
|
12
pubspec.lock
12
pubspec.lock
@ -198,14 +198,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
carousel_slider:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: carousel_slider
|
|
||||||
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "5.0.0"
|
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2278,10 +2270,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec"
|
sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.5.5"
|
version: "5.6.0"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -18,7 +18,6 @@ dependencies:
|
|||||||
flutter_markdown: ^0.7.1
|
flutter_markdown: ^0.7.1
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.0
|
||||||
flutter_secure_storage: ^9.2.1
|
flutter_secure_storage: ^9.2.1
|
||||||
carousel_slider: ^5.0.0
|
|
||||||
url_launcher: ^6.2.6
|
url_launcher: ^6.2.6
|
||||||
infinite_scroll_pagination: ^4.0.0
|
infinite_scroll_pagination: ^4.0.0
|
||||||
image_picker: ^1.1.1
|
image_picker: ^1.1.1
|
||||||
|
Loading…
Reference in New Issue
Block a user