💄 New layout for article for optimized reading experience

🐛 Bug fixes on pending post media list
This commit is contained in:
LittleSheep 2024-12-07 21:33:01 +08:00
parent b583780cfc
commit 2a837227d5
6 changed files with 186 additions and 30 deletions

View File

@ -383,5 +383,7 @@
"accountStatusOnline": "Online", "accountStatusOnline": "Online",
"accountStatusOffline": "Offline", "accountStatusOffline": "Offline",
"accountStatusLastSeen": "Last seen at {}", "accountStatusLastSeen": "Last seen at {}",
"postArticle": "Article on the Solar Network" "postArticle": "Article on the Solar Network",
"articleWrittenAt": "Written at {}",
"articleEditedAt": "Edited at {}"
} }

View File

@ -383,5 +383,8 @@
"accountStatusOnline": "在线", "accountStatusOnline": "在线",
"accountStatusOffline": "离线", "accountStatusOffline": "离线",
"accountStatusLastSeen": "最后一次在 {} 上线", "accountStatusLastSeen": "最后一次在 {} 上线",
"postArticle": "Solar Network 上的文章" "postArticle": "Solar Network 上的文章",
"articleWrittenAt": "发表于 {}",
"articleEditedAt": "编辑于 {}"
} }

View File

@ -264,6 +264,7 @@ class PostWriteController extends ChangeNotifier {
final item = await _uploadAttachment(context, media); final item = await _uploadAttachment(context, media);
attachments[idx] = PostWriteMedia(item); attachments[idx] = PostWriteMedia(item);
isBusy = false;
notifyListeners(); notifyListeners();
} }
@ -395,6 +396,9 @@ class PostWriteController extends ChangeNotifier {
attachments.add(thumbnail!); attachments.add(thumbnail!);
thumbnail = null; thumbnail = null;
} else { } else {
if (thumbnail != null) {
attachments.add(thumbnail!);
}
thumbnail = attachments[idx]; thumbnail = attachments[idx];
attachments.removeAt(idx); attachments.removeAt(idx);
} }

View File

