💄 Optimize post compose and more
This commit is contained in:
@@ -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."
|
||||||
}
|
}
|
@@ -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(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@@ -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
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@@ -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()],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user