diff --git a/lib/controllers/memory_slide_controller.dart b/lib/controllers/memory_slide_controller.dart index 6138ba5..3dc75a3 100644 --- a/lib/controllers/memory_slide_controller.dart +++ b/lib/controllers/memory_slide_controller.dart @@ -19,6 +19,8 @@ class MemorySlideController extends PropertyChangeNotifier { int get index => _index; bool get completed => _completed; + bool get isLast => _index == memoryLength - 1; + void setPaused(bool paused) { _paused = paused; notifyListeners('paused'); @@ -30,7 +32,7 @@ class MemorySlideController extends PropertyChangeNotifier { } void next() { - if (_index == memoryLength - 1) { + if (isLast) { _completed = true; notifyListeners('completed'); } else { diff --git a/lib/managers/file_manager.dart b/lib/managers/file_manager.dart index c913cbd..e368cd3 100644 --- a/lib/managers/file_manager.dart +++ b/lib/managers/file_manager.dart @@ -95,4 +95,20 @@ class FileManager { return data; } + + static Future deleteFile(final String path) async { + final response = + await supabase.from('memories').delete().eq('location', path).execute(); + + if (response.error != null) { + throw Exception('Error deleting file: ${response.error!.message}'); + } + + final storageResponse = + await supabase.storage.from('memories').remove([path]); + + if (storageResponse.error != null) { + throw Exception('Error deleting file: ${storageResponse.error!.message}'); + } + } } diff --git a/lib/models/memory_pack.dart b/lib/models/memory_pack.dart new file mode 100644 index 0000000..48d53a5 --- /dev/null +++ b/lib/models/memory_pack.dart @@ -0,0 +1,61 @@ +import 'dart:math'; + +import 'package:property_change_notifier/property_change_notifier.dart'; +import 'package:share_location/foreign_types/memory.dart'; + +class MemoryPack extends PropertyChangeNotifier { + final List _memories; + int _currentMemoryIndex = 0; + bool _paused = false; + bool _completed = false; + + MemoryPack(this._memories); + + List get memories => [..._memories]; + int get currentMemoryIndex => _currentMemoryIndex; + Memory get currentMemory => _memories[_currentMemoryIndex]; + bool get paused => _paused; + bool get completed => _completed; + bool get isLast => _currentMemoryIndex == _memories.length - 1; + + void setPaused(bool paused) { + _paused = paused; + notifyListeners('paused'); + } + + void next() { + if (isLast) { + _completed = true; + notifyListeners('completed'); + } else { + _paused = false; + _completed = false; + _currentMemoryIndex++; + notifyListeners(); + } + } + + void previous() { + _currentMemoryIndex = max(_currentMemoryIndex - 1, 0); + _paused = false; + _completed = false; + notifyListeners(); + } + + void reset() { + _completed = false; + _paused = false; + _currentMemoryIndex = 0; + notifyListeners(); + } + + void removeMemory(int index) { + _memories.removeAt(index); + notifyListeners(); + } + + void removeCurrentMemory() => removeMemory(_currentMemoryIndex); + + void pause() => setPaused(true); + void resume() => setPaused(false); +} diff --git a/lib/models/timeline_overlay.dart b/lib/models/timeline_overlay.dart index 10b6500..0a1ba94 100644 --- a/lib/models/timeline_overlay.dart +++ b/lib/models/timeline_overlay.dart @@ -1,15 +1,35 @@ -import 'package:flutter/material.dart'; +import 'package:property_change_notifier/property_change_notifier.dart'; -class TimelineOverlay extends ChangeNotifier { +enum TimelineState { + loading, + paused, + playing, + completed, +} + +class TimelineOverlay extends PropertyChangeNotifier { bool _showOverlay = true; + TimelineState _state = TimelineState.loading; bool get showOverlay => _showOverlay; + TimelineState get state => _state; void hideOverlay() => setShowOverlay(false); void restoreOverlay() => setShowOverlay(true); void setShowOverlay(bool showOverlay) { _showOverlay = showOverlay; + notifyListeners('showOverlay'); + } + + void setState(TimelineState state) { + _state = state; + notifyListeners('state'); + } + + void reset() { + _showOverlay = true; + _state = TimelineState.loading; notifyListeners(); } } diff --git a/lib/widgets/memory_sheet.dart b/lib/widgets/memory_sheet.dart new file mode 100644 index 0000000..cac48e7 --- /dev/null +++ b/lib/widgets/memory_sheet.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:share_location/constants/spacing.dart'; +import 'package:share_location/foreign_types/memory.dart'; +import 'package:share_location/managers/file_manager.dart'; +import 'package:share_location/utils/loadable.dart'; +import 'package:share_location/widgets/modal_sheet.dart'; + +class MemorySheet extends StatefulWidget { + final Memory memory; + + const MemorySheet({ + Key? key, + required this.memory, + }) : super(key: key); + + @override + State createState() => _MemorySheetState(); +} + +class _MemorySheetState extends State with Loadable { + Future deleteFile() => FileManager.deleteFile(widget.memory.location); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return ModalSheet( + child: Column( + children: [ + Text( + 'Edit Memory', + style: theme.textTheme.headline1, + ), + const SizedBox(height: MEDIUM_SPACE), + ListTile( + leading: Icon(Icons.delete_forever_sharp), + title: Text('Delete Memory'), + onTap: isLoading + ? null + : () async { + await callWithLoading(deleteFile); + + if (mounted) { + Navigator.pop(context); + } + }, + trailing: isLoading ? const CircularProgressIndicator() : null, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/memory_slide.dart b/lib/widgets/memory_slide.dart index 1413cc0..0dffa98 100644 --- a/lib/widgets/memory_slide.dart +++ b/lib/widgets/memory_slide.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:share_location/controllers/memory_slide_controller.dart'; import 'package:share_location/controllers/status_controller.dart'; import 'package:share_location/enums.dart'; import 'package:share_location/foreign_types/memory.dart'; @@ -14,12 +13,10 @@ const DEFAULT_IMAGE_DURATION = Duration(seconds: 5); class MemorySlide extends StatefulWidget { final Memory memory; - final MemorySlideController controller; const MemorySlide({ Key? key, required this.memory, - required this.controller, }) : super(key: key); @override @@ -36,15 +33,19 @@ class _MemorySlideState extends State void initState() { super.initState(); - widget.controller.addListener(() { + final timelineOverlay = context.read(); + + timelineOverlay.addListener(() { if (!mounted) { return; } - if (widget.controller.paused) { - controller?.stop(); - } else { - controller?.start(); + switch (timelineOverlay.state) { + case TimelineState.playing: + controller?.start(); + break; + default: + controller?.stop(); } }); } @@ -57,11 +58,23 @@ class _MemorySlideState extends State } void initializeAnimation(final Duration duration) { + final timelineOverlay = context.read(); + this.duration = duration; controller = StatusController( duration: duration, - )..addListener(widget.controller.setDone, ['done']); + ); + + controller!.addListener(() { + if (!mounted) { + return; + } + + if (controller!.done) { + timelineOverlay.setState(TimelineState.completed); + } + }, ['done']); setState(() {}); } @@ -86,17 +99,17 @@ class _MemorySlideState extends State if (mounted) { initializeAnimation(controller.value.duration); - widget.controller.addListener(() { + overlayController.addListener(() { if (!mounted) { return; } - if (widget.controller.paused) { - controller.pause(); - } else { + if (overlayController.state == TimelineState.playing) { controller.play(); + } else { + controller.pause(); } - }); + }, ['state']); } }, ), diff --git a/lib/widgets/modal_sheet.dart b/lib/widgets/modal_sheet.dart new file mode 100644 index 0000000..dbf2626 --- /dev/null +++ b/lib/widgets/modal_sheet.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:share_location/constants/spacing.dart'; + +class ModalSheet extends StatelessWidget { + final Widget child; + + const ModalSheet({ + Key? key, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(MEDIUM_SPACE), + decoration: BoxDecoration( + color: theme.bottomSheetTheme.modalBackgroundColor ?? + theme.bottomAppBarColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), + ), + ), + child: child, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/timeline_page.dart b/lib/widgets/timeline_page.dart index a065feb..6d31267 100644 --- a/lib/widgets/timeline_page.dart +++ b/lib/widgets/timeline_page.dart @@ -4,21 +4,19 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:share_location/constants/spacing.dart'; -import 'package:share_location/controllers/memory_slide_controller.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/memory_sheet.dart'; import 'package:share_location/widgets/memory_slide.dart'; class TimelinePage extends StatefulWidget { final DateTime date; - final List memories; final VoidCallback onPreviousTimeline; final VoidCallback onNextTimeline; const TimelinePage({ Key? key, required this.date, - required this.memories, required this.onPreviousTimeline, required this.onNextTimeline, }) : super(key: key); @@ -30,7 +28,6 @@ class TimelinePage extends StatefulWidget { class _TimelinePageState extends State { final timelineOverlayController = TimelineOverlay(); final pageController = PageController(); - late final MemorySlideController controller; Timer? overlayRemover; @@ -38,37 +35,75 @@ class _TimelinePageState extends State { void initState() { super.initState(); - controller = MemorySlideController(memoryLength: widget.memories.length); - controller.addListener(() { - if (controller.done) { - controller.next(); + final memoryPack = context.read(); - pageController.nextPage( - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - ); + timelineOverlayController.addListener(() { + if (!mounted) { + return; } - }, ['done']); - controller.addListener(() { - if (controller.completed) { + + if (timelineOverlayController.state == TimelineState.completed) { + timelineOverlayController.reset(); + memoryPack.next(); + } + }, ['state']); + + memoryPack.addListener(() { + if (!mounted) { + return; + } + + if (memoryPack.completed) { widget.onNextTimeline(); + memoryPack.reset(); } }, ['completed']); - } - @override - void dispose() { - controller.dispose(); - timelineOverlayController.dispose(); + memoryPack.addListener(() { + if (!mounted) { + return; + } - super.dispose(); + if (memoryPack.currentMemoryIndex != pageController.page) { + pageController.animateToPage( + memoryPack.currentMemoryIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutQuad, + ); + } + }); } @override Widget build(BuildContext context) { return GestureDetector( + onDoubleTap: () async { + final memoryPack = context.read(); + + memoryPack.pause(); + timelineOverlayController.hideOverlay(); + + await showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (_) => MemorySheet( + memory: memoryPack.currentMemory, + ), + ); + + if (!mounted) { + return; + } + + memoryPack.removeCurrentMemory(); + memoryPack.resume(); + timelineOverlayController.restoreOverlay(); + }, onTapDown: (_) { - controller.setPaused(true); + final memoryPack = context.read(); + + memoryPack.pause(); overlayRemover = Timer( const Duration(milliseconds: 200), @@ -76,28 +111,32 @@ class _TimelinePageState extends State { ); }, onTapUp: (_) { + final memoryPack = context.read(); + overlayRemover?.cancel(); - - controller.setPaused(false); - + memoryPack.resume(); timelineOverlayController.restoreOverlay(); }, onTapCancel: () { + final memoryPack = context.read(); + overlayRemover?.cancel(); timelineOverlayController.restoreOverlay(); - controller.setPaused(false); + memoryPack.resume(); }, onHorizontalDragEnd: (details) { + final memoryPack = context.read(); + if (details.primaryVelocity! < 0) { - controller.next(); + memoryPack.next(); pageController.nextPage( duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut, ); } else if (details.primaryVelocity! > 0) { - controller.previous(); + memoryPack.previous(); pageController.previousPage( duration: const Duration(milliseconds: 200), @@ -110,16 +149,19 @@ class _TimelinePageState extends State { child: Stack( fit: StackFit.expand, children: [ - PageView.builder( - controller: pageController, - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - itemBuilder: (_, __) => MemorySlide( - key: Key(controller.index.toString()), - controller: controller, - memory: widget.memories[controller.index], - ), - itemCount: widget.memories.length, + 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( diff --git a/lib/widgets/timeline_scroll.dart b/lib/widgets/timeline_scroll.dart index 4016ae9..a8fd996 100644 --- a/lib/widgets/timeline_scroll.dart +++ b/lib/widgets/timeline_scroll.dart @@ -1,6 +1,8 @@ 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/utils/loadable.dart'; import 'package:share_location/widgets/timeline_page.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -18,7 +20,7 @@ class TimelineScroll extends StatefulWidget { class _TimelineScrollState extends State with Loadable { final pageController = PageController(); - dynamic timeline; + Map? timeline; @override initState() { @@ -41,7 +43,7 @@ class _TimelineScrollState extends State with Loadable { }); } - static Map> convertMemoriesToTimeline( + static Map convertMemoriesToTimeline( final List memories, ) { final map = >{}; @@ -55,7 +57,14 @@ class _TimelineScrollState extends State with Loadable { } } - return map; + return Map.fromEntries( + map.entries.map( + (entry) => MapEntry( + entry.key, + MemoryPack(entry.value), + ), + ), + ); } @override @@ -70,22 +79,24 @@ class _TimelineScrollState extends State with Loadable { body: PageView.builder( controller: pageController, scrollDirection: Axis.vertical, - itemCount: timeline.length, - itemBuilder: (_, index) => TimelinePage( - date: DateTime.parse(timeline.keys.toList()[index]), - memories: timeline.values.toList()[index], - onNextTimeline: () { - pageController.nextPage( - duration: const Duration(milliseconds: 500), - curve: Curves.ease, - ); - }, - onPreviousTimeline: () { - pageController.previousPage( - duration: const Duration(milliseconds: 500), - curve: Curves.ease, - ); - }, + itemCount: timeline!.length, + itemBuilder: (_, index) => ChangeNotifierProvider( + create: (_) => timeline!.values.elementAt(index), + child: TimelinePage( + date: DateTime.parse(timeline!.keys.toList()[index]), + onNextTimeline: () { + pageController.nextPage( + duration: const Duration(milliseconds: 500), + curve: Curves.ease, + ); + }, + onPreviousTimeline: () { + pageController.previousPage( + duration: const Duration(milliseconds: 500), + curve: Curves.ease, + ); + }, + ), ), ), );