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;
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<String> {
}
void next() {
if (_index == memoryLength - 1) {
if (isLast) {
_completed = true;
notifyListeners('completed');
} else {

View File

@ -95,4 +95,20 @@ class FileManager {
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;
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();
}
}

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: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<MemorySlide>
void initState() {
super.initState();
widget.controller.addListener(() {
final timelineOverlay = context.read<TimelineOverlay>();
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<MemorySlide>
}
void initializeAnimation(final Duration duration) {
final timelineOverlay = context.read<TimelineOverlay>();
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<MemorySlide>
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']);
}
},
),

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: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<Memory> 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<TimelinePage> {
final timelineOverlayController = TimelineOverlay();
final pageController = PageController();
late final MemorySlideController controller;
Timer? overlayRemover;
@ -38,37 +35,75 @@ class _TimelinePageState extends State<TimelinePage> {
void initState() {
super.initState();
controller = MemorySlideController(memoryLength: widget.memories.length);
controller.addListener(() {
if (controller.done) {
controller.next();
final memoryPack = context.read<MemoryPack>();
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>();
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>();
memoryPack.pause();
overlayRemover = Timer(
const Duration(milliseconds: 200),
@ -76,28 +111,32 @@ class _TimelinePageState extends State<TimelinePage> {
);
},
onTapUp: (_) {
final memoryPack = context.read<MemoryPack>();
overlayRemover?.cancel();
controller.setPaused(false);
memoryPack.resume();
timelineOverlayController.restoreOverlay();
},
onTapCancel: () {
final memoryPack = context.read<MemoryPack>();
overlayRemover?.cancel();
timelineOverlayController.restoreOverlay();
controller.setPaused(false);
memoryPack.resume();
},
onHorizontalDragEnd: (details) {
final memoryPack = context.read<MemoryPack>();
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<TimelinePage> {
child: Stack(
fit: StackFit.expand,
children: <Widget>[
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<MemoryPack>(
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(

View File

@ -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<TimelineScroll> with Loadable {
final pageController = PageController();
dynamic timeline;
Map<String, MemoryPack>? timeline;
@override
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 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
@ -70,22 +79,24 @@ class _TimelineScrollState extends State<TimelineScroll> 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<MemoryPack>(
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,
);
},
),
),
),
);