@ -1,44 +1,48 @@
import 'dart:ui'; import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:syntax_highlight/syntax_highlight.dart'; import 'package:syntax_highlight/syntax_highlight.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:uuid/uuid.dart';
class MarkdownTextContent extends StatefulWidget { import 'attachment/attachment_detail.dart';
class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final bool isSelectable; final bool isSelectable;
final bool isLargeText;
final bool isAutoWarp; final bool isAutoWarp;
final TextScaler? textScaler;
final List<SnAttachment?>? attachments;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
required this.content, required this.content,
this.isSelectable = false, this.isSelectable = false,
this.isLargeText = false,
this.isAutoWarp = false, this.isAutoWarp = false,
this.textScaler,
this.attachments,
}); });
@override
State<MarkdownTextContent> createState() => _MarkdownTextContentState();
}
class _MarkdownTextContentState extends State<MarkdownTextContent> {
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
return Markdown( return Markdown(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
data: widget.content, data: content,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
styleSheet: MarkdownStyleSheet.fromTheme( styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context), Theme.of(context),
).copyWith( ).copyWith(
textScaler: TextScaler.linear(widget.isLargeText ? 1.1 : 1), textScaler: textScaler,
blockquote: TextStyle( blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
@ -73,7 +77,7 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> {
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, ...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
], ],
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
if (widget.isAutoWarp) markdown.LineBreakSyntax(), if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(), _UserNameCardInlineSyntax(),
markdown.AutolinkSyntax(), markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
@ -85,7 +89,12 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> {
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
if (href == null) return; if (href == null) return;
if (href.startsWith('solink://')) { if (href.startsWith('solink://')) {
// final segments = href.replaceFirst('solink://', '').split('/'); final uri = href.replaceFirst('solink://', '');
final segments = uri.split('/');
switch (segments[0]) {
default:
GoRouter.of(context).push(uri);
}
return; return;
} }
@ -99,7 +108,57 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> {
double? width, height; double? width, height;
BoxFit? fit; BoxFit? fit;
if (url.startsWith('solink://')) { if (url.startsWith('solink://')) {
// final segments = url.replaceFirst('solink://', '').split('/'); final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'attachments':
final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1],
orElse: () => null,
);
if (attachment != null) {
const uuid = Uuid();
final heroTag = uuid.v4();
return GestureDetector(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: attachment.metadata['ratio'] ??
switch (attachment.mimetype.split('/').firstOrNull) {
'audio' => 16 / 9,
'video' => 16 / 9,
_ => 1,
}
.toDouble(),
child: AttachmentItem(
data: attachment,
heroTag: heroTag,
),
),
),
),
onTap: () {
context.pushTransparentRoute(
AttachmentZoomView(
data: [attachment],
initialIndex: 0,
heroTags: [heroTag],
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
);
}
break;
}
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return UniversalImage( return UniversalImage(
@ -114,7 +173,7 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.isSelectable) { if (isSelectable) {
return SelectionArea(child: _buildContent(context)); return SelectionArea(child: _buildContent(context));
} }
return _buildContent(context); return _buildContent(context);
@ -153,13 +212,11 @@ class _MarkdownTextCodeElement extends MarkdownElementBuilder {
child: FutureBuilder( child: FutureBuilder(
future: (() async { future: (() async {
final docPath = '../../../'; final docPath = '../../../';
final highlightingPath = final highlightingPath = join(docPath, 'assets/highlighting', language);
join(docPath, 'assets/highlighting', language);
await Highlighter.initialize([highlightingPath]); await Highlighter.initialize([highlightingPath]);
return Highlighter( return Highlighter(
language: highlightingPath, language: highlightingPath,
theme: PlatformDispatcher.instance.platformBrightness == theme: PlatformDispatcher.instance.platformBrightness == Brightness.light
Brightness.light
? await HighlighterTheme.loadLightTheme() ? await HighlighterTheme.loadLightTheme()
: await HighlighterTheme.loadDarkTheme(), : await HighlighterTheme.loadDarkTheme(),
); );

View File

@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:popover/popover.dart'; import 'package:popover/popover.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -136,8 +137,14 @@ class PostItem extends StatelessWidget {
}, },
).padding(horizontal: 12, vertical: 8), ).padding(horizontal: 12, vertical: 8),
if (data.body['title'] != null || data.body['description'] != null) if (data.body['title'] != null || data.body['description'] != null)
_PostHeadline(data: data).padding(horizontal: 16, bottom: 8), _PostHeadline(
_PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), data: data,
isEnlarge: data.type == 'article' && showFullPost,
).padding(horizontal: 16, bottom: 8),
_PostContentBody(
data: data,
isEnlarge: data.type == 'article' && showFullPost,
).padding(horizontal: 16, bottom: 6),
if (data.repostTo != null) if (data.repostTo != null)
_PostQuoteContent(child: data.repostTo!).padding( _PostQuoteContent(child: data.repostTo!).padding(
horizontal: 12, horizontal: 12,
@ -156,7 +163,7 @@ class PostItem extends StatelessWidget {
], ],
), ),
), ),
if (data.preload?.attachments?.isNotEmpty ?? false) if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article')
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
@ -292,11 +299,82 @@ class _PostBottomAction extends StatelessWidget {
class _PostHeadline extends StatelessWidget { class _PostHeadline extends StatelessWidget {
final SnPost data; final SnPost data;
final bool isEnlarge;
const _PostHeadline({super.key, required this.data}); const _PostHeadline({
super.key,
required this.data,
this.isEnlarge = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isEnlarge) {
final sn = context.read<SnNetworkProvider>();
final textScaler = TextScaler.linear(1.5);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data.preload?.thumbnail != null)
Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.preload!.thumbnail!.rid),
fit: BoxFit.cover,
),
),
),
),
if (data.body['title'] != null)
Text(
data.body['title'],
style: Theme.of(context).textTheme.titleMedium,
textScaler: TextScaler.linear(1.4),
),
if (data.body['description'] != null)
Text(
data.body['description'],
style: Theme.of(context).textTheme.bodyMedium,
textScaler: TextScaler.linear(1.1),
),
if (data.body['description'] != null) const Gap(8) else const Gap(4),
Row(
children: [
Text(
'articleWrittenAt'.tr(
args: [DateFormat('y/M/d HH:mm').format(data.createdAt)],
),
style: TextStyle(fontSize: 13),
),
const Gap(8),
if (data.updatedAt != data.createdAt)
Text(
'articleEditedAt'.tr(
args: [DateFormat('y/M/d HH:mm').format(data.updatedAt)],
),
style: TextStyle(fontSize: 13),
),
],
).opacity(0.75),
const Gap(8),
const Divider(height: 1, thickness: 1),
const Gap(8),
],
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -502,14 +580,22 @@ class _PostContentHeader extends StatelessWidget {
} }
class _PostContentBody extends StatelessWidget { class _PostContentBody extends StatelessWidget {
final dynamic data; final SnPost data;
final bool isEnlarge;
const _PostContentBody({this.data}); const _PostContentBody({
required this.data,
this.isEnlarge = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (data['content'] == null) return const SizedBox.shrink(); if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent(content: data['content']); return MarkdownTextContent(
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
attachments: data.preload?.attachments,
);
} }
} }
@ -537,7 +623,7 @@ class _PostQuoteContent extends StatelessWidget {
showMenu: false, showMenu: false,
onDeleted: () {}, onDeleted: () {},
).padding(bottom: 4), ).padding(bottom: 4),
_PostContentBody(data: child.body), _PostContentBody(data: child),
], ],
), ),
); );

View File

@ -208,7 +208,11 @@ class PostMediaPendingList extends StatelessWidget {
), ),
), ),
), ),
if (thumbnail != null) const VerticalDivider(width: 1).padding(horizontal: 8), if (thumbnail != null)
const VerticalDivider(width: 1, thickness: 1).padding(
horizontal: 12,
vertical: 16,
),
Expanded( Expanded(
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,