improved memory handling

This commit is contained in:
Myzel394 2022-08-14 20:37:36 +02:00
parent 2339fd27a0
commit 8409939825
9 changed files with 332 additions and 75 deletions

View File

@ -19,6 +19,8 @@ class MemorySlideController extends PropertyChangeNotifier<String> {
int get index => _index; int get index => _index;
bool get completed => _completed; bool get completed => _completed;
bool get isLast => _index == memoryLength - 1;
void setPaused(bool paused) { void setPaused(bool paused) {
_paused = paused; _paused = paused;
notifyListeners('paused'); notifyListeners('paused');
@ -30,7 +32,7 @@ class MemorySlideController extends PropertyChangeNotifier<String> {
} }
void next() { void next() {
if (_index == memoryLength - 1) { if (isLast) {
_completed = true; _completed = true;
notifyListeners('completed'); notifyListeners('completed');
} else { } else {

View File

@ -95,4 +95,20 @@ class FileManager {
return data; return data;
} }
static Future<void> 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}');
}
}
} }

View File

@ -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<String> {
final List<Memory> _memories;
int _currentMemoryIndex = 0;
bool _paused = false;
bool _completed = false;
MemoryPack(this._memories);
List<Memory> 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);
}

View File

@ -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<String> {
bool _showOverlay = true; bool _showOverlay = true;
TimelineState _state = TimelineState.loading;
bool get showOverlay => _showOverlay; bool get showOverlay => _showOverlay;
TimelineState get state => _state;
void hideOverlay() => setShowOverlay(false); void hideOverlay() => setShowOverlay(false);
void restoreOverlay() => setShowOverlay(true); void restoreOverlay() => setShowOverlay(true);
void setShowOverlay(bool showOverlay) { void setShowOverlay(bool showOverlay) {
_showOverlay = showOverlay; _showOverlay = showOverlay;
notifyListeners('showOverlay');
}
void setState(TimelineState state) {
_state = state;
notifyListeners('state');
}
void reset() {
_showOverlay = true;
_state = TimelineState.loading;
notifyListeners(); notifyListeners();
} }
} }

View File

@ -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<MemorySheet> createState() => _MemorySheetState();
}
class _MemorySheetState extends State<MemorySheet> with Loadable {
Future<void> deleteFile() => FileManager.deleteFile(widget.memory.location);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ModalSheet(
child: Column(
children: <Widget>[
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,
),
],
),
);
}
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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/controllers/status_controller.dart';
import 'package:share_location/enums.dart'; import 'package:share_location/enums.dart';
import 'package:share_location/foreign_types/memory.dart'; import 'package:share_location/foreign_types/memory.dart';
@ -14,12 +13,10 @@ const DEFAULT_IMAGE_DURATION = Duration(seconds: 5);
class MemorySlide extends StatefulWidget { class MemorySlide extends StatefulWidget {
final Memory memory; final Memory memory;
final MemorySlideController controller;
const MemorySlide({ const MemorySlide({
Key? key, Key? key,
required this.memory, required this.memory,
required this.controller,
}) : super(key: key); }) : super(key: key);
@override @override
@ -36,15 +33,19 @@ class _MemorySlideState extends State<MemorySlide>
void initState() { void initState() {
super.initState(); super.initState();
widget.controller.addListener(() { final timelineOverlay = context.read<TimelineOverlay>();
timelineOverlay.addListener(() {
if (!mounted) { if (!mounted) {
return; return;
} }
if (widget.controller.paused) { switch (timelineOverlay.state) {
controller?.stop(); case TimelineState.playing:
} else { controller?.start();
controller?.start(); break;
default:
controller?.stop();
} }
}); });
} }
@ -57,11 +58,23 @@ class _MemorySlideState extends State<MemorySlide>
} }
void initializeAnimation(final Duration duration) { void initializeAnimation(final Duration duration) {
final timelineOverlay = context.read<TimelineOverlay>();
this.duration = duration; this.duration = duration;
controller = StatusController( controller = StatusController(
duration: duration, duration: duration,
)..addListener(widget.controller.setDone, ['done']); );
controller!.addListener(() {
if (!mounted) {
return;
}
if (controller!.done) {
timelineOverlay.setState(TimelineState.completed);
}
}, ['done']);
setState(() {}); setState(() {});
} }
@ -86,17 +99,17 @@ class _MemorySlideState extends State<MemorySlide>
if (mounted) { if (mounted) {
initializeAnimation(controller.value.duration); initializeAnimation(controller.value.duration);
widget.controller.addListener(() { overlayController.addListener(() {
if (!mounted) { if (!mounted) {
return; return;
} }
if (widget.controller.paused) { if (overlayController.state == TimelineState.playing) {
controller.pause();
} else {
controller.play(); controller.play();
} else {
controller.pause();
} }
}); }, ['state']);
} }
}, },
), ),

View File

@ -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: <Widget>[
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,
),
),
],
);
}
}

View File

@ -4,21 +4,19 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:share_location/constants/spacing.dart'; import 'package:share_location/constants/spacing.dart';
import 'package:share_location/controllers/memory_slide_controller.dart'; import 'package:share_location/models/memory_pack.dart';
import 'package:share_location/foreign_types/memory.dart';
import 'package:share_location/models/timeline_overlay.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'; import 'package:share_location/widgets/memory_slide.dart';
class TimelinePage extends StatefulWidget { class TimelinePage extends StatefulWidget {
final DateTime date; final DateTime date;
final List<Memory> memories;
final VoidCallback onPreviousTimeline; final VoidCallback onPreviousTimeline;
final VoidCallback onNextTimeline; final VoidCallback onNextTimeline;
const TimelinePage({ const TimelinePage({
Key? key, Key? key,
required this.date, required this.date,
required this.memories,
required this.onPreviousTimeline, required this.onPreviousTimeline,
required this.onNextTimeline, required this.onNextTimeline,
}) : super(key: key); }) : super(key: key);
@ -30,7 +28,6 @@ class TimelinePage extends StatefulWidget {
class _TimelinePageState extends State<TimelinePage> { class _TimelinePageState extends State<TimelinePage> {
final timelineOverlayController = TimelineOverlay(); final timelineOverlayController = TimelineOverlay();
final pageController = PageController(); final pageController = PageController();
late final MemorySlideController controller;
Timer? overlayRemover; Timer? overlayRemover;
@ -38,37 +35,75 @@ class _TimelinePageState extends State<TimelinePage> {
void initState() { void initState() {
super.initState(); super.initState();
controller = MemorySlideController(memoryLength: widget.memories.length); final memoryPack = context.read<MemoryPack>();
controller.addListener(() {
if (controller.done) {
controller.next();
pageController.nextPage( timelineOverlayController.addListener(() {
duration: const Duration(milliseconds: 200), if (!mounted) {
curve: Curves.linearToEaseOut, return;
);
} }
}, ['done']);
controller.addListener(() { if (timelineOverlayController.state == TimelineState.completed) {
if (controller.completed) { timelineOverlayController.reset();
memoryPack.next();
}
}, ['state']);
memoryPack.addListener(() {
if (!mounted) {
return;
}
if (memoryPack.completed) {
widget.onNextTimeline(); widget.onNextTimeline();
memoryPack.reset();
} }
}, ['completed']); }, ['completed']);
}
@override memoryPack.addListener(() {
void dispose() { if (!mounted) {
controller.dispose(); return;
timelineOverlayController.dispose(); }
super.dispose(); if (memoryPack.currentMemoryIndex != pageController.page) {
pageController.animateToPage(
memoryPack.currentMemoryIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutQuad,
);
}
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onDoubleTap: () async {
final memoryPack = context.read<MemoryPack>();
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: (_) { onTapDown: (_) {
controller.setPaused(true); final memoryPack = context.read<MemoryPack>();
memoryPack.pause();
overlayRemover = Timer( overlayRemover = Timer(
const Duration(milliseconds: 200), const Duration(milliseconds: 200),
@ -76,28 +111,32 @@ class _TimelinePageState extends State<TimelinePage> {
); );
}, },
onTapUp: (_) { onTapUp: (_) {
final memoryPack = context.read<MemoryPack>();
overlayRemover?.cancel(); overlayRemover?.cancel();
memoryPack.resume();
controller.setPaused(false);
timelineOverlayController.restoreOverlay(); timelineOverlayController.restoreOverlay();
}, },
onTapCancel: () { onTapCancel: () {
final memoryPack = context.read<MemoryPack>();
overlayRemover?.cancel(); overlayRemover?.cancel();
timelineOverlayController.restoreOverlay(); timelineOverlayController.restoreOverlay();
controller.setPaused(false); memoryPack.resume();
}, },
onHorizontalDragEnd: (details) { onHorizontalDragEnd: (details) {
final memoryPack = context.read<MemoryPack>();
if (details.primaryVelocity! < 0) { if (details.primaryVelocity! < 0) {
controller.next(); memoryPack.next();
pageController.nextPage( pageController.nextPage(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut, curve: Curves.linearToEaseOut,
); );
} else if (details.primaryVelocity! > 0) { } else if (details.primaryVelocity! > 0) {
controller.previous(); memoryPack.previous();
pageController.previousPage( pageController.previousPage(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@ -110,16 +149,19 @@ class _TimelinePageState extends State<TimelinePage> {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
PageView.builder( Consumer<MemoryPack>(
controller: pageController, builder: (_, memoryPack, __) {
physics: const NeverScrollableScrollPhysics(), return PageView.builder(
scrollDirection: Axis.horizontal, controller: pageController,
itemBuilder: (_, __) => MemorySlide( physics: const NeverScrollableScrollPhysics(),
key: Key(controller.index.toString()), scrollDirection: Axis.horizontal,
controller: controller, itemBuilder: (_, index) => MemorySlide(
memory: widget.memories[controller.index], key: Key(memoryPack.memories[index].filename),
), memory: memoryPack.memories[index],
itemCount: widget.memories.length, ),
itemCount: memoryPack.memories.length,
);
},
), ),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:share_location/foreign_types/memory.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/utils/loadable.dart';
import 'package:share_location/widgets/timeline_page.dart'; import 'package:share_location/widgets/timeline_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@ -18,7 +20,7 @@ class TimelineScroll extends StatefulWidget {
class _TimelineScrollState extends State<TimelineScroll> with Loadable { class _TimelineScrollState extends State<TimelineScroll> with Loadable {
final pageController = PageController(); final pageController = PageController();
dynamic timeline; Map<String, MemoryPack>? timeline;
@override @override
initState() { initState() {
@ -41,7 +43,7 @@ class _TimelineScrollState extends State<TimelineScroll> with Loadable {
}); });
} }
static Map<String, List<Memory>> convertMemoriesToTimeline( static Map<String, MemoryPack> convertMemoriesToTimeline(
final List<Memory> memories, final List<Memory> memories,
) { ) {
final map = <String, List<Memory>>{}; final map = <String, List<Memory>>{};
@ -55,7 +57,14 @@ class _TimelineScrollState extends State<TimelineScroll> with Loadable {
} }
} }
return map; return Map.fromEntries(
map.entries.map(
(entry) => MapEntry<String, MemoryPack>(
entry.key,
MemoryPack(entry.value),
),
),
);
} }
@override @override
@ -70,22 +79,24 @@ class _TimelineScrollState extends State<TimelineScroll> with Loadable {
body: PageView.builder( body: PageView.builder(
controller: pageController, controller: pageController,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
itemCount: timeline.length, itemCount: timeline!.length,
itemBuilder: (_, index) => TimelinePage( itemBuilder: (_, index) => ChangeNotifierProvider<MemoryPack>(
date: DateTime.parse(timeline.keys.toList()[index]), create: (_) => timeline!.values.elementAt(index),
memories: timeline.values.toList()[index], child: TimelinePage(
onNextTimeline: () { date: DateTime.parse(timeline!.keys.toList()[index]),
pageController.nextPage( onNextTimeline: () {
duration: const Duration(milliseconds: 500), pageController.nextPage(
curve: Curves.ease, duration: const Duration(milliseconds: 500),
); curve: Curves.ease,
}, );
onPreviousTimeline: () { },
pageController.previousPage( onPreviousTimeline: () {
duration: const Duration(milliseconds: 500), pageController.previousPage(
curve: Curves.ease, duration: const Duration(milliseconds: 500),
); curve: Curves.ease,
}, );
},
),
), ),
), ),
); );