diff --git a/android/fastlane/metadata/android/de-DE/changelogs/1000000077.txt b/android/fastlane/metadata/android/de-DE/changelogs/1000000077.txt new file mode 100644 index 0000000..da6c014 --- /dev/null +++ b/android/fastlane/metadata/android/de-DE/changelogs/1000000077.txt @@ -0,0 +1 @@ +Unterstützung für das Hochladen von Fotos hinzugefügt. \ No newline at end of file diff --git a/android/fastlane/metadata/android/de-DE/changelogs/default.txt b/android/fastlane/metadata/android/de-DE/changelogs/default.txt index 1d8b4be..da6c014 100644 --- a/android/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/android/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1 +1 @@ -Öffnungszeitenbearbeitung hinzugefügt \ No newline at end of file +Unterstützung für das Hochladen von Fotos hinzugefügt. \ No newline at end of file diff --git a/android/fastlane/metadata/android/en-US/changelogs/1000000077.txt b/android/fastlane/metadata/android/en-US/changelogs/1000000077.txt new file mode 100644 index 0000000..45c7113 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/1000000077.txt @@ -0,0 +1 @@ +Added support for uploading photos. \ No newline at end of file diff --git a/android/fastlane/metadata/android/en-US/changelogs/default.txt b/android/fastlane/metadata/android/en-US/changelogs/default.txt index 71bcbc2..45c7113 100644 --- a/android/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/android/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1 +1 @@ -Added editing of opening hours \ No newline at end of file +Added support for uploading photos. \ No newline at end of file diff --git a/android/fastlane/metadata/android/es-ES/changelogs/1000000077.txt b/android/fastlane/metadata/android/es-ES/changelogs/1000000077.txt new file mode 100644 index 0000000..6c5d144 --- /dev/null +++ b/android/fastlane/metadata/android/es-ES/changelogs/1000000077.txt @@ -0,0 +1 @@ +Se añadió soporte para subir fotos. \ No newline at end of file diff --git a/android/fastlane/metadata/android/es-ES/changelogs/default.txt b/android/fastlane/metadata/android/es-ES/changelogs/default.txt index 3561cf0..6c5d144 100644 --- a/android/fastlane/metadata/android/es-ES/changelogs/default.txt +++ b/android/fastlane/metadata/android/es-ES/changelogs/default.txt @@ -1 +1 @@ -Se añadió la edición de horas de apertura \ No newline at end of file +Se añadió soporte para subir fotos. \ No newline at end of file diff --git a/android/fastlane/metadata/android/fr-FR/changelogs/1000000077.txt b/android/fastlane/metadata/android/fr-FR/changelogs/1000000077.txt new file mode 100644 index 0000000..da4df0a --- /dev/null +++ b/android/fastlane/metadata/android/fr-FR/changelogs/1000000077.txt @@ -0,0 +1 @@ +Ajout du support pour le téléchargement de photos. \ No newline at end of file diff --git a/android/fastlane/metadata/android/fr-FR/changelogs/default.txt b/android/fastlane/metadata/android/fr-FR/changelogs/default.txt index 7e68111..da4df0a 100644 --- a/android/fastlane/metadata/android/fr-FR/changelogs/default.txt +++ b/android/fastlane/metadata/android/fr-FR/changelogs/default.txt @@ -1 +1 @@ -Ajout de l'édition des heures d'ouverture \ No newline at end of file +Ajout du support pour le téléchargement de photos. \ No newline at end of file diff --git a/android/fastlane/metadata/android/pl-PL/changelogs/1000000077.txt b/android/fastlane/metadata/android/pl-PL/changelogs/1000000077.txt new file mode 100644 index 0000000..68dfa95 --- /dev/null +++ b/android/fastlane/metadata/android/pl-PL/changelogs/1000000077.txt @@ -0,0 +1 @@ +Dodano obsługę przesyłania zdjęć. \ No newline at end of file diff --git a/android/fastlane/metadata/android/pl-PL/changelogs/default.txt b/android/fastlane/metadata/android/pl-PL/changelogs/default.txt index 3992ba4..68dfa95 100644 --- a/android/fastlane/metadata/android/pl-PL/changelogs/default.txt +++ b/android/fastlane/metadata/android/pl-PL/changelogs/default.txt @@ -1 +1 @@ -Dodano edycję godzin otwarcia \ No newline at end of file +Dodano obsługę przesyłania zdjęć. \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 357dbbe..b225d12 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -61,6 +61,11 @@ PODS: - Flutter (1.0.0) - flutter_compass (0.0.1): - Flutter + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder - flutter_keyboard_visibility_temp_fork (0.0.1): - Flutter - flutter_web_auth_2 (5.0.0): @@ -122,6 +127,8 @@ PODS: - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - heif_converter (1.0.2): + - Flutter - image_picker_ios (0.0.1): - Flutter - in_app_review (2.0.0): @@ -131,6 +138,21 @@ PODS: - json-enum (1.2.3) - jsonlogic (1.2.4): - json-enum (~> 1.2.3) + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): + - libwebp/webp + - libwebp/mux (1.5.0): + - libwebp/demux + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): + - libwebp/sharpyuv + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) - maps_launcher (0.0.1): - Flutter - Mixpanel-swift (6.2.0): @@ -152,6 +174,12 @@ PODS: - Flutter - FlutterMacOS - PromisesObjC (2.4.0) + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) + - SDWebImageWebPCoder (0.15.0): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) - Sentry/HybridSDK (8.58.1) - sentry_flutter (9.17.0): - Flutter @@ -163,6 +191,28 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - TensorFlowLiteC (2.12.0): + - TensorFlowLiteC/Core (= 2.12.0) + - TensorFlowLiteC/Core (2.12.0) + - TensorFlowLiteC/CoreML (2.12.0): + - TensorFlowLiteC/Core + - TensorFlowLiteC/Metal (2.12.0): + - TensorFlowLiteC/Core + - TensorFlowLiteSwift (2.12.0): + - TensorFlowLiteSwift/Core (= 2.12.0) + - TensorFlowLiteSwift/Core (2.12.0): + - TensorFlowLiteC (= 2.12.0) + - TensorFlowLiteSwift/CoreML (2.12.0): + - TensorFlowLiteC/CoreML (= 2.12.0) + - TensorFlowLiteSwift/Core (= 2.12.0) + - TensorFlowLiteSwift/Metal (2.12.0): + - TensorFlowLiteC/Metal (= 2.12.0) + - TensorFlowLiteSwift/Core (= 2.12.0) + - tflite_flutter (0.0.1): + - Flutter + - TensorFlowLiteSwift (= 2.12.0) + - TensorFlowLiteSwift/CoreML (= 2.12.0) + - TensorFlowLiteSwift/Metal (= 2.12.0) - url_launcher_ios (0.0.1): - Flutter @@ -173,9 +223,11 @@ DEPENDENCIES: - firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`) - Flutter (from `Flutter`) - flutter_compass (from `.symlinks/plugins/flutter_compass/ios`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) + - heif_converter (from `.symlinks/plugins/heif_converter/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) @@ -186,6 +238,7 @@ DEPENDENCIES: - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - tflite_flutter (from `.symlinks/plugins/tflite_flutter/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -204,10 +257,16 @@ SPEC REPOS: - GoogleUtilities - json-enum - jsonlogic + - libwebp + - Mantle - Mixpanel-swift - nanopb - PromisesObjC + - SDWebImage + - SDWebImageWebPCoder - Sentry + - TensorFlowLiteC + - TensorFlowLiteSwift EXTERNAL SOURCES: connectivity_plus: @@ -222,12 +281,16 @@ EXTERNAL SOURCES: :path: Flutter flutter_compass: :path: ".symlinks/plugins/flutter_compass/ios" + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_keyboard_visibility_temp_fork: :path: ".symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios" flutter_web_auth_2: :path: ".symlinks/plugins/flutter_web_auth_2/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/darwin" + heif_converter: + :path: ".symlinks/plugins/heif_converter/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" in_app_review: @@ -248,6 +311,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + tflite_flutter: + :path: ".symlinks/plugins/tflite_flutter/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" @@ -267,17 +332,21 @@ SPEC CHECKSUMS: FirebaseSharedSwift: bccaff90721d14bafc14be34f28b77fdd7c91dc9 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439 geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e GoogleAdsOnDeviceConversion: 02fc8fe599acd867e328321effb0d9b2d023a38a GoogleAppMeasurement: 3b4687de50ab25ee2d4d541849f10ca8df862a12 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + heif_converter: 5087f3c01bcc9c38b24084cd3700cc6064274672 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e json-enum: 57ad746d2f0d7852796e9aa50267bd84a778222e jsonlogic: 006f892470384401b8ca5b5d8d4cdadb3a0d5c9b + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45 Mixpanel-swift: 9b4d920ceaf40f6145ca9fd84d4acca49df44a5e mixpanel_flutter: d61b2d8480daea10afe0c8ec438d88bbe978b713 @@ -285,10 +354,15 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 Sentry: 958d9619ceccf6abb8c4736003fa336dac1a80a7 sentry_flutter: b4ad2ac16ea7ad64fd5254c049a5fd5d06510283 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + TensorFlowLiteC: 20785a69299185a379ba9852b6625f00afd7984a + TensorFlowLiteSwift: 3a4928286e9e35bdd3e17970f48e53c80d25e793 + tflite_flutter: 64b192e11352fe36943ab6656e1d49207f1a5595 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d PODFILE CHECKSUM: d636971c0395b4869ef5655cb3deda6ce66668db diff --git a/ios/build/.last_build_id b/ios/build/.last_build_id deleted file mode 100644 index b4eb19c..0000000 --- a/ios/build/.last_build_id +++ /dev/null @@ -1 +0,0 @@ -b17e8ce20ac7fe97796ada35205d4836 \ No newline at end of file diff --git a/ios/fastlane/metadata/de-DE/release_notes.txt b/ios/fastlane/metadata/de-DE/release_notes.txt index 1d8b4be..da6c014 100644 --- a/ios/fastlane/metadata/de-DE/release_notes.txt +++ b/ios/fastlane/metadata/de-DE/release_notes.txt @@ -1 +1 @@ -Öffnungszeitenbearbeitung hinzugefügt \ No newline at end of file +Unterstützung für das Hochladen von Fotos hinzugefügt. \ No newline at end of file diff --git a/ios/fastlane/metadata/en-US/release_notes.txt b/ios/fastlane/metadata/en-US/release_notes.txt index 71bcbc2..45c7113 100644 --- a/ios/fastlane/metadata/en-US/release_notes.txt +++ b/ios/fastlane/metadata/en-US/release_notes.txt @@ -1 +1 @@ -Added editing of opening hours \ No newline at end of file +Added support for uploading photos. \ No newline at end of file diff --git a/ios/fastlane/metadata/es-ES/release_notes.txt b/ios/fastlane/metadata/es-ES/release_notes.txt index 3561cf0..6c5d144 100644 --- a/ios/fastlane/metadata/es-ES/release_notes.txt +++ b/ios/fastlane/metadata/es-ES/release_notes.txt @@ -1 +1 @@ -Se añadió la edición de horas de apertura \ No newline at end of file +Se añadió soporte para subir fotos. \ No newline at end of file diff --git a/ios/fastlane/metadata/fr-FR/release_notes.txt b/ios/fastlane/metadata/fr-FR/release_notes.txt index 7e68111..da4df0a 100644 --- a/ios/fastlane/metadata/fr-FR/release_notes.txt +++ b/ios/fastlane/metadata/fr-FR/release_notes.txt @@ -1 +1 @@ -Ajout de l'édition des heures d'ouverture \ No newline at end of file +Ajout du support pour le téléchargement de photos. \ No newline at end of file diff --git a/ios/fastlane/metadata/it/release_notes.txt b/ios/fastlane/metadata/it/release_notes.txt index 26bc9a8..0513557 100644 --- a/ios/fastlane/metadata/it/release_notes.txt +++ b/ios/fastlane/metadata/it/release_notes.txt @@ -1 +1 @@ -Aggiunta la modifica degli orari di apertura \ No newline at end of file +Aggiunta la supporto per il caricamento di foto. \ No newline at end of file diff --git a/ios/fastlane/metadata/pl/release_notes.txt b/ios/fastlane/metadata/pl/release_notes.txt index 3992ba4..68dfa95 100644 --- a/ios/fastlane/metadata/pl/release_notes.txt +++ b/ios/fastlane/metadata/pl/release_notes.txt @@ -1 +1 @@ -Dodano edycję godzin otwarcia \ No newline at end of file +Dodano obsługę przesyłania zdjęć. \ No newline at end of file diff --git a/lib/bloc/edit/edit_cubit.dart b/lib/bloc/edit/edit_cubit.dart index dc13307..7028c01 100644 --- a/lib/bloc/edit/edit_cubit.dart +++ b/lib/bloc/edit/edit_cubit.dart @@ -15,6 +15,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:in_app_review/in_app_review.dart'; import 'package:latlong2/latlong.dart'; +import 'package:nsfw_detector_flutter/nsfw_detector_flutter.dart'; class EditCubit extends Cubit { EditCubit({ @@ -244,6 +245,81 @@ class EditCubit extends Cubit { } } + Future isPhotoUnsafe(File file) async { + try { + var result = await NsfwDetector.instance.detectNSFWFromFile(file); + return result?.isNsfw ?? false; + } catch (_) { + return false; + } + } + + Future submitPhoto(Defibrillator defibrillator, File file) async { + emit(state.copyWith(photoStatus: PhotoStatus.uploading)); + try { + var authenticated = await pointsRepository.authenticate(); + if (!authenticated) { + emit(state.copyWith( + photoStatus: PhotoStatus.uploadFailure, + photoErrorMessage: 'Authentication failed')); + return; + } + final photoBytes = await file.readAsBytes(); + await pointsRepository.uploadPhoto( + nodeId: defibrillator.id, file: file); + final imageUrl = + await pointsRepository.getBackendImageUrl(defibrillator.id); + final updated = defibrillator.copyWith( + image: (imageUrl != null && imageUrl.isNotEmpty) + ? imageUrl + : defibrillator.image, + photoBytes: photoBytes); + await pendingChangesRepository.register(PendingChange( + type: PendingChangeType.edit, + defibrillatorId: updated.id, + snapshot: updated.copyWith(), + createdAt: DateTime.now(), + )); + final updatedPendingChanges = await pendingChangesRepository.fetch(); + analytics.event(name: photoUploadEvent); + if (!Platform.environment.containsKey('FLUTTER_TEST')) { + mixpanel.track(photoUploadEvent, + properties: updated.getEventProperties()); + } + emit(state.copyWith( + pendingChanges: updatedPendingChanges, + photoStatus: PhotoStatus.uploadSuccess, + photoUpdatedDefibrillator: updated)); + } catch (error) { + emit(state.copyWith( + photoStatus: PhotoStatus.uploadFailure, + photoErrorMessage: error.toString())); + } + } + + Future reportPhoto(Defibrillator defibrillator, String photoId) async { + emit(state.copyWith(photoStatus: PhotoStatus.reporting)); + try { + await pointsRepository.reportPhoto(photoId); + analytics.event(name: photoReportEvent); + if (!Platform.environment.containsKey('FLUTTER_TEST')) { + mixpanel.track(photoReportEvent, properties: { + ...defibrillator.getEventProperties(), + 'aed_photo_id': photoId, + }); + } + emit(state.copyWith(photoStatus: PhotoStatus.reportSuccess)); + } catch (error) { + emit(state.copyWith( + photoStatus: PhotoStatus.reportFailure, + photoErrorMessage: error.toString())); + } + } + + void resetPhotoStatus() { + emit(state.copyWith(photoStatus: PhotoStatus.idle)); + } + void maybeRequestReview() { if (kDebugMode) return; Future.delayed(const Duration(seconds: 2)).then((_) async { diff --git a/lib/bloc/edit/edit_state.dart b/lib/bloc/edit/edit_state.dart index b63ad73..83f3ca3 100644 --- a/lib/bloc/edit/edit_state.dart +++ b/lib/bloc/edit/edit_state.dart @@ -4,15 +4,37 @@ import 'package:aed_map/models/user.dart'; import 'package:equatable/equatable.dart'; import 'package:latlong2/latlong.dart'; +enum PhotoStatus { + idle, + uploading, + uploadSuccess, + uploadFailure, + reporting, + reportSuccess, + reportFailure, +} + abstract class EditState extends Equatable { final bool enabled; final LatLng cursor; final User? user; final List pendingChanges; final String? errorMessage; + final PhotoStatus photoStatus; + final String? photoErrorMessage; + final Defibrillator? photoUpdatedDefibrillator; @override - List get props => [enabled, cursor, user, pendingChanges, errorMessage]; + List get props => [ + enabled, + cursor, + user, + pendingChanges, + errorMessage, + photoStatus, + photoErrorMessage, + photoUpdatedDefibrillator, + ]; const EditState({ required this.enabled, @@ -20,6 +42,9 @@ abstract class EditState extends Equatable { required this.user, required this.pendingChanges, this.errorMessage, + this.photoStatus = PhotoStatus.idle, + this.photoErrorMessage, + this.photoUpdatedDefibrillator, }); EditState copyWith({ @@ -28,6 +53,9 @@ abstract class EditState extends Equatable { User? user, List? pendingChanges, String? errorMessage, + PhotoStatus? photoStatus, + String? photoErrorMessage, + Defibrillator? photoUpdatedDefibrillator, }); } @@ -38,6 +66,9 @@ class EditReady extends EditState { super.user, super.pendingChanges = const [], super.errorMessage, + super.photoStatus, + super.photoErrorMessage, + super.photoUpdatedDefibrillator, }); @override @@ -47,6 +78,9 @@ class EditReady extends EditState { User? user, List? pendingChanges, String? errorMessage, + PhotoStatus? photoStatus, + String? photoErrorMessage, + Defibrillator? photoUpdatedDefibrillator, }) { return EditReady( enabled: enabled ?? this.enabled, @@ -54,6 +88,9 @@ class EditReady extends EditState { user: user ?? this.user, pendingChanges: pendingChanges ?? this.pendingChanges, errorMessage: errorMessage, + photoStatus: photoStatus ?? this.photoStatus, + photoErrorMessage: photoErrorMessage, + photoUpdatedDefibrillator: photoUpdatedDefibrillator, ); } } @@ -66,6 +103,9 @@ class EditInProgress extends EditState { super.user, super.pendingChanges = const [], super.errorMessage, + super.photoStatus, + super.photoErrorMessage, + super.photoUpdatedDefibrillator, this.indoor = 'no', this.access = 'public', this.description = '', @@ -87,6 +127,9 @@ class EditInProgress extends EditState { description, pendingChanges, errorMessage, + photoStatus, + photoErrorMessage, + photoUpdatedDefibrillator, ]; @override @@ -100,6 +143,9 @@ class EditInProgress extends EditState { User? user, List? pendingChanges, String? errorMessage, + PhotoStatus? photoStatus, + String? photoErrorMessage, + Defibrillator? photoUpdatedDefibrillator, }) { return EditInProgress( defibrillator: defibrillator ?? this.defibrillator, @@ -111,6 +157,9 @@ class EditInProgress extends EditState { user: user ?? this.user, pendingChanges: pendingChanges ?? this.pendingChanges, errorMessage: errorMessage, + photoStatus: photoStatus ?? this.photoStatus, + photoErrorMessage: photoErrorMessage, + photoUpdatedDefibrillator: photoUpdatedDefibrillator, ); } } \ No newline at end of file diff --git a/lib/bloc/points/points_cubit.dart b/lib/bloc/points/points_cubit.dart index 774d152..0c38512 100644 --- a/lib/bloc/points/points_cubit.dart +++ b/lib/bloc/points/points_cubit.dart @@ -127,8 +127,6 @@ class PointsCubit extends Cubit { emit((state as PointsLoadSuccess) .copyWith(selected: defibrillator, hash: generateRandomString(32))); } - - loadImage(); } void update(Defibrillator defibrillator) { @@ -197,12 +195,4 @@ class PointsCubit extends Cubit { .toList(); } - Future loadImage() async { - var state = this.state; - if (state is PointsLoadSuccess) { - var url = await pointsRepository.getImage(state.selected); - var defibrillator = state.selected.copyWith(image: url); - emit(state.copyWith(selected: defibrillator, hash: generateRandomString(32))); - } - } } \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 9e18e26..8ecab6d 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -33,3 +33,5 @@ const requestReviewEvent = 'review_requested'; const deleteEvent = 'delete'; const livechatEvent = 'livechat'; const pendingChangesEvent = 'pending_changes'; +const photoUploadEvent = 'photo_upload'; +const photoReportEvent = 'photo_report'; diff --git a/lib/generated/i18n/app_localizations.dart b/lib/generated/i18n/app_localizations.dart index 87b8747..49e8afa 100644 --- a/lib/generated/i18n/app_localizations.dart +++ b/lib/generated/i18n/app_localizations.dart @@ -825,6 +825,90 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Failed to save changes (HTTP {code})'** String osmErrorGeneric(int code); + + /// No description provided for @addPhoto. + /// + /// In en, this message translates to: + /// **'Add photo'** + String get addPhoto; + + /// No description provided for @changePhoto. + /// + /// In en, this message translates to: + /// **'Set photo'** + String get changePhoto; + + /// No description provided for @photo. + /// + /// In en, this message translates to: + /// **'Photo'** + String get photo; + + /// No description provided for @chooseFromGallery. + /// + /// In en, this message translates to: + /// **'Choose from gallery'** + String get chooseFromGallery; + + /// No description provided for @takePhoto. + /// + /// In en, this message translates to: + /// **'Take photo'** + String get takePhoto; + + /// No description provided for @photoLicenseInfo. + /// + /// In en, this message translates to: + /// **'All AED photos are hosted by OpenStreetMap Polska and licensed under Creative Commons Zero (CC0 v1.0). By uploading a photo you agree to the terms of this license.\n\nIn short, this means the photos may be used by anyone for any purpose. Attribution is not required.'** + String get photoLicenseInfo; + + /// No description provided for @photoLicenseLink. + /// + /// In en, this message translates to: + /// **'Creative Commons Zero (CC0 v1.0)'** + String get photoLicenseLink; + + /// No description provided for @nsfwBlockedTitle. + /// + /// In en, this message translates to: + /// **'Photo rejected'** + String get nsfwBlockedTitle; + + /// No description provided for @nsfwBlockedMessage. + /// + /// In en, this message translates to: + /// **'This photo cannot be added because it was detected as inappropriate.'** + String get nsfwBlockedMessage; + + /// No description provided for @reportPhoto. + /// + /// In en, this message translates to: + /// **'Report photo'** + String get reportPhoto; + + /// No description provided for @reportPhotoConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to report this photo as inappropriate?'** + String get reportPhotoConfirm; + + /// No description provided for @photoReported. + /// + /// In en, this message translates to: + /// **'Photo has been reported'** + String get photoReported; + + /// No description provided for @photoUploaded. + /// + /// In en, this message translates to: + /// **'Photo saved successfully'** + String get photoUploaded; + + /// No description provided for @photoUploadFailed. + /// + /// In en, this message translates to: + /// **'Failed to upload photo'** + String get photoUploadFailed; } class _AppLocalizationsDelegate diff --git a/lib/generated/i18n/app_localizations_de.dart b/lib/generated/i18n/app_localizations_de.dart index 9a9c9ba..5eac1ba 100644 --- a/lib/generated/i18n/app_localizations_de.dart +++ b/lib/generated/i18n/app_localizations_de.dart @@ -398,4 +398,49 @@ class AppLocalizationsDe extends AppLocalizations { String osmErrorGeneric(int code) { return 'Änderungen konnten nicht gespeichert werden (HTTP $code)'; } + + @override + String get addPhoto => 'Foto hinzufügen'; + + @override + String get changePhoto => 'Foto festlegen'; + + @override + String get photo => 'Foto'; + + @override + String get chooseFromGallery => 'Aus Galerie wählen'; + + @override + String get takePhoto => 'Foto aufnehmen'; + + @override + String get photoLicenseInfo => + 'Alle AED-Fotos werden von OpenStreetMap Polska gehostet und stehen unter der Creative Commons Zero (CC0 v1.0)-Lizenz. Durch das Hochladen eines Fotos stimmen Sie den Bedingungen dieser Lizenz zu.\n\nKurz gesagt bedeutet dies, dass die Fotos von jedermann für jeden Zweck verwendet werden dürfen. Eine Namensnennung ist nicht erforderlich.'; + + @override + String get photoLicenseLink => 'Creative Commons Zero (CC0 v1.0)'; + + @override + String get nsfwBlockedTitle => 'Foto abgelehnt'; + + @override + String get nsfwBlockedMessage => + 'Dieses Foto kann nicht hinzugefügt werden, da es als unangemessen erkannt wurde.'; + + @override + String get reportPhoto => 'Foto melden'; + + @override + String get reportPhotoConfirm => + 'Möchten Sie dieses Foto wirklich als unangemessen melden?'; + + @override + String get photoReported => 'Foto wurde gemeldet'; + + @override + String get photoUploaded => 'Foto erfolgreich gespeichert'; + + @override + String get photoUploadFailed => 'Foto konnte nicht hochgeladen werden'; } diff --git a/lib/generated/i18n/app_localizations_en.dart b/lib/generated/i18n/app_localizations_en.dart index cb80db8..1abbc25 100644 --- a/lib/generated/i18n/app_localizations_en.dart +++ b/lib/generated/i18n/app_localizations_en.dart @@ -394,4 +394,49 @@ class AppLocalizationsEn extends AppLocalizations { String osmErrorGeneric(int code) { return 'Failed to save changes (HTTP $code)'; } + + @override + String get addPhoto => 'Add photo'; + + @override + String get changePhoto => 'Set photo'; + + @override + String get photo => 'Photo'; + + @override + String get chooseFromGallery => 'Choose from gallery'; + + @override + String get takePhoto => 'Take photo'; + + @override + String get photoLicenseInfo => + 'All AED photos are hosted by OpenStreetMap Polska and licensed under Creative Commons Zero (CC0 v1.0). By uploading a photo you agree to the terms of this license.\n\nIn short, this means the photos may be used by anyone for any purpose. Attribution is not required.'; + + @override + String get photoLicenseLink => 'Creative Commons Zero (CC0 v1.0)'; + + @override + String get nsfwBlockedTitle => 'Photo rejected'; + + @override + String get nsfwBlockedMessage => + 'This photo cannot be added because it was detected as inappropriate.'; + + @override + String get reportPhoto => 'Report photo'; + + @override + String get reportPhotoConfirm => + 'Are you sure you want to report this photo as inappropriate?'; + + @override + String get photoReported => 'Photo has been reported'; + + @override + String get photoUploaded => 'Photo saved successfully'; + + @override + String get photoUploadFailed => 'Failed to upload photo'; } diff --git a/lib/generated/i18n/app_localizations_es.dart b/lib/generated/i18n/app_localizations_es.dart index ccbdd19..e553faa 100644 --- a/lib/generated/i18n/app_localizations_es.dart +++ b/lib/generated/i18n/app_localizations_es.dart @@ -398,4 +398,49 @@ class AppLocalizationsEs extends AppLocalizations { String osmErrorGeneric(int code) { return 'No se pudieron guardar los cambios (HTTP $code)'; } + + @override + String get addPhoto => 'Añadir foto'; + + @override + String get changePhoto => 'Establecer foto'; + + @override + String get photo => 'Foto'; + + @override + String get chooseFromGallery => 'Elegir de la galería'; + + @override + String get takePhoto => 'Tomar foto'; + + @override + String get photoLicenseInfo => + 'Todas las fotos AED están alojadas por OpenStreetMap Polska y tienen licencia Creative Commons Zero (CC0 v1.0). Al subir una foto, aceptas los términos de esta licencia.\n\nEn resumen, esto significa que las fotos pueden ser utilizadas por cualquiera para cualquier fin. No se requiere atribución.'; + + @override + String get photoLicenseLink => 'Creative Commons Zero (CC0 v1.0)'; + + @override + String get nsfwBlockedTitle => 'Foto rechazada'; + + @override + String get nsfwBlockedMessage => + 'Esta foto no puede añadirse porque fue detectada como inapropiada.'; + + @override + String get reportPhoto => 'Reportar foto'; + + @override + String get reportPhotoConfirm => + '¿Estás seguro de que deseas reportar esta foto como inapropiada?'; + + @override + String get photoReported => 'La foto ha sido reportada'; + + @override + String get photoUploaded => 'Foto guardada correctamente'; + + @override + String get photoUploadFailed => 'No se pudo subir la foto'; } diff --git a/lib/generated/i18n/app_localizations_fr.dart b/lib/generated/i18n/app_localizations_fr.dart index 2aababe..3754cf0 100644 --- a/lib/generated/i18n/app_localizations_fr.dart +++ b/lib/generated/i18n/app_localizations_fr.dart @@ -401,4 +401,49 @@ class AppLocalizationsFr extends AppLocalizations { String osmErrorGeneric(int code) { return 'Échec de la sauvegarde des modifications (HTTP $code)'; } + + @override + String get addPhoto => 'Ajouter une photo'; + + @override + String get changePhoto => 'Définir la photo'; + + @override + String get photo => 'Photo'; + + @override + String get chooseFromGallery => 'Choisir dans la galerie'; + + @override + String get takePhoto => 'Prendre une photo'; + + @override + String get photoLicenseInfo => + 'Toutes les photos AED sont hébergées par OpenStreetMap Polska et sont sous licence Creative Commons Zero (CC0 v1.0). En téléchargeant une photo, vous acceptez les conditions de cette licence.\n\nEn bref, cela signifie que les photos peuvent être utilisées par n\'importe qui à n\'importe quelle fin. L\'attribution n\'est pas requise.'; + + @override + String get photoLicenseLink => 'Creative Commons Zero (CC0 v1.0)'; + + @override + String get nsfwBlockedTitle => 'Photo rejetée'; + + @override + String get nsfwBlockedMessage => + 'Cette photo ne peut pas être ajoutée car elle a été détectée comme inappropriée.'; + + @override + String get reportPhoto => 'Signaler la photo'; + + @override + String get reportPhotoConfirm => + 'Êtes-vous sûr de vouloir signaler cette photo comme inappropriée ?'; + + @override + String get photoReported => 'La photo a été signalée'; + + @override + String get photoUploaded => 'Photo enregistrée avec succès'; + + @override + String get photoUploadFailed => 'Échec du téléchargement de la photo'; } diff --git a/lib/generated/i18n/app_localizations_it.dart b/lib/generated/i18n/app_localizations_it.dart index 4fabcb4..271dcd9 100644 --- a/lib/generated/i18n/app_localizations_it.dart +++ b/lib/generated/i18n/app_localizations_it.dart @@ -397,4 +397,49 @@ class AppLocalizationsIt extends AppLocalizations { String osmErrorGeneric(int code) { return 'Impossibile salvare le modifiche (HTTP $code)'; } + + @override + String get addPhoto => 'Aggiungi foto'; + + @override + String get changePhoto => 'Imposta foto'; + + @override + String get photo => 'Foto'; + + @override + String get chooseFromGallery => 'Scegli dalla galleria'; + + @override + String get takePhoto => 'Scatta una foto'; + + @override + String get photoLicenseInfo => + 'Tutte le foto AED sono ospitate da OpenStreetMap Polska e sono concesse in licenza Creative Commons Zero (CC0 v1.0). Caricando una foto, accetti i termini di questa licenza.\n\nIn breve, ciò significa che le foto possono essere utilizzate da chiunque per qualsiasi scopo. L\'attribuzione non è richiesta.'; + + @override + String get photoLicenseLink => 'Creative Commons Zero (CC0 v1.0)'; + + @override + String get nsfwBlockedTitle => 'Foto rifiutata'; + + @override + String get nsfwBlockedMessage => + 'Questa foto non può essere aggiunta perché è stata rilevata come inappropriata.'; + + @override + String get reportPhoto => 'Segnala foto'; + + @override + String get reportPhotoConfirm => + 'Sei sicuro di voler segnalare questa foto come inappropriata?'; + + @override + String get photoReported => 'La foto è stata segnalata'; + + @override + String get photoUploaded => 'Foto salvata con successo'; + + @override + String get photoUploadFailed => 'Impossibile caricare la foto'; } diff --git a/lib/generated/i18n/app_localizations_pl.dart b/lib/generated/i18n/app_localizations_pl.dart index a82bf4e..75ee540 100644 --- a/lib/generated/i18n/app_localizations_pl.dart +++ b/lib/generated/i18n/app_localizations_pl.dart @@ -396,4 +396,49 @@ class AppLocalizationsPl extends AppLocalizations { String osmErrorGeneric(int code) { return 'Nie udało się zapisać zmian (HTTP $code)'; } + + @override + String get addPhoto => 'Dodaj zdjęcie'; + + @override + String get changePhoto => 'Ustaw zdjęcie'; + + @override + String get photo => 'Zdjęcie'; + + @override + String get chooseFromGallery => 'Wybierz z galerii'; + + @override + String get takePhoto => 'Zrób zdjęcie'; + + @override + String get photoLicenseInfo => + 'Wszystkie zdjęcia AED są hostowane przez OpenStreetMap Polska i są na licencji Creative Commons Zero (CC0 v1.0). Wysyłając zdjęcie zgadzasz się na warunki tej licencji.\n\nW skrócie oznacza to, że zdjęcia mogą być potem wykorzystane przez kogokolwiek w dowolnym celu. Nie jest konieczne wskazanie autora.'; + + @override + String get photoLicenseLink => 'Creative Commons Zero (CC0 v1.0)'; + + @override + String get nsfwBlockedTitle => 'Zdjęcie odrzucone'; + + @override + String get nsfwBlockedMessage => + 'To zdjęcie nie może być dodane, ponieważ zostało uznane za nieodpowiednie.'; + + @override + String get reportPhoto => 'Zgłoś zdjęcie'; + + @override + String get reportPhotoConfirm => + 'Czy na pewno chcesz zgłosić to zdjęcie jako nieodpowiednie?'; + + @override + String get photoReported => 'Zdjęcie zostało zgłoszone'; + + @override + String get photoUploaded => 'Zdjęcie zostało zapisane'; + + @override + String get photoUploadFailed => 'Nie udało się przesłać zdjęcia'; } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f622b0c..bae1da3 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -162,5 +162,19 @@ "example": "500" } } - } + }, + "addPhoto": "Foto hinzufügen", + "changePhoto": "Foto festlegen", + "photo": "Foto", + "chooseFromGallery": "Aus Galerie wählen", + "takePhoto": "Foto aufnehmen", + "photoLicenseInfo": "Alle AED-Fotos werden von OpenStreetMap Polska gehostet und stehen unter der Creative Commons Zero (CC0 v1.0)-Lizenz. Durch das Hochladen eines Fotos stimmen Sie den Bedingungen dieser Lizenz zu.\n\nKurz gesagt bedeutet dies, dass die Fotos von jedermann für jeden Zweck verwendet werden dürfen. Eine Namensnennung ist nicht erforderlich.", + "photoLicenseLink": "Creative Commons Zero (CC0 v1.0)", + "nsfwBlockedTitle": "Foto abgelehnt", + "nsfwBlockedMessage": "Dieses Foto kann nicht hinzugefügt werden, da es als unangemessen erkannt wurde.", + "reportPhoto": "Foto melden", + "reportPhotoConfirm": "Möchten Sie dieses Foto wirklich als unangemessen melden?", + "photoReported": "Foto wurde gemeldet", + "photoUploaded": "Foto erfolgreich gespeichert", + "photoUploadFailed": "Foto konnte nicht hochgeladen werden" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2b0b906..f57e980 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -165,5 +165,19 @@ "example": "500" } } - } + }, + "addPhoto": "Add photo", + "changePhoto": "Set photo", + "photo": "Photo", + "chooseFromGallery": "Choose from gallery", + "takePhoto": "Take photo", + "photoLicenseInfo": "All AED photos are hosted by OpenStreetMap Polska and licensed under Creative Commons Zero (CC0 v1.0). By uploading a photo you agree to the terms of this license.\n\nIn short, this means the photos may be used by anyone for any purpose. Attribution is not required.", + "photoLicenseLink": "Creative Commons Zero (CC0 v1.0)", + "nsfwBlockedTitle": "Photo rejected", + "nsfwBlockedMessage": "This photo cannot be added because it was detected as inappropriate.", + "reportPhoto": "Report photo", + "reportPhotoConfirm": "Are you sure you want to report this photo as inappropriate?", + "photoReported": "Photo has been reported", + "photoUploaded": "Photo saved successfully", + "photoUploadFailed": "Failed to upload photo" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f932e08..0c34095 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -162,5 +162,19 @@ "example": "500" } } - } + }, + "addPhoto": "Añadir foto", + "changePhoto": "Establecer foto", + "photo": "Foto", + "chooseFromGallery": "Elegir de la galería", + "takePhoto": "Tomar foto", + "photoLicenseInfo": "Todas las fotos AED están alojadas por OpenStreetMap Polska y tienen licencia Creative Commons Zero (CC0 v1.0). Al subir una foto, aceptas los términos de esta licencia.\n\nEn resumen, esto significa que las fotos pueden ser utilizadas por cualquiera para cualquier fin. No se requiere atribución.", + "photoLicenseLink": "Creative Commons Zero (CC0 v1.0)", + "nsfwBlockedTitle": "Foto rechazada", + "nsfwBlockedMessage": "Esta foto no puede añadirse porque fue detectada como inapropiada.", + "reportPhoto": "Reportar foto", + "reportPhotoConfirm": "¿Estás seguro de que deseas reportar esta foto como inapropiada?", + "photoReported": "La foto ha sido reportada", + "photoUploaded": "Foto guardada correctamente", + "photoUploadFailed": "No se pudo subir la foto" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e49b429..e676dd1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -162,5 +162,19 @@ "example": "500" } } - } + }, + "addPhoto": "Ajouter une photo", + "changePhoto": "Définir la photo", + "photo": "Photo", + "chooseFromGallery": "Choisir dans la galerie", + "takePhoto": "Prendre une photo", + "photoLicenseInfo": "Toutes les photos AED sont hébergées par OpenStreetMap Polska et sont sous licence Creative Commons Zero (CC0 v1.0). En téléchargeant une photo, vous acceptez les conditions de cette licence.\n\nEn bref, cela signifie que les photos peuvent être utilisées par n'importe qui à n'importe quelle fin. L'attribution n'est pas requise.", + "photoLicenseLink": "Creative Commons Zero (CC0 v1.0)", + "nsfwBlockedTitle": "Photo rejetée", + "nsfwBlockedMessage": "Cette photo ne peut pas être ajoutée car elle a été détectée comme inappropriée.", + "reportPhoto": "Signaler la photo", + "reportPhotoConfirm": "Êtes-vous sûr de vouloir signaler cette photo comme inappropriée ?", + "photoReported": "La photo a été signalée", + "photoUploaded": "Photo enregistrée avec succès", + "photoUploadFailed": "Échec du téléchargement de la photo" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index f9cecf4..c66d21f 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -162,5 +162,19 @@ "example": "500" } } - } + }, + "addPhoto": "Aggiungi foto", + "changePhoto": "Imposta foto", + "photo": "Foto", + "chooseFromGallery": "Scegli dalla galleria", + "takePhoto": "Scatta una foto", + "photoLicenseInfo": "Tutte le foto AED sono ospitate da OpenStreetMap Polska e sono concesse in licenza Creative Commons Zero (CC0 v1.0). Caricando una foto, accetti i termini di questa licenza.\n\nIn breve, ciò significa che le foto possono essere utilizzate da chiunque per qualsiasi scopo. L'attribuzione non è richiesta.", + "photoLicenseLink": "Creative Commons Zero (CC0 v1.0)", + "nsfwBlockedTitle": "Foto rifiutata", + "nsfwBlockedMessage": "Questa foto non può essere aggiunta perché è stata rilevata come inappropriata.", + "reportPhoto": "Segnala foto", + "reportPhotoConfirm": "Sei sicuro di voler segnalare questa foto come inappropriata?", + "photoReported": "La foto è stata segnalata", + "photoUploaded": "Foto salvata con successo", + "photoUploadFailed": "Impossibile caricare la foto" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 7943c68..439a842 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -165,5 +165,19 @@ "example": "500" } } - } + }, + "addPhoto": "Dodaj zdjęcie", + "changePhoto": "Ustaw zdjęcie", + "photo": "Zdjęcie", + "chooseFromGallery": "Wybierz z galerii", + "takePhoto": "Zrób zdjęcie", + "photoLicenseInfo": "Wszystkie zdjęcia AED są hostowane przez OpenStreetMap Polska i są na licencji Creative Commons Zero (CC0 v1.0). Wysyłając zdjęcie zgadzasz się na warunki tej licencji.\n\nW skrócie oznacza to, że zdjęcia mogą być potem wykorzystane przez kogokolwiek w dowolnym celu. Nie jest konieczne wskazanie autora.", + "photoLicenseLink": "Creative Commons Zero (CC0 v1.0)", + "nsfwBlockedTitle": "Zdjęcie odrzucone", + "nsfwBlockedMessage": "To zdjęcie nie może być dodane, ponieważ zostało uznane za nieodpowiednie.", + "reportPhoto": "Zgłoś zdjęcie", + "reportPhotoConfirm": "Czy na pewno chcesz zgłosić to zdjęcie jako nieodpowiednie?", + "photoReported": "Zdjęcie zostało zgłoszone", + "photoUploaded": "Zdjęcie zostało zapisane", + "photoUploadFailed": "Nie udało się przesłać zdjęcia" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7214d33..8fa07bc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:mixpanel_flutter/mixpanel_flutter.dart'; +import 'package:nsfw_detector_flutter/nsfw_detector_flutter.dart'; import 'package:plausible_analytics/plausible_analytics.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -53,6 +54,7 @@ void main() async { }); remoteConfig.fetchAndActivate(); mixpanel = await Mixpanel.init(mixpanelToken, trackAutomaticEvents: true); + await NsfwDetector.initialize(threshold: 0.7); await SentryFlutter.init( (options) { options.dsn = @@ -169,7 +171,9 @@ class Home extends StatelessWidget { Widget build(BuildContext context) { return BlocListener( listenWhen: (previous, current) => - previous is EditReady && current is EditInProgress, + previous is EditReady && + current is EditInProgress && + current.photoStatus == PhotoStatus.idle, listener: (BuildContext context, state) async { if (state is EditInProgress) { var editCubit = context.read(); diff --git a/lib/models/aed.dart b/lib/models/aed.dart index e6a3a59..0b25827 100644 --- a/lib/models/aed.dart +++ b/lib/models/aed.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:aed_map/constants.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; @@ -16,6 +18,7 @@ class Defibrillator { String? openingHours; String? access; String? image; + Uint8List? photoBytes; Defibrillator( {required this.location, @@ -26,7 +29,25 @@ class Defibrillator { this.phone, this.openingHours, this.image = '', - this.access = 'yes'}); + this.access = 'yes', + List? photoBytes}) + : photoBytes = photoBytes != null ? Uint8List.fromList(photoBytes) : null; + + String? get photoId { + final url = image; + if (url == null || url.isEmpty) return null; + try { + final segments = Uri.parse(url).pathSegments; + if (segments.isEmpty) return null; + final filename = segments.last; + if (filename.isEmpty) return null; + final dot = filename.lastIndexOf('.'); + if (dot <= 0) return filename; + return filename.substring(0, dot); + } catch (_) { + return null; + } + } String? getAccessComment(AppLocalizations appLocalizations) { return translateAccessComment(access, appLocalizations); @@ -144,6 +165,7 @@ class Defibrillator { String? openingHours, String? access, String? image, + Uint8List? photoBytes, Map? colors, Map? filenames, }) { @@ -157,6 +179,7 @@ class Defibrillator { openingHours: openingHours ?? this.openingHours, access: access ?? this.access, image: image ?? this.image, + photoBytes: photoBytes ?? this.photoBytes, ); } @@ -166,7 +189,8 @@ class Defibrillator { a.operator == b.operator && a.phone == b.phone && a.openingHours == b.openingHours && - a.access == b.access; + a.access == b.access && + a.image == b.image; } Map getEventProperties() { diff --git a/lib/models/pending_change.dart b/lib/models/pending_change.dart index 861fe95..01cf606 100644 --- a/lib/models/pending_change.dart +++ b/lib/models/pending_change.dart @@ -33,6 +33,10 @@ class PendingChange { 'phone': snapshot.phone, 'openingHours': snapshot.openingHours, 'access': snapshot.access, + 'image': snapshot.image, + 'photoBytes': snapshot.photoBytes != null + ? base64Encode(snapshot.photoBytes!) + : null, }, }; } @@ -52,6 +56,10 @@ class PendingChange { phone: snapshotJson['phone'] as String?, openingHours: snapshotJson['openingHours'] as String?, access: snapshotJson['access'] as String?, + image: snapshotJson['image'] as String?, + photoBytes: snapshotJson['photoBytes'] != null + ? base64Decode(snapshotJson['photoBytes'] as String) + : null, ), ); } diff --git a/lib/repositories/pending_changes_repository.dart b/lib/repositories/pending_changes_repository.dart index e22fb11..9640e29 100644 --- a/lib/repositories/pending_changes_repository.dart +++ b/lib/repositories/pending_changes_repository.dart @@ -26,13 +26,19 @@ class PendingChangesRepository { final freshIds = freshDataset.map((defibrillator) => defibrillator.id).toSet(); final freshById = {for (final defibrillator in freshDataset) defibrillator.id: defibrillator}; + final now = DateTime.now(); changes.removeWhere((change) { + if (now.difference(change.createdAt).inHours >= 24) return true; switch (change.type) { case PendingChangeType.add: return freshIds.contains(change.defibrillatorId); case PendingChangeType.edit: final freshDefibrillator = freshById[change.defibrillatorId]; if (freshDefibrillator == null) return false; + if (change.snapshot.photoBytes != null) { + final freshImage = freshDefibrillator.image; + return freshImage != null && freshImage.isNotEmpty; + } return Defibrillator.tagsEqual(change.snapshot, freshDefibrillator); case PendingChangeType.delete: return !freshIds.contains(change.defibrillatorId); diff --git a/lib/repositories/points_repository.dart b/lib/repositories/points_repository.dart index a3b7b36..b59a7f9 100644 --- a/lib/repositories/points_repository.dart +++ b/lib/repositories/points_repository.dart @@ -14,6 +14,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; import 'package:xml/xml.dart'; import '../models/user.dart'; @@ -22,7 +23,8 @@ class PointsRepository { static const String defibrillatorListKey = 'aed_list_json_2'; static const String defibrillatorListUpdateTimestamp = 'aed_update'; - static const devMode = kDebugMode; + // static const devMode = kDebugMode; + static const devMode = false; Future get cacheFile async { if (Platform.environment.containsKey('FLUTTER_TEST')) { @@ -98,7 +100,8 @@ class PointsRepository { operator: row['properties']['operator'], phone: row['properties']['phone'], openingHours: row['properties']['opening_hours'], - access: row['properties']['access'])); + access: row['properties']['access'], + image: row['properties']['image'])); }); print('Loaded ${defibrillators.length} defibrillators!'); defibrillators = defibrillators.map((defibrillator) { @@ -279,18 +282,59 @@ class PointsRepository { return defibrillator; } - Future getImage(Defibrillator defibrillator) async { + Future uploadPhoto( + {required int nodeId, required File file}) async { + if (token == null) { + throw Exception('Not authenticated'); + } + var uri = Uri.parse('https://back.openaedmap.org/api/v1/photos/upload'); + var request = http.MultipartRequest('POST', uri); + request.fields['node_id'] = nodeId.toString(); + request.fields['file_license'] = 'CC0'; + request.fields['oauth2_credentials'] = json.encode({ + 'access_token': token, + 'token_type': 'Bearer', + 'scope': 'read_prefs', + }); + final bytes = await file.readAsBytes(); + request.files.add(http.MultipartFile.fromBytes( + 'file', + bytes, + filename: 'photo.jpg', + contentType: MediaType('application', 'octet-stream'), + )); + var streamedResponse = await request.send(); + var response = await http.Response.fromStream(streamedResponse); + print('Photo upload response [${response.statusCode}]: ${response.body}'); + if (response.statusCode != 200) { + throw OsmApiException(response.statusCode, response.body); + } + } + + Future reportPhoto(String photoId) async { + var response = await http.post( + Uri.parse('https://back.openaedmap.org/api/v1/photos/report'), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + body: 'id=${Uri.encodeComponent(photoId)}'); + if (response.statusCode != 200) { + throw OsmApiException(response.statusCode, response.body); + } + } + + Future getBackendImageUrl(int nodeId) async { try { - var response = await http.get(Uri.parse( - 'https://back.openaedmap.org/api/v1/node/${defibrillator.id}')); - var result = jsonDecode(response.body); - if (result['elements'][0]['@photo_url'].toString().length > 10) { - return 'https://back.openaedmap.org${result['elements'][0]['@photo_url']}'; + var response = await http.get( + Uri.parse('https://back.openaedmap.org/api/v1/node/$nodeId')); + var payload = json.decode(response.body); + if ((payload['elements'] as List).isNotEmpty) { + var element = payload['elements'][0] as Map; + var tags = element['tags'] as Map?; + return tags?['image'] as String?; } - return null; - } catch (err) { - return null; - } + } catch (_) {} + return null; } Future getNode(int id) async { @@ -310,6 +354,7 @@ class PointsRepository { openingHours: tags['opening_hours'], operator: tags['operator'], phone: tags['phone'], + image: tags['image'], ); } } catch (_) {} diff --git a/lib/screens/edit/edit_form.dart b/lib/screens/edit/edit_form.dart index 6a9bc16..80aa883 100644 --- a/lib/screens/edit/edit_form.dart +++ b/lib/screens/edit/edit_form.dart @@ -14,6 +14,7 @@ import '../../bloc/points/points_cubit.dart'; import '../../generated/i18n/app_localizations.dart'; import '../../models/aed.dart'; import '../../shared/utils.dart'; +import '../photo/photo_source_bottom_sheet.dart'; import 'opening_hours_editor.dart'; class EditForm extends StatelessWidget { @@ -153,6 +154,17 @@ class EditForm extends StatelessWidget { ), ], ), + SettingsSection( + title: Text(appLocalizations.photo), + tiles: [ + SettingsTile.navigation( + leading: const Icon(CupertinoIcons.camera), + title: Text(appLocalizations.changePhoto), + onPressed: (tileContext) => + showPhotoSourceSheet(tileContext, state.defibrillator), + ), + ], + ), SettingsSection( title: Text(appLocalizations.location), tiles: [ diff --git a/lib/screens/map/bottom_panel.dart b/lib/screens/map/bottom_panel.dart index a420a4a..3d99448 100644 --- a/lib/screens/map/bottom_panel.dart +++ b/lib/screens/map/bottom_panel.dart @@ -22,6 +22,7 @@ import '../../bloc/routing/routing_cubit.dart'; import '../../bloc/routing/routing_state.dart'; import '../../generated/i18n/app_localizations.dart'; import '../../models/aed.dart'; +import '../../screens/photo/photo_source_bottom_sheet.dart'; import '../../shared/utils.dart'; class BottomPanel extends StatelessWidget { @@ -32,7 +33,43 @@ class BottomPanel extends StatelessWidget { @override Widget build(BuildContext context) { var appLocalizations = AppLocalizations.of(context)!; - return BlocListener( + return BlocListener( + listenWhen: (previous, current) => + previous.photoStatus != current.photoStatus && + (current.photoStatus == PhotoStatus.reportSuccess || + current.photoStatus == PhotoStatus.reportFailure), + listener: (context, state) { + if (state.photoStatus == PhotoStatus.reportSuccess) { + context.read().resetPhotoStatus(); + showCupertinoDialog( + context: context, + builder: (dialogContext) => CupertinoAlertDialog( + content: Text(appLocalizations.photoReported), + actions: [ + CupertinoDialogAction( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } else if (state.photoStatus == PhotoStatus.reportFailure) { + context.read().resetPhotoStatus(); + showCupertinoDialog( + context: context, + builder: (dialogContext) => CupertinoAlertDialog( + content: Text(appLocalizations.photoUploadFailed), + actions: [ + CupertinoDialogAction( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + }, + child: BlocListener( listener: (context, state) { if (state is PointsLoadSuccess) { context.read().open(); @@ -363,19 +400,75 @@ class BottomPanel extends StatelessWidget { } return Container(); })), - const SizedBox(height: 12), - if ((state.selected.image ?? '').isNotEmpty) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - width: double.infinity, - fit: BoxFit.cover, - imageUrl: state.selected.image ?? '', - placeholder: (context, url) => Container(), - errorWidget: (context, url, error) => - const Icon(Icons.error), + if (state.selected.photoBytes == null && + (state.selected.image ?? '').isEmpty) ...[ + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: CupertinoButton( + padding: const EdgeInsets.symmetric(vertical: 12), + color: CupertinoColors.secondarySystemBackground + .resolveFrom(context), + onPressed: () => showPhotoSourceSheet( + context, state.selected), + child: Text( + appLocalizations.addPhoto, + style: TextStyle( + color: CupertinoColors.label.resolveFrom(context), + ), + ), ), ), + ], + if (state.selected.photoBytes != null || + (state.selected.image ?? '').isNotEmpty) ...[ + const SizedBox(height: 12), + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: state.selected.photoBytes != null + ? Image.memory( + state.selected.photoBytes!, + width: double.infinity, + fit: BoxFit.cover, + ) + : CachedNetworkImage( + width: double.infinity, + fit: BoxFit.cover, + imageUrl: state.selected.image ?? '', + placeholder: (context, url) => Container(), + errorWidget: (context, url, error) => + const Icon(Icons.error), + ), + ), + if (state.selected.photoId != null) + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: () => showReportPhotoDialog( + context, + appLocalizations, + state.selected, + state.selected.photoId!), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.all(6), + child: const Icon( + Icons.flag_outlined, + color: Colors.white70, + size: 18, + ), + ), + ), + ), + ], + ), + ], ], ), ), @@ -386,6 +479,7 @@ class BottomPanel extends StatelessWidget { } return Container(); }), + ), ); } @@ -395,3 +489,31 @@ class BottomPanel extends StatelessWidget { context.read().show(); } } + +Future showReportPhotoDialog( + BuildContext context, + AppLocalizations appLocalizations, + Defibrillator defibrillator, + String photoId) async { + await showCupertinoDialog( + context: context, + builder: (dialogContext) => CupertinoAlertDialog( + title: Text(appLocalizations.reportPhoto), + content: Text(appLocalizations.reportPhotoConfirm), + actions: [ + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.of(dialogContext).pop(); + context.read().reportPhoto(defibrillator, photoId); + }, + child: Text(appLocalizations.reportPhoto), + ), + CupertinoDialogAction( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(appLocalizations.cancel), + ), + ], + ), + ); +} diff --git a/lib/screens/photo/photo_confirmation_page.dart b/lib/screens/photo/photo_confirmation_page.dart new file mode 100644 index 0000000..3df568b --- /dev/null +++ b/lib/screens/photo/photo_confirmation_page.dart @@ -0,0 +1,138 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:aed_map/bloc/edit/edit_cubit.dart'; +import 'package:aed_map/bloc/edit/edit_state.dart'; +import 'package:aed_map/bloc/points/points_cubit.dart'; +import 'package:aed_map/models/aed.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../generated/i18n/app_localizations.dart'; + +class PhotoConfirmationPage extends StatelessWidget { + const PhotoConfirmationPage( + {super.key, required this.defibrillator, required this.file}); + + final Defibrillator defibrillator; + final File file; + + @override + Widget build(BuildContext context) { + var appLocalizations = AppLocalizations.of(context)!; + return BlocListener( + listenWhen: (previous, current) => + previous.photoStatus != current.photoStatus, + listener: (context, state) { + if (state.photoStatus == PhotoStatus.uploadSuccess && + state.photoUpdatedDefibrillator != null) { + context.read().update(state.photoUpdatedDefibrillator!); + context.read().resetPhotoStatus(); + Navigator.of(context).popUntil((route) => route.isFirst); + } else if (state.photoStatus == PhotoStatus.uploadFailure) { + var message = state.photoErrorMessage ?? ''; + context.read().resetPhotoStatus(); + showCupertinoDialog( + context: context, + builder: (dialogContext) => CupertinoAlertDialog( + title: Text(appLocalizations.photoUploadFailed), + content: Text(message), + actions: [ + CupertinoDialogAction( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + }, + child: CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + leading: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: () => Navigator.of(context).pop(), + child: Text(appLocalizations.cancel), + ), + middle: Text(appLocalizations.addPhoto), + ), + child: SafeArea( + bottom: false, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.photoStatus != current.photoStatus, + builder: (context, state) { + var isUploading = state.photoStatus == PhotoStatus.uploading; + return ListView( + padding: const EdgeInsets.all(16), + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + file, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 16), + buildLicenseSection(context, appLocalizations), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: CupertinoButton.filled( + onPressed: isUploading + ? null + : () => context + .read() + .submitPhoto(defibrillator, file), + child: isUploading + ? const CupertinoActivityIndicator( + color: CupertinoColors.white) + : Text(appLocalizations.save), + ), + ), + ], + ); + }, + ), + ), + ), + ); + } + + Widget buildLicenseSection( + BuildContext context, AppLocalizations appLocalizations) { + var parts = appLocalizations.photoLicenseInfo + .split(appLocalizations.photoLicenseLink); + return RichText( + text: TextSpan( + style: TextStyle( + fontSize: 15, + color: CupertinoColors.label.resolveFrom(context), + ), + children: [ + if (parts.isNotEmpty) TextSpan(text: parts[0]), + WidgetSpan( + child: GestureDetector( + onTap: () => launchUrl( + Uri.parse( + 'https://creativecommons.org/publicdomain/zero/1.0/'), + mode: LaunchMode.externalApplication, + ), + child: Text( + appLocalizations.photoLicenseLink, + style: const TextStyle( + fontSize: 15, + color: CupertinoColors.activeBlue, + ), + ), + ), + ), + if (parts.length > 1) TextSpan(text: parts[1]), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/photo/photo_source_bottom_sheet.dart b/lib/screens/photo/photo_source_bottom_sheet.dart new file mode 100644 index 0000000..ae87b50 --- /dev/null +++ b/lib/screens/photo/photo_source_bottom_sheet.dart @@ -0,0 +1,106 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:aed_map/bloc/edit/edit_cubit.dart'; +import 'package:aed_map/bloc/points/points_cubit.dart'; +import 'package:aed_map/models/aed.dart'; +import 'package:aed_map/screens/photo/photo_confirmation_page.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:heif_converter/heif_converter.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../generated/i18n/app_localizations.dart'; + +Future showPhotoSourceSheet( + BuildContext context, Defibrillator defibrillator) async { + var appLocalizations = AppLocalizations.of(context)!; + await showCupertinoModalPopup( + context: context, + builder: (sheetContext) => CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(sheetContext).pop(); + await pickAndProceed( + context, defibrillator, ImageSource.gallery); + }, + child: Text(appLocalizations.chooseFromGallery), + ), + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(sheetContext).pop(); + await pickAndProceed( + context, defibrillator, ImageSource.camera); + }, + child: Text(appLocalizations.takePhoto), + ), + ], + cancelButton: CupertinoActionSheetAction( + isDestructiveAction: true, + onPressed: () => Navigator.of(sheetContext).pop(), + child: Text(appLocalizations.cancel), + ), + ), + ); +} + +Future pickAndProceed(BuildContext context, Defibrillator defibrillator, + ImageSource source) async { + var appLocalizations = AppLocalizations.of(context)!; + var picked = await ImagePicker().pickImage(source: source); + if (picked == null) return; + + var path = picked.path; + final lower = path.toLowerCase(); + if (lower.endsWith('.heic') || lower.endsWith('.heif')) { + final converted = await HeifConverter.convert(path, format: 'jpg'); + if (converted != null) { + path = converted; + } + } + final compressed = await FlutterImageCompress.compressAndGetFile( + path, + '${path}_compressed.jpg', + quality: 75, + format: CompressFormat.jpeg, + ); + var file = compressed != null ? File(compressed.path) : File(path); + var editCubit = context.read(); + var unsafe = await editCubit.isPhotoUnsafe(file); + + if (unsafe) { + await showCupertinoDialog( + context: context, + builder: (dialogContext) => CupertinoAlertDialog( + title: Text(appLocalizations.nsfwBlockedTitle), + content: Text(appLocalizations.nsfwBlockedMessage), + actions: [ + CupertinoDialogAction( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('OK'), + ), + ], + ), + ); + return; + } + + var pointsCubit = context.read(); + await Navigator.of(context).push( + CupertinoPageRoute( + builder: (pageContext) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: editCubit), + BlocProvider.value(value: pointsCubit), + ], + child: PhotoConfirmationPage( + defibrillator: defibrillator, + file: file, + ), + ), + ), + ); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index a481b28..cfee477 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -491,6 +491,54 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.dev" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -743,6 +791,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + heif_converter: + dependency: "direct main" + description: + name: heif_converter + sha256: "5e48e4bb5fc5d94bb01492d7e120279eb8836e3cacc7b89d6aa7ba3103f6143a" + url: "https://pub.dev" + source: hosted + version: "1.0.2" hooks: dependency: transitive description: @@ -1084,6 +1140,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + nsfw_detector_flutter: + dependency: "direct main" + description: + name: nsfw_detector_flutter + sha256: "15b467b6b920ca9375177138e21c06cccfcf513fa3a4b3eeb1b8a4721e07f4b3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" octo_image: dependency: transitive description: @@ -1284,6 +1348,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" rbush: dependency: transitive description: @@ -1577,6 +1649,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.16" + tflite_flutter: + dependency: transitive + description: + name: tflite_flutter + sha256: "0bba9040d8decda0960d7abf8eabf32243bf092bc7d0084e8e19681866b0bdbe" + url: "https://pub.dev" + source: hosted + version: "0.12.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a86722f..c54585c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: App showing available AEDs publish_to: "none" # Remember to run: node whatsNew.js -version: 1.0.76+76 +version: 1.0.77+77 environment: sdk: ^3.5.3 @@ -40,6 +40,7 @@ dependencies: feedback: ^3.2.0 dotted_border: ^3.1.0 image_picker: ^1.2.1 + flutter_image_compress: ^2.3.0 image: ^4.8.0 dio: ^5.9.2 google_polyline_algorithm: ^3.1.0 @@ -59,6 +60,8 @@ dependencies: flutter_web_auth_2: ^5.0.2 font_awesome_flutter: ^11.0.0 osm_opening_hours: ^1.0.2 + nsfw_detector_flutter: ^2.0.0 + heif_converter: ^1.0.2 dev_dependencies: integration_test: