added location functionality to memories

This commit is contained in:
Myzel394 2022-08-18 22:31:34 +02:00
parent 0cd42ec03a
commit 359ed768c5
21 changed files with 597 additions and 102 deletions

View File

@ -1,5 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="floss.myzel394.quid_faciam_hodie">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:label="Quid faciam hodie?"
android:name="${applicationName}"
@ -35,6 +42,4 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

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

View File

@ -72,5 +72,8 @@
<key>NSPhotoLibraryUsageDescription</key>
<string>Accessing your gallery allows you to save your memories</string>
<key>NSLocationUsageDescription</key>
<string>Accessing your location allows you to tag your memories</string>
</dict>
</plist>

View File

@ -17,3 +17,4 @@ const WELCOME_SCREEN_PHOTOS_QUERIES = [
'friends',
'romantic',
];
const ACCURACY_IN_METERS_FOR_PINPOINT = 20;

View File

@ -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<String, dynamic> 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<String, dynamic> 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<File> downloadToFile() => FileManager.downloadFile(
'memories',
location,
filePath,
);
}

View File

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

View File

@ -47,6 +47,7 @@
"permissionsRequiredPageOpenSettings": "Einstellungen öffnen",
"permissionsRequiredPageGrantCameraPermission": "Kamera-Berechtigung erteilen",
"permissionsRequiredPageGrantMicrophonePermission": "Mikrofon-Berechtigung erteilen",
"permissionsRequiredPageGrantLocationPermission": "Standort-Berechtigung erteilen",
"memoryViewIsDownloading": "Erinnerung wird heruntergeladen",

View File

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

View File

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

View File

@ -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<bool> hasGrantedPermissions() async =>
(await Permission.camera.isGranted) &&
(await Permission.microphone.isGranted) &&
(await Permission.location.isGranted);
}

View File

@ -22,6 +22,7 @@ class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
bool hasDeniedForever = false;
bool hasGrantedCameraPermission = false;
bool hasGrantedMicrophonePermission = false;
bool hasGrantedLocationPermission = false;
@override
void initState() {
@ -33,12 +34,15 @@ class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
Future<void> 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<PermissionsRequiredPage> {
return;
}
if (cameraStatus.isGranted && microphoneStatus.isGranted) {
if (cameraStatus.isGranted &&
microphoneStatus.isGranted &&
locationStatus.isGranted) {
widget.onPermissionsGranted();
}
}
@ -136,6 +142,29 @@ class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
),
),
),
PlatformTextButton(
onPressed: hasGrantedLocationPermission
? null
: () async {
await Permission.location.request();
await checkPermissions();
},
child: IconButtonChild(
icon: Icon(context.platformIcons.location),
label: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
localizations
.permissionsRequiredPageGrantMicrophonePermission,
),
if (hasGrantedLocationPermission)
Icon(context.platformIcons.checkMark),
if (!hasGrantedLocationPermission) const SizedBox(),
],
),
),
),
],
],
);

View File

@ -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<LoginScreen> 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();

View File

@ -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<MainScreen> 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<MainScreen> 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());

View File

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

View File

@ -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<ServerLoadingScreen> {
}
Future<void> load() async {
if (!(await GlobalValuesManager.hasGrantedPermissions())) {
Navigator.pushReplacementNamed(
context,
GrantPermissionScreen.ID,
);
}
await GlobalValuesManager.waitForServerInitialization();
final memories = context.read<Memories>();
@ -45,6 +53,10 @@ class _ServerLoadingScreenState extends State<ServerLoadingScreen> {
await memories.initialize();
}
if (!mounted) {
return;
}
if (widget.nextScreen == null) {
Navigator.pushNamed(
context,

View File

@ -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<MemoryLocationView> createState() => _MemoryLocationViewState();
}
class _MemoryLocationViewState extends State<MemoryLocationView> {
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<StaticPositionGeoPoint> 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: <Widget>[
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: <Widget>[
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),
),
),
],
),
),
],
);
}
}

View File

@ -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<MemorySheet> with Loadable {
Future<void> 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<MemorySheet> 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<MemorySheet> 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<MemorySheet> 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: <Widget>[
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: <Widget>[
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>[
widget.memory.location == null
? const SizedBox.shrink()
: MemoryLocationView(
location: widget.memory.location!,
),
Padding(
padding: const EdgeInsets.all(MEDIUM_SPACE),
child: Column(
children: <Widget>[
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),
)
],
],
),
),
);
}

View File

@ -0,0 +1,18 @@
import 'dart:io';
import 'package:flutter_exif_plugin/flutter_exif_plugin.dart';
import 'package:location/location.dart';
Future<void> 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();
}

View File

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

View File

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

View File

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