improvements

This commit is contained in:
Myzel394 2022-08-16 16:53:42 +02:00
parent f300e52993
commit 3441938e11
9 changed files with 339 additions and 233 deletions

View File

@ -2,6 +2,8 @@ import 'dart:collection';
const DURATION_INFINITY = Duration(days: 999);
const SECONDARY_BUTTONS_DURATION_MULTIPLIER = 1.8;
final CALENDAR_DATE_IN_DURATION_MULTIPLIER = 1.1;
const PHOTO_SHOW_AFTER_CREATION_DURATION = Duration(milliseconds: 500);
final UnmodifiableSetView<double> DEFAULT_ZOOM_LEVELS =
UnmodifiableSetView({0.6, 1, 2, 5, 10, 20, 50, 100});
const CALENDAR_DATE_IN_MAX_DELAY = Duration(milliseconds: 500);

View File

@ -2,6 +2,7 @@ import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:share_location/constants/apis.dart';
import 'package:share_location/screens/calendar_screen.dart';
import 'package:share_location/screens/grant_permission_screen.dart';
@ -9,10 +10,12 @@ import 'package:share_location/screens/login_screen.dart';
import 'package:share_location/screens/main_screen.dart';
import 'package:share_location/screens/timeline_screen.dart';
import 'package:share_location/screens/welcome_screen.dart';
import 'package:share_location/utils/auth_required.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'managers/global_values_manager.dart';
import 'managers/startup_page_manager.dart';
import 'models/memories.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -35,7 +38,7 @@ void main() async {
runApp(MyApp(initialPage: initialPage));
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
final String initialPage;
const MyApp({
@ -43,9 +46,38 @@ class MyApp extends StatelessWidget {
required this.initialPage,
}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends AuthRequiredState<MyApp> {
final memories = Memories();
@override
void initState() {
super.initState();
memories.addListener(() {
setState(() {
});
}, ['isInitializing']);
}
@override
void onAuthenticated(Session session) {
memories.initialize();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
if (memories.isInitializing) {
return SizedBox();
}
return ChangeNotifierProvider.value(
value: memories,
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark().copyWith(
textTheme: ThemeData.dark().textTheme.copyWith(
@ -71,7 +103,8 @@ class MyApp extends StatelessWidget {
GrantPermissionScreen.ID: (context) => const GrantPermissionScreen(),
CalendarScreen.ID: (context) => const CalendarScreen(),
},
initialRoute: initialPage,
initialRoute: widget.initialPage,
),
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:share_location/foreign_types/memory.dart';
import 'package:share_location/helpers/iterate_months.dart';
class CalendarManager {
final Map<DateTime, Set<String>> _values;
CalendarManager({
required final List<Memory> memories,
}) : _values = mapFromMemoriesList(memories);
static DateTime createDateKey(final DateTime date) =>
DateTime(date.year, date.month, date.day);
static Map<DateTime, Set<String>> mapFromMemoriesList(
final List<Memory> memories) {
final map = <DateTime, Set<String>>{};
for (final memory in memories) {
final key = createDateKey(memory.creationDate);
if (map.containsKey(key)) {
map[key]!.add(memory.id);
} else {
map[key] = {
memory.id,
};
}
}
return map;
}
static Map<DateTime, Map<DateTime, int>> fillEmptyMonths(
Map<DateTime, Map<DateTime, int>> monthMapping) {
final earliestDate =
monthMapping.keys.reduce((a, b) => a.isBefore(b) ? a : b);
final latestDate = monthMapping.keys.reduce((a, b) => a.isAfter(b) ? a : b);
final filledMonthMapping = <DateTime, Map<DateTime, int>>{};
for (final date in iterateMonths(earliestDate, latestDate)) {
filledMonthMapping[date] = monthMapping[date] ?? {};
}
return filledMonthMapping;
}
Map<DateTime, Map<DateTime, int>> getMonthDayAmountMapping() {
final map = <DateTime, Map<DateTime, int>>{};
for (final entry in _values.entries) {
final date = entry.key;
final monthDate = DateTime(date.year, date.month, 1);
final memoryIDs = entry.value;
if (map.containsKey(monthDate)) {
map[monthDate]![date] = memoryIDs.length;
} else {
map[monthDate] = {
date: memoryIDs.length,
};
}
}
return map;
}
Map<DateTime, Map<DateTime, int>> getMappingForList() {
final monthMapping = fillEmptyMonths(getMonthDayAmountMapping());
return Map.fromEntries(
monthMapping.entries.toList()..sort((a, b) => b.key.compareTo(a.key)),
);
}
}

114
lib/models/memories.dart Normal file
View File

@ -0,0 +1,114 @@
import 'package:property_change_notifier/property_change_notifier.dart';
import 'package:share_location/foreign_types/memory.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
class Memories extends PropertyChangeNotifier<String> {
final List<Memory> _memories = [];
Memories();
RealtimeSubscription? _serverSubscription;
bool _isInitializing = true;
List<Memory> get memories => _memories;
bool get isInitializing => _isInitializing;
@override
void dispose() {
_serverSubscription?.unsubscribe();
super.dispose();
}
void addMemory(final Memory memory) {
_memories.add(memory);
notifyListeners('memories');
}
void addAllMemories(final List<Memory> memories) {
_memories.addAll(memories);
notifyListeners('memories');
}
void removeMemory(final Memory memory) {
_memories.remove(memory);
notifyListeners('memories');
}
void removeMemoryByID(final String id) {
_memories.removeWhere((memory) => memory.id == id);
notifyListeners('memories');
}
void setIsInitializing(final bool value) {
_isInitializing = value;
notifyListeners('isInitializing');
}
void sortMemories() {
_memories.sort((a, b) => b.creationDate.compareTo(a.creationDate));
notifyListeners('memories');
}
Future<void> initialize() async {
setIsInitializing(true);
await _loadInitialData();
setIsInitializing(false);
notifyListeners();
// Watch new updates
_serverSubscription = supabase
.from('memories')
.on(SupabaseEventTypes.all, _onServerUpdate)
.subscribe();
}
Future<void> _onServerUpdate(
final SupabaseRealtimePayload response,
) async {
if (response == null) {
return;
}
switch (response.eventType) {
case 'INSERT':
final memory = Memory.parse(response.newRecord!);
addMemory(memory);
break;
case 'DELETE':
final id = response.oldRecord!['id'];
removeMemoryByID(id);
break;
// Used for easier debugging
case 'UPDATE':
final memory = Memory.parse(response.newRecord!);
final id = response.oldRecord!['id'];
removeMemoryByID(id);
addMemory(memory);
break;
}
sortMemories();
}
Future<void> _loadInitialData() async {
final response = await supabase
.from('memories')
.select()
.order('created_at', ascending: false)
.execute();
final newMemories = List<Memory>.from(
List<Map<String, dynamic>>.from(response.data).map(Memory.parse),
);
addAllMemories(newMemories);
}
}

View File

@ -1,19 +1,17 @@
import 'dart:math';
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<String> {
final Map<String, MemoryPack> _timeline;
final Map<DateTime, List<Memory>> _timeline;
TimelineModel({
Map<String, MemoryPack>? timeline,
}) : _timeline = timeline ?? {};
required final List<Memory> memories,
}) : _timeline = mapFromMemoriesList(memories);
RealtimeSubscription? _serverSubscription;
@ -22,38 +20,35 @@ class TimelineModel extends PropertyChangeNotifier<String> {
bool _paused = false;
bool _isInitializing = true;
Map<String, MemoryPack> get values => _timeline;
Map<DateTime, List<Memory>> get values => _timeline;
int get length => _timeline.length;
int get currentIndex => _currentIndex;
int get memoryIndex => _memoryIndex;
bool get paused => _paused;
bool get isInitializing => _isInitializing;
DateTime dateAtIndex(final int index) =>
DateTime.parse(_timeline.keys.elementAt(index));
DateTime dateAtIndex(final int index) => _timeline.keys.elementAt(index);
MemoryPack atIndex(final int index) => _timeline.values.elementAt(index);
List<Memory> atIndex(final int index) => _timeline.values.elementAt(index);
MemoryPack get _currentMemoryPack => atIndex(currentIndex);
bool get _isAtLastMemory =>
_memoryIndex == _currentMemoryPack.memories.length - 1;
Memory get currentMemory =>
_currentMemoryPack.memories.elementAt(_memoryIndex);
List<Memory> get _currentMemoryPack => atIndex(currentIndex);
bool get _isAtLastMemory => _memoryIndex == _currentMemoryPack.length - 1;
Memory get currentMemory => _currentMemoryPack.elementAt(_memoryIndex);
void _removeEmptyDates() {
_timeline.removeWhere((key, value) => value.memories.isEmpty);
_timeline.removeWhere((key, memories) => memories.isEmpty);
}
static formatCreationDateKey(final DateTime date) =>
DateFormat('yyyy-MM-dd').format(date);
static DateTime createDateKey(final DateTime date) =>
DateTime(date.year, date.month, date.day);
static Map<String, MemoryPack> mapFromMemoriesList(
static Map<DateTime, List<Memory>> mapFromMemoriesList(
final List<Memory> memories,
) {
final map = <String, List<Memory>>{};
final map = <DateTime, List<Memory>>{};
for (final memory in memories) {
final key = formatCreationDateKey(memory.creationDate);
final key = createDateKey(memory.creationDate);
if (map.containsKey(key)) {
map[key]!.add(memory);
@ -62,14 +57,7 @@ class TimelineModel extends PropertyChangeNotifier<String> {
}
}
return Map.fromEntries(
map.entries.map(
(entry) => MapEntry<String, MemoryPack>(
entry.key,
MemoryPack(entry.value),
),
),
);
return map;
}
@override
@ -85,7 +73,7 @@ class TimelineModel extends PropertyChangeNotifier<String> {
void setMemoryIndex(final int index) {
_memoryIndex = min(
_timeline.values.elementAt(_currentIndex).memories.length - 1,
_timeline.values.elementAt(_currentIndex).length - 1,
max(0, index),
);
notifyListeners('memoryIndex');
@ -118,7 +106,7 @@ class TimelineModel extends PropertyChangeNotifier<String> {
}
setCurrentIndex(currentIndex - 1);
setMemoryIndex(_currentMemoryPack.memories.length - 1);
setMemoryIndex(_currentMemoryPack.length - 1);
}
void nextMemory() {
@ -137,100 +125,13 @@ class TimelineModel extends PropertyChangeNotifier<String> {
}
}
Future<void> initialize() async {
void refresh(final List<Memory> memories) {
setIsInitializing(true);
await _listenToServer();
_timeline.clear();
_timeline.addAll(mapFromMemoriesList(memories));
_removeEmptyDates();
setIsInitializing(false);
}
void _insertMemory(final Memory memory) {
final key = formatCreationDateKey(memory.creationDate);
if (!_timeline.containsKey(key)) {
_timeline[key] = MemoryPack([memory]);
return;
}
final memoryPack = _timeline[key]!;
memoryPack.addMemory(memory);
}
void _updateMemory(final String id, final Memory memory) {
final key = formatCreationDateKey(memory.creationDate);
if (!_timeline.containsKey(key)) {
_timeline[key] = MemoryPack([memory]);
return;
}
final memoryPack = _timeline[key]!;
memoryPack.updateWithNewMemory(id, memory);
}
void _deleteMemory(final String id) {
// Search for correct `Memory` and remove it
for (final memories in _timeline.values) {
memories.memories.removeWhere((memory) => memory.id == id);
}
_removeEmptyDates();
}
Future<void> _fetchInitialData() async {
final response = await supabase
.from('memories')
.select()
.order('created_at', ascending: false)
.execute();
final memories = List<Memory>.from(
List<Map<String, dynamic>>.from(response.data).map(Memory.parse),
);
values
..clear()
..addAll(mapFromMemoriesList(memories));
}
Future<void> _onServerUpdate(
final SupabaseRealtimePayload response,
) async {
if (response == null) {
return;
}
switch (response.eventType) {
case 'INSERT':
final memory = Memory.parse(response.newRecord!);
_insertMemory(memory);
break;
case 'UPDATE':
final memoryID = response.oldRecord!['id'];
final memory = Memory.parse(response.newRecord!);
_updateMemory(memoryID, memory);
break;
case 'DELETE':
final id = response.oldRecord!['id'];
_deleteMemory(id);
break;
}
notifyListeners();
}
Future<void> _listenToServer() async {
await _fetchInitialData();
notifyListeners();
// Watch new updates
_serverSubscription = supabase
.from('memories')
.on(SupabaseEventTypes.all, _onServerUpdate)
.subscribe();
}
}

View File

@ -1,67 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:provider/provider.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/helpers/iterate_months.dart';
import 'package:share_location/models/calendar.dart';
import 'package:share_location/managers/calendar_manager.dart';
import 'package:share_location/models/memories.dart';
import 'package:share_location/widgets/calendar_month.dart';
import 'package:share_location/widgets/days_of_week_strip.dart';
class CalendarScreen extends StatefulWidget {
class CalendarScreen extends StatelessWidget {
static const ID = 'calendar';
const CalendarScreen({Key? key}) : super(key: key);
@override
State<CalendarScreen> createState() => _CalendarScreenState();
}
class _CalendarScreenState extends State<CalendarScreen> {
final calendar = CalendarModel();
@override
void initState() {
super.initState();
calendar.initialize();
calendar.addListener(() {
setState(() {});
});
}
static Map<DateTime, Map<DateTime, int>> fillEmptyMonths(
Map<DateTime, Map<DateTime, int>> monthMapping) {
final earliestDate =
monthMapping.keys.reduce((a, b) => a.isBefore(b) ? a : b);
final latestDate = monthMapping.keys.reduce((a, b) => a.isAfter(b) ? a : b);
final filledMonthMapping = <DateTime, Map<DateTime, int>>{};
for (final date in iterateMonths(earliestDate, latestDate)) {
filledMonthMapping[date] = monthMapping[date] ?? {};
}
return filledMonthMapping;
}
const CalendarScreen({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (calendar.isInitializing) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
final memoriesManager = context.read<Memories>();
final theme = Theme.of(context);
final monthMapping = fillEmptyMonths(calendar.getMonthDayAmountMapping());
final sortedMonthMapping = Map.fromEntries(
monthMapping.entries.toList()..sort((a, b) => b.key.compareTo(a.key)),
);
return Scaffold(
body: CustomScrollView(
final calendarManager = CalendarManager(memories: memoriesManager.memories);
final monthMapping = calendarManager.getMappingForList();
return Consumer<Memories>(
builder: (context, memories, _) => Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(vertical: MEDIUM_SPACE),
child: CustomScrollView(
reverse: true,
slivers: [
SliverStickyHeader(
@ -72,17 +37,24 @@ class _CalendarScreenState extends State<CalendarScreen> {
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => CalendarMonth(
year: sortedMonthMapping.keys.elementAt(index).year,
month: sortedMonthMapping.keys.elementAt(index).month,
dayAmountMap: sortedMonthMapping.values.elementAt(index),
),
childCount: sortedMonthMapping.length,
(context, index) {
final date = monthMapping.keys.elementAt(index);
final dayMapping = monthMapping.values.elementAt(index);
return CalendarMonth(
year: date.year,
month: date.month,
dayAmountMap: dayMapping,
);
},
childCount: monthMapping.length,
),
),
),
],
),
),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:share_location/extensions/date.dart';
import 'package:share_location/models/memories.dart';
import 'package:share_location/models/timeline.dart';
import 'package:share_location/utils/loadable.dart';
import 'package:share_location/widgets/timeline_page.dart';
@ -26,7 +27,7 @@ class TimelineScreen extends StatefulWidget {
class _TimelineScreenState extends State<TimelineScreen> with Loadable {
final pageController = PageController();
final timeline = TimelineModel();
late final TimelineModel timeline;
bool _ignorePageChanges = false;
Future<void> _goToPage(final int page) async {
@ -45,7 +46,13 @@ class _TimelineScreenState extends State<TimelineScreen> with Loadable {
initState() {
super.initState();
timeline.initialize();
final memoriesModel = context.read<Memories>();
timeline = TimelineModel(memories: memoriesModel.memories);
memoriesModel.addListener(() {
timeline.refresh(memoriesModel.memories);
}, ['memories']);
// Update page view
timeline.addListener(() async {
@ -86,22 +93,16 @@ class _TimelineScreenState extends State<TimelineScreen> with Loadable {
return timeline.values.keys
.toList()
.indexWhere((date) => DateTime.parse(date).isSameDay(widget.date!));
.indexWhere((date) => date.isSameDay(widget.date!));
}
@override
Widget build(BuildContext context) {
if (timeline.isInitializing) {
return const Center(
child: CircularProgressIndicator(),
);
}
return WillPopScope(
onWillPop: () async {
await Navigator.pushNamed(context, CalendarScreen.ID);
await Navigator.pushReplacementNamed(context, CalendarScreen.ID);
return true;
return false;
},
child: Scaffold(
body: ChangeNotifierProvider.value(
@ -124,7 +125,7 @@ class _TimelineScreenState extends State<TimelineScreen> with Loadable {
},
itemBuilder: (_, index) => TimelinePage(
date: timeline.dateAtIndex(index),
memoryPack: timeline.atIndex(index),
memories: timeline.atIndex(index),
),
),
),

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_calendar_widget/flutter_calendar_widget.dart';
import 'package:intl/intl.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/constants/values.dart';
import 'package:share_location/screens/timeline_screen.dart';
import 'package:share_location/widgets/delay_render.dart';
import 'package:share_location/widgets/fade_and_move_in_animation.dart';
@ -37,11 +38,18 @@ class MonthCalendarBuilder extends CalendarBuilder {
return amount / highestAmountOfEvents;
}();
final duration = Duration(milliseconds: Random().nextInt(800));
final delay = Duration(
microseconds:
Random().nextInt(CALENDAR_DATE_IN_MAX_DELAY.inMicroseconds));
return DelayRender(
delay: duration,
delay: delay,
child: FadeAndMoveInAnimation(
opacityDuration:
DEFAULT_OPACITY_DURATION * CALENDAR_DATE_IN_DURATION_MULTIPLIER,
translationDuration:
DEFAULT_TRANSLATION_DURATION * CALENDAR_DATE_IN_DURATION_MULTIPLIER,
translationOffset: const Offset(0.0, -MEDIUM_SPACE),
child: Opacity(
opacity: () {
if (type.isOutSide) {

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:share_location/models/memory_pack.dart';
import 'package:share_location/foreign_types/memory.dart';
import 'package:share_location/models/timeline.dart';
import 'package:share_location/models/timeline_overlay.dart';
import 'package:share_location/widgets/memory_sheet.dart';
@ -11,12 +11,12 @@ import 'package:share_location/widgets/timeline_overlay.dart';
class TimelinePage extends StatefulWidget {
final DateTime date;
final MemoryPack memoryPack;
final List<Memory> memories;
const TimelinePage({
Key? key,
required this.date,
required this.memoryPack,
required this.memories,
}) : super(key: key);
@override
@ -165,14 +165,14 @@ class _TimelinePageState extends State<TimelinePage> {
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
itemBuilder: (_, index) => MemorySlide(
key: Key(widget.memoryPack.memories[index].filename),
memory: widget.memoryPack.memories[index],
key: Key(widget.memories[index].filename),
memory: widget.memories[index],
),
itemCount: widget.memoryPack.memories.length,
itemCount: widget.memories.length,
),
TimelineOverlay(
date: widget.date,
memoriesAmount: widget.memoryPack.memories.length,
memoriesAmount: widget.memories.length,
memoryIndex: timeline.memoryIndex + 1,
),
],