import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../providers/sake_list_provider.dart'; import '../../widgets/map/prefecture_tile_map.dart'; import '../../theme/app_theme.dart'; import '../../models/maps/japan_map_data.dart'; class BreweryMapScreen extends ConsumerStatefulWidget { const BreweryMapScreen({super.key}); @override ConsumerState createState() => _BreweryMapScreenState(); } class _BreweryMapScreenState extends ConsumerState { final TransformationController _transformationController = TransformationController(); bool _isMapInitialized = false; @override void dispose() { _transformationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final sakeListAsync = ref.watch(sakeListProvider); return sakeListAsync.when( data: (sakeList) { // Extract visited prefectures final visitedPrefectures = sakeList .map((s) => s.displayData.prefecture) .where((p) => p.isNotEmpty) .where((p) => p != '不明' && p != '海外') .map((p) => p) .toSet(); final totalPrefs = 47; final visitedCount = visitedPrefectures.length; final progress = visitedCount / totalPrefs; return Scaffold( appBar: AppBar( title: const Text('酒蔵マップ'), centerTitle: true, ), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 12), // 1. Stats Card (Compact) Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: _buildStatsCard(context, progress, visitedCount), ), const SizedBox(height: 8), // 2. Legend Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, // Left align children: [ _buildLegendDot(Colors.grey[300]!, '未開拓'), const SizedBox(width: 12), _buildLegendDot(AppTheme.posimaiBlue, '制覇済'), ], ), ), const SizedBox(height: 4), // 3. Map (Maximized & Interactive) // Use LayoutBuilder to determine available width and set initial scale SizedBox( height: 420, // Increased to 420 to prevent Okinawa from being cut off child: LayoutBuilder( builder: (context, constraints) { // Map logical width is approx 13 cols * (46 + 4) = 650 const double mapWidth = 650.0; // Calculate scale to fit width (95% to allow slight margin) final availableWidth = constraints.maxWidth; final fitScale = (availableWidth / mapWidth) * 0.95; if (!_isMapInitialized) { // Center horizontally final xOffset = (availableWidth - (mapWidth * fitScale)) / 2; final matrix = Matrix4.identity() ..translate(xOffset, 10.0) ..scale(fitScale); _transformationController.value = matrix; _isMapInitialized = true; } return Stack( children: [ InteractiveViewer( transformationController: _transformationController, // Large boundary margin to allow panning even at min scale boundaryMargin: const EdgeInsets.all(500), // Allow zooming out strictly to the fit size (with 5% margin for gesture safety) minScale: fitScale * 0.95, maxScale: fitScale * 6.0, constrained: false, child: PrefectureTileMap( visitedPrefectures: visitedPrefectures, onPrefectureTap: (pref) { _showPrefectureStats(context, pref, sakeList); }, ), ), Positioned( bottom: 16, right: 16, child: FloatingActionButton.small( heroTag: 'map_reset', backgroundColor: Colors.white.withValues(alpha: 0.9), foregroundColor: AppTheme.posimaiBlue, elevation: 2, onPressed: () { // Reset to initial state (Fit Width) final xOffset = (availableWidth - (mapWidth * fitScale)) / 2; final matrix = Matrix4.identity() ..translate(xOffset, 10.0) ..scale(fitScale); _transformationController.value = matrix; }, child: const Icon(LucideIcons.rotateCcw, size: 20), ), ), ], ); } ), ), const SizedBox(height: 12), // 4. Regional Status Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('地域別制覇状況', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 12), _buildRegionalStatusGrid(context, visitedPrefectures, sakeList), ], ), ), const SizedBox(height: 40), ], ), ), ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Center(child: Text('エラーが発生しました: $err')), ); } // Helper to show prefecture stats toast/snackbar void _showPrefectureStats(BuildContext context, String pref, List sakeList) { final count = sakeList.where((s) => s.displayData.prefecture?.contains(pref) ?? false).length; ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$pref: $count本 記録済み'), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ); } Widget _buildStatsCard(BuildContext context, double progress, int visitedCount) { return Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), // Compact padding decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( children: [ Text('制覇率', style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: 4), Text( '${(progress * 100).toStringAsFixed(1)}%', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: AppTheme.posimaiBlue), ), ], ), Container(width: 1, height: 30, color: Colors.grey[200]), Column( children: [ Text('制覇数', style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: 4), RichText( text: TextSpan( children: [ TextSpan( text: '$visitedCount', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).textTheme.bodyLarge?.color), ), TextSpan(text: ' / 47', style: TextStyle(fontSize: 14, color: Colors.grey[500])), ], ), ), ], ), ], ), ); } Widget _buildRegionalStatusGrid(BuildContext context, Set visitedPrefectures, List sakeList) { // Group logic could be moved to JapanMapData helper if complex, but simple loop works here final regions = JapanMapData.regionNames.keys.toList(); return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, // Reverting to 2 columns as requested to avoid overflow childAspectRatio: 2.5, // Wider aspect ratio for 2 columns crossAxisSpacing: 12, mainAxisSpacing: 12, ), itemCount: regions.length, itemBuilder: (context, index) { final regionId = regions[index]; final regionName = JapanMapData.regionNames[regionId] ?? ''; final prefsInRegion = JapanMapData.prefectureNames.entries .where((e) => JapanMapData.getRegionId(e.key) == regionId) .map((e) { // Fix: Don't strip '道' from '北海道'. Only strip suffixes from others. if (e.value == '北海道') return '北海道'; return e.value.replaceAll(RegExp(r'(都|府|県)$'), ''); }) .toList(); final totalInRegion = prefsInRegion.length; final visitedInRegion = prefsInRegion.where((p) => visitedPrefectures.any((vp) => vp.startsWith(p))).length; final isComplete = visitedInRegion == totalInRegion && totalInRegion > 0; return _buildRegionCard(context, regionName, visitedInRegion, totalInRegion, isComplete, () { _showRegionDetailDialog(context, regionName, prefsInRegion, sakeList); }); }, ); } void _showRegionDetailDialog(BuildContext context, String regionName, List prefs, List sakeList) { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return DraggableScrollableSheet( initialChildSize: 0.5, minChildSize: 0.3, maxChildSize: 0.8, expand: false, builder: (context, scrollController) { return Column( children: [ const SizedBox(height: 12), Container(width: 40, height: 4, decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2))), const SizedBox(height: 16), Text('$regionNameの制覇状況', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), Expanded( child: ListView.separated( controller: scrollController, itemCount: prefs.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final pref = prefs[index]; // Find count final count = sakeList.where((s) => s.displayData.prefecture?.contains(pref) ?? false).length; final isConquered = count > 0; return ListTile( leading: Icon( isConquered ? LucideIcons.checkCircle2 : LucideIcons.circle, color: isConquered ? AppTheme.posimaiBlue : Colors.grey[300], ), title: Text(pref), trailing: Text( '$count本', style: TextStyle( fontWeight: isConquered ? FontWeight.bold : FontWeight.normal, color: isConquered ? AppTheme.posimaiBlue : Colors.grey ) ), ); }, ), ), ], ); }, ); }, ); } Widget _buildRegionCard(BuildContext context, String name, int current, int total, bool isComplete, VoidCallback onTap) { final color = isComplete ? AppTheme.posimaiBlue : Theme.of(context).cardColor; final textColor = isComplete ? Colors.white : Theme.of(context).textTheme.bodyLarge?.color; final subTextColor = isComplete ? Colors.white.withValues(alpha: 0.8) : Colors.grey[600]; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), // Tighter padding decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(12), border: isComplete ? null : Border.all(color: Colors.grey[200]!), // Lighter border boxShadow: [ if(!isComplete) BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0,2)) ] ), child: Column( // changed to column for 3-grid layout mainAxisAlignment: MainAxisAlignment.center, children: [ Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: textColor)), const SizedBox(height: 2), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text('$current', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor)), Text('/$total', style: TextStyle(fontSize: 11, color: subTextColor)), ], ) ], ), ), ); } Widget _buildLegendDot(Color color, String label) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container(width: 10, height: 10, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(2))), const SizedBox(width: 4), Text(label, style: const TextStyle(fontSize: 11, color: Colors.grey)), ], ); } }