mirror of
https://github.com/Myzel394/quid_faciam_hodie.git
synced 2025-06-18 15:25:27 +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"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="floss.myzel394.quid_faciam_hodie">
|
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
|
<application
|
||||||
android:label="Quid faciam hodie?"
|
android:label="Quid faciam hodie?"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@ -35,6 +42,4 @@
|
|||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -55,6 +55,9 @@ post_install do |installer|
|
|||||||
|
|
||||||
## dart: PermissionGroup.microphone
|
## dart: PermissionGroup.microphone
|
||||||
'PERMISSION_MICROPHONE=1',
|
'PERMISSION_MICROPHONE=1',
|
||||||
|
|
||||||
|
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
|
||||||
|
'PERMISSION_LOCATION=1',
|
||||||
]
|
]
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -72,5 +72,8 @@
|
|||||||
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Accessing your gallery allows you to save your memories</string>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -17,3 +17,4 @@ const WELCOME_SCREEN_PHOTOS_QUERIES = [
|
|||||||
'friends',
|
'friends',
|
||||||
'romantic',
|
'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/enums.dart';
|
||||||
import 'package:quid_faciam_hodie/managers/file_manager.dart';
|
import 'package:quid_faciam_hodie/managers/file_manager.dart';
|
||||||
|
|
||||||
|
import 'memory_location.dart';
|
||||||
|
|
||||||
class Memory {
|
class Memory {
|
||||||
final String id;
|
final String id;
|
||||||
final DateTime creationDate;
|
final DateTime creationDate;
|
||||||
final String location;
|
final String filePath;
|
||||||
final bool isPublic;
|
final bool isPublic;
|
||||||
final String userID;
|
final String userID;
|
||||||
|
final MemoryLocation? location;
|
||||||
|
|
||||||
const Memory({
|
const Memory({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.creationDate,
|
required this.creationDate,
|
||||||
required this.location,
|
required this.filePath,
|
||||||
required this.isPublic,
|
required this.isPublic,
|
||||||
required this.userID,
|
required this.userID,
|
||||||
|
this.location,
|
||||||
});
|
});
|
||||||
|
|
||||||
static parse(Map<String, dynamic> jsonData) {
|
static parse(final Map<String, dynamic> jsonData) => Memory(
|
||||||
return Memory(
|
id: jsonData['id'],
|
||||||
id: jsonData['id'],
|
creationDate: DateTime.parse(jsonData['created_at']),
|
||||||
creationDate: DateTime.parse(jsonData['created_at']),
|
filePath: jsonData['location'],
|
||||||
location: jsonData['location'],
|
isPublic: jsonData['is_public'],
|
||||||
isPublic: jsonData['is_public'],
|
userID: jsonData['user_id'],
|
||||||
userID: jsonData['user_id'],
|
location: MemoryLocation.parse(jsonData),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
String get filename => basename(location);
|
String get filename => basename(filePath);
|
||||||
|
|
||||||
MemoryType get type =>
|
MemoryType get type =>
|
||||||
filename.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video;
|
filename.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video;
|
||||||
|
|
||||||
Future<File> downloadToFile() => FileManager.downloadFile(
|
Future<File> downloadToFile() => FileManager.downloadFile(
|
||||||
'memories',
|
'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",
|
"permissionsRequiredPageOpenSettings": "Einstellungen öffnen",
|
||||||
"permissionsRequiredPageGrantCameraPermission": "Kamera-Berechtigung erteilen",
|
"permissionsRequiredPageGrantCameraPermission": "Kamera-Berechtigung erteilen",
|
||||||
"permissionsRequiredPageGrantMicrophonePermission": "Mikrofon-Berechtigung erteilen",
|
"permissionsRequiredPageGrantMicrophonePermission": "Mikrofon-Berechtigung erteilen",
|
||||||
|
"permissionsRequiredPageGrantLocationPermission": "Standort-Berechtigung erteilen",
|
||||||
|
|
||||||
|
|
||||||
"memoryViewIsDownloading": "Erinnerung wird heruntergeladen",
|
"memoryViewIsDownloading": "Erinnerung wird heruntergeladen",
|
||||||
|
@ -65,6 +65,7 @@
|
|||||||
"permissionsRequiredPageOpenSettings": "Open Settings",
|
"permissionsRequiredPageOpenSettings": "Open Settings",
|
||||||
"permissionsRequiredPageGrantCameraPermission": "Grant camera permission",
|
"permissionsRequiredPageGrantCameraPermission": "Grant camera permission",
|
||||||
"permissionsRequiredPageGrantMicrophonePermission": "Grant microphone permission",
|
"permissionsRequiredPageGrantMicrophonePermission": "Grant microphone permission",
|
||||||
|
"permissionsRequiredPageGrantLocationPermission": "Grant location permission",
|
||||||
|
|
||||||
|
|
||||||
"memoryViewIsDownloading": "Downloading memory",
|
"memoryViewIsDownloading": "Downloading memory",
|
||||||
@ -89,6 +90,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"memorySheetMapEstimatedAddressLabel": "Estimated Address",
|
||||||
|
"memorySheetMapOpenNavigation": "Open Navigation",
|
||||||
|
|
||||||
|
|
||||||
"emptyScreenTitle": "Houston, we have a problem",
|
"emptyScreenTitle": "Houston, we have a problem",
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:location/location.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:quid_faciam_hodie/foreign_types/memory.dart';
|
import 'package:quid_faciam_hodie/foreign_types/memory.dart';
|
||||||
import 'package:quid_faciam_hodie/managers/cache_manager.dart';
|
import 'package:quid_faciam_hodie/managers/cache_manager.dart';
|
||||||
@ -32,7 +33,11 @@ class FileManager {
|
|||||||
return Memory.parse(response.data);
|
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();
|
await GlobalValuesManager.waitForServerInitialization();
|
||||||
|
|
||||||
final basename = uuid.v4();
|
final basename = uuid.v4();
|
||||||
@ -46,10 +51,22 @@ class FileManager {
|
|||||||
throw Exception('Error uploading file: ${response.error!.message}');
|
throw Exception('Error uploading file: ${response.error!.message}');
|
||||||
}
|
}
|
||||||
|
|
||||||
final memoryResponse = await supabase.from('memories').insert({
|
final Map<String, dynamic> data = {
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
'location': path,
|
'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) {
|
if (memoryResponse.error != null) {
|
||||||
throw Exception('Error creating memory: ${response.error!.message}');
|
throw Exception('Error creating memory: ${response.error!.message}');
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:quid_faciam_hodie/constants/apis.dart';
|
import 'package:quid_faciam_hodie/constants/apis.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
@ -45,4 +46,9 @@ class GlobalValuesManager {
|
|||||||
|
|
||||||
await _serverInitializationFuture;
|
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 hasDeniedForever = false;
|
||||||
bool hasGrantedCameraPermission = false;
|
bool hasGrantedCameraPermission = false;
|
||||||
bool hasGrantedMicrophonePermission = false;
|
bool hasGrantedMicrophonePermission = false;
|
||||||
|
bool hasGrantedLocationPermission = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -33,12 +34,15 @@ class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
|
|||||||
Future<void> checkPermissions() async {
|
Future<void> checkPermissions() async {
|
||||||
final cameraStatus = await Permission.camera.status;
|
final cameraStatus = await Permission.camera.status;
|
||||||
final microphoneStatus = await Permission.microphone.status;
|
final microphoneStatus = await Permission.microphone.status;
|
||||||
|
final locationStatus = await Permission.location.status;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
hasGrantedCameraPermission = cameraStatus.isGranted;
|
hasGrantedCameraPermission = cameraStatus.isGranted;
|
||||||
hasGrantedMicrophonePermission = microphoneStatus.isGranted;
|
hasGrantedMicrophonePermission = microphoneStatus.isGranted;
|
||||||
|
hasGrantedLocationPermission = locationStatus.isGranted;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// These permissions are crucially required for the app to work
|
||||||
if (cameraStatus.isPermanentlyDenied ||
|
if (cameraStatus.isPermanentlyDenied ||
|
||||||
microphoneStatus.isPermanentlyDenied) {
|
microphoneStatus.isPermanentlyDenied) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -48,7 +52,9 @@ class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cameraStatus.isGranted && microphoneStatus.isGranted) {
|
if (cameraStatus.isGranted &&
|
||||||
|
microphoneStatus.isGranted &&
|
||||||
|
locationStatus.isGranted) {
|
||||||
widget.onPermissionsGranted();
|
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:quid_faciam_hodie/widgets/icon_button_child.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import 'main_screen.dart';
|
||||||
|
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
@ -25,6 +27,17 @@ class _LoginScreenState extends AuthState<LoginScreen> with Loadable {
|
|||||||
final emailController = TextEditingController();
|
final emailController = TextEditingController();
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onAuthenticated(Session session) {
|
||||||
|
if (session.user != null) {
|
||||||
|
Navigator.pushNamedAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MainScreen.ID,
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
emailController.dispose();
|
emailController.dispose();
|
||||||
|
@ -7,6 +7,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.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/spacing.dart';
|
||||||
import 'package:quid_faciam_hodie/constants/values.dart';
|
import 'package:quid_faciam_hodie/constants/values.dart';
|
||||||
import 'package:quid_faciam_hodie/extensions/snackbar.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/screens/main_screen/settings_button_overlay.dart';
|
||||||
import 'package:quid_faciam_hodie/utils/auth_required.dart';
|
import 'package:quid_faciam_hodie/utils/auth_required.dart';
|
||||||
import 'package:quid_faciam_hodie/utils/loadable.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/animate_in_builder.dart';
|
||||||
import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.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/icon_button_child.dart';
|
||||||
@ -189,6 +192,13 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final file = File((await controller!.takePicture()).path);
|
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(() {
|
setState(() {
|
||||||
uploadingPhotoAnimation = file.readAsBytesSync();
|
uploadingPhotoAnimation = file.readAsBytesSync();
|
||||||
@ -200,7 +210,7 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FileManager.uploadFile(_user, file);
|
await FileManager.uploadFile(_user, file, locationData: locationData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isMaterial(context))
|
if (isMaterial(context))
|
||||||
context.showErrorSnackBar(message: error.toString());
|
context.showErrorSnackBar(message: error.toString());
|
||||||
|
@ -11,8 +11,8 @@ class SettingsButtonOverlay extends StatelessWidget {
|
|||||||
return Positioned(
|
return Positioned(
|
||||||
left: SMALL_SPACE,
|
left: SMALL_SPACE,
|
||||||
top: SMALL_SPACE,
|
top: SMALL_SPACE,
|
||||||
child: PlatformTextButton(
|
child: PlatformIconButton(
|
||||||
child: Icon(
|
icon: Icon(
|
||||||
context.platformIcons.settings,
|
context.platformIcons.settings,
|
||||||
color: Colors.white,
|
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/constants/spacing.dart';
|
||||||
import 'package:quid_faciam_hodie/managers/global_values_manager.dart';
|
import 'package:quid_faciam_hodie/managers/global_values_manager.dart';
|
||||||
import 'package:quid_faciam_hodie/models/memories.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 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'empty_screen.dart';
|
import 'empty_screen.dart';
|
||||||
@ -35,6 +36,13 @@ class _ServerLoadingScreenState extends State<ServerLoadingScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
|
if (!(await GlobalValuesManager.hasGrantedPermissions())) {
|
||||||
|
Navigator.pushReplacementNamed(
|
||||||
|
context,
|
||||||
|
GrantPermissionScreen.ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await GlobalValuesManager.waitForServerInitialization();
|
await GlobalValuesManager.waitForServerInitialization();
|
||||||
|
|
||||||
final memories = context.read<Memories>();
|
final memories = context.read<Memories>();
|
||||||
@ -45,6 +53,10 @@ class _ServerLoadingScreenState extends State<ServerLoadingScreen> {
|
|||||||
await memories.initialize();
|
await memories.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.nextScreen == null) {
|
if (widget.nextScreen == null) {
|
||||||
Navigator.pushNamed(
|
Navigator.pushNamed(
|
||||||
context,
|
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/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.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/managers/file_manager.dart';
|
||||||
import 'package:quid_faciam_hodie/utils/loadable.dart';
|
import 'package:quid_faciam_hodie/utils/loadable.dart';
|
||||||
import 'package:quid_faciam_hodie/utils/theme.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 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import 'memory_location_view.dart';
|
||||||
|
|
||||||
class MemorySheet extends StatefulWidget {
|
class MemorySheet extends StatefulWidget {
|
||||||
final Memory memory;
|
final Memory memory;
|
||||||
final BuildContext sheetContext;
|
final BuildContext sheetContext;
|
||||||
@ -32,7 +35,7 @@ final supabase = Supabase.instance.client;
|
|||||||
|
|
||||||
class _MemorySheetState extends State<MemorySheet> with Loadable {
|
class _MemorySheetState extends State<MemorySheet> with Loadable {
|
||||||
Future<void> deleteFile() async {
|
Future<void> deleteFile() async {
|
||||||
await FileManager.deleteFile(widget.memory.location);
|
await FileManager.deleteFile(widget.memory.filePath);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@ -62,7 +65,8 @@ class _MemorySheetState extends State<MemorySheet> with Loadable {
|
|||||||
|
|
||||||
if (isMaterial(context))
|
if (isMaterial(context))
|
||||||
context.showSuccessSnackBar(
|
context.showSuccessSnackBar(
|
||||||
message: localizations.memorySheetSavedToGallery);
|
message: localizations.memorySheetSavedToGallery,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isMaterial(context))
|
if (isMaterial(context))
|
||||||
context.showErrorSnackBar(message: localizations.generalError);
|
context.showErrorSnackBar(message: localizations.generalError);
|
||||||
@ -90,11 +94,13 @@ class _MemorySheetState extends State<MemorySheet> with Loadable {
|
|||||||
if (isNowPublic) {
|
if (isNowPublic) {
|
||||||
if (isMaterial(context))
|
if (isMaterial(context))
|
||||||
context.showSuccessSnackBar(
|
context.showSuccessSnackBar(
|
||||||
message: localizations.memorySheetMemoryUpdatedToPublic);
|
message: localizations.memorySheetMemoryUpdatedToPublic,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (isMaterial(context))
|
if (isMaterial(context))
|
||||||
context.showSuccessSnackBar(
|
context.showSuccessSnackBar(
|
||||||
message: localizations.memorySheetMemoryUpdatedToPrivate);
|
message: localizations.memorySheetMemoryUpdatedToPrivate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isMaterial(context))
|
if (isMaterial(context))
|
||||||
@ -123,88 +129,137 @@ class _MemorySheetState extends State<MemorySheet> with Loadable {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localizations = AppLocalizations.of(context)!;
|
final localizations = AppLocalizations.of(context)!;
|
||||||
|
final backgroundColor = platformThemeData(
|
||||||
|
context,
|
||||||
|
material: (data) =>
|
||||||
|
data.bottomSheetTheme.modalBackgroundColor ?? data.bottomAppBarColor,
|
||||||
|
cupertino: (data) => data.barBackgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
return ModalSheet(
|
return ExpandableBottomSheet(
|
||||||
child: Column(
|
background: GestureDetector(
|
||||||
children: <Widget>[
|
onTap: () => Navigator.pop(context),
|
||||||
Text(
|
),
|
||||||
localizations.memorySheetTitle,
|
persistentHeader: Container(
|
||||||
style: getTitleTextStyle(context),
|
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(
|
child: Column(
|
||||||
leading: PlatformWidget(
|
children: <Widget>[
|
||||||
cupertino: (_, __) => Icon(
|
const Padding(
|
||||||
CupertinoIcons.down_arrow,
|
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),
|
color: getBodyTextColor(context),
|
||||||
),
|
),
|
||||||
material: (_, __) => Icon(
|
title: Text(
|
||||||
Icons.download,
|
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),
|
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"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
|
dio:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.6"
|
||||||
expandable_bottom_sheet:
|
expandable_bottom_sheet:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -160,6 +167,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.2"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -172,6 +186,20 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_platform_widgets:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -273,6 +301,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
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:
|
gotrue:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -343,6 +378,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
lottie:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -671,7 +727,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.4"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
@ -58,6 +58,10 @@ dependencies:
|
|||||||
settings_ui: ^2.0.2
|
settings_ui: ^2.0.2
|
||||||
coast: ^2.0.2
|
coast: ^2.0.2
|
||||||
http: ^0.13.5
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user