Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import 'features/detail/screen/detail_screen.dart';
import 'features/gallery/screen/gallery_screen.dart';
import 'features/result/screen/result_screen.dart';
import 'features/locations/screen/locations_screen.dart';
import 'features/settings/screen/settings_screen.dart';
import 'features/account/screen/account_screen.dart';
import 'models/app_settings.dart';
import 'services/settings_service.dart';
import 'models/captured_media.dart';
import 'models/location_info.dart';

Expand All @@ -30,23 +34,42 @@ class ResultScreenArgs {
final MediaType type;
}

class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key, required this.permissionsGranted});

final bool permissionsGranted;

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
AppSettings _settings = const AppSettings();

@override
void initState() {
super.initState();
const SettingsService().load().then((settings) {
if (mounted) setState(() => _settings = settings);
});
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GPS Camera',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: _settings.themeMode,
initialRoute: AppConstants.routeCamera,
routes: {
AppConstants.routeCamera: (_) =>
CameraScreen(permissionsGranted: permissionsGranted),
CameraScreen(permissionsGranted: widget.permissionsGranted),
AppConstants.routeGallery: (_) => const GalleryScreen(),
AppConstants.routeLocations: (_) => const LocationsScreen(),
AppConstants.routeSettings: (_) => const SettingsScreen(),
AppConstants.routeAccount: (_) => const AccountScreen(),
},
onGenerateRoute: (settings) {
if (settings.name == AppConstants.routeDetail) {
Expand Down
2 changes: 2 additions & 0 deletions lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class AppConstants {
static const String routeDetail = '/detail';
static const String routeResult = '/result';
static const String routeLocations = '/locations';
static const String routeSettings = '/settings';
static const String routeAccount = '/account';

static const int minZoom = 1;
static const int maxZoom = 8;
Expand Down
32 changes: 32 additions & 0 deletions lib/core/theme/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,36 @@ class AppTheme {
),
);
}

static ThemeData get darkTheme {
final base = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
brightness: Brightness.dark,
primary: AppColors.primary,
),
scaffoldBackgroundColor: Colors.black,
);

return base.copyWith(
appBarTheme: const AppBarTheme(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
titleTextStyle: AppTextStyles.appBarTitle,
iconTheme: IconThemeData(color: Colors.white),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
textStyle: AppTextStyles.pillButton,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
),
);
}
}
164 changes: 98 additions & 66 deletions lib/core/widgets/location_stamp_card.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:permission_handler/permission_handler.dart';

import '../../models/app_settings.dart';
import '../../models/location_info.dart';
import '../../services/google_map_service.dart';
import '../../services/settings_service.dart';

class LocationStampCard extends StatelessWidget {
const LocationStampCard({
Expand All @@ -23,37 +24,43 @@ class LocationStampCard extends StatelessWidget {
Widget build(BuildContext context) {
final info = locationInfo;

return AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
transitionBuilder: (child, animation) {
final offset = Tween<Offset>(
begin: const Offset(0, 0.25),
end: Offset.zero,
).animate(animation);
return FadeTransition(
opacity: animation,
child: SlideTransition(position: offset, child: child),
);
},
child: ClipRRect(
key: ValueKey(info?.address ?? 'fallback'),
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
child: Container(
width: cardWidth,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(color: Colors.black26, blurRadius: 8),
],
return FutureBuilder<AppSettings>(
future: const SettingsService().load(),
builder: (context, snapshot) {
final settings = snapshot.data ?? const AppSettings();
return AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
transitionBuilder: (child, animation) {
final offset = Tween<Offset>(
begin: const Offset(0, 0.25),
end: Offset.zero,
).animate(animation);
return FadeTransition(
opacity: animation,
child: SlideTransition(position: offset, child: child),
);
},
child: ClipRRect(
key: ValueKey(info?.address ?? 'fallback'),
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
child: Container(
width: cardWidth,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(color: Colors.black26, blurRadius: 8),
],
),
child: info == null ? _fallback() : _content(info, settings),
),
),
child: info == null ? _fallback() : _content(info),
),
),
),
);
},
);
}

Expand All @@ -67,14 +74,14 @@ class LocationStampCard extends StatelessWidget {
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.location_off),
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('📍 Location unavailable'),
const Text('Getting GPS and map...'),
TextButton(
onPressed: openAppSettings,
child: const Text('Enable location'),
Expand All @@ -86,41 +93,11 @@ class LocationStampCard extends StatelessWidget {
);
}

Widget _content(LocationInfo info) {
Widget _content(LocationInfo info, AppSettings settings) {
return Row(
children: [
if (showMap)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 70,
height: 70,
child: FlutterMap(
options: MapOptions(
initialCenter: LatLng(info.latitude, info.longitude),
initialZoom: 14,
interactionOptions:
const InteractionOptions(flags: InteractiveFlag.none),
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
),
MarkerLayer(markers: [
Marker(
point: LatLng(info.latitude, info.longitude),
width: 24,
height: 24,
child: const Icon(Icons.location_pin,
color: Colors.red, size: 16),
)
])
],
),
),
),
const SizedBox(width: 8),
if (showMap) _StaticMap(info: info, settings: settings),
if (showMap) const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down Expand Up @@ -152,10 +129,65 @@ class LocationStampCard extends StatelessWidget {
Text(info.time,
style: TextStyle(fontSize: 11, color: Colors.grey.shade700)),
]),
if (settings.showCoordinates) ...[
const SizedBox(height: 2),
Text(
'${info.latitude.toStringAsFixed(5)}, ${info.longitude.toStringAsFixed(5)}',
style: TextStyle(fontSize: 10, color: Colors.grey.shade700),
),
],
if (settings.showCompassSpeedAltitude) ...[
const SizedBox(height: 2),
Text(
'W -- C ${info.heading?.toStringAsFixed(0) ?? '--'}° S ${info.speedMetersPerSecond?.toStringAsFixed(1) ?? '--'} A ${info.altitude?.toStringAsFixed(0) ?? '--'}m',
style: TextStyle(fontSize: 10, color: Colors.grey.shade700),
),
],
],
),
),
],
);
}
}

class _StaticMap extends StatelessWidget {
const _StaticMap({required this.info, required this.settings});

final LocationInfo info;
final AppSettings settings;

@override
Widget build(BuildContext context) {
final uri = const GoogleMapService().staticMapUri(
latitude: info.latitude,
longitude: info.longitude,
width: 200,
height: 150,
zoom: settings.mapZoomLevel.round(),
mapStyle: settings.mapStyle,
);

return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 70,
height: 70,
child: Image.network(
uri.toString(),
fit: BoxFit.cover,
loadingBuilder: (context, child, progress) => progress == null
? child
: Container(
color: Colors.grey.shade300,
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
),
errorBuilder: (_, __, ___) => Container(
color: Colors.grey.shade300,
child: const Icon(Icons.map_outlined, color: Colors.black54),
),
),
),
);
}
}
Loading
Loading