diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 53c7526..c233f0a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,12 @@
+
+
+
+
+
+
+
-
-
diff --git a/ios/Podfile b/ios/Podfile
index f5d7214..13cdd7b 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -55,6 +55,9 @@ post_install do |installer|
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=1',
+
+ ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
+ 'PERMISSION_LOCATION=1',
]
end
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index c163fa0..9e55a95 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -72,5 +72,8 @@
NSPhotoLibraryUsageDescription
Accessing your gallery allows you to save your memories
+
+ NSLocationUsageDescription
+ Accessing your location allows you to tag your memories
diff --git a/lib/constants/values.dart b/lib/constants/values.dart
index a2552c2..00800e1 100644
--- a/lib/constants/values.dart
+++ b/lib/constants/values.dart
@@ -17,3 +17,4 @@ const WELCOME_SCREEN_PHOTOS_QUERIES = [
'friends',
'romantic',
];
+const ACCURACY_IN_METERS_FOR_PINPOINT = 20;
diff --git a/lib/foreign_types/memory.dart b/lib/foreign_types/memory.dart
index 7f23ecd..943eb19 100644
--- a/lib/foreign_types/memory.dart
+++ b/lib/foreign_types/memory.dart
@@ -4,38 +4,41 @@ import 'package:path/path.dart';
import 'package:quid_faciam_hodie/enums.dart';
import 'package:quid_faciam_hodie/managers/file_manager.dart';
+import 'memory_location.dart';
+
class Memory {
final String id;
final DateTime creationDate;
- final String location;
+ final String filePath;
final bool isPublic;
final String userID;
+ final MemoryLocation? location;
const Memory({
required this.id,
required this.creationDate,
- required this.location,
+ required this.filePath,
required this.isPublic,
required this.userID,
+ this.location,
});
- static parse(Map jsonData) {
- return Memory(
- id: jsonData['id'],
- creationDate: DateTime.parse(jsonData['created_at']),
- location: jsonData['location'],
- isPublic: jsonData['is_public'],
- userID: jsonData['user_id'],
- );
- }
+ static parse(final Map jsonData) => Memory(
+ id: jsonData['id'],
+ creationDate: DateTime.parse(jsonData['created_at']),
+ filePath: jsonData['location'],
+ isPublic: jsonData['is_public'],
+ userID: jsonData['user_id'],
+ location: MemoryLocation.parse(jsonData),
+ );
- String get filename => basename(location);
+ String get filename => basename(filePath);
MemoryType get type =>
filename.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video;
Future downloadToFile() => FileManager.downloadFile(
'memories',
- location,
+ filePath,
);
}
diff --git a/lib/foreign_types/memory_location.dart b/lib/foreign_types/memory_location.dart
new file mode 100644
index 0000000..7b16b75
--- /dev/null
+++ b/lib/foreign_types/memory_location.dart
@@ -0,0 +1,32 @@
+class MemoryLocation {
+ final double latitude;
+ final double longitude;
+ final double speed;
+ final double accuracy;
+ final double altitude;
+ final double heading;
+
+ const MemoryLocation({
+ required this.latitude,
+ required this.longitude,
+ required this.speed,
+ required this.accuracy,
+ required this.altitude,
+ required this.heading,
+ });
+
+ static MemoryLocation? parse(final Map jsonData) {
+ try {
+ return MemoryLocation(
+ latitude: (jsonData['location_latitude'] as num).toDouble(),
+ longitude: (jsonData['location_longitude'] as num).toDouble(),
+ speed: (jsonData['location_speed'] as num).toDouble(),
+ accuracy: (jsonData['location_accuracy'] as num).toDouble(),
+ altitude: (jsonData['location_altitude'] as num).toDouble(),
+ heading: (jsonData['location_heading'] as num).toDouble(),
+ );
+ } catch (error) {
+ return null;
+ }
+ }
+}
diff --git a/lib/locale/l10n/app_de.arb b/lib/locale/l10n/app_de.arb
index b2f23a2..5eeb6f4 100644
--- a/lib/locale/l10n/app_de.arb
+++ b/lib/locale/l10n/app_de.arb
@@ -47,6 +47,7 @@
"permissionsRequiredPageOpenSettings": "Einstellungen öffnen",
"permissionsRequiredPageGrantCameraPermission": "Kamera-Berechtigung erteilen",
"permissionsRequiredPageGrantMicrophonePermission": "Mikrofon-Berechtigung erteilen",
+ "permissionsRequiredPageGrantLocationPermission": "Standort-Berechtigung erteilen",
"memoryViewIsDownloading": "Erinnerung wird heruntergeladen",
diff --git a/lib/locale/l10n/app_en.arb b/lib/locale/l10n/app_en.arb
index b590ba8..dc34770 100644
--- a/lib/locale/l10n/app_en.arb
+++ b/lib/locale/l10n/app_en.arb
@@ -65,6 +65,7 @@
"permissionsRequiredPageOpenSettings": "Open Settings",
"permissionsRequiredPageGrantCameraPermission": "Grant camera permission",
"permissionsRequiredPageGrantMicrophonePermission": "Grant microphone permission",
+ "permissionsRequiredPageGrantLocationPermission": "Grant location permission",
"memoryViewIsDownloading": "Downloading memory",
@@ -89,6 +90,8 @@
}
}
},
+ "memorySheetMapEstimatedAddressLabel": "Estimated Address",
+ "memorySheetMapOpenNavigation": "Open Navigation",
"emptyScreenTitle": "Houston, we have a problem",
diff --git a/lib/managers/file_manager.dart b/lib/managers/file_manager.dart
index e40359a..9e03c5d 100644
--- a/lib/managers/file_manager.dart
+++ b/lib/managers/file_manager.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:location/location.dart';
import 'package:path_provider/path_provider.dart';
import 'package:quid_faciam_hodie/foreign_types/memory.dart';
import 'package:quid_faciam_hodie/managers/cache_manager.dart';
@@ -32,7 +33,11 @@ class FileManager {
return Memory.parse(response.data);
}
- static uploadFile(final User user, final File file) async {
+ static uploadFile(
+ final User user,
+ final File file, {
+ LocationData? locationData,
+ }) async {
await GlobalValuesManager.waitForServerInitialization();
final basename = uuid.v4();
@@ -46,10 +51,22 @@ class FileManager {
throw Exception('Error uploading file: ${response.error!.message}');
}
- final memoryResponse = await supabase.from('memories').insert({
+ final Map data = {
'user_id': user.id,
'location': path,
- }).execute();
+ };
+
+ if (locationData != null) {
+ data['location_latitude'] = locationData.latitude!;
+ data['location_longitude'] = locationData.longitude!;
+ data['location_speed'] = locationData.speed!;
+ data['location_accuracy'] = locationData.accuracy!;
+ data['location_altitude'] = locationData.altitude!;
+ data['location_heading'] = locationData.heading!;
+ }
+
+ final memoryResponse =
+ await supabase.from('memories').insert(data).execute();
if (memoryResponse.error != null) {
throw Exception('Error creating memory: ${response.error!.message}');
diff --git a/lib/managers/global_values_manager.dart b/lib/managers/global_values_manager.dart
index d9202ff..d1fce63 100644
--- a/lib/managers/global_values_manager.dart
+++ b/lib/managers/global_values_manager.dart
@@ -1,5 +1,6 @@
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
+import 'package:permission_handler/permission_handler.dart';
import 'package:quid_faciam_hodie/constants/apis.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
@@ -45,4 +46,9 @@ class GlobalValuesManager {
await _serverInitializationFuture;
}
+
+ static Future hasGrantedPermissions() async =>
+ (await Permission.camera.isGranted) &&
+ (await Permission.microphone.isGranted) &&
+ (await Permission.location.isGranted);
}
diff --git a/lib/screens/grant_permission_screen/permissions_required_page.dart b/lib/screens/grant_permission_screen/permissions_required_page.dart
index 6fc5a11..e19137c 100644
--- a/lib/screens/grant_permission_screen/permissions_required_page.dart
+++ b/lib/screens/grant_permission_screen/permissions_required_page.dart
@@ -22,6 +22,7 @@ class _PermissionsRequiredPageState extends State {
bool hasDeniedForever = false;
bool hasGrantedCameraPermission = false;
bool hasGrantedMicrophonePermission = false;
+ bool hasGrantedLocationPermission = false;
@override
void initState() {
@@ -33,12 +34,15 @@ class _PermissionsRequiredPageState extends State {
Future checkPermissions() async {
final cameraStatus = await Permission.camera.status;
final microphoneStatus = await Permission.microphone.status;
+ final locationStatus = await Permission.location.status;
setState(() {
hasGrantedCameraPermission = cameraStatus.isGranted;
hasGrantedMicrophonePermission = microphoneStatus.isGranted;
+ hasGrantedLocationPermission = locationStatus.isGranted;
});
+ // These permissions are crucially required for the app to work
if (cameraStatus.isPermanentlyDenied ||
microphoneStatus.isPermanentlyDenied) {
setState(() {
@@ -48,7 +52,9 @@ class _PermissionsRequiredPageState extends State {
return;
}
- if (cameraStatus.isGranted && microphoneStatus.isGranted) {
+ if (cameraStatus.isGranted &&
+ microphoneStatus.isGranted &&
+ locationStatus.isGranted) {
widget.onPermissionsGranted();
}
}
@@ -136,6 +142,29 @@ class _PermissionsRequiredPageState extends State {
),
),
),
+ PlatformTextButton(
+ onPressed: hasGrantedLocationPermission
+ ? null
+ : () async {
+ await Permission.location.request();
+ await checkPermissions();
+ },
+ child: IconButtonChild(
+ icon: Icon(context.platformIcons.location),
+ label: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ localizations
+ .permissionsRequiredPageGrantMicrophonePermission,
+ ),
+ if (hasGrantedLocationPermission)
+ Icon(context.platformIcons.checkMark),
+ if (!hasGrantedLocationPermission) const SizedBox(),
+ ],
+ ),
+ ),
+ ),
],
],
);
diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart
index 040513e..50dbbdd 100644
--- a/lib/screens/login_screen.dart
+++ b/lib/screens/login_screen.dart
@@ -10,6 +10,8 @@ import 'package:quid_faciam_hodie/utils/loadable.dart';
import 'package:quid_faciam_hodie/widgets/icon_button_child.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
+import 'main_screen.dart';
+
final supabase = Supabase.instance.client;
class LoginScreen extends StatefulWidget {
@@ -25,6 +27,17 @@ class _LoginScreenState extends AuthState with Loadable {
final emailController = TextEditingController();
final passwordController = TextEditingController();
+ @override
+ void onAuthenticated(Session session) {
+ if (session.user != null) {
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ MainScreen.ID,
+ (_) => false,
+ );
+ }
+ }
+
@override
void dispose() {
emailController.dispose();
diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart
index 91ebbc0..0e4956d 100644
--- a/lib/screens/main_screen.dart
+++ b/lib/screens/main_screen.dart
@@ -7,6 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
+import 'package:location/location.dart';
+import 'package:permission_handler/permission_handler.dart';
import 'package:quid_faciam_hodie/constants/spacing.dart';
import 'package:quid_faciam_hodie/constants/values.dart';
import 'package:quid_faciam_hodie/extensions/snackbar.dart';
@@ -15,6 +17,7 @@ import 'package:quid_faciam_hodie/managers/global_values_manager.dart';
import 'package:quid_faciam_hodie/screens/main_screen/settings_button_overlay.dart';
import 'package:quid_faciam_hodie/utils/auth_required.dart';
import 'package:quid_faciam_hodie/utils/loadable.dart';
+import 'package:quid_faciam_hodie/utils/tag_location_to_image.dart';
import 'package:quid_faciam_hodie/widgets/animate_in_builder.dart';
import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart';
import 'package:quid_faciam_hodie/widgets/icon_button_child.dart';
@@ -189,6 +192,13 @@ class _MainScreenState extends AuthRequiredState with Loadable {
}
final file = File((await controller!.takePicture()).path);
+ LocationData? locationData;
+
+ if (Platform.isAndroid && (await Permission.location.isGranted)) {
+ locationData = await Location().getLocation();
+
+ await tagLocationToImage(file, locationData);
+ }
setState(() {
uploadingPhotoAnimation = file.readAsBytesSync();
@@ -200,7 +210,7 @@ class _MainScreenState extends AuthRequiredState with Loadable {
);
try {
- await FileManager.uploadFile(_user, file);
+ await FileManager.uploadFile(_user, file, locationData: locationData);
} catch (error) {
if (isMaterial(context))
context.showErrorSnackBar(message: error.toString());
diff --git a/lib/screens/main_screen/settings_button_overlay.dart b/lib/screens/main_screen/settings_button_overlay.dart
index b5fec54..1378b65 100644
--- a/lib/screens/main_screen/settings_button_overlay.dart
+++ b/lib/screens/main_screen/settings_button_overlay.dart
@@ -11,8 +11,8 @@ class SettingsButtonOverlay extends StatelessWidget {
return Positioned(
left: SMALL_SPACE,
top: SMALL_SPACE,
- child: PlatformTextButton(
- child: Icon(
+ child: PlatformIconButton(
+ icon: Icon(
context.platformIcons.settings,
color: Colors.white,
),
diff --git a/lib/screens/server_loading_screen.dart b/lib/screens/server_loading_screen.dart
index 6cdfecb..93aca73 100644
--- a/lib/screens/server_loading_screen.dart
+++ b/lib/screens/server_loading_screen.dart
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:quid_faciam_hodie/constants/spacing.dart';
import 'package:quid_faciam_hodie/managers/global_values_manager.dart';
import 'package:quid_faciam_hodie/models/memories.dart';
+import 'package:quid_faciam_hodie/screens/grant_permission_screen.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'empty_screen.dart';
@@ -35,6 +36,13 @@ class _ServerLoadingScreenState extends State {
}
Future load() async {
+ if (!(await GlobalValuesManager.hasGrantedPermissions())) {
+ Navigator.pushReplacementNamed(
+ context,
+ GrantPermissionScreen.ID,
+ );
+ }
+
await GlobalValuesManager.waitForServerInitialization();
final memories = context.read();
@@ -45,6 +53,10 @@ class _ServerLoadingScreenState extends State {
await memories.initialize();
}
+ if (!mounted) {
+ return;
+ }
+
if (widget.nextScreen == null) {
Navigator.pushNamed(
context,
diff --git a/lib/screens/timeline_screen/memory_location_view.dart b/lib/screens/timeline_screen/memory_location_view.dart
new file mode 100644
index 0000000..cb7bab2
--- /dev/null
+++ b/lib/screens/timeline_screen/memory_location_view.dart
@@ -0,0 +1,149 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_osm_plugin/flutter_osm_plugin.dart';
+import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
+import 'package:http/http.dart' as http;
+import 'package:quid_faciam_hodie/constants/spacing.dart';
+import 'package:quid_faciam_hodie/constants/values.dart';
+import 'package:quid_faciam_hodie/foreign_types/memory_location.dart';
+import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart';
+import 'package:quid_faciam_hodie/widgets/icon_button_child.dart';
+import 'package:quid_faciam_hodie/widgets/key_value_info.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class MemoryLocationView extends StatefulWidget {
+ final MemoryLocation location;
+
+ const MemoryLocationView({
+ Key? key,
+ required this.location,
+ }) : super(key: key);
+
+ @override
+ State createState() => _MemoryLocationViewState();
+}
+
+class _MemoryLocationViewState extends State {
+ late final MapController controller;
+ String address = '';
+
+ @override
+ void initState() {
+ super.initState();
+
+ lookupAddress();
+
+ controller = MapController(
+ initPosition: GeoPoint(
+ latitude: widget.location.latitude,
+ longitude: widget.location.longitude,
+ ),
+ );
+ }
+
+ void lookupAddress() async {
+ final url =
+ 'https://geocode.maps.co/reverse?lat=${widget.location.latitude}&lon=${widget.location.longitude}';
+ final uri = Uri.parse(url);
+
+ final response = await http.get(uri);
+
+ if (response.statusCode != 200) {
+ setState(() {
+ address = '';
+ });
+ } else {
+ setState(() {
+ address = jsonDecode(response.body)['display_name'];
+ });
+ }
+ }
+
+ void drawCircle() => controller.drawCircle(
+ CircleOSM(
+ key: 'accuracy',
+ color: Colors.blue,
+ centerPoint: GeoPoint(
+ latitude: widget.location.latitude,
+ longitude: widget.location.longitude,
+ ),
+ radius: widget.location.accuracy,
+ strokeWidth: 4,
+ ),
+ );
+
+ List get staticPoints {
+ if (widget.location.accuracy <= ACCURACY_IN_METERS_FOR_PINPOINT) {
+ return [
+ StaticPositionGeoPoint(
+ 'position',
+ const MarkerIcon(
+ icon: Icon(Icons.location_on, size: 150, color: Colors.blue),
+ ),
+ [
+ GeoPoint(
+ latitude: widget.location.latitude,
+ longitude: widget.location.longitude,
+ )
+ ],
+ )
+ ];
+ } else {
+ return [];
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final localizations = AppLocalizations.of(context)!;
+
+ return Column(
+ children: [
+ SizedBox(
+ width: double.infinity,
+ height: 400,
+ child: OSMFlutter(
+ controller: controller,
+ initZoom: 14,
+ stepZoom: 1.0,
+ staticPoints: staticPoints,
+ onMapIsReady: (_) {
+ drawCircle();
+ },
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE),
+ child: Column(
+ children: [
+ const SizedBox(height: MEDIUM_SPACE),
+ if (address.isNotEmpty) ...[
+ FadeAndMoveInAnimation(
+ child: KeyValueInfo(
+ title: localizations.memorySheetMapEstimatedAddressLabel,
+ value: address,
+ icon: context.platformIcons.location,
+ ),
+ ),
+ const SizedBox(height: MEDIUM_SPACE),
+ ],
+ PlatformTextButton(
+ onPressed: () {
+ final url =
+ 'geo:0,0?q=${widget.location.latitude},${widget.location.longitude} ($address)';
+ launchUrl(Uri.parse(url));
+ },
+ child: IconButtonChild(
+ icon: Icon(context.platformIcons.location),
+ label: Text(localizations.memorySheetMapOpenNavigation),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/screens/timeline_screen/memory_sheet.dart b/lib/screens/timeline_screen/memory_sheet.dart
index 683b1b3..8f1af12 100644
--- a/lib/screens/timeline_screen/memory_sheet.dart
+++ b/lib/screens/timeline_screen/memory_sheet.dart
@@ -1,3 +1,4 @@
+import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -11,9 +12,11 @@ import 'package:quid_faciam_hodie/foreign_types/memory.dart';
import 'package:quid_faciam_hodie/managers/file_manager.dart';
import 'package:quid_faciam_hodie/utils/loadable.dart';
import 'package:quid_faciam_hodie/utils/theme.dart';
-import 'package:quid_faciam_hodie/widgets/modal_sheet.dart';
+import 'package:quid_faciam_hodie/widgets/sheet_indicator.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
+import 'memory_location_view.dart';
+
class MemorySheet extends StatefulWidget {
final Memory memory;
final BuildContext sheetContext;
@@ -32,7 +35,7 @@ final supabase = Supabase.instance.client;
class _MemorySheetState extends State with Loadable {
Future deleteFile() async {
- await FileManager.deleteFile(widget.memory.location);
+ await FileManager.deleteFile(widget.memory.filePath);
if (mounted) {
Navigator.pop(context);
@@ -62,7 +65,8 @@ class _MemorySheetState extends State with Loadable {
if (isMaterial(context))
context.showSuccessSnackBar(
- message: localizations.memorySheetSavedToGallery);
+ message: localizations.memorySheetSavedToGallery,
+ );
} catch (error) {
if (isMaterial(context))
context.showErrorSnackBar(message: localizations.generalError);
@@ -90,11 +94,13 @@ class _MemorySheetState extends State with Loadable {
if (isNowPublic) {
if (isMaterial(context))
context.showSuccessSnackBar(
- message: localizations.memorySheetMemoryUpdatedToPublic);
+ message: localizations.memorySheetMemoryUpdatedToPublic,
+ );
} else {
if (isMaterial(context))
context.showSuccessSnackBar(
- message: localizations.memorySheetMemoryUpdatedToPrivate);
+ message: localizations.memorySheetMemoryUpdatedToPrivate,
+ );
}
} catch (error) {
if (isMaterial(context))
@@ -123,88 +129,137 @@ class _MemorySheetState extends State with Loadable {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
+ final backgroundColor = platformThemeData(
+ context,
+ material: (data) =>
+ data.bottomSheetTheme.modalBackgroundColor ?? data.bottomAppBarColor,
+ cupertino: (data) => data.barBackgroundColor,
+ );
- return ModalSheet(
- child: Column(
- children: [
- Text(
- localizations.memorySheetTitle,
- style: getTitleTextStyle(context),
+ return ExpandableBottomSheet(
+ background: GestureDetector(
+ onTap: () => Navigator.pop(context),
+ ),
+ persistentHeader: Container(
+ padding: const EdgeInsets.all(MEDIUM_SPACE),
+ decoration: BoxDecoration(
+ color: backgroundColor,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(LARGE_SPACE),
+ topRight: Radius.circular(LARGE_SPACE),
),
- const SizedBox(height: MEDIUM_SPACE),
- ListTile(
- leading: PlatformWidget(
- cupertino: (_, __) => Icon(
- CupertinoIcons.down_arrow,
+ ),
+ child: Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.symmetric(
+ vertical: MEDIUM_SPACE,
+ horizontal: MEDIUM_SPACE,
+ ),
+ child: SheetIndicator(),
+ ),
+ Text(
+ localizations.memorySheetTitle,
+ style: getTitleTextStyle(context),
+ ),
+ const SizedBox(height: MEDIUM_SPACE),
+ ListTile(
+ leading: PlatformWidget(
+ cupertino: (_, __) => Icon(
+ CupertinoIcons.down_arrow,
+ color: getBodyTextColor(context),
+ ),
+ material: (_, __) => Icon(
+ Icons.download,
+ color: getBodyTextColor(context),
+ ),
+ ),
+ title: Text(
+ localizations.memorySheetDownloadMemory,
+ style: getBodyTextTextStyle(context),
+ ),
+ enabled: !getIsLoadingSpecificID('download'),
+ onTap: getIsLoadingSpecificID('download')
+ ? null
+ : () => callWithLoading(downloadFile, 'download'),
+ trailing: getIsLoadingSpecificID('download')
+ ? buildLoadingIndicator()
+ : null,
+ ),
+ ListTile(
+ leading: Icon(
+ widget.memory.isPublic
+ ? Icons.public_off_rounded
+ : Icons.public,
color: getBodyTextColor(context),
),
- material: (_, __) => Icon(
- Icons.download,
+ title: Text(
+ widget.memory.isPublic
+ ? localizations.memorySheetUpdateMemoryMakePrivate
+ : localizations.memorySheetUpdateMemoryMakePublic,
+ style: getBodyTextTextStyle(context),
+ ),
+ enabled: !getIsLoadingSpecificID('public'),
+ onTap: getIsLoadingSpecificID('public')
+ ? null
+ : () => callWithLoading(changeVisibility, 'public'),
+ trailing: getIsLoadingSpecificID('public')
+ ? buildLoadingIndicator()
+ : null,
+ ),
+ ListTile(
+ leading: Icon(
+ context.platformIcons.delete,
color: getBodyTextColor(context),
),
+ title: Text(
+ localizations.memorySheetDeleteMemory,
+ style: getBodyTextTextStyle(context),
+ ),
+ enabled: !getIsLoadingSpecificID('delete'),
+ onTap: getIsLoadingSpecificID('delete')
+ ? null
+ : () => callWithLoading(deleteFile, 'delete'),
+ trailing: getIsLoadingSpecificID('delete')
+ ? buildLoadingIndicator()
+ : null,
),
- title: Text(
- localizations.memorySheetDownloadMemory,
- style: getBodyTextTextStyle(context),
+ ],
+ ),
+ ),
+ expandableContent: Container(
+ width: double.infinity,
+ color: backgroundColor,
+ child: Column(
+ children: [
+ widget.memory.location == null
+ ? const SizedBox.shrink()
+ : MemoryLocationView(
+ location: widget.memory.location!,
+ ),
+ Padding(
+ padding: const EdgeInsets.all(MEDIUM_SPACE),
+ child: Column(
+ children: [
+ Text(
+ localizations.memorySheetCreatedAtDataKey(
+ DateFormat.jms().format(
+ widget.memory.creationDate,
+ ),
+ ),
+ style: getBodyTextTextStyle(context),
+ ),
+ const SizedBox(height: SMALL_SPACE),
+ Text(
+ widget.memory.id,
+ textAlign: TextAlign.center,
+ style: getBodyTextTextStyle(context),
+ ),
+ ],
+ ),
),
- enabled: !getIsLoadingSpecificID('download'),
- onTap: getIsLoadingSpecificID('download')
- ? null
- : () => callWithLoading(downloadFile, 'download'),
- trailing: getIsLoadingSpecificID('download')
- ? buildLoadingIndicator()
- : null,
- ),
- ListTile(
- leading: Icon(
- widget.memory.isPublic ? Icons.public_off_rounded : Icons.public,
- color: getBodyTextColor(context),
- ),
- title: Text(
- widget.memory.isPublic
- ? localizations.memorySheetUpdateMemoryMakePrivate
- : localizations.memorySheetUpdateMemoryMakePublic,
- style: getBodyTextTextStyle(context),
- ),
- enabled: !getIsLoadingSpecificID('public'),
- onTap: getIsLoadingSpecificID('public')
- ? null
- : () => callWithLoading(changeVisibility, 'public'),
- trailing: getIsLoadingSpecificID('public')
- ? buildLoadingIndicator()
- : null,
- ),
- ListTile(
- leading: Icon(
- context.platformIcons.delete,
- color: getBodyTextColor(context),
- ),
- title: Text(
- localizations.memorySheetDeleteMemory,
- style: getBodyTextTextStyle(context),
- ),
- enabled: !getIsLoadingSpecificID('delete'),
- onTap: getIsLoadingSpecificID('delete')
- ? null
- : () => callWithLoading(deleteFile, 'delete'),
- trailing: getIsLoadingSpecificID('delete')
- ? buildLoadingIndicator()
- : null,
- ),
- const SizedBox(height: MEDIUM_SPACE),
- Text(
- localizations.memorySheetCreatedAtDataKey(DateFormat.jms().format(
- widget.memory.creationDate,
- )),
- style: getBodyTextTextStyle(context),
- ),
- const SizedBox(height: SMALL_SPACE),
- Text(
- widget.memory.id,
- textAlign: TextAlign.center,
- style: getBodyTextTextStyle(context),
- )
- ],
+ ],
+ ),
),
);
}
diff --git a/lib/utils/tag_location_to_image.dart b/lib/utils/tag_location_to_image.dart
new file mode 100644
index 0000000..33108ae
--- /dev/null
+++ b/lib/utils/tag_location_to_image.dart
@@ -0,0 +1,18 @@
+import 'dart:io';
+
+import 'package:flutter_exif_plugin/flutter_exif_plugin.dart';
+import 'package:location/location.dart';
+
+Future tagLocationToImage(
+ final File file,
+ final LocationData locationData,
+) async {
+ final exif = FlutterExif.fromPath(file.absolute.path);
+
+ await exif.setLatLong(locationData.latitude!, locationData.longitude!);
+ await exif.setAltitude(locationData.altitude!);
+ await exif.setAttribute('accuracy', locationData.accuracy!.toString());
+ await exif.setAttribute('speed', locationData.speed!.toString());
+
+ await exif.saveAttributes();
+}
diff --git a/lib/widgets/key_value_info.dart b/lib/widgets/key_value_info.dart
new file mode 100644
index 0000000..e6ba821
--- /dev/null
+++ b/lib/widgets/key_value_info.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
+import 'package:quid_faciam_hodie/constants/spacing.dart';
+import 'package:quid_faciam_hodie/extensions/snackbar.dart';
+import 'package:quid_faciam_hodie/utils/theme.dart';
+
+class KeyValueInfo extends StatelessWidget {
+ final String title;
+ final String value;
+ final bool valueCopyable;
+ final IconData? icon;
+ final String? disclaimer;
+
+ const KeyValueInfo({
+ Key? key,
+ required this.title,
+ required this.value,
+ this.valueCopyable = true,
+ this.icon,
+ this.disclaimer,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: MEDIUM_SPACE,
+ horizontal: SMALL_SPACE,
+ ),
+ child: ListTile(
+ title: Row(
+ children: [
+ if (icon != null)
+ Padding(
+ padding: const EdgeInsets.only(right: SMALL_SPACE),
+ child: Icon(icon),
+ ),
+ Expanded(
+ flex: 2,
+ child: Text(
+ title,
+ style: getSubTitleTextStyle(context),
+ ),
+ ),
+ Expanded(
+ flex: 3,
+ child: Text(
+ value,
+ style: getBodyTextTextStyle(context),
+ ),
+ ),
+ ],
+ ),
+ trailing: valueCopyable
+ ? PlatformIconButton(
+ icon: const Icon(Icons.content_copy),
+ onPressed: () {
+ HapticFeedback.lightImpact();
+ Clipboard.setData(ClipboardData(text: value));
+
+ if (isMaterial(context)) {
+ context.showSuccessSnackBar(
+ message: 'Copied to clipboard!',
+ );
+ }
+ },
+ )
+ : null,
+ ),
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index bc5bc3b..b807c6a 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -120,6 +120,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
+ dio:
+ dependency: transitive
+ description:
+ name: dio
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.0.6"
expandable_bottom_sheet:
dependency: "direct main"
description:
@@ -160,6 +167,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2"
+ flutter_exif_plugin:
+ dependency: "direct main"
+ description:
+ name: flutter_exif_plugin
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -172,6 +186,20 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_osm_interface:
+ dependency: transitive
+ description:
+ name: flutter_osm_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.26"
+ flutter_osm_plugin:
+ dependency: "direct main"
+ description:
+ name: flutter_osm_plugin
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.39.0"
flutter_platform_widgets:
dependency: "direct main"
description:
@@ -273,6 +301,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.2"
+ google_polyline_algorithm:
+ dependency: transitive
+ description:
+ name: google_polyline_algorithm
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.1.0"
gotrue:
dependency: transitive
description:
@@ -343,6 +378,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
+ location:
+ dependency: "direct main"
+ description:
+ name: location
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.4.0"
+ location_platform_interface:
+ dependency: transitive
+ description:
+ name: location_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.3.0"
+ location_web:
+ dependency: transitive
+ description:
+ name: location_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.1.1"
lottie:
dependency: "direct main"
description:
@@ -671,7 +727,7 @@ packages:
source: hosted
version: "2.0.4"
url_launcher:
- dependency: transitive
+ dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
diff --git a/pubspec.yaml b/pubspec.yaml
index 5466173..c7bc0c8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -58,6 +58,10 @@ dependencies:
settings_ui: ^2.0.2
coast: ^2.0.2
http: ^0.13.5
+ location: ^4.4.0
+ flutter_exif_plugin: ^1.1.0
+ flutter_osm_plugin: ^0.39.0
+ url_launcher: ^6.1.5
dev_dependencies:
flutter_test: