Add initial image attachments implementation

codemagic-setup
Hank Grabowski 2022-12-26 15:26:30 -05:00
rodzic 6589d4f572
commit 4e6bf2750e
19 zmienionych plików z 738 dodań i 51 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

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

Wyświetl plik

@ -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(() {});
},
),
),
],
);
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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,
});
}

Wyświetl plik

@ -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}';
}
}

Wyświetl plik

@ -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');
}
}

Wyświetl plik

@ -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}';
}
}

Wyświetl plik

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

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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;

Wyświetl plik

@ -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);
});
}
}

Wyświetl plik

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

Wyświetl plik

@ -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"

Wyświetl plik

@ -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: