From fd186f83912e84862ffdcc9a5dc12d13f80c253e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 31 Jul 2025 02:01:18 +0800 Subject: [PATCH] :lipstick: Optimize post compose and more --- assets/i18n/en-US.json | 7 +- lib/route.dart | 11 +-- lib/screens/about.dart | 1 + lib/screens/account.dart | 2 +- lib/screens/posts/compose.dart | 131 +++++++++++++++------------ lib/widgets/post/compose_shared.dart | 130 +++++++++++++------------- 6 files changed, 155 insertions(+), 127 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 1eb2011..e1f1cae 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -147,8 +147,7 @@ "addVideo": "Add video", "addPhoto": "Add photo", "addFile": "Add file", - "addAttachmentById": "Add Attachment by ID", - "enterFileId": "Enter File ID", + "linkAttachment": "Link Attachment", "fileIdCannotBeEmpty": "File ID cannot be empty", "failedToFetchFile": "Failed to fetch file: {}", "createDirectMessage": "Send new DM", @@ -706,5 +705,7 @@ "aboutDeviceName": "Device Name", "aboutDeviceIdentifier": "Device Identifier", "donate": "Donate", - "donateDescription": "Support us to continue developing the Solar Network and keep the server up and running." + "donateDescription": "Support us to continue developing the Solar Network and keep the server up and running.", + "fileId": "File ID", + "fileIdHint": "The file ID is the ID you get after upload the file via the Solar Network Drive." } \ No newline at end of file diff --git a/lib/route.dart b/lib/route.dart index 19e1505..f0eb695 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -287,12 +287,6 @@ final routerProvider = Provider((ref) { builder: (context, state) => const AboutScreen(), ), - GoRoute( - name: 'reportList', - path: '/safety/reports/me', - builder: (context, state) => const AbuseReportListScreen(), - ), - GoRoute( name: 'reportDetail', path: '/safety/reports/me/:id', @@ -462,6 +456,11 @@ final routerProvider = Provider((ref) { path: '/account/me/settings', builder: (context, state) => const AccountSettingsScreen(), ), + GoRoute( + name: 'reportList', + path: '/safety/reports/me', + builder: (context, state) => const AbuseReportListScreen(), + ), ], ), ], diff --git a/lib/screens/about.dart b/lib/screens/about.dart index ccbd320..989bf48 100644 --- a/lib/screens/about.dart +++ b/lib/screens/about.dart @@ -93,6 +93,7 @@ class _AboutScreenState extends ConsumerState { final theme = Theme.of(context); return AppScaffold( + noBackground: false, appBar: AppBar(title: Text('about'.tr()), elevation: 0), body: _isLoading diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 245df8a..fd0dbc7 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -231,7 +231,7 @@ class AccountScreen extends HookConsumerWidget { ListTile( minTileHeight: 48, title: Text('abuseReports').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.gavel), trailing: const Icon(Symbols.chevron_right), onTap: () => context.pushNamed('reportList'), diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index 1728df3..10fd30d 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -291,39 +291,6 @@ class PostComposeScreen extends HookConsumerWidget { appBar: AppBar( leading: const PageBackButton(), actions: [ - if (originalPost == null) // Only show drafts for new posts - IconButton( - icon: const Icon(Symbols.draft), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: - (context) => DraftManagerSheet( - onDraftSelected: (draftId) { - final draft = - ref.read( - composeStorageNotifierProvider, - )[draftId]; - if (draft != null) { - state.titleController.text = draft.title ?? ''; - state.descriptionController.text = - draft.description ?? ''; - state.contentController.text = - draft.content ?? ''; - state.visibility.value = draft.visibility; - } - }, - ), - ); - }, - tooltip: 'drafts'.tr(), - ), - IconButton( - icon: const Icon(Symbols.save), - onPressed: () => ComposeLogic.saveDraft(ref, state), - tooltip: 'saveDraft'.tr(), - ), IconButton( icon: const Icon(Symbols.settings), onPressed: showSettingsSheet, @@ -457,30 +424,82 @@ class PostComposeScreen extends HookConsumerWidget { // Bottom toolbar Material( elevation: 4, - child: Row( - children: [ - IconButton( - onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), - icon: const Icon(Symbols.add_a_photo), - color: colorScheme.primary, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 560), + child: Row( + children: [ + IconButton( + onPressed: + () => ComposeLogic.pickPhotoMedia(ref, state), + tooltip: 'addPhoto'.tr(), + icon: const Icon(Symbols.add_a_photo), + color: colorScheme.primary, + ), + IconButton( + onPressed: + () => ComposeLogic.pickVideoMedia(ref, state), + tooltip: 'addVideo'.tr(), + icon: const Icon(Symbols.videocam), + color: colorScheme.primary, + ), + IconButton( + onPressed: + () => ComposeLogic.linkAttachment( + ref, + state, + context, + ), + icon: const Icon(Symbols.attach_file), + tooltip: 'linkAttachment'.tr(), + color: colorScheme.primary, + ), + Spacer(), + if (originalPost == null && state.isEmpty) + IconButton( + icon: const Icon(Symbols.draft), + color: colorScheme.primary, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: + (context) => DraftManagerSheet( + onDraftSelected: (draftId) { + final draft = + ref.read( + composeStorageNotifierProvider, + )[draftId]; + if (draft != null) { + state.titleController.text = + draft.title ?? ''; + state.descriptionController.text = + draft.description ?? ''; + state.contentController.text = + draft.content ?? ''; + state.visibility.value = + draft.visibility; + } + }, + ), + ); + }, + tooltip: 'drafts'.tr(), + ) + else if (originalPost == null) + IconButton( + icon: const Icon(Symbols.save), + color: colorScheme.primary, + onPressed: () => ComposeLogic.saveDraft(ref, state), + tooltip: 'saveDraft'.tr(), + ), + ], + ).padding( + bottom: MediaQuery.of(context).padding.bottom + 16, + horizontal: 16, + top: 8, ), - IconButton( - onPressed: () => ComposeLogic.pickVideoMedia(ref, state), - icon: const Icon(Symbols.videocam), - color: colorScheme.primary, - ), - IconButton( - onPressed: - () => - ComposeLogic.addAttachmentById(ref, state, context), - icon: const Icon(Symbols.attach_file), - color: colorScheme.primary, - ), - ], - ).padding( - bottom: MediaQuery.of(context).padding.bottom + 16, - horizontal: 16, - top: 8, + ), ), ), ], diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index be5d19d..36cf029 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/models/file.dart'; @@ -13,7 +14,10 @@ import 'package:island/pods/network.dart'; import 'package:island/services/file.dart'; import 'package:island/services/compose_storage_db.dart'; import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:pasteboard/pasteboard.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'dart:async'; import 'dart:developer'; @@ -60,6 +64,9 @@ class ComposeState { _autoSaveTimer?.cancel(); _autoSaveTimer = null; } + + bool get isEmpty => + attachments.value.isEmpty && contentController.text.isEmpty; } class ComposeLogic { @@ -392,7 +399,7 @@ class ComposeLogic { ]; } - static Future addAttachmentById( + static Future linkAttachment( WidgetRef ref, ComposeState state, BuildContext context, @@ -400,79 +407,80 @@ class ComposeLogic { final TextEditingController idController = TextEditingController(); String? errorMessage; - await showDialog( + await showModalBottomSheet( context: context, builder: (BuildContext dialogContext) { return StatefulBuilder( builder: (context, setState) { - return AlertDialog( - title: Text('addAttachmentById'.tr()), - content: Column( + return SheetScaffold( + titleText: 'linkAttachment'.tr(), + child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: idController, decoration: InputDecoration( - hintText: 'enterFileId'.tr(), + labelText: 'fileId'.tr(), + helperText: 'fileIdHint'.tr(), + helperMaxLines: 3, errorText: errorMessage, + border: OutlineInputBorder(), + ), + ), + const Gap(16), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + icon: const Icon(Symbols.add), + label: Text('add'.tr()), + onPressed: () async { + final fileId = idController.text.trim(); + if (fileId.isEmpty) { + setState(() { + errorMessage = 'fileIdCannotBeEmpty'.tr(); + }); + return; + } + + try { + final client = ref.read(apiClientProvider); + final response = await client.get( + '/drive/files/$fileId/info', + ); + final SnCloudFile cloudFile = SnCloudFile.fromJson( + response.data, + ); + + state.attachments.value = [ + ...state.attachments.value, + UniversalFile( + data: cloudFile, + type: switch (cloudFile.mimeType + ?.split('/') + .firstOrNull) { + 'image' => UniversalFileType.image, + 'video' => UniversalFileType.video, + 'audio' => UniversalFileType.audio, + _ => UniversalFileType.file, + }, + ), + ]; + if (context.mounted) { + Navigator.of(dialogContext).pop(); + } + } catch (e) { + setState(() { + errorMessage = 'failedToFetchFile'.tr( + args: [e.toString()], + ); + }); + } + }, ), ), ], - ), - actions: [ - TextButton( - child: Text('cancel'.tr()), - onPressed: () { - Navigator.of(dialogContext).pop(); - }, - ), - TextButton( - child: Text('add'.tr()), - onPressed: () async { - final fileId = idController.text.trim(); - if (fileId.isEmpty) { - setState(() { - errorMessage = 'fileIdCannotBeEmpty'.tr(); - }); - return; - } - - try { - final client = ref.read(apiClientProvider); - final response = await client.get( - '/drive/files/$fileId/info', - ); - final SnCloudFile cloudFile = SnCloudFile.fromJson( - response.data, - ); - - state.attachments.value = [ - ...state.attachments.value, - UniversalFile( - data: cloudFile, - type: switch (cloudFile.mimeType - ?.split('/') - .firstOrNull) { - 'image' => UniversalFileType.image, - 'video' => UniversalFileType.video, - 'audio' => UniversalFileType.audio, - _ => UniversalFileType.file, - }, - ), - ]; - if (context.mounted) { - Navigator.of(dialogContext).pop(); - } - } catch (e) { - setState(() { - errorMessage = 'failedToFetchFile'.tr( - args: [e.toString()], - ); - }); - } - }, - ), - ], + ).padding(horizontal: 24, vertical: 24), ); }, );