diff --git a/lib/extensions/snackbar.dart b/lib/extensions/snackbar.dart index 5bee0d5..99ef987 100644 --- a/lib/extensions/snackbar.dart +++ b/lib/extensions/snackbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:share_location/constants/values.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -26,30 +27,42 @@ extension ShowSnackBar on BuildContext { ); } + showToast({ + required final String message, + final Toast toastLength = Toast.LENGTH_SHORT, + final Color backgroundColor = Colors.white, + final Color textColor = Colors.black, + }) { + Fluttertoast.showToast( + msg: message, + toastLength: toastLength, + backgroundColor: backgroundColor, + textColor: textColor, + ); + } + void showErrorSnackBar({ required final String message, - final BuildContext? context, }) { showSnackBar( message: message, backgroundColor: Colors.red, + duration: const Duration(milliseconds: 550), ); } void showSuccessSnackBar({ required final String message, - final BuildContext? context, }) { showSnackBar( message: message, - duration: const Duration(milliseconds: 550), backgroundColor: Colors.green, + duration: const Duration(milliseconds: 550), ); } void showPendingSnackBar({ required final String message, - final BuildContext? context, }) { pendingSnackBar = showSnackBar( message: message, @@ -57,4 +70,26 @@ extension ShowSnackBar on BuildContext { duration: DURATION_INFINITY, ); } + + void showSuccessToast({ + required final String message, + }) { + showToast( + message: message, + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + ); + } + + void showErrorToast({ + required final String message, + }) { + showToast( + message: message, + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + ); + } } diff --git a/lib/models/timeline.dart b/lib/models/timeline.dart index d8538d6..ff91669 100644 --- a/lib/models/timeline.dart +++ b/lib/models/timeline.dart @@ -4,6 +4,9 @@ import 'package:intl/intl.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; import 'package:share_location/foreign_types/memory.dart'; import 'package:share_location/models/memory_pack.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +final supabase = Supabase.instance.client; class TimelineModel extends PropertyChangeNotifier { final Map _timeline; @@ -15,14 +18,16 @@ class TimelineModel extends PropertyChangeNotifier { int _currentIndex = 0; int _memoryIndex = 0; bool _paused = false; + bool _isInitializing = true; Map get values => _timeline; int get length => _timeline.length; int get currentIndex => _currentIndex; int get memoryIndex => _memoryIndex; bool get paused => _paused; + bool get isInitializing => _isInitializing; - static TimelineModel fromMemoriesList( + static Map mapFromMemoriesList( final List memories, ) { final map = >{}; @@ -36,7 +41,7 @@ class TimelineModel extends PropertyChangeNotifier { } } - final data = Map.fromEntries( + return Map.fromEntries( map.entries.map( (entry) => MapEntry( entry.key, @@ -44,10 +49,6 @@ class TimelineModel extends PropertyChangeNotifier { ), ), ); - - return TimelineModel( - timeline: data, - ); } DateTime dateAtIndex(final int index) => @@ -62,16 +63,8 @@ class TimelineModel extends PropertyChangeNotifier { Memory get currentMemory => currentMemoryPack.memories.elementAt(_memoryIndex); - void removeEmptyDates() { - final previousLength = _timeline.length; - + void _removeEmptyDates() { _timeline.removeWhere((key, value) => value.memories.isEmpty); - - final newLength = _timeline.length; - - if (previousLength != newLength) { - notifyListeners(); - } } void setCurrentIndex(final int index) { @@ -92,14 +85,22 @@ class TimelineModel extends PropertyChangeNotifier { notifyListeners('paused'); } + void setIsInitializing(bool isInitializing) { + _isInitializing = isInitializing; + notifyListeners('isInitializing'); + } + void removeMemory( final int timelineIndex, final int memoryIndex, ) { _timeline.values.elementAt(timelineIndex).memories.removeAt(memoryIndex); + _removeEmptyDates(); notifyListeners(); } + void removeCurrentMemory() => removeMemory(_currentIndex, _memoryIndex); + void pause() => setPaused(true); void resume() => setPaused(false); @@ -135,4 +136,29 @@ class TimelineModel extends PropertyChangeNotifier { setMemoryIndex(memoryIndex - 1); } } + + Future initialize() async { + setIsInitializing(true); + + await refreshFromServer(); + + setIsInitializing(false); + } + + Future refreshFromServer() async { + final response = await supabase + .from('memories') + .select() + .order('created_at', ascending: false) + .execute(); + final memories = List.from( + List>.from(response.data).map(Memory.parse), + ); + + values + ..clear() + ..addAll(mapFromMemoriesList(memories)); + + notifyListeners(); + } } diff --git a/lib/widgets/memory_sheet.dart b/lib/widgets/memory_sheet.dart index 1d8e62f..48ad7c6 100644 --- a/lib/widgets/memory_sheet.dart +++ b/lib/widgets/memory_sheet.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:share_location/constants/spacing.dart'; import 'package:share_location/enums.dart'; @@ -8,31 +7,37 @@ 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'; +import 'package:supabase_flutter/supabase_flutter.dart'; class MemorySheet extends StatefulWidget { final Memory memory; - final VoidCallback onMemoryDeleted; final BuildContext sheetContext; + final VoidCallback onDelete; + final VoidCallback onVisibilityChanged; const MemorySheet({ Key? key, required this.memory, - required this.onMemoryDeleted, required this.sheetContext, + required this.onDelete, + required this.onVisibilityChanged, }) : super(key: key); @override State createState() => _MemorySheetState(); } +final supabase = Supabase.instance.client; + class _MemorySheetState extends State with Loadable { Future deleteFile() async { await FileManager.deleteFile(widget.memory.location); - widget.onMemoryDeleted(); if (mounted) { Navigator.pop(context); } + + widget.onDelete(); } Future downloadFile() async { @@ -57,13 +62,31 @@ class _MemorySheetState extends State with Loadable { context.showSuccessSnackBar(message: 'File saved to Gallery!'); } catch (error) { - context.showErrorSnackBar(message: 'There was an error'); - Fluttertoast.showToast( - msg: 'There was an error', - toastLength: Toast.LENGTH_SHORT, - backgroundColor: Colors.red, - textColor: Colors.white, - ); + context.showErrorSnackBar(message: 'There was an error.'); + } + } + + Future changeVisibility() async { + final isNowPublic = !widget.memory.isPublic == true; + + try { + await supabase.from('memories').update({ + 'is_public': !widget.memory.isPublic, + }).match({ + 'id': widget.memory.id, + }).execute(); + + Navigator.pop(context); + + if (isNowPublic) { + context.showSuccessSnackBar(message: 'Your Memory is public now!'); + } else { + context.showSuccessSnackBar(message: 'Your Memory is private now.'); + } + + widget.onVisibilityChanged(); + } catch (error) { + context.showErrorSnackBar(message: 'There was an error.'); } } @@ -103,6 +126,20 @@ class _MemorySheetState extends State with Loadable { ? buildLoadingIndicator() : null, ), + ListTile( + leading: Icon(widget.memory.isPublic + ? Icons.public_off_rounded + : Icons.public_rounded), + title: + Text(widget.memory.isPublic ? 'Make private' : 'Make public'), + enabled: !getIsLoadingSpecificID('public'), + onTap: getIsLoadingSpecificID('public') + ? null + : () => callWithLoading(changeVisibility, 'public'), + trailing: getIsLoadingSpecificID('public') + ? buildLoadingIndicator() + : null, + ), ListTile( leading: const Icon(Icons.delete_forever_sharp), title: const Text('Delete Memory'), diff --git a/lib/widgets/timeline_page.dart b/lib/widgets/timeline_page.dart index 85f4335..e04cb6f 100644 --- a/lib/widgets/timeline_page.dart +++ b/lib/widgets/timeline_page.dart @@ -122,7 +122,12 @@ class _TimelinePageState extends State { builder: (sheetContext) => MemorySheet( memory: timeline.currentMemory, sheetContext: sheetContext, - onMemoryDeleted: timeline.removeEmptyDates, + onDelete: () async { + timeline.removeCurrentMemory(); + }, + onVisibilityChanged: () async { + timeline.refreshFromServer(); + }, ), ); diff --git a/lib/widgets/timeline_scroll.dart b/lib/widgets/timeline_scroll.dart index 9563c88..54f5e7d 100644 --- a/lib/widgets/timeline_scroll.dart +++ b/lib/widgets/timeline_scroll.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:share_location/foreign_types/memory.dart'; import 'package:share_location/models/timeline.dart'; import 'package:share_location/utils/loadable.dart'; import 'package:share_location/widgets/timeline_page.dart'; @@ -19,54 +18,41 @@ class TimelineScroll extends StatefulWidget { class _TimelineScrollState extends State with Loadable { final pageController = PageController(); - TimelineModel? timeline; + final timeline = TimelineModel(); @override initState() { super.initState(); - loadTimeline(); + + timeline.initialize(); + + // Update page view + timeline.addListener(() { + if (timeline.currentIndex != pageController.page) { + pageController.animateToPage( + timeline.currentIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutQuad, + ); + } + }, ['currentIndex']); + + // Update page when initializing is done + timeline.addListener(() { + setState(() {}); + }, ['isInitializing']); } @override dispose() { pageController.dispose(); - timeline?.dispose(); - super.dispose(); } - Future loadTimeline() async { - timeline?.dispose(); - - final response = await supabase - .from('memories') - .select() - .order('created_at', ascending: false) - .execute(); - final memories = List.from( - List>.from(response.data).map(Memory.parse)); - final newTimeline = TimelineModel.fromMemoriesList(memories); - - setState(() { - timeline = newTimeline; - }); - - // Update page - newTimeline.addListener(() { - if (newTimeline.currentIndex != pageController.page) { - pageController.animateToPage( - newTimeline.currentIndex, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutQuad, - ); - } - }, ['currentIndex']); - } - @override Widget build(BuildContext context) { - if (timeline == null) { + if (timeline.isInitializing) { return const Center( child: CircularProgressIndicator(), ); @@ -78,17 +64,17 @@ class _TimelineScrollState extends State with Loadable { child: PageView.builder( controller: pageController, scrollDirection: Axis.vertical, - itemCount: timeline!.values.length, + itemCount: timeline.values.length, onPageChanged: (newPage) { - if (timeline!.currentIndex != newPage) { + if (timeline.currentIndex != newPage) { // User manually changed page - timeline!.setCurrentIndex(newPage); + timeline.setCurrentIndex(newPage); - timeline!.setMemoryIndex(0); + timeline.setMemoryIndex(0); } }, itemBuilder: (_, index) => TimelinePage( - date: timeline!.dateAtIndex(index), + date: timeline.dateAtIndex(index), ), ), ),