diff --git a/lib/models/timeline.dart b/lib/models/timeline.dart new file mode 100644 index 0000000..7bee85b --- /dev/null +++ b/lib/models/timeline.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:share_location/foreign_types/memory.dart'; +import 'package:share_location/models/memory_pack.dart'; + +class TimelineModel extends ChangeNotifier { + final Map _timeline; + + TimelineModel({ + Map? timeline, + }) : _timeline = timeline ?? {}; + + Map get values => _timeline; + + static TimelineModel fromMemoriesList( + final List memories, + ) { + final map = >{}; + + for (final memory in memories) { + final date = DateFormat('yyyy-MM-dd').format(memory.creationDate); + if (map.containsKey(date)) { + map[date]!.add(memory); + } else { + map[date] = [memory]; + } + } + + final data = Map.fromEntries( + map.entries.map( + (entry) => MapEntry( + entry.key, + MemoryPack(entry.value), + ), + ), + ); + + return TimelineModel( + timeline: data, + ); + } + + @override + void dispose() { + _timeline.values.forEach((memoryPack) { + memoryPack.dispose(); + }); + + super.dispose(); + } + + void removeEmptyDates() { + final previousLength = _timeline.length; + + _timeline.removeWhere((key, value) => value.memories.isEmpty); + + final newLength = _timeline.length; + + if (previousLength != newLength) { + notifyListeners(); + } + } + + DateTime dateAtIndex(final int index) => + DateTime.parse(_timeline.keys.elementAt(index)); + + MemoryPack atIndex(final int index) => _timeline.values.elementAt(index); +} diff --git a/lib/widgets/memory_slide.dart b/lib/widgets/memory_slide.dart index 0dffa98..7677005 100644 --- a/lib/widgets/memory_slide.dart +++ b/lib/widgets/memory_slide.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:share_location/controllers/status_controller.dart'; import 'package:share_location/enums.dart'; import 'package:share_location/foreign_types/memory.dart'; +import 'package:share_location/models/memory_pack.dart'; import 'package:share_location/models/timeline_overlay.dart'; import 'package:share_location/widgets/status.dart'; @@ -59,6 +60,7 @@ class _MemorySlideState extends State void initializeAnimation(final Duration duration) { final timelineOverlay = context.read(); + final memoryPack = context.read(); this.duration = duration; @@ -72,7 +74,8 @@ class _MemorySlideState extends State } if (controller!.done) { - timelineOverlay.setState(TimelineState.completed); + timelineOverlay.reset(); + memoryPack.next(); } }, ['done']); @@ -81,10 +84,10 @@ class _MemorySlideState extends State @override Widget build(BuildContext context) { - return Consumer( - builder: (context, overlayController, _) => Status( + return Consumer( + builder: (context, memoryPack, _) => Status( controller: controller, - disabled: !overlayController.showOverlay, + disabled: memoryPack.paused, child: MemoryView( creationDate: widget.memory.creationDate, location: widget.memory.location, @@ -99,17 +102,17 @@ class _MemorySlideState extends State if (mounted) { initializeAnimation(controller.value.duration); - overlayController.addListener(() { + memoryPack.addListener(() { if (!mounted) { return; } - if (overlayController.state == TimelineState.playing) { - controller.play(); - } else { + if (memoryPack.paused) { controller.pause(); + } else { + controller.play(); } - }, ['state']); + }, ['paused']); } }, ), diff --git a/lib/widgets/timeline_page.dart b/lib/widgets/timeline_page.dart index 6d31267..2995e6e 100644 --- a/lib/widgets/timeline_page.dart +++ b/lib/widgets/timeline_page.dart @@ -13,12 +13,14 @@ class TimelinePage extends StatefulWidget { final DateTime date; final VoidCallback onPreviousTimeline; final VoidCallback onNextTimeline; + final VoidCallback onMemoryRemoved; const TimelinePage({ Key? key, required this.date, required this.onPreviousTimeline, required this.onNextTimeline, + required this.onMemoryRemoved, }) : super(key: key); @override @@ -48,6 +50,18 @@ class _TimelinePageState extends State { } }, ['state']); + memoryPack.addListener(() { + if (!mounted) { + return; + } + + if (memoryPack.paused) { + timelineOverlayController.hideOverlay(); + } else { + timelineOverlayController.restoreOverlay(); + } + }, ['state']); + memoryPack.addListener(() { if (!mounted) { return; @@ -81,7 +95,6 @@ class _TimelinePageState extends State { final memoryPack = context.read(); memoryPack.pause(); - timelineOverlayController.hideOverlay(); await showModalBottomSheet( context: context, @@ -98,16 +111,15 @@ class _TimelinePageState extends State { memoryPack.removeCurrentMemory(); memoryPack.resume(); - timelineOverlayController.restoreOverlay(); + + widget.onMemoryRemoved(); }, onTapDown: (_) { final memoryPack = context.read(); - memoryPack.pause(); - overlayRemover = Timer( const Duration(milliseconds: 200), - timelineOverlayController.hideOverlay, + memoryPack.pause, ); }, onTapUp: (_) { @@ -115,14 +127,11 @@ class _TimelinePageState extends State { overlayRemover?.cancel(); memoryPack.resume(); - timelineOverlayController.restoreOverlay(); }, onTapCancel: () { final memoryPack = context.read(); overlayRemover?.cancel(); - - timelineOverlayController.restoreOverlay(); memoryPack.resume(); }, onHorizontalDragEnd: (details) { @@ -146,34 +155,30 @@ class _TimelinePageState extends State { }, child: ChangeNotifierProvider( create: (_) => timelineOverlayController, - child: Stack( - fit: StackFit.expand, - children: [ - Consumer( - builder: (_, memoryPack, __) { - return PageView.builder( - controller: pageController, - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - itemBuilder: (_, index) => MemorySlide( - key: Key(memoryPack.memories[index].filename), - memory: memoryPack.memories[index], - ), - itemCount: memoryPack.memories.length, - ); - }, - ), - Padding( - padding: const EdgeInsets.only( - top: LARGE_SPACE, - left: MEDIUM_SPACE, - right: MEDIUM_SPACE, + child: Consumer( + builder: (_, memoryPack, __) => Stack( + fit: StackFit.expand, + children: [ + PageView.builder( + controller: pageController, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemBuilder: (_, index) => MemorySlide( + key: Key(memoryPack.memories[index].filename), + memory: memoryPack.memories[index], + ), + itemCount: memoryPack.memories.length, ), - child: Consumer( - builder: (context, overlayController, _) => AnimatedOpacity( + Padding( + padding: const EdgeInsets.only( + top: LARGE_SPACE, + left: MEDIUM_SPACE, + right: MEDIUM_SPACE, + ), + child: AnimatedOpacity( duration: const Duration(milliseconds: 500), curve: Curves.linearToEaseOut, - opacity: overlayController.showOverlay ? 1.0 : 0.0, + opacity: memoryPack.paused ? 0.0 : 1.0, child: Text( DateFormat('dd. MMMM yyyy').format(widget.date), textAlign: TextAlign.center, @@ -181,8 +186,26 @@ class _TimelinePageState extends State { ), ), ), - ), - ], + Positioned( + right: SMALL_SPACE, + bottom: SMALL_SPACE * 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: SMALL_SPACE), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 500), + curve: Curves.linearToEaseOut, + opacity: memoryPack.paused ? 0.0 : 1.0, + child: Consumer( + builder: (_, memoryPack, __) => Text( + '${memoryPack.currentMemoryIndex + 1}/${memoryPack.memories.length}', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/widgets/timeline_scroll.dart b/lib/widgets/timeline_scroll.dart index a8fd996..1b4a7b4 100644 --- a/lib/widgets/timeline_scroll.dart +++ b/lib/widgets/timeline_scroll.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:share_location/foreign_types/memory.dart'; -import 'package:share_location/models/memory_pack.dart'; +import 'package:share_location/models/timeline.dart'; import 'package:share_location/utils/loadable.dart'; import 'package:share_location/widgets/timeline_page.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -20,7 +19,7 @@ class TimelineScroll extends StatefulWidget { class _TimelineScrollState extends State with Loadable { final pageController = PageController(); - Map? timeline; + TimelineModel? timeline; @override initState() { @@ -28,7 +27,18 @@ class _TimelineScrollState extends State with Loadable { loadTimeline(); } + @override + dispose() { + pageController.dispose(); + + timeline?.dispose(); + + super.dispose(); + } + Future loadTimeline() async { + timeline?.dispose(); + final response = await supabase .from('memories') .select() @@ -36,35 +46,16 @@ class _TimelineScrollState extends State with Loadable { .execute(); final memories = List.from( List>.from(response.data).map(Memory.parse)); - final timelineMapped = convertMemoriesToTimeline(memories); setState(() { - timeline = timelineMapped; + timeline = TimelineModel.fromMemoriesList(memories); }); - } - static Map convertMemoriesToTimeline( - final List memories, - ) { - final map = >{}; - - for (final memory in memories) { - final date = DateFormat('yyyy-MM-dd').format(memory.creationDate); - if (map.containsKey(date)) { - map[date]!.add(memory); - } else { - map[date] = [memory]; + timeline!.addListener(() { + if (mounted) { + setState(() {}); } - } - - return Map.fromEntries( - map.entries.map( - (entry) => MapEntry( - entry.key, - MemoryPack(entry.value), - ), - ), - ); + }); } @override @@ -79,11 +70,12 @@ class _TimelineScrollState extends State with Loadable { body: PageView.builder( controller: pageController, scrollDirection: Axis.vertical, - itemCount: timeline!.length, - itemBuilder: (_, index) => ChangeNotifierProvider( - create: (_) => timeline!.values.elementAt(index), + itemCount: timeline!.values.length, + itemBuilder: (_, index) => ChangeNotifierProvider.value( + value: timeline!.atIndex(index), child: TimelinePage( - date: DateTime.parse(timeline!.keys.toList()[index]), + date: timeline!.dateAtIndex(index), + onMemoryRemoved: () => timeline!.removeEmptyDates(), onNextTimeline: () { pageController.nextPage( duration: const Duration(milliseconds: 500),