💄 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",
"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."
}

View File

@@ -287,12 +287,6 @@ final routerProvider = Provider<GoRouter>((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<GoRouter>((ref) {
path: '/account/me/settings',
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);
return AppScaffold(
noBackground: false,
appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body:
_isLoading

View File

@@ -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'),

View File

@@ -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,
),
),
),
],

View File

@@ -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<void> addAttachmentById(
static Future<void> 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: <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()],
);
});
}
},
),
],
).padding(horizontal: 24, vertical: 24),
);
},
);