Post alias

This commit is contained in:
LittleSheep 2024-08-17 18:44:20 +08:00
parent 454f711656
commit 3bea3a114a
11 changed files with 132 additions and 53 deletions

View File

@ -17,6 +17,7 @@ import 'package:shared_preferences/shared_preferences.dart';
class PostEditorController extends GetxController { class PostEditorController extends GetxController {
late final SharedPreferences _prefs; late final SharedPreferences _prefs;
final aliasController = TextEditingController();
final titleController = TextEditingController(); final titleController = TextEditingController();
final descriptionController = TextEditingController(); final descriptionController = TextEditingController();
final contentController = TextEditingController(); final contentController = TextEditingController();
@ -197,16 +198,23 @@ class PostEditorController extends GetxController {
type = value.type; type = value.type;
editTo.value = value; editTo.value = value;
realmZone.value = value.realm;
isDraft.value = value.isDraft ?? false; isDraft.value = value.isDraft ?? false;
aliasController.text = value.alias ?? '';
titleController.text = value.body['title'] ?? ''; titleController.text = value.body['title'] ?? '';
descriptionController.text = value.body['description'] ?? ''; descriptionController.text = value.body['description'] ?? '';
contentController.text = value.body['content'] ?? ''; contentController.text = value.body['content'] ?? '';
publishedAt.value = value.publishedAt; publishedAt.value = value.publishedAt;
publishedUntil.value = value.publishedUntil; publishedUntil.value = value.publishedUntil;
tags.value = tags.value = List.from(
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(); value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(),
growable: true,
);
tags.refresh(); tags.refresh();
attachments.value = value.body['attachments']?.cast<int>() ?? List.empty(); attachments.value = List.from(
value.body['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh(); attachments.refresh();
thumbnail.value = value.body['thumbnail']; thumbnail.value = value.body['thumbnail'];
@ -256,6 +264,7 @@ class PostEditorController extends GetxController {
Map<String, dynamic> get payload { Map<String, dynamic> get payload {
return { return {
'alias': aliasController.text,
'title': title, 'title': title,
'description': description, 'description': description,
'content': contentController.text, 'content': contentController.text,
@ -277,20 +286,33 @@ class PostEditorController extends GetxController {
set payload(Map<String, dynamic> value) { set payload(Map<String, dynamic> value) {
type = value['type']; type = value['type'];
tags.value = value['tags'].map((x) => x['alias']).toList().cast<String>(); tags.value = List.from(
value['tags'].map((x) => x['alias']).toList(),
growable: true,
);
aliasController.text = value['alias'] ?? '';
titleController.text = value['title'] ?? ''; titleController.text = value['title'] ?? '';
descriptionController.text = value['description'] ?? ''; descriptionController.text = value['description'] ?? '';
contentController.text = value['content'] ?? ''; contentController.text = value['content'] ?? '';
attachments.value = value['attachments'].cast<int>() ?? List.empty(); attachments.value = List.from(
value['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh(); attachments.refresh();
thumbnail.value = value['thumbnail']; thumbnail.value = value['thumbnail'];
visibility.value = value['visibility']; visibility.value = value['visibility'];
isDraft.value = value['is_draft']; isDraft.value = value['is_draft'];
if (value['visible_users'] != null) { if (value['visible_users'] != null) {
visibleUsers.value = value['visible_users'].cast<int>(); visibleUsers.value = List.from(
value['visible_users'],
growable: true,
);
} }
if (value['invisible_users'] != null) { if (value['invisible_users'] != null) {
invisibleUsers.value = value['invisible_users'].cast<int>(); invisibleUsers.value = List.from(
value['invisible_users'],
growable: true,
);
} }
if (value['published_at'] != null) { if (value['published_at'] != null) {
publishedAt.value = DateTime.parse(value['published_at']).toLocal(); publishedAt.value = DateTime.parse(value['published_at']).toLocal();
@ -319,6 +341,7 @@ class PostEditorController extends GetxController {
bool get isNotEmpty { bool get isNotEmpty {
return [ return [
aliasController.text.isNotEmpty,
titleController.text.isNotEmpty, titleController.text.isNotEmpty,
descriptionController.text.isNotEmpty, descriptionController.text.isNotEmpty,
contentController.text.isNotEmpty, contentController.text.isNotEmpty,

View File

@ -8,6 +8,8 @@ class Post {
DateTime updatedAt; DateTime updatedAt;
DateTime? editedAt; DateTime? editedAt;
DateTime? deletedAt; DateTime? deletedAt;
String? alias;
String? areaAlias;
dynamic body; dynamic body;
List<Tag>? tags; List<Tag>? tags;
List<Category>? categories; List<Category>? categories;
@ -33,6 +35,8 @@ class Post {
required this.updatedAt, required this.updatedAt,
required this.editedAt, required this.editedAt,
required this.deletedAt, required this.deletedAt,
required this.alias,
required this.areaAlias,
required this.type, required this.type,
required this.body, required this.body,
required this.tags, required this.tags,
@ -60,6 +64,8 @@ class Post {
deletedAt: json['deleted_at'] != null deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at']) ? DateTime.parse(json['deleted_at'])
: null, : null,
alias: json['alias'],
areaAlias: json['area_alias'],
type: json['type'], type: json['type'],
body: json['body'], body: json['body'],
tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(), tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(),
@ -101,6 +107,8 @@ class Post {
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
'edited_at': editedAt?.toIso8601String(), 'edited_at': editedAt?.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(), 'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'area_alias': areaAlias,
'type': type, 'type': type,
'body': body, 'body': body,
'tags': tags, 'tags': tags,

View File

@ -176,10 +176,19 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
children: [ children: [
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow, tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
title: Text( title: Row(
_editorController.title ?? 'title'.tr, children: [
maxLines: 1, Text(
overflow: TextOverflow.ellipsis, _editorController.title ?? 'title'.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 6),
if (_editorController.aliasController.text.isNotEmpty)
Badge(
label: Text('#${_editorController.aliasController.text}'),
),
],
), ),
subtitle: Text( subtitle: Text(
_editorController.description ?? 'description'.tr, _editorController.description ?? 'description'.tr,
@ -255,6 +264,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
), ),
], ],
), ),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -265,10 +275,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [
if (_isBusy)
const LinearProgressIndicator()
.animate()
.scaleX(),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,

View File

@ -13,6 +13,7 @@ const i18nEnglish = {
'more': 'More', 'more': 'More',
'share': 'Share', 'share': 'Share',
'shareNoUri': 'Share text content', 'shareNoUri': 'Share text content',
'alias': 'Alias',
'feed': 'Feed', 'feed': 'Feed',
'unlink': 'Unlink', 'unlink': 'Unlink',
'feedSearch': 'Search Feed', 'feedSearch': 'Search Feed',

View File

@ -21,6 +21,7 @@ const i18nSimplifiedChinese = {
'more': '更多', 'more': '更多',
'share': '分享', 'share': '分享',
'shareNoUri': '分享文字内容', 'shareNoUri': '分享文字内容',
'alias': '别名',
'feed': '资讯', 'feed': '资讯',
'unlink': '移除链接', 'unlink': '移除链接',
'feedSearch': '搜索资讯', 'feedSearch': '搜索资讯',

View File

@ -238,8 +238,8 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
bool _showContent = false; bool _showContent = false;
void _startLoad() { Future<void> _startLoad() async {
_player.open( await _player.open(
Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}')), Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}')),
play: false, play: false,
); );
@ -249,7 +249,9 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_showContent = widget.autoload; if (widget.autoload) {
_startLoad();
}
} }
@override @override

View File

@ -57,10 +57,12 @@ class _AttachmentListState extends State<AttachmentList> {
} }
attach.listMetadata(widget.attachmentsId).then((result) { attach.listMetadata(widget.attachmentsId).then((result) {
setState(() { if (mounted) {
_attachmentsMeta = result; setState(() {
_isLoading = false; _attachmentsMeta = result;
}); _isLoading = false;
});
}
_calculateAspectRatio(); _calculateAspectRatio();
}); });
} }
@ -111,6 +113,7 @@ class _AttachmentListState extends State<AttachmentList> {
showBadge: _attachmentsMeta.length > 1 && !widget.isGrid, showBadge: _attachmentsMeta.length > 1 && !widget.isGrid,
showBorder: widget.attachmentsId.length > 1, showBorder: widget.attachmentsId.length > 1,
showMature: _showMature, showMature: _showMature,
autoload: widget.autoload,
onReveal: (value) { onReveal: (value) {
setState(() => _showMature = value); setState(() => _showMature = value);
}, },
@ -138,8 +141,9 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final isNotPureImage = _attachmentsMeta final isNotPureImage = _attachmentsMeta.any(
.any((x) => x?.mimetype.split('/').firstOrNull != 'image'); (x) => x?.mimetype.split('/').firstOrNull != 'image',
);
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
const radius = BorderRadius.all(Radius.circular(8)); const radius = BorderRadius.all(Radius.circular(8));
return GridView.builder( return GridView.builder(
@ -157,8 +161,10 @@ class _AttachmentListState extends State<AttachmentList> {
final element = _attachmentsMeta[idx]; final element = _attachmentsMeta[idx];
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: border: Border.all(
Border.all(color: Theme.of(context).dividerColor, width: 1), color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius, borderRadius: radius,
), ),
child: ClipRRect( child: ClipRRect(

View File

@ -19,7 +19,7 @@ class PostEditorCategoriesDialog extends StatelessWidget {
initialTags: controller.tags, initialTags: controller.tags,
hintText: 'postTagsPlaceholder'.tr, hintText: 'postTagsPlaceholder'.tr,
onUpdate: (value) { onUpdate: (value) {
controller.tags.value = value; controller.tags.value = List.from(value, growable: true);
controller.tags.refresh(); controller.tags.refresh();
}, },
), ),

View File

@ -14,12 +14,25 @@ class PostEditorOverviewDialog extends StatelessWidget {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField(
autofocus: true,
autocorrect: true,
controller: controller.aliasController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
hintText: 'alias'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextField( TextField(
autofocus: true, autofocus: true,
autocorrect: true, autocorrect: true,
controller: controller.titleController, controller: controller.titleController,
decoration: InputDecoration( decoration: InputDecoration(
border: const UnderlineInputBorder(), isDense: true,
border: const OutlineInputBorder(),
hintText: 'title'.tr, hintText: 'title'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
@ -33,7 +46,8 @@ class PostEditorOverviewDialog extends StatelessWidget {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
controller: controller.descriptionController, controller: controller.descriptionController,
decoration: InputDecoration( decoration: InputDecoration(
border: const UnderlineInputBorder(), isDense: true,
border: const OutlineInputBorder(),
hintText: 'description'.tr, hintText: 'description'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),

View File

@ -42,10 +42,16 @@ class _PostActionState extends State<PostAction> {
Future<void> _doShare({bool noUri = false}) async { Future<void> _doShare({bool noUri = false}) async {
ShareResult result; ShareResult result;
String id;
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
if (widget.item.alias?.isNotEmpty ?? false) {
id = '${widget.item.areaAlias}:${widget.item.alias}';
} else {
id = '${widget.item.id}';
}
if ((PlatformInfo.isAndroid || PlatformInfo.isIOS) && !noUri) { if ((PlatformInfo.isAndroid || PlatformInfo.isIOS) && !noUri) {
result = await Share.shareUri( result = await Share.shareUri(
Uri.parse('https://solsynth.dev/posts/${widget.item.id}'), Uri.parse('https://solsynth.dev/posts/$id'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
} else { } else {
@ -59,7 +65,7 @@ class _PostActionState extends State<PostAction> {
'username': widget.item.author.nick, 'username': widget.item.author.nick,
'content': 'content':
'${extraContent.join('\n')}${isExtraNotEmpty ? '\n\n' : ''}${widget.item.body['content'] ?? 'no content'}', '${extraContent.join('\n')}${isExtraNotEmpty ? '\n\n' : ''}${widget.item.body['content'] ?? 'no content'}',
'link': 'https://solsynth.dev/posts/${widget.item.id}', 'link': 'https://solsynth.dev/posts/$id',
}), }),
subject: 'postShareSubject'.trParams({ subject: 'postShareSubject'.trParams({
'username': widget.item.author.nick, 'username': widget.item.author.nick,
@ -96,9 +102,27 @@ class _PostActionState extends State<PostAction> {
'postActionList'.tr, 'postActionList'.tr,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
Text( Row(
'#${widget.item.id.toString().padLeft(8, '0')}', children: [
style: Theme.of(context).textTheme.bodySmall, Text(
'#${widget.item.id.toString().padLeft(8, '0')}',
style: Theme.of(context).textTheme.bodySmall,
),
if (widget.item.alias?.isNotEmpty ?? false)
Text(
'·',
style: Theme.of(context).textTheme.bodySmall,
).paddingSymmetric(horizontal: 6),
if (widget.item.alias?.isNotEmpty ?? false)
Expanded(
child: Text(
'${widget.item.areaAlias}:${widget.item.alias}',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
), ),
], ],
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),

View File

@ -78,23 +78,17 @@ class _PostItemState extends State<PostItem> {
Widget _buildThumbnail() { Widget _buildThumbnail() {
if (widget.item.body['thumbnail'] == null) return const SizedBox(); if (widget.item.body['thumbnail'] == null) return const SizedBox();
const radius = BorderRadius.all(Radius.circular(8)); final border = BorderSide(
return AspectRatio( color: Theme.of(context).dividerColor,
aspectRatio: 16 / 9, width: 0.3,
child: Container( );
decoration: BoxDecoration( return Container(
border: Border.all( decoration: BoxDecoration(border: Border(top: border, bottom: border)),
color: Theme.of(context).dividerColor, child: AspectRatio(
width: 0.3, aspectRatio: 16 / 9,
), child: AttachmentSelfContainedEntry(
borderRadius: radius, id: widget.item.body['thumbnail'],
), parentId: 'p${item.id}-thumbnail',
child: ClipRRect(
borderRadius: radius,
child: AttachmentSelfContainedEntry(
id: widget.item.body['thumbnail'],
parentId: 'p${item.id}-thumbnail',
),
), ),
), ),
); );
@ -307,7 +301,7 @@ class _PostItemState extends State<PostItem> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), _buildThumbnail(),
_buildHeader().paddingSymmetric(horizontal: 12), _buildHeader().paddingSymmetric(horizontal: 12),
_buildHeaderDivider().paddingSymmetric(horizontal: 12), _buildHeaderDivider().paddingSymmetric(horizontal: 12),
Stack( Stack(
@ -381,7 +375,7 @@ class _PostItemState extends State<PostItem> {
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), _buildThumbnail().paddingOnly(bottom: 4),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [