diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 2d86f59..186b732 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -76,34 +76,6 @@ class _HomeScreenState extends State { if (_followMe) setState(() => _followMe = false); }, ), - // Zoom buttons - Positioned( - right: 10, - bottom: MediaQuery.of(context).padding.bottom + kBottomButtonBarMargin + 120, - child: Column( - children: [ - FloatingActionButton( - mini: true, - onPressed: () { - final currentZoom = _mapController.camera.zoom; - _mapController.move(_mapController.camera.center, currentZoom + 0.5); - }, - child: Icon(Icons.add), - heroTag: 'zoom_in', - ), - SizedBox(height: 8), - FloatingActionButton( - mini: true, - onPressed: () { - final currentZoom = _mapController.camera.zoom; - _mapController.move(_mapController.camera.center, currentZoom - 0.5); - }, - child: Icon(Icons.remove), - heroTag: 'zoom_out', - ), - ], - ), - ), Align( alignment: Alignment.bottomCenter, child: Padding( diff --git a/lib/screens/settings_screen_sections/tile_provider_section.dart b/lib/screens/settings_screen_sections/tile_provider_section.dart index 8d5dab7..9e31aab 100644 --- a/lib/screens/settings_screen_sections/tile_provider_section.dart +++ b/lib/screens/settings_screen_sections/tile_provider_section.dart @@ -10,86 +10,28 @@ class TileProviderSection extends StatelessWidget { @override Widget build(BuildContext context) { - final appState = context.watch(); - final selectedTileType = appState.selectedTileType; - final allTileTypes = []; - for (final provider in appState.tileProviders) { - allTileTypes.addAll(provider.availableTileTypes); - } - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Map Type', - style: Theme.of(context).textTheme.titleMedium, - ), - TextButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const TileProviderManagementScreen(), - ), - ); - }, - child: const Text('Manage Providers'), - ), - ], + Text( + 'Map Tiles', + style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), - if (allTileTypes.isEmpty) - const Text('No tile providers available') - else - ...allTileTypes.map((tileType) { - final provider = appState.tileProviders - .firstWhere((p) => p.tileTypes.contains(tileType)); - final isSelected = selectedTileType?.id == tileType.id; - final isUsable = !tileType.requiresApiKey || provider.isUsable; - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: Radio( - value: tileType.id, - groupValue: selectedTileType?.id, - onChanged: isUsable ? (String? value) { - if (value != null) { - appState.setSelectedTileType(value); - } - } : null, - ), - title: Text( - '${provider.name} - ${tileType.name}', - style: TextStyle( - color: isUsable ? null : Theme.of(context).disabledColor, + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const TileProviderManagementScreen(), ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tileType.attribution, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: isUsable ? null : Theme.of(context).disabledColor, - ), - ), - if (!isUsable) - Text( - 'Requires API key', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.error, - fontStyle: FontStyle.italic, - ), - ), - ], - ), - onTap: isUsable ? () { - appState.setSelectedTileType(tileType.id); - } : null, - ); - }), + ); + }, + icon: const Icon(Icons.settings), + label: const Text('Manage Providers'), + ), + ), ], ); } diff --git a/lib/widgets/map/layer_selector_button.dart b/lib/widgets/map/layer_selector_button.dart new file mode 100644 index 0000000..740ff13 --- /dev/null +++ b/lib/widgets/map/layer_selector_button.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../app_state.dart'; +import '../../models/tile_provider.dart'; + +class LayerSelectorButton extends StatelessWidget { + const LayerSelectorButton({super.key}); + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + mini: true, + onPressed: () => _showLayerSelector(context), + child: const Icon(Icons.layers), + ); + } + + void _showLayerSelector(BuildContext context) { + showDialog( + context: context, + builder: (context) => const _LayerSelectorDialog(), + ); + } +} + +class _LayerSelectorDialog extends StatefulWidget { + const _LayerSelectorDialog(); + + @override + State<_LayerSelectorDialog> createState() => _LayerSelectorDialogState(); +} + +class _LayerSelectorDialogState extends State<_LayerSelectorDialog> { + String? _selectedTileTypeId; + + @override + void initState() { + super.initState(); + final appState = context.read(); + _selectedTileTypeId = appState.selectedTileType?.id; + } + + @override + Widget build(BuildContext context) { + final appState = context.watch(); + final providers = appState.tileProviders; + + // Group tile types by provider for display + final providerGroups = >{}; + for (final provider in providers) { + final availableTypes = provider.availableTileTypes; + if (availableTypes.isNotEmpty) { + providerGroups[provider] = availableTypes; + } + } + + return Dialog( + child: Container( + width: double.maxFinite, + constraints: const BoxConstraints(maxHeight: 500), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + ), + child: Row( + children: [ + const Icon(Icons.layers), + const SizedBox(width: 8), + const Text( + 'Select Map Layer', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + // Content + Flexible( + child: ListView( + padding: EdgeInsets.zero, + children: [ + if (providerGroups.isEmpty) + const Padding( + padding: EdgeInsets.all(24), + child: Center( + child: Text('No tile providers available'), + ), + ) + else + ...providerGroups.entries.map((entry) { + final provider = entry.key; + final tileTypes = entry.value; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Provider header + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: Theme.of(context).colorScheme.surface, + child: Text( + provider.name, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + // Tile types + ...tileTypes.map((tileType) => _TileTypeListItem( + tileType: tileType, + provider: provider, + isSelected: _selectedTileTypeId == tileType.id, + onSelected: () { + setState(() { + _selectedTileTypeId = tileType.id; + }); + appState.setSelectedTileType(tileType.id); + Navigator.of(context).pop(); + }, + )), + ], + ); + }), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _TileTypeListItem extends StatelessWidget { + final TileType tileType; + final TileProvider provider; + final bool isSelected; + final VoidCallback onSelected; + + const _TileTypeListItem({ + required this.tileType, + required this.provider, + required this.isSelected, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + borderRadius: BorderRadius.circular(4), + ), + child: tileType.previewTile != null + ? ClipRRect( + borderRadius: BorderRadius.circular(3), + child: Image.memory( + tileType.previewTile!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => _FallbackPreview(), + ), + ) + : _FallbackPreview(), + ), + title: Text( + tileType.name, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : null, + color: isSelected ? Theme.of(context).colorScheme.primary : null, + ), + ), + subtitle: Text( + tileType.attribution, + style: Theme.of(context).textTheme.bodySmall, + ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: Theme.of(context).colorScheme.primary, + ) + : null, + onTap: onSelected, + ); + } +} + +class _FallbackPreview extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + color: Colors.grey.shade200, + child: const Center( + child: Icon( + Icons.map, + size: 24, + color: Colors.grey, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/map/map_overlays.dart b/lib/widgets/map/map_overlays.dart index c05529e..c508aea 100644 --- a/lib/widgets/map/map_overlays.dart +++ b/lib/widgets/map/map_overlays.dart @@ -3,18 +3,21 @@ import 'package:flutter_map/flutter_map.dart'; import '../../app_state.dart'; import '../../dev_config.dart'; +import 'layer_selector_button.dart'; /// Widget that renders all map overlay UI elements class MapOverlays extends StatelessWidget { final MapController mapController; final UploadMode uploadMode; final AddCameraSession? session; + final String? attribution; // Attribution for current tile provider const MapOverlays({ super.key, required this.mapController, required this.uploadMode, this.session, + this.attribution, }); @override @@ -78,17 +81,52 @@ class MapOverlays extends StatelessWidget { ), // Attribution overlay - Positioned( - bottom: kAttributionBottomOffset, - left: 10, - child: Container( - color: Colors.white70, - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: const Text( - '© OpenStreetMap and contributors', - style: TextStyle(fontSize: 11), + if (attribution != null) + Positioned( + bottom: kAttributionBottomOffset, + left: 10, + child: Container( + color: Colors.white70, + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: Text( + attribution!, + style: const TextStyle(fontSize: 11), + ), ), ), + + // Zoom and layer controls (bottom-right) + Positioned( + bottom: 80, + right: 16, + child: Column( + children: [ + // Layer selector button + const LayerSelectorButton(), + const SizedBox(height: 8), + // Zoom in button + FloatingActionButton( + mini: true, + heroTag: "zoom_in", + onPressed: () { + final zoom = mapController.camera.zoom; + mapController.move(mapController.camera.center, zoom + 1); + }, + child: const Icon(Icons.add), + ), + const SizedBox(height: 8), + // Zoom out button + FloatingActionButton( + mini: true, + heroTag: "zoom_out", + onPressed: () { + final zoom = mapController.camera.zoom; + mapController.move(mapController.camera.center, zoom - 1); + }, + child: const Icon(Icons.remove), + ), + ], + ), ), // Fixed pin when adding camera diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 09cc2fc..ccfbd34 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -347,6 +347,7 @@ class MapViewState extends State { mapController: _controller, uploadMode: appState.uploadMode, session: session, + attribution: appState.selectedTileType?.attribution, ), // Network status indicator (top-left)