mirror of
https://github.com/Myzel394/quid_faciam_hodie.git
synced 2025-06-18 23:35:25 +02:00
added location functionality to memories
This commit is contained in:
parent
0cd42ec03a
commit
359ed768c5
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -17,3 +17,4 @@ const WELCOME_SCREEN_PHOTOS_QUERIES = [
|
||||
'friends',
|
||||
'romantic',
|
||||
];
|
||||
const ACCURACY_IN_METERS_FOR_PINPOINT = 20;
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
32
lib/foreign_types/memory_location.dart
Normal file
32
lib/foreign_types/memory_location.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -47,6 +47,7 @@
|
||||
"permissionsRequiredPageOpenSettings": "Einstellungen öffnen",
|
||||
"permissionsRequiredPageGrantCameraPermission": "Kamera-Berechtigung erteilen",
|
||||
"permissionsRequiredPageGrantMicrophonePermission": "Mikrofon-Berechtigung erteilen",
|
||||
"permissionsRequiredPageGrantLocationPermission": "Standort-Berechtigung erteilen",
|
||||
|
||||
|
||||
"memoryViewIsDownloading": "Erinnerung wird heruntergeladen",
|
||||
|
@ -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",
|
||||
|
@ -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}');
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
|
149
lib/screens/timeline_screen/memory_location_view.dart
Normal file
149
lib/screens/timeline_screen/memory_location_view.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
18
lib/utils/tag_location_to_image.dart
Normal file
18
lib/utils/tag_location_to_image.dart
Normal 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();
|
||||
}
|
75
lib/widgets/key_value_info.dart
Normal file
75
lib/widgets/key_value_info.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
58
pubspec.lock
58
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"
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user