💄 Optimize post compose and more

This commit is contained in:
2025-07-31 02:01:18 +08:00
parent 262d36cd2d
commit fd186f8391
6 changed files with 155 additions and 127 deletions

View File

@@ -147,8 +147,7 @@
"addVideo": "Add video", "addVideo": "Add video",
"addPhoto": "Add photo", "addPhoto": "Add photo",
"addFile": "Add file", "addFile": "Add file",
"addAttachmentById": "Add Attachment by ID", "linkAttachment": "Link Attachment",
"enterFileId": "Enter File ID",
"fileIdCannotBeEmpty": "File ID cannot be empty", "fileIdCannotBeEmpty": "File ID cannot be empty",
"failedToFetchFile": "Failed to fetch file: {}", "failedToFetchFile": "Failed to fetch file: {}",
"createDirectMessage": "Send new DM", "createDirectMessage": "Send new DM",
@@ -706,5 +705,7 @@
"aboutDeviceName": "Device Name", "aboutDeviceName": "Device Name",
"aboutDeviceIdentifier": "Device Identifier", "aboutDeviceIdentifier": "Device Identifier",
"donate": "Donate", "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."
} }

View File

@@ -287,12 +287,6 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const AboutScreen(), builder: (context, state) => const AboutScreen(),
), ),
GoRoute(
name: 'reportList',
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
GoRoute( GoRoute(
name: 'reportDetail', name: 'reportDetail',
path: '/safety/reports/me/:id', path: '/safety/reports/me/:id',
@@ -462,6 +456,11 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account/me/settings', path: '/account/me/settings',
builder: (context, state) => const AccountSettingsScreen(), builder: (context, state) => const AccountSettingsScreen(),
), ),
GoRoute(
name: 'reportList',
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
], ],
), ),
], ],

View File

@@ -93,6 +93,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
final theme = Theme.of(context); final theme = Theme.of(context);
return AppScaffold( return AppScaffold(
noBackground: false,
appBar: AppBar(title: Text('about'.tr()), elevation: 0), appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body: body:
_isLoading _isLoading

View File

@@ -231,7 +231,7 @@ class AccountScreen extends HookConsumerWidget {
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: Text('abuseReports').tr(), title: Text('abuseReports').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.gavel), leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () => context.pushNamed('reportList'), onTap: () => context.pushNamed('reportList'),

View File

@@ -291,39 +291,6 @@ class PostComposeScreen extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
actions: [ 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( IconButton(
icon: const Icon(Symbols.settings), icon: const Icon(Symbols.settings),
onPressed: showSettingsSheet, onPressed: showSettingsSheet,
@@ -457,30 +424,82 @@ class PostComposeScreen extends HookConsumerWidget {
// Bottom toolbar // Bottom toolbar
Material( Material(
elevation: 4, elevation: 4,
child: Row( child: Center(
children: [ child: ConstrainedBox(
IconButton( constraints: BoxConstraints(maxWidth: 560),
onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), child: Row(
icon: const Icon(Symbols.add_a_photo), children: [
color: colorScheme.primary, 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,
), ),
), ),
], ],

View File

@@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
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:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.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/file.dart';
import 'package:island/services/compose_storage_db.dart'; import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/alert.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:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart';
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
@@ -60,6 +64,9 @@ class ComposeState {
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
_autoSaveTimer = null; _autoSaveTimer = null;
} }
bool get isEmpty =>
attachments.value.isEmpty && contentController.text.isEmpty;
} }
class ComposeLogic { class ComposeLogic {
@@ -392,7 +399,7 @@ class ComposeLogic {
]; ];
} }
static Future<void> addAttachmentById( static Future<void> linkAttachment(
WidgetRef ref, WidgetRef ref,
ComposeState state, ComposeState state,
BuildContext context, BuildContext context,
@@ -400,79 +407,80 @@ class ComposeLogic {
final TextEditingController idController = TextEditingController(); final TextEditingController idController = TextEditingController();
String? errorMessage; String? errorMessage;
await showDialog( await showModalBottomSheet(
context: context, context: context,
builder: (BuildContext dialogContext) { builder: (BuildContext dialogContext) {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
return AlertDialog( return SheetScaffold(
title: Text('addAttachmentById'.tr()), titleText: 'linkAttachment'.tr(),
content: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
TextField( TextField(
controller: idController, controller: idController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'enterFileId'.tr(), labelText: 'fileId'.tr(),
helperText: 'fileIdHint'.tr(),
helperMaxLines: 3,
errorText: errorMessage, 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()],
);
});
}
},
), ),
), ),
], ],
), ).padding(horizontal: 24, vertical: 24),
actions: <Widget>[
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()],
);
});
}
},
),
],
); );
}, },
); );