import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:latlong2/latlong.dart';
import 'package:map_app/models/map_state.dart';
import 'package:map_app/models/poem_item.dart';
import 'package:map_app/providers/bgm_player_provider.dart';
import 'package:map_app/providers/can_get_item_provider.dart';
import 'package:map_app/providers/focus_item_provider.dart';
import 'package:map_app/providers/map_state_provider.dart';
import 'package:map_app/providers/poem_items_provider.dart';
import 'package:map_app/providers/poem_player_provider.dart';
import 'package:map_app/utils/firebase_utils.dart';
import 'package:map_app/utils/vibration_utils.dart';
import 'package:map_app/views/luggage_view.dart';
import 'package:map_app/views/marker_view.dart';
import 'package:map_app/views/poem_sheet_view.dart';
import 'package:particles_flutter/particles_flutter.dart';
class MapPage extends HookConsumerWidget {
const MapPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref
.watch(bgmPlayerNotifierProvider(BgmType.map).notifier)
.playAsBGM(volume: FirebaseUtils.bgmLevel);
return const PopScope(
canPop: false,
child: Scaffold(
body: Stack(children: [
_CustomMapFilter(child: _MapView()),
Snow(),
]),
),
);
}
}
const defaultZoom = 17.0;
class _MapView extends HookConsumerWidget {
const _MapView();
@override
Widget build(BuildContext context, WidgetRef ref) {
useEffect(() {
VibrationChecker().startCheck(ref);
return;
}, []);
ref.listen(poemPlayerNotifierProvider, (prev, next) {
if (prev == null && next != null) {
showDialog(
context: context,
barrierDismissible: false,
useSafeArea: false,
builder: (BuildContext context) => Dialog.fullscreen(
child: const PoemSheetView(),
backgroundColor: MediaQuery.of(context).orientation ==
Orientation.landscape
? Colors.white
: Colors.transparent,
));
}
// playerで読んでいる詩の位置にマップを連動させる
if (next != null && prev != null) {
ref
.read(mapStateNotifierProvider.notifier)
.moveCenterTo(next.toLatLng(), zoom: defaultZoom);
}
});
return const Stack(
children: [
_MapArea(),
Align(
alignment: Alignment.bottomLeft,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
BgmButton(),
LuggageButton(),
],
)),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CompassButton(),
NaviModeButton(),
],
)),
),
],
);
}
}
class _MapArea extends ConsumerWidget {
const _MapArea();
@override
Widget build(BuildContext context, WidgetRef ref) {
final mapState = ref.watch(mapStateNotifierProvider);
final notifier = ref.watch(mapStateNotifierProvider.notifier);
return FlutterMap(
mapController: ref.watch(mapControllerProvider),
options: MapOptions(
backgroundColor: Colors.transparent,
keepAlive: true,
initialCenter: const LatLng(32.74472, 129.87361),
initialZoom: defaultZoom,
maxZoom: 19,
minZoom: 4,
onPointerDown: (tapPosition, point) {
notifier.changeNaviMode(next: false);
},
onMapEvent: (_) {
notifier.onMapRotate();
},
onMapReady: notifier.onMapRotate,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
),
PolylineLayer(
polylines: _lines(mapState, ref),
),
CircleLayer(
circles: [
if (mapState.currentPlace != null)
CircleMarker(
point: mapState.currentPlace!,
radius: needMeter,
useRadiusInMeter: true,
color: Colors.blue.withOpacity(0.4),
borderColor: Colors.black87,
),
],
),
MarkerLayer(
rotate: true,
markers: [
if (mapState.currentPlace != null)
Marker(
alignment: Alignment.center,
width: 50,
height: 50,
point: mapState.currentPlace!,
child: const CurrentMarkerView(),
),
..._poemMarkers(
mapState,
ref,
),
],
),
],
);
}
List _lines(MapState mapState, WidgetRef ref) {
if (!mapState.hasCurrentPlace) {
return [];
}
return ref.watch(poemItemsNotifierProvider).list.map((poemItem) {
final color = ref.watch(poemItemsNotifierProvider).isGot(poemItem.id)
? Colors.green
: Colors.red;
final inFocus = ref.watch(focusItemNotifierProvider) == null ||
ref.watch(focusItemNotifierProvider) == poemItem.id;
return Polyline(
points: [
mapState.currentPlace!,
poemItem.toLatLng(),
],
strokeWidth: 0.5,
gradientColors: [
color.withOpacity(0.5).withOpacity(inFocus ? 1.0 : 0.3),
color.withOpacity(inFocus ? 1.0 : 0.1)
],
);
}).toList();
}
List _poemMarkers(MapState mapState, WidgetRef ref) {
return ref
.watch(poemItemsNotifierProvider)
.list
.map((item) => Marker(
alignment: Alignment.topCenter,
point: item.toLatLng(),
child: PoemMarkerView(item: item),
))
.toList();
}
}
class _CustomMapFilter extends StatelessWidget {
const _CustomMapFilter({
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.blueAccent.withOpacity(0.1),
BlendMode.plus,
),
child: child);
}
}
class CompassButton extends ConsumerWidget {
const CompassButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final mapHeading =
ref.watch(mapStateNotifierProvider.select((state) => state.mapHeading));
final notifier = ref.watch(mapStateNotifierProvider.notifier);
return FilledButton.tonal(
child: Transform.rotate(
angle: mapHeading * pi / 180,
child: Icon(
Icons.assistant_navigation,
color: mapHeading == 0.0 ? Colors.blue : Colors.black54,
)),
onPressed: () {
notifier
..changeNaviMode(next: false)
..resetMapHeading();
},
);
}
}
class NaviModeButton extends ConsumerWidget {
const NaviModeButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final naviMode =
ref.watch(mapStateNotifierProvider.select((state) => state.naviMode));
final notifier = ref.watch(mapStateNotifierProvider.notifier);
final canGet =
ref.watch(canGetItemListProvider.select((value) => value.length != 0));
return FilledButton.tonal(
onPressed: () => notifier.changeNaviMode(next: !naviMode),
child: NaviModeButtonIcon(
naviMode: naviMode,
canGet: canGet,
),
);
;
}
}
class NaviModeButtonIcon extends HookWidget {
const NaviModeButtonIcon(
{super.key, required this.naviMode, required this.canGet});
final bool naviMode;
final bool canGet;
@override
Widget build(BuildContext context) {
final animationController = useAnimationController(
duration: const Duration(milliseconds: 300),
);
useAnimation(animationController);
useEffect(() {
animationController.repeat();
return () {};
}, const []);
return Icon(
Icons.my_location_rounded,
color: naviMode
? Colors.blue
: ((canGet && animationController.value > 0.5)
? Colors.red
: Colors.black54),
);
}
}
class BgmButton extends ConsumerWidget {
const BgmButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final playerState = ref.watch(bgmPlayerNotifierProvider(BgmType.map));
final notifier = ref.watch(bgmPlayerNotifierProvider(BgmType.map).notifier);
return FilledButton.tonal(
child: Icon(
playerState == PlayerState.playing ? Icons.volume_up : Icons.volume_off,
color: Colors.black54,
),
onPressed: () async {
playerState == PlayerState.playing
? await notifier.pause()
: await notifier.resume();
},
);
}
}
class LuggageButton extends ConsumerWidget {
const LuggageButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return FilledButton.tonal(
child: const Icon(
Icons.luggage,
color: Colors.black54,
),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => const LuggagePage()));
});
}
}
class Snow extends ConsumerWidget {
const Snow({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final particleColor =
ref.watch(canGetItemListProvider.select((value) => value.isEmpty)