kopia lustrzana https://gitlab.com/mysocialportal/relatica
Add initial image attachments implementation
rodzic
6589d4f572
commit
4e6bf2750e
|
@ -1,23 +1,65 @@
|
|||
PODS:
|
||||
- DKImagePickerController/Core (4.3.4):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.4)
|
||||
- DKImagePickerController/PhotoGallery (4.3.4):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.4)
|
||||
- DKPhotoGallery (0.0.17):
|
||||
- DKPhotoGallery/Core (= 0.0.17)
|
||||
- DKPhotoGallery/Model (= 0.0.17)
|
||||
- DKPhotoGallery/Preview (= 0.0.17)
|
||||
- DKPhotoGallery/Resource (= 0.0.17)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_secure_storage (3.3.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- SDWebImage (5.13.2):
|
||||
- SDWebImage/Core (= 5.13.2)
|
||||
- SDWebImage/Core (5.13.2)
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- SwiftyGif (5.4.3)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
|
@ -25,13 +67,21 @@ DEPENDENCIES:
|
|||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- FMDB
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
shared_preferences_ios:
|
||||
|
@ -42,12 +92,18 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
|
|
|
@ -47,5 +47,11 @@
|
|||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Required for taking photos and recording videos.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Required for recording videos</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Required for loading existing pictures.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../models/media_attachment_uploads/media_upload_attachment.dart';
|
||||
import '../padding.dart';
|
||||
|
||||
class MediaUploadEditorControl extends StatefulWidget {
|
||||
final MediaUploadAttachment media;
|
||||
final Function()? onDelete;
|
||||
|
||||
const MediaUploadEditorControl(
|
||||
{super.key, required this.media, this.onDelete});
|
||||
|
||||
@override
|
||||
State<MediaUploadEditorControl> createState() =>
|
||||
_MediaUploadEditorControlState();
|
||||
}
|
||||
|
||||
class _MediaUploadEditorControlState extends State<MediaUploadEditorControl> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const thumbnailsize = 100.0;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: thumbnailsize,
|
||||
height: thumbnailsize,
|
||||
child: widget.media.getPreviewImage(),
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: widget.media.remoteFilename,
|
||||
onChanged: (value) => widget.media.remoteFilename = value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Filename (optional)',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: widget.onDelete,
|
||||
icon: Icon(Icons.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: widget.media.description,
|
||||
onChanged: (value) => widget.media.description = value,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description/ALT Text',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../../models/media_attachment_uploads/entry_media_items.dart';
|
||||
import '../../services/media_upload_attachment_helper.dart';
|
||||
import '../../utils/snackbar_builder.dart';
|
||||
import '../padding.dart';
|
||||
import 'media_upload_editor_control.dart';
|
||||
|
||||
final _logger = Logger('$MediaUploadsControl');
|
||||
|
||||
class MediaUploadsControl extends StatefulWidget {
|
||||
final EntryMediaItems entryMediaItems;
|
||||
|
||||
const MediaUploadsControl({super.key, required this.entryMediaItems});
|
||||
|
||||
@override
|
||||
State<MediaUploadsControl> createState() => _MediaUploadsControlState();
|
||||
}
|
||||
|
||||
class _MediaUploadsControlState extends State<MediaUploadsControl> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building');
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Images',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await MediaUploadAttachmentHelper.getNewImagesFromCamera()
|
||||
.match(
|
||||
onSuccess: (newEntries) => setState(() => widget
|
||||
.entryMediaItems.attachments
|
||||
.addAll(newEntries)),
|
||||
onError: (error) {
|
||||
if (mounted) {
|
||||
buildSnackbar(context,
|
||||
'Error selecting attachments: $error');
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await MediaUploadAttachmentHelper.getImagesFromGallery()
|
||||
.match(
|
||||
onSuccess: (newEntries) => setState(() => widget
|
||||
.entryMediaItems.attachments
|
||||
.addAll(newEntries)),
|
||||
onError: (error) {
|
||||
if (mounted) {
|
||||
buildSnackbar(context,
|
||||
'Error selecting attachments: $error');
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.add_to_photos),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
const VerticalPadding(),
|
||||
...widget.entryMediaItems.attachments.map(
|
||||
(m) => MediaUploadEditorControl(
|
||||
media: m,
|
||||
onDelete: () {
|
||||
widget.entryMediaItems.attachments.remove(m);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
|
@ -12,6 +13,7 @@ import 'models/exec_error.dart';
|
|||
import 'models/gallery_data.dart';
|
||||
import 'models/group_data.dart';
|
||||
import 'models/image_entry.dart';
|
||||
import 'models/media_attachment_uploads/image_types_enum.dart';
|
||||
import 'models/timeline_entry.dart';
|
||||
import 'models/user_notification.dart';
|
||||
import 'serializers/friendica/connection_friendica_extensions.dart';
|
||||
|
@ -286,17 +288,21 @@ class FriendicaClient {
|
|||
));
|
||||
}
|
||||
|
||||
FutureResult<TimelineEntry, ExecError> createNewStatus(
|
||||
{required String text,
|
||||
String spoilerText = '',
|
||||
String inReplyToId = ''}) async {
|
||||
FutureResult<TimelineEntry, ExecError> createNewStatus({
|
||||
required String text,
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
List<String> mediaIds = const [],
|
||||
}) async {
|
||||
_logger.finest(() =>
|
||||
'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId');
|
||||
'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId, with media: $mediaIds');
|
||||
final url = Uri.parse('https://$serverName/api/v1/statuses');
|
||||
final body = {
|
||||
'status': text,
|
||||
'visibility': 'public',
|
||||
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
|
||||
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
|
||||
if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
|
||||
};
|
||||
final result = await _postUrl(url, body);
|
||||
if (result.isFailure) {
|
||||
|
@ -444,6 +450,40 @@ class FriendicaClient {
|
|||
.copy(status: ConnectionStatus.you, network: 'friendica'));
|
||||
}
|
||||
|
||||
FutureResult<ImageEntry, ExecError> uploadFileAsAttachment({
|
||||
required List<int> bytes,
|
||||
String description = '',
|
||||
String album = '',
|
||||
String fileName = '',
|
||||
}) async {
|
||||
final postUri = Uri.parse('https://$serverName/api/friendica/photo/create');
|
||||
final request = http.MultipartRequest('POST', postUri);
|
||||
request.headers['Authorization'] = _authHeader;
|
||||
request.fields['desc'] = description;
|
||||
request.fields['album'] = album;
|
||||
request.files.add(await http.MultipartFile.fromBytes(
|
||||
'media',
|
||||
filename: fileName,
|
||||
contentType:
|
||||
MediaType.parse('image/${ImageTypes.fromExtension(fileName).name}'),
|
||||
bytes));
|
||||
final response = await request.send();
|
||||
final body = utf8.decode(await response.stream.toBytes());
|
||||
if (response.statusCode != 200) {
|
||||
return Result.error(
|
||||
ExecError(
|
||||
type: ErrorType.missingEndpoint,
|
||||
message: body,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final imageDataJson = jsonDecode(body);
|
||||
final newImageData = ImageEntryFriendicaExtension.fromJson(imageDataJson);
|
||||
|
||||
return Result.ok(newImageData);
|
||||
}
|
||||
|
||||
FutureResult<String, ExecError> _getUrl(Uri url) async {
|
||||
_logger.finest('GET: $url');
|
||||
try {
|
||||
|
@ -474,6 +514,7 @@ class FriendicaClient {
|
|||
final response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Cookies': 'XDEBUG_SESSION=PHPSTORM;path=/;',
|
||||
'Authorization': _authHeader,
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
},
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
@ -6,6 +9,10 @@ final getIt = GetIt.instance;
|
|||
|
||||
String randomId() => const Uuid().v4().toString();
|
||||
|
||||
final platformHasCamera = Platform.isIOS || Platform.isAndroid;
|
||||
|
||||
final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
Future<bool?> showConfirmDialog(BuildContext context, String caption) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'services/auth_service.dart';
|
|||
import 'services/connections_manager.dart';
|
||||
import 'services/entry_manager_service.dart';
|
||||
import 'services/gallery_service.dart';
|
||||
import 'services/media_upload_attachment_helper.dart';
|
||||
import 'services/notifications_manager.dart';
|
||||
import 'services/secrets_service.dart';
|
||||
import 'services/timeline_manager.dart';
|
||||
|
@ -45,6 +46,8 @@ void main() async {
|
|||
getIt.registerSingleton<SecretsService>(secretsService);
|
||||
getIt.registerSingleton<AuthService>(authService);
|
||||
getIt.registerSingleton<TimelineManager>(timelineManager);
|
||||
getIt.registerLazySingleton<MediaUploadAttachmentHelper>(
|
||||
() => MediaUploadAttachmentHelper());
|
||||
getIt.registerLazySingleton<NotificationsManager>(
|
||||
() => NotificationsManager());
|
||||
await secretsService.initialize().andThenSuccessAsync((credentials) async {
|
||||
|
|
|
@ -7,17 +7,18 @@ class ImageEntry {
|
|||
final DateTime created;
|
||||
final int height;
|
||||
final int width;
|
||||
final List<ImageEntryScale> scales;
|
||||
|
||||
ImageEntry({
|
||||
required this.id,
|
||||
required this.album,
|
||||
required this.filename,
|
||||
required this.description,
|
||||
required this.thumbnailUrl,
|
||||
required this.created,
|
||||
required this.height,
|
||||
required this.width,
|
||||
});
|
||||
ImageEntry(
|
||||
{required this.id,
|
||||
required this.album,
|
||||
required this.filename,
|
||||
required this.description,
|
||||
required this.thumbnailUrl,
|
||||
required this.created,
|
||||
required this.height,
|
||||
required this.width,
|
||||
required this.scales});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
|
@ -27,3 +28,21 @@ class ImageEntry {
|
|||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
class ImageEntryScale {
|
||||
final String id;
|
||||
final int scale;
|
||||
final Uri link;
|
||||
final int width;
|
||||
final int height;
|
||||
final int size;
|
||||
|
||||
ImageEntryScale({
|
||||
required this.id,
|
||||
required this.scale,
|
||||
required this.link,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.size,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import 'media_upload_attachment.dart';
|
||||
|
||||
class EntryMediaItems {
|
||||
final String albumName;
|
||||
|
||||
final List<MediaUploadAttachment> attachments;
|
||||
|
||||
EntryMediaItems({
|
||||
this.albumName = '',
|
||||
List<MediaUploadAttachment>? existingAttachments,
|
||||
}) : attachments = existingAttachments ?? [];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EntryMediaItems{albumName: $albumName, attachments: $attachments}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
enum ImageTypes {
|
||||
gif,
|
||||
png,
|
||||
jpg;
|
||||
|
||||
static ImageTypes fromExtension(String path) {
|
||||
final extension = p.extension(path).toLowerCase();
|
||||
|
||||
if (extension == '.png') {
|
||||
return ImageTypes.png;
|
||||
}
|
||||
|
||||
if (extension == '.jpg' || extension == '.jpeg') {
|
||||
return ImageTypes.jpg;
|
||||
}
|
||||
|
||||
if (extension == '.gif') {
|
||||
return ImageTypes.gif;
|
||||
}
|
||||
|
||||
throw ArgumentError('Unknown image extension: $extension');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MediaUploadAttachment {
|
||||
final String localFilePath;
|
||||
|
||||
final String remoteUrl;
|
||||
|
||||
final bool isExistingServerItem;
|
||||
|
||||
String description;
|
||||
|
||||
String remoteFilename;
|
||||
|
||||
MediaUploadAttachment({
|
||||
this.localFilePath = '',
|
||||
this.remoteUrl = '',
|
||||
this.isExistingServerItem = false,
|
||||
this.description = '',
|
||||
this.remoteFilename = '',
|
||||
});
|
||||
|
||||
factory MediaUploadAttachment.newItem(String localFilename) =>
|
||||
MediaUploadAttachment(
|
||||
localFilePath: localFilename,
|
||||
isExistingServerItem: false,
|
||||
remoteFilename: '',
|
||||
);
|
||||
|
||||
factory MediaUploadAttachment.existingItem(String remoteUrl) =>
|
||||
MediaUploadAttachment(
|
||||
remoteUrl: remoteUrl,
|
||||
isExistingServerItem: true,
|
||||
);
|
||||
|
||||
Widget getPreviewImage() {
|
||||
if (isExistingServerItem) {
|
||||
return CachedNetworkImage(imageUrl: remoteUrl);
|
||||
}
|
||||
|
||||
return Image.file(File(localFilePath));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MediaUploadAttachment{localFilename: $localFilePath, remoteUrl: $remoteUrl, isExistingServerItem: $isExistingServerItem, description: $description}';
|
||||
}
|
||||
}
|
|
@ -3,9 +3,12 @@ import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../controls/entry_media_attachments/media_uploads_control.dart';
|
||||
import '../controls/padding.dart';
|
||||
import '../controls/timeline/status_header_control.dart';
|
||||
import '../models/media_attachment_uploads/entry_media_items.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
@ -24,7 +27,9 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
static final _logger = Logger('$EditorScreen');
|
||||
final contentController = TextEditingController();
|
||||
final spoilerController = TextEditingController();
|
||||
final localEntryTemporaryId = const Uuid().v4();
|
||||
TimelineEntry? parentEntry;
|
||||
final entryMediaItems = EntryMediaItems();
|
||||
|
||||
var isSubmitting = false;
|
||||
|
||||
|
@ -32,6 +37,9 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
|
||||
String get statusType => widget.parentId.isEmpty ? 'Post' : 'Comment';
|
||||
|
||||
String get localEntryId =>
|
||||
widget.id.isNotEmpty ? widget.id : localEntryTemporaryId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!isComment) {
|
||||
|
@ -60,6 +68,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
contentController.text,
|
||||
spoilerText: spoilerController.text,
|
||||
inReplyToId: widget.parentId,
|
||||
mediaItems: entryMediaItems,
|
||||
);
|
||||
setState(() {
|
||||
isSubmitting = false;
|
||||
|
@ -124,26 +133,10 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: isSubmitting
|
||||
? null
|
||||
: () async => createStatus(context, manager),
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed: isSubmitting
|
||||
? null
|
||||
: () {
|
||||
context.pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
MediaUploadsControl(
|
||||
entryMediaItems: entryMediaItems,
|
||||
),
|
||||
buildButtonBar(context, manager),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -191,4 +184,26 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildButtonBar(BuildContext context, TimelineManager manager) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed:
|
||||
isSubmitting ? null : () async => createStatus(context, manager),
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed: isSubmitting
|
||||
? null
|
||||
: () {
|
||||
context.pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import '../../models/gallery_data.dart';
|
|||
|
||||
extension GalleryDataFriendicaExtensions on GalleryData {
|
||||
static GalleryData fromJson(Map<String, dynamic> json) => GalleryData(
|
||||
count: int.tryParse(json['count']) ?? -1,
|
||||
count: json['count'] ?? -1,
|
||||
name: json['name'] ?? 'Unknown',
|
||||
created: DateTime.tryParse(json['created']) ?? DateTime(0));
|
||||
}
|
||||
|
|
|
@ -4,14 +4,26 @@ import '../../models/media_attachment.dart';
|
|||
|
||||
extension ImageEntryFriendicaExtension on ImageEntry {
|
||||
static ImageEntry fromJson(Map<String, dynamic> json) => ImageEntry(
|
||||
id: json['id'],
|
||||
album: json['album'],
|
||||
filename: json['filename'],
|
||||
description: json['desc'],
|
||||
thumbnailUrl: json['thumb'],
|
||||
created: DateTime.tryParse(json['created']) ?? DateTime(0),
|
||||
height: json['height'],
|
||||
id: json['id'],
|
||||
album: json['album'],
|
||||
filename: json['filename'],
|
||||
description: json['desc'],
|
||||
thumbnailUrl: json['thumb'] ?? '',
|
||||
created: DateTime.tryParse(json['created']) ?? DateTime(0),
|
||||
height: json['height'],
|
||||
width: json['width'],
|
||||
scales: (json['scales'] as List<dynamic>? ?? [])
|
||||
.map((scaleJson) => _scaleFromJson(scaleJson))
|
||||
.toList());
|
||||
|
||||
static ImageEntryScale _scaleFromJson(Map<String, dynamic> json) =>
|
||||
ImageEntryScale(
|
||||
id: json['id'].toString(),
|
||||
scale: json['scale'],
|
||||
link: Uri.parse(json['link']),
|
||||
width: json['width'],
|
||||
height: json['height'],
|
||||
size: json['size'],
|
||||
);
|
||||
|
||||
MediaAttachment toMediaAttachment() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../friendica_client.dart';
|
||||
|
@ -7,8 +8,10 @@ import '../globals.dart';
|
|||
import '../models/TimelineIdentifiers.dart';
|
||||
import '../models/entry_tree_item.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/media_attachment_uploads/entry_media_items.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import 'auth_service.dart';
|
||||
import 'media_upload_attachment_helper.dart';
|
||||
|
||||
class EntryManagerService extends ChangeNotifier {
|
||||
static final _logger = Logger('$EntryManagerService');
|
||||
|
@ -60,8 +63,12 @@ class EntryManagerService extends ChangeNotifier {
|
|||
));
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> createNewStatus(String text,
|
||||
{String spoilerText = '', String inReplyToId = ''}) async {
|
||||
FutureResult<bool, ExecError> createNewStatus(
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
required EntryMediaItems mediaItems,
|
||||
}) async {
|
||||
_logger.finest('Creating new post: $text');
|
||||
final auth = getIt<AuthService>();
|
||||
final clientResult = auth.currentClient;
|
||||
|
@ -71,12 +78,53 @@ class EntryManagerService extends ChangeNotifier {
|
|||
}
|
||||
|
||||
final client = clientResult.value;
|
||||
|
||||
final mediaIds = <String>[];
|
||||
for (final item in mediaItems.attachments) {
|
||||
if (item.isExistingServerItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String extension = p.extension(item.localFilePath);
|
||||
late final String filename;
|
||||
if (item.remoteFilename.isEmpty) {
|
||||
filename = p.basename(item.localFilePath);
|
||||
} else {
|
||||
if (item.remoteFilename
|
||||
.toLowerCase()
|
||||
.endsWith(extension.toLowerCase())) {
|
||||
filename = item.remoteFilename;
|
||||
} else {
|
||||
filename = "${item.remoteFilename}$extension";
|
||||
}
|
||||
}
|
||||
|
||||
final uploadResult =
|
||||
await MediaUploadAttachmentHelper.getUploadableImageBytes(
|
||||
item.localFilePath,
|
||||
).andThenAsync(
|
||||
(imageBytes) async => await client.uploadFileAsAttachment(
|
||||
bytes: imageBytes,
|
||||
album: mediaItems.albumName,
|
||||
description: item.description,
|
||||
fileName: filename,
|
||||
),
|
||||
);
|
||||
if (uploadResult.isSuccess) {
|
||||
mediaIds.add(uploadResult.value.scales.first.id);
|
||||
} else {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: 'Error uploading image: ${uploadResult.error}'));
|
||||
}
|
||||
}
|
||||
|
||||
final result = await client
|
||||
.createNewStatus(
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
)
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaIds: mediaIds)
|
||||
.andThenSuccessAsync((item) async {
|
||||
await processNewItems([item], client.credentials.username, null);
|
||||
return item;
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/media_attachment_uploads/image_types_enum.dart';
|
||||
import '../models/media_attachment_uploads/media_upload_attachment.dart';
|
||||
|
||||
class MediaUploadAttachmentHelper {
|
||||
static final _logger = Logger('$MediaUploadAttachmentHelper');
|
||||
|
||||
static FutureResult<List<MediaUploadAttachment>, ExecError>
|
||||
getNewImagesFromCamera() async {
|
||||
final file = await ImagePicker().pickImage(source: ImageSource.camera);
|
||||
if (file != null) {
|
||||
return Result.ok([MediaUploadAttachment.newItem(file.path)]);
|
||||
}
|
||||
{
|
||||
return Result.ok([]);
|
||||
}
|
||||
}
|
||||
|
||||
static FutureResult<List<MediaUploadAttachment>, ExecError>
|
||||
getImagesFromGallery() async {
|
||||
final files = <XFile>[];
|
||||
if (useImagePicker) {
|
||||
final picker = ImagePicker();
|
||||
final selectedFiles = await picker.pickMultiImage();
|
||||
files.addAll(selectedFiles);
|
||||
} else {
|
||||
await _desktopImagesFromDisk().match(
|
||||
onSuccess: (selectedFiles) => files.addAll(selectedFiles),
|
||||
onError: (error) => _logger.severe(error),
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(
|
||||
files.map((f) => MediaUploadAttachment.newItem(f.path)).toList());
|
||||
}
|
||||
|
||||
static FutureResult<List<XFile>, dynamic> _desktopImagesFromDisk() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
allowMultiple: true,
|
||||
);
|
||||
if (result == null) {
|
||||
return Result.ok([]);
|
||||
}
|
||||
final files = result.files.map((f) => XFile(f.path!)).toList();
|
||||
return Result.ok(files);
|
||||
}
|
||||
|
||||
static Result<List<int>, dynamic> getUploadableImageBytes(String path,
|
||||
{int maxSizeBytes = 800000}) {
|
||||
return runCatching(() {
|
||||
final file = File(path);
|
||||
final fileBytes = file.readAsBytesSync();
|
||||
var size = file.statSync().size;
|
||||
|
||||
if (size <= maxSizeBytes) {
|
||||
return Result.ok(fileBytes);
|
||||
}
|
||||
|
||||
final imageType = ImageTypes.fromExtension(path);
|
||||
var scale = maxSizeBytes / size;
|
||||
late final Image original;
|
||||
switch (imageType) {
|
||||
case ImageTypes.gif:
|
||||
original = decodeGif(fileBytes)!;
|
||||
break;
|
||||
case ImageTypes.png:
|
||||
original = decodePng(fileBytes)!;
|
||||
break;
|
||||
case ImageTypes.jpg:
|
||||
original = decodeJpg(fileBytes)!;
|
||||
break;
|
||||
}
|
||||
late List<int> resizedBytes;
|
||||
while (size > maxSizeBytes) {
|
||||
final newWidth = (original.width * sqrt(scale)).toInt();
|
||||
final resized = copyResize(original, width: (newWidth).toInt());
|
||||
switch (imageType) {
|
||||
case ImageTypes.gif:
|
||||
resizedBytes = encodeGif(resized);
|
||||
break;
|
||||
case ImageTypes.png:
|
||||
resizedBytes = encodePng(resized);
|
||||
break;
|
||||
case ImageTypes.jpg:
|
||||
resizedBytes = encodeJpg(resized, quality: 80);
|
||||
break;
|
||||
}
|
||||
size = resizedBytes.length;
|
||||
scale /= 2;
|
||||
}
|
||||
|
||||
return Result.ok(resizedBytes);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import '../models/TimelineIdentifiers.dart';
|
|||
import '../models/entry_tree_item.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/group_data.dart';
|
||||
import '../models/media_attachment_uploads/entry_media_items.dart';
|
||||
import '../models/timeline.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import 'auth_service.dart';
|
||||
|
@ -60,12 +61,17 @@ class TimelineManager extends ChangeNotifier {
|
|||
);
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> createNewStatus(String text,
|
||||
{String spoilerText = '', String inReplyToId = ''}) async {
|
||||
FutureResult<bool, ExecError> createNewStatus(
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
required EntryMediaItems mediaItems,
|
||||
}) async {
|
||||
final result = await getIt<EntryManagerService>().createNewStatus(
|
||||
text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaItems: mediaItems,
|
||||
);
|
||||
if (result.isSuccess) {
|
||||
_logger.finest('Notifying listeners of new status created');
|
||||
|
|
100
pubspec.lock
100
pubspec.lock
|
@ -1,6 +1,13 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.5"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -64,6 +71,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.3+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -120,6 +141,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.2.4"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -153,6 +181,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -268,6 +303,48 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.6"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.5+4"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.10"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.6+3"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -346,7 +423,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.0.2"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
|
@ -408,6 +485,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -422,6 +506,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.6.2"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -679,6 +770,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+2"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
sdks:
|
||||
dart: ">=2.18.2 <3.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
|
|
@ -30,6 +30,10 @@ dependencies:
|
|||
time_machine: ^0.9.17
|
||||
url_launcher: ^6.1.6
|
||||
flutter_dotenv: ^5.0.2
|
||||
image_picker: ^0.8.6
|
||||
file_picker: ^5.2.4
|
||||
path: ^1.8.2
|
||||
image: ^3.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Ładowanie…
Reference in New Issue