import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/model/metadata/enums/metadata_type.dart'; import 'package:aves/model/metadata/fields.dart'; import 'package:aves/services/common/services.dart'; import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; abstract class MetadataEditService { Future> rotate(AvesEntry entry, {required bool clockwise}); Future> flip(AvesEntry entry); Future> editExifDate(AvesEntry entry, DateModifier modifier); Future> editMetadata(AvesEntry entry, Map modifier, {bool autoCorrectTrailerOffset = true}); Future> removeTrailerVideo(AvesEntry entry); Future> removeTypes(AvesEntry entry, Set types); } class PlatformMetadataEditService implements MetadataEditService { static const _platform = MethodChannel('deckers.thibault/aves/metadata_edit'); @override Future> rotate(AvesEntry entry, {required bool clockwise}) async { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await _platform.invokeMethod('rotate', { 'entry': entry.toPlatformEntryMap(), 'clockwise': clockwise, }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await _processPlatformException(entry, e, stack); } return {}; } @override Future> flip(AvesEntry entry) async { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await _platform.invokeMethod('flip', { 'entry': entry.toPlatformEntryMap(), }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await _processPlatformException(entry, e, stack); } return {}; } @override Future> editExifDate(AvesEntry entry, DateModifier modifier) async { try { final result = await _platform.invokeMethod('editDate', { 'entry': entry.toPlatformEntryMap(), 'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch, 'shiftMinutes': modifier.shiftMinutes, 'fields': modifier.fields.where((v) => v.type == MetadataType.exif).map((v) => v.toPlatform).whereNotNull().toList(), }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await _processPlatformException(entry, e, stack); } return {}; } @override Future> editMetadata( AvesEntry entry, Map metadata, { bool autoCorrectTrailerOffset = true, }) async { try { final result = await _platform.invokeMethod('editMetadata', { 'entry': entry.toPlatformEntryMap(), 'metadata': metadata.map((type, value) => MapEntry(type.toPlatform, value)), 'autoCorrectTrailerOffset': autoCorrectTrailerOffset, }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await _processPlatformException(entry, e, stack); } return {}; } @override Future> removeTrailerVideo(AvesEntry entry) async { try { final result = await _platform.invokeMethod('removeTrailerVideo', { 'entry': entry.toPlatformEntryMap(), }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await _processPlatformException(entry, e, stack); } return {}; } @override Future> removeTypes(AvesEntry entry, Set types) async { try { final result = await _platform.invokeMethod('removeTypes', { 'entry': entry.toPlatformEntryMap(), 'types': types.map((v) => v.toPlatform).toList(), }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await _processPlatformException(entry, e, stack); } return {}; } Future _processPlatformException(AvesEntry entry, PlatformException e, StackTrace stack) async { if (!entry.isMissingAtPath) { final code = e.code; if (code.endsWith('mp4largemoov')) { await reportService.recordError(_Mp4LargeMoovException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); } else if (code.endsWith('mp4largeother')) { await reportService.recordError(_Mp4LargeOtherException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); } else if (code.endsWith('filenotfound')) { await reportService.recordError(_FileNotFoundException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); } else { await reportService.recordError(e, stack); } } } } // distinct exceptions to convince Crashlytics to split reports into distinct issues class _Mp4LargeMoovException extends PlatformException { _Mp4LargeMoovException({ required super.code, required super.message, required super.details, required super.stacktrace, }); } class _Mp4LargeOtherException extends PlatformException { _Mp4LargeOtherException({ required super.code, required super.message, required super.details, required super.stacktrace, }); } class _FileNotFoundException extends PlatformException { _FileNotFoundException({ required super.code, required super.message, required super.details, required super.stacktrace, }); }