✨ Post alias
This commit is contained in:
		| @@ -17,6 +17,7 @@ import 'package:shared_preferences/shared_preferences.dart'; | ||||
| class PostEditorController extends GetxController { | ||||
|   late final SharedPreferences _prefs; | ||||
|  | ||||
|   final aliasController = TextEditingController(); | ||||
|   final titleController = TextEditingController(); | ||||
|   final descriptionController = TextEditingController(); | ||||
|   final contentController = TextEditingController(); | ||||
| @@ -197,16 +198,23 @@ class PostEditorController extends GetxController { | ||||
|  | ||||
|     type = value.type; | ||||
|     editTo.value = value; | ||||
|     realmZone.value = value.realm; | ||||
|     isDraft.value = value.isDraft ?? false; | ||||
|     aliasController.text = value.alias ?? ''; | ||||
|     titleController.text = value.body['title'] ?? ''; | ||||
|     descriptionController.text = value.body['description'] ?? ''; | ||||
|     contentController.text = value.body['content'] ?? ''; | ||||
|     publishedAt.value = value.publishedAt; | ||||
|     publishedUntil.value = value.publishedUntil; | ||||
|     tags.value = | ||||
|         value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(); | ||||
|     tags.value = List.from( | ||||
|       value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(), | ||||
|       growable: true, | ||||
|     ); | ||||
|     tags.refresh(); | ||||
|     attachments.value = value.body['attachments']?.cast<int>() ?? List.empty(); | ||||
|     attachments.value = List.from( | ||||
|       value.body['attachments'] ?? List.empty(), | ||||
|       growable: true, | ||||
|     ); | ||||
|     attachments.refresh(); | ||||
|     thumbnail.value = value.body['thumbnail']; | ||||
|  | ||||
| @@ -256,6 +264,7 @@ class PostEditorController extends GetxController { | ||||
|  | ||||
|   Map<String, dynamic> get payload { | ||||
|     return { | ||||
|       'alias': aliasController.text, | ||||
|       'title': title, | ||||
|       'description': description, | ||||
|       'content': contentController.text, | ||||
| @@ -277,20 +286,33 @@ class PostEditorController extends GetxController { | ||||
|  | ||||
|   set payload(Map<String, dynamic> value) { | ||||
|     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'] ?? ''; | ||||
|     descriptionController.text = value['description'] ?? ''; | ||||
|     contentController.text = value['content'] ?? ''; | ||||
|     attachments.value = value['attachments'].cast<int>() ?? List.empty(); | ||||
|     attachments.value = List.from( | ||||
|       value['attachments'] ?? List.empty(), | ||||
|       growable: true, | ||||
|     ); | ||||
|     attachments.refresh(); | ||||
|     thumbnail.value = value['thumbnail']; | ||||
|     visibility.value = value['visibility']; | ||||
|     isDraft.value = value['is_draft']; | ||||
|     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) { | ||||
|       invisibleUsers.value = value['invisible_users'].cast<int>(); | ||||
|       invisibleUsers.value = List.from( | ||||
|         value['invisible_users'], | ||||
|         growable: true, | ||||
|       ); | ||||
|     } | ||||
|     if (value['published_at'] != null) { | ||||
|       publishedAt.value = DateTime.parse(value['published_at']).toLocal(); | ||||
| @@ -319,6 +341,7 @@ class PostEditorController extends GetxController { | ||||
|  | ||||
|   bool get isNotEmpty { | ||||
|     return [ | ||||
|       aliasController.text.isNotEmpty, | ||||
|       titleController.text.isNotEmpty, | ||||
|       descriptionController.text.isNotEmpty, | ||||
|       contentController.text.isNotEmpty, | ||||
|   | ||||
| @@ -8,6 +8,8 @@ class Post { | ||||
|   DateTime updatedAt; | ||||
|   DateTime? editedAt; | ||||
|   DateTime? deletedAt; | ||||
|   String? alias; | ||||
|   String? areaAlias; | ||||
|   dynamic body; | ||||
|   List<Tag>? tags; | ||||
|   List<Category>? categories; | ||||
| @@ -33,6 +35,8 @@ class Post { | ||||
|     required this.updatedAt, | ||||
|     required this.editedAt, | ||||
|     required this.deletedAt, | ||||
|     required this.alias, | ||||
|     required this.areaAlias, | ||||
|     required this.type, | ||||
|     required this.body, | ||||
|     required this.tags, | ||||
| @@ -60,6 +64,8 @@ class Post { | ||||
|         deletedAt: json['deleted_at'] != null | ||||
|             ? DateTime.parse(json['deleted_at']) | ||||
|             : null, | ||||
|         alias: json['alias'], | ||||
|         areaAlias: json['area_alias'], | ||||
|         type: json['type'], | ||||
|         body: json['body'], | ||||
|         tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(), | ||||
| @@ -101,6 +107,8 @@ class Post { | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'edited_at': editedAt?.toIso8601String(), | ||||
|         'deleted_at': deletedAt?.toIso8601String(), | ||||
|         'alias': alias, | ||||
|         'area_alias': areaAlias, | ||||
|         'type': type, | ||||
|         'body': body, | ||||
|         'tags': tags, | ||||
|   | ||||
| @@ -176,10 +176,19 @@ class _PostPublishScreenState extends State<PostPublishScreen> { | ||||
|           children: [ | ||||
|             ListTile( | ||||
|               tileColor: Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|               title: Text( | ||||
|                 _editorController.title ?? 'title'.tr, | ||||
|                 maxLines: 1, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               title: Row( | ||||
|                 children: [ | ||||
|                   Text( | ||||
|                     _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( | ||||
|                 _editorController.description ?? 'description'.tr, | ||||
| @@ -255,6 +264,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> { | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             if (_isBusy) const LinearProgressIndicator().animate().scaleX(), | ||||
|             Expanded( | ||||
|               child: Row( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
| @@ -265,10 +275,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> { | ||||
|                         Expanded( | ||||
|                           child: ListView( | ||||
|                             children: [ | ||||
|                               if (_isBusy) | ||||
|                                 const LinearProgressIndicator() | ||||
|                                     .animate() | ||||
|                                     .scaleX(), | ||||
|                               Container( | ||||
|                                 padding: const EdgeInsets.symmetric( | ||||
|                                   horizontal: 16, | ||||
|   | ||||
| @@ -13,6 +13,7 @@ const i18nEnglish = { | ||||
|   'more': 'More', | ||||
|   'share': 'Share', | ||||
|   'shareNoUri': 'Share text content', | ||||
|   'alias': 'Alias', | ||||
|   'feed': 'Feed', | ||||
|   'unlink': 'Unlink', | ||||
|   'feedSearch': 'Search Feed', | ||||
|   | ||||
| @@ -21,6 +21,7 @@ const i18nSimplifiedChinese = { | ||||
|   'more': '更多', | ||||
|   'share': '分享', | ||||
|   'shareNoUri': '分享文字内容', | ||||
|   'alias': '别名', | ||||
|   'feed': '资讯', | ||||
|   'unlink': '移除链接', | ||||
|   'feedSearch': '搜索资讯', | ||||
|   | ||||
| @@ -238,8 +238,8 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> { | ||||
|  | ||||
|   bool _showContent = false; | ||||
|  | ||||
|   void _startLoad() { | ||||
|     _player.open( | ||||
|   Future<void> _startLoad() async { | ||||
|     await _player.open( | ||||
|       Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}')), | ||||
|       play: false, | ||||
|     ); | ||||
| @@ -249,7 +249,9 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> { | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _showContent = widget.autoload; | ||||
|     if (widget.autoload) { | ||||
|       _startLoad(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -57,10 +57,12 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|     } | ||||
|  | ||||
|     attach.listMetadata(widget.attachmentsId).then((result) { | ||||
|       setState(() { | ||||
|         _attachmentsMeta = result; | ||||
|         _isLoading = false; | ||||
|       }); | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           _attachmentsMeta = result; | ||||
|           _isLoading = false; | ||||
|         }); | ||||
|       } | ||||
|       _calculateAspectRatio(); | ||||
|     }); | ||||
|   } | ||||
| @@ -111,6 +113,7 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|       showBadge: _attachmentsMeta.length > 1 && !widget.isGrid, | ||||
|       showBorder: widget.attachmentsId.length > 1, | ||||
|       showMature: _showMature, | ||||
|       autoload: widget.autoload, | ||||
|       onReveal: (value) { | ||||
|         setState(() => _showMature = value); | ||||
|       }, | ||||
| @@ -138,8 +141,9 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final isNotPureImage = _attachmentsMeta | ||||
|         .any((x) => x?.mimetype.split('/').firstOrNull != 'image'); | ||||
|     final isNotPureImage = _attachmentsMeta.any( | ||||
|       (x) => x?.mimetype.split('/').firstOrNull != 'image', | ||||
|     ); | ||||
|     if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { | ||||
|       const radius = BorderRadius.all(Radius.circular(8)); | ||||
|       return GridView.builder( | ||||
| @@ -157,8 +161,10 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|           final element = _attachmentsMeta[idx]; | ||||
|           return Container( | ||||
|             decoration: BoxDecoration( | ||||
|               border: | ||||
|                   Border.all(color: Theme.of(context).dividerColor, width: 1), | ||||
|               border: Border.all( | ||||
|                 color: Theme.of(context).dividerColor, | ||||
|                 width: 1, | ||||
|               ), | ||||
|               borderRadius: radius, | ||||
|             ), | ||||
|             child: ClipRRect( | ||||
|   | ||||
| @@ -19,7 +19,7 @@ class PostEditorCategoriesDialog extends StatelessWidget { | ||||
|             initialTags: controller.tags, | ||||
|             hintText: 'postTagsPlaceholder'.tr, | ||||
|             onUpdate: (value) { | ||||
|               controller.tags.value = value; | ||||
|               controller.tags.value = List.from(value, growable: true); | ||||
|               controller.tags.refresh(); | ||||
|             }, | ||||
|           ), | ||||
|   | ||||
| @@ -14,12 +14,25 @@ class PostEditorOverviewDialog extends StatelessWidget { | ||||
|       content: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         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( | ||||
|             autofocus: true, | ||||
|             autocorrect: true, | ||||
|             controller: controller.titleController, | ||||
|             decoration: InputDecoration( | ||||
|               border: const UnderlineInputBorder(), | ||||
|               isDense: true, | ||||
|               border: const OutlineInputBorder(), | ||||
|               hintText: 'title'.tr, | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
| @@ -33,7 +46,8 @@ class PostEditorOverviewDialog extends StatelessWidget { | ||||
|             keyboardType: TextInputType.multiline, | ||||
|             controller: controller.descriptionController, | ||||
|             decoration: InputDecoration( | ||||
|               border: const UnderlineInputBorder(), | ||||
|               isDense: true, | ||||
|               border: const OutlineInputBorder(), | ||||
|               hintText: 'description'.tr, | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|   | ||||
| @@ -42,10 +42,16 @@ class _PostActionState extends State<PostAction> { | ||||
|  | ||||
|   Future<void> _doShare({bool noUri = false}) async { | ||||
|     ShareResult result; | ||||
|     String id; | ||||
|     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) { | ||||
|       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, | ||||
|       ); | ||||
|     } else { | ||||
| @@ -59,7 +65,7 @@ class _PostActionState extends State<PostAction> { | ||||
|           'username': widget.item.author.nick, | ||||
|           '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({ | ||||
|           'username': widget.item.author.nick, | ||||
| @@ -96,9 +102,27 @@ class _PostActionState extends State<PostAction> { | ||||
|                 'postActionList'.tr, | ||||
|                 style: Theme.of(context).textTheme.headlineSmall, | ||||
|               ), | ||||
|               Text( | ||||
|                 '#${widget.item.id.toString().padLeft(8, '0')}', | ||||
|                 style: Theme.of(context).textTheme.bodySmall, | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   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), | ||||
|   | ||||
| @@ -78,23 +78,17 @@ class _PostItemState extends State<PostItem> { | ||||
|  | ||||
|   Widget _buildThumbnail() { | ||||
|     if (widget.item.body['thumbnail'] == null) return const SizedBox(); | ||||
|     const radius = BorderRadius.all(Radius.circular(8)); | ||||
|     return AspectRatio( | ||||
|       aspectRatio: 16 / 9, | ||||
|       child: Container( | ||||
|         decoration: BoxDecoration( | ||||
|           border: Border.all( | ||||
|             color: Theme.of(context).dividerColor, | ||||
|             width: 0.3, | ||||
|           ), | ||||
|           borderRadius: radius, | ||||
|         ), | ||||
|         child: ClipRRect( | ||||
|           borderRadius: radius, | ||||
|           child: AttachmentSelfContainedEntry( | ||||
|             id: widget.item.body['thumbnail'], | ||||
|             parentId: 'p${item.id}-thumbnail', | ||||
|           ), | ||||
|     final border = BorderSide( | ||||
|       color: Theme.of(context).dividerColor, | ||||
|       width: 0.3, | ||||
|     ); | ||||
|     return Container( | ||||
|       decoration: BoxDecoration(border: Border(top: border, bottom: border)), | ||||
|       child: AspectRatio( | ||||
|         aspectRatio: 16 / 9, | ||||
|         child: AttachmentSelfContainedEntry( | ||||
|           id: widget.item.body['thumbnail'], | ||||
|           parentId: 'p${item.id}-thumbnail', | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
| @@ -307,7 +301,7 @@ class _PostItemState extends State<PostItem> { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           _buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), | ||||
|           _buildThumbnail(), | ||||
|           _buildHeader().paddingSymmetric(horizontal: 12), | ||||
|           _buildHeaderDivider().paddingSymmetric(horizontal: 12), | ||||
|           Stack( | ||||
| @@ -381,7 +375,7 @@ class _PostItemState extends State<PostItem> { | ||||
|       closedBuilder: (_, openContainer) => Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           _buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), | ||||
|           _buildThumbnail().paddingOnly(bottom: 4), | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user