support gifs and allow videos to pick from the gallery

This commit is contained in:
otsmr 2025-11-02 23:01:53 +01:00
parent e1232f45b5
commit 9356a1fc70
7 changed files with 114 additions and 66 deletions

View file

@ -161,13 +161,12 @@ class MediaFileService {
return;
}
switch (mediaFile.type) {
case MediaType.gif:
case MediaType.image:
// all images are already compress..
break;
case MediaType.video:
await createThumbnailsForVideo(storedPath, thumbnailPath);
case MediaType.gif:
Log.error('Thumbnail for .gif is not implemented yet');
}
}
@ -183,8 +182,7 @@ class MediaFileService {
case MediaType.video:
await compressAndOverlayVideo(this);
case MediaType.gif:
originalPath.renameSync(tempPath.path);
Log.error('Compression for .gif is not implemented yet.');
originalPath.copySync(tempPath.path);
}
}

View file

@ -45,7 +45,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
_imageSaving = true;
});
if (widget.mediaService.mediaFile.type == MediaType.image) {
if (widget.mediaService.mediaFile.type == MediaType.image ||
widget.mediaService.mediaFile.type == MediaType.gif) {
await widget.storeImageAsOriginal();
}

View file

@ -285,9 +285,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<Uint8List?>? imageBytes,
File? videoFilePath, {
bool sharedFromGallery = false,
MediaType? mediaType,
}) async {
final type = mediaType ??
((videoFilePath != null) ? MediaType.video : MediaType.image);
final mediaFileService = await initializeMediaUpload(
(videoFilePath != null) ? MediaType.video : MediaType.image,
type,
gUser.defaultShowTime,
);
if (!mounted) return true;
@ -377,14 +380,42 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
_sharePreviewIsShown = true;
});
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
final pickedFile = await picker.pickMedia();
if (pickedFile != null) {
final imageFile = File(pickedFile.path);
final imageExtensions = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
'.heic',
'.heif',
'.avif',
];
Log.info('Picket from gallery: ${pickedFile.path}');
File? videoFilePath;
Future<Uint8List>? imageBytes;
MediaType? mediaType;
final isImage =
imageExtensions.any((ext) => pickedFile.name.contains(ext));
if (isImage) {
if (pickedFile.name.contains('.gif')) {
mediaType = MediaType.gif;
}
imageBytes = pickedFile.readAsBytes();
} else {
videoFilePath = File(pickedFile.path);
}
await pushMediaEditor(
imageFile.readAsBytes(),
null,
imageBytes,
videoFilePath,
sharedFromGallery: true,
mediaType: mediaType,
);
}
setState(() {

View file

@ -55,6 +55,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
double widthRatio = 1;
double heightRatio = 1;
double pixelRatio = 1;
Uint8List? imageBytes;
VideoPlayerController? videoController;
ImageItem currentImage = ImageItem();
ScreenshotController screenshotController = ScreenshotController();
@ -66,13 +67,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
void initState() {
super.initState();
layers.add(FilterLayerData());
if (media.type != MediaType.gif) {
layers.add(FilterLayerData());
}
if (widget.sendToGroup != null) {
selectedGroupIds.add(widget.sendToGroup!.groupId);
}
if (widget.mediaFileService.mediaFile.type == MediaType.image) {
if (widget.mediaFileService.mediaFile.type == MediaType.video ||
widget.mediaFileService.mediaFile.type == MediaType.gif) {
if (widget.imageBytesFuture != null) {
loadImage(widget.imageBytesFuture!);
} else {
@ -124,52 +128,55 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
return [];
}
return <Widget>[
ActionButton(
Icons.text_fields_rounded,
tooltipText: context.lang.addTextItem,
onPressed: () async {
layers = layers.where((x) => !x.isDeleted).toList();
if (layers.any((x) => x.isEditing)) return;
undoLayers.clear();
removedLayers.clear();
layers.add(
TextLayerData(
textLayersBefore: layers.whereType<TextLayerData>().length,
),
);
setState(() {});
},
),
if (media.type != MediaType.gif)
ActionButton(
Icons.text_fields_rounded,
tooltipText: context.lang.addTextItem,
onPressed: () async {
layers = layers.where((x) => !x.isDeleted).toList();
if (layers.any((x) => x.isEditing)) return;
undoLayers.clear();
removedLayers.clear();
layers.add(
TextLayerData(
textLayersBefore: layers.whereType<TextLayerData>().length,
),
);
setState(() {});
},
),
const SizedBox(height: 8),
ActionButton(
Icons.draw_rounded,
tooltipText: context.lang.addDrawing,
onPressed: () async {
undoLayers.clear();
removedLayers.clear();
layers.add(DrawLayerData());
setState(() {});
},
),
if (media.type != MediaType.gif)
ActionButton(
Icons.draw_rounded,
tooltipText: context.lang.addDrawing,
onPressed: () async {
undoLayers.clear();
removedLayers.clear();
layers.add(DrawLayerData());
setState(() {});
},
),
const SizedBox(height: 8),
ActionButton(
Icons.add_reaction_outlined,
tooltipText: context.lang.addEmoji,
onPressed: () async {
final layer = await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (BuildContext context) {
return const Emojis();
},
) as Layer?;
if (layer == null) return;
undoLayers.clear();
removedLayers.clear();
layers.add(layer);
setState(() {});
},
),
if (media.type != MediaType.gif)
ActionButton(
Icons.add_reaction_outlined,
tooltipText: context.lang.addEmoji,
onPressed: () async {
final layer = await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (BuildContext context) {
return const Emojis();
},
) as Layer?;
if (layer == null) return;
undoLayers.clear();
removedLayers.clear();
layers.add(layer);
setState(() {});
},
),
const SizedBox(height: 8),
NotificationBadge(
count: (media.type == MediaType.video)
@ -356,12 +363,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (mediaService.tempPath.existsSync()) {
mediaService.tempPath.deleteSync();
}
final imageBytes = await getEditedImageBytes();
if (imageBytes == null) return false;
if (media.type == MediaType.image) {
mediaService.originalPath.writeAsBytesSync(imageBytes);
if (media.type == MediaType.gif) {
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
} else {
mediaService.overlayImagePath.writeAsBytesSync(imageBytes);
final imageBytes = await getEditedImageBytes();
if (imageBytes == null) return false;
if (media.type == MediaType.image || media.type == MediaType.gif) {
mediaService.originalPath.writeAsBytesSync(imageBytes);
} else if (media.type == MediaType.video) {
mediaService.overlayImagePath.writeAsBytesSync(imageBytes);
} else {
Log.error('MediaType not supported: ${media.type}');
}
}
// In case the image was already stored, then rename the stored image.
@ -374,7 +387,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}
Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
await currentImage.load(await imageBytesFuture);
imageBytes = await imageBytesFuture;
await currentImage.load(imageBytes);
if (isDisposed) return;
if (!context.mounted) return;

View file

@ -298,7 +298,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (gUser.storeMediaFilesInGallery) {
if (currentMedia!.mediaFile.type == MediaType.video) {
await saveVideoToGallery(currentMedia!.storedPath.path);
} else if (currentMedia!.mediaFile.type == MediaType.image) {
} else if (currentMedia!.mediaFile.type == MediaType.image ||
currentMedia!.mediaFile.type == MediaType.gif) {
final imageBytes = await currentMedia!.storedPath.readAsBytes();
await saveImageToGallery(imageBytes);
}
@ -466,7 +467,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: VideoPlayer(videoController!),
),
if (currentMedia != null &&
currentMedia!.mediaFile.type == MediaType.image)
currentMedia!.mediaFile.type == MediaType.image ||
currentMedia!.mediaFile.type == MediaType.gif)
Positioned.fill(
child: Image.file(
currentMedia!.tempPath,

View file

@ -57,7 +57,8 @@ class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
if (media.thumbnailPath.existsSync())
Image.file(media.thumbnailPath)
else if (media.storedPath.existsSync() &&
media.mediaFile.type == MediaType.image)
media.mediaFile.type == MediaType.image ||
media.mediaFile.type == MediaType.gif)
Image.file(media.storedPath)
else
const Text('Media file removed.'),

View file

@ -75,7 +75,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
try {
if (item.mediaFile.type == MediaType.video) {
await saveVideoToGallery(item.storedPath.path);
} else if (item.mediaFile.type == MediaType.image) {
} else if (item.mediaFile.type == MediaType.image ||
item.mediaFile.type == MediaType.gif) {
final imageBytes = await item.storedPath.readAsBytes();
await saveImageToGallery(imageBytes);
}