mirror of
https://github.com/Myzel394/quid_faciam_hodie.git
synced 2025-06-19 15:45:26 +02:00
improvements
This commit is contained in:
parent
f300e52993
commit
3441938e11
@ -2,6 +2,8 @@ import 'dart:collection';
|
|||||||
|
|
||||||
const DURATION_INFINITY = Duration(days: 999);
|
const DURATION_INFINITY = Duration(days: 999);
|
||||||
const SECONDARY_BUTTONS_DURATION_MULTIPLIER = 1.8;
|
const SECONDARY_BUTTONS_DURATION_MULTIPLIER = 1.8;
|
||||||
|
final CALENDAR_DATE_IN_DURATION_MULTIPLIER = 1.1;
|
||||||
const PHOTO_SHOW_AFTER_CREATION_DURATION = Duration(milliseconds: 500);
|
const PHOTO_SHOW_AFTER_CREATION_DURATION = Duration(milliseconds: 500);
|
||||||
final UnmodifiableSetView<double> DEFAULT_ZOOM_LEVELS =
|
final UnmodifiableSetView<double> DEFAULT_ZOOM_LEVELS =
|
||||||
UnmodifiableSetView({0.6, 1, 2, 5, 10, 20, 50, 100});
|
UnmodifiableSetView({0.6, 1, 2, 5, 10, 20, 50, 100});
|
||||||
|
const CALENDAR_DATE_IN_MAX_DELAY = Duration(milliseconds: 500);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:camera/camera.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:share_location/constants/apis.dart';
|
import 'package:share_location/constants/apis.dart';
|
||||||
import 'package:share_location/screens/calendar_screen.dart';
|
import 'package:share_location/screens/calendar_screen.dart';
|
||||||
import 'package:share_location/screens/grant_permission_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/main_screen.dart';
|
||||||
import 'package:share_location/screens/timeline_screen.dart';
|
import 'package:share_location/screens/timeline_screen.dart';
|
||||||
import 'package:share_location/screens/welcome_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 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'managers/global_values_manager.dart';
|
import 'managers/global_values_manager.dart';
|
||||||
import 'managers/startup_page_manager.dart';
|
import 'managers/startup_page_manager.dart';
|
||||||
|
import 'models/memories.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -35,7 +38,7 @@ void main() async {
|
|||||||
runApp(MyApp(initialPage: initialPage));
|
runApp(MyApp(initialPage: initialPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatefulWidget {
|
||||||
final String initialPage;
|
final String initialPage;
|
||||||
|
|
||||||
const MyApp({
|
const MyApp({
|
||||||
@ -43,35 +46,65 @@ class MyApp extends StatelessWidget {
|
|||||||
required this.initialPage,
|
required this.initialPage,
|
||||||
}) : super(key: key);
|
}) : 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
if (memories.isInitializing) {
|
||||||
title: 'Flutter Demo',
|
return SizedBox();
|
||||||
theme: ThemeData.dark().copyWith(
|
}
|
||||||
textTheme: ThemeData.dark().textTheme.copyWith(
|
|
||||||
headline1: const TextStyle(
|
return ChangeNotifierProvider.value(
|
||||||
fontSize: 32,
|
value: memories,
|
||||||
fontWeight: FontWeight.w500,
|
child: MaterialApp(
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: ThemeData.dark().copyWith(
|
||||||
|
textTheme: ThemeData.dark().textTheme.copyWith(
|
||||||
|
headline1: const TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
helperMaxLines: 10,
|
||||||
|
errorMaxLines: 10,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
|
||||||
filled: true,
|
|
||||||
helperMaxLines: 10,
|
|
||||||
errorMaxLines: 10,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
routes: {
|
||||||
|
WelcomeScreen.ID: (context) => const WelcomeScreen(),
|
||||||
|
MainScreen.ID: (context) => const MainScreen(),
|
||||||
|
LoginScreen.ID: (context) => const LoginScreen(),
|
||||||
|
TimelineScreen.ID: (context) => const TimelineScreen(),
|
||||||
|
GrantPermissionScreen.ID: (context) => const GrantPermissionScreen(),
|
||||||
|
CalendarScreen.ID: (context) => const CalendarScreen(),
|
||||||
|
},
|
||||||
|
initialRoute: widget.initialPage,
|
||||||
),
|
),
|
||||||
routes: {
|
|
||||||
WelcomeScreen.ID: (context) => const WelcomeScreen(),
|
|
||||||
MainScreen.ID: (context) => const MainScreen(),
|
|
||||||
LoginScreen.ID: (context) => const LoginScreen(),
|
|
||||||
TimelineScreen.ID: (context) => const TimelineScreen(),
|
|
||||||
GrantPermissionScreen.ID: (context) => const GrantPermissionScreen(),
|
|
||||||
CalendarScreen.ID: (context) => const CalendarScreen(),
|
|
||||||
},
|
|
||||||
initialRoute: initialPage,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
75
lib/managers/calendar_manager.dart
Normal file
75
lib/managers/calendar_manager.dart
Normal 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
114
lib/models/memories.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,17 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:property_change_notifier/property_change_notifier.dart';
|
import 'package:property_change_notifier/property_change_notifier.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:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
class TimelineModel extends PropertyChangeNotifier<String> {
|
class TimelineModel extends PropertyChangeNotifier<String> {
|
||||||
final Map<String, MemoryPack> _timeline;
|
final Map<DateTime, List<Memory>> _timeline;
|
||||||
|
|
||||||
TimelineModel({
|
TimelineModel({
|
||||||
Map<String, MemoryPack>? timeline,
|
required final List<Memory> memories,
|
||||||
}) : _timeline = timeline ?? {};
|
}) : _timeline = mapFromMemoriesList(memories);
|
||||||
|
|
||||||
RealtimeSubscription? _serverSubscription;
|
RealtimeSubscription? _serverSubscription;
|
||||||
|
|
||||||
@ -22,38 +20,35 @@ class TimelineModel extends PropertyChangeNotifier<String> {
|
|||||||
bool _paused = false;
|
bool _paused = false;
|
||||||
bool _isInitializing = true;
|
bool _isInitializing = true;
|
||||||
|
|
||||||
Map<String, MemoryPack> get values => _timeline;
|
Map<DateTime, List<Memory>> get values => _timeline;
|
||||||
int get length => _timeline.length;
|
int get length => _timeline.length;
|
||||||
int get currentIndex => _currentIndex;
|
int get currentIndex => _currentIndex;
|
||||||
int get memoryIndex => _memoryIndex;
|
int get memoryIndex => _memoryIndex;
|
||||||
bool get paused => _paused;
|
bool get paused => _paused;
|
||||||
bool get isInitializing => _isInitializing;
|
bool get isInitializing => _isInitializing;
|
||||||
|
|
||||||
DateTime dateAtIndex(final int index) =>
|
DateTime dateAtIndex(final int index) => _timeline.keys.elementAt(index);
|
||||||
DateTime.parse(_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);
|
List<Memory> get _currentMemoryPack => atIndex(currentIndex);
|
||||||
bool get _isAtLastMemory =>
|
bool get _isAtLastMemory => _memoryIndex == _currentMemoryPack.length - 1;
|
||||||
_memoryIndex == _currentMemoryPack.memories.length - 1;
|
Memory get currentMemory => _currentMemoryPack.elementAt(_memoryIndex);
|
||||||
Memory get currentMemory =>
|
|
||||||
_currentMemoryPack.memories.elementAt(_memoryIndex);
|
|
||||||
|
|
||||||
void _removeEmptyDates() {
|
void _removeEmptyDates() {
|
||||||
_timeline.removeWhere((key, value) => value.memories.isEmpty);
|
_timeline.removeWhere((key, memories) => memories.isEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
static formatCreationDateKey(final DateTime date) =>
|
static DateTime createDateKey(final DateTime date) =>
|
||||||
DateFormat('yyyy-MM-dd').format(date);
|
DateTime(date.year, date.month, date.day);
|
||||||
|
|
||||||
static Map<String, MemoryPack> mapFromMemoriesList(
|
static Map<DateTime, List<Memory>> mapFromMemoriesList(
|
||||||
final List<Memory> memories,
|
final List<Memory> memories,
|
||||||
) {
|
) {
|
||||||
final map = <String, List<Memory>>{};
|
final map = <DateTime, List<Memory>>{};
|
||||||
|
|
||||||
for (final memory in memories) {
|
for (final memory in memories) {
|
||||||
final key = formatCreationDateKey(memory.creationDate);
|
final key = createDateKey(memory.creationDate);
|
||||||
|
|
||||||
if (map.containsKey(key)) {
|
if (map.containsKey(key)) {
|
||||||
map[key]!.add(memory);
|
map[key]!.add(memory);
|
||||||
@ -62,14 +57,7 @@ class TimelineModel extends PropertyChangeNotifier<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Map.fromEntries(
|
return map;
|
||||||
map.entries.map(
|
|
||||||
(entry) => MapEntry<String, MemoryPack>(
|
|
||||||
entry.key,
|
|
||||||
MemoryPack(entry.value),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -85,7 +73,7 @@ class TimelineModel extends PropertyChangeNotifier<String> {
|
|||||||
|
|
||||||
void setMemoryIndex(final int index) {
|
void setMemoryIndex(final int index) {
|
||||||
_memoryIndex = min(
|
_memoryIndex = min(
|
||||||
_timeline.values.elementAt(_currentIndex).memories.length - 1,
|
_timeline.values.elementAt(_currentIndex).length - 1,
|
||||||
max(0, index),
|
max(0, index),
|
||||||
);
|
);
|
||||||
notifyListeners('memoryIndex');
|
notifyListeners('memoryIndex');
|
||||||
@ -118,7 +106,7 @@ class TimelineModel extends PropertyChangeNotifier<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCurrentIndex(currentIndex - 1);
|
setCurrentIndex(currentIndex - 1);
|
||||||
setMemoryIndex(_currentMemoryPack.memories.length - 1);
|
setMemoryIndex(_currentMemoryPack.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void nextMemory() {
|
void nextMemory() {
|
||||||
@ -137,100 +125,13 @@ class TimelineModel extends PropertyChangeNotifier<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
void refresh(final List<Memory> memories) {
|
||||||
setIsInitializing(true);
|
setIsInitializing(true);
|
||||||
|
|
||||||
await _listenToServer();
|
_timeline.clear();
|
||||||
|
_timeline.addAll(mapFromMemoriesList(memories));
|
||||||
|
_removeEmptyDates();
|
||||||
|
|
||||||
setIsInitializing(false);
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,87 +1,59 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_sticky_header/flutter_sticky_header.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/constants/spacing.dart';
|
||||||
import 'package:share_location/helpers/iterate_months.dart';
|
import 'package:share_location/managers/calendar_manager.dart';
|
||||||
import 'package:share_location/models/calendar.dart';
|
import 'package:share_location/models/memories.dart';
|
||||||
import 'package:share_location/widgets/calendar_month.dart';
|
import 'package:share_location/widgets/calendar_month.dart';
|
||||||
import 'package:share_location/widgets/days_of_week_strip.dart';
|
import 'package:share_location/widgets/days_of_week_strip.dart';
|
||||||
|
|
||||||
class CalendarScreen extends StatefulWidget {
|
class CalendarScreen extends StatelessWidget {
|
||||||
static const ID = 'calendar';
|
static const ID = 'calendar';
|
||||||
|
|
||||||
const CalendarScreen({Key? key}) : super(key: key);
|
const CalendarScreen({
|
||||||
|
Key? key,
|
||||||
@override
|
}) : super(key: key);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (calendar.isInitializing) {
|
final memoriesManager = context.read<Memories>();
|
||||||
return const Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
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(
|
final calendarManager = CalendarManager(memories: memoriesManager.memories);
|
||||||
body: CustomScrollView(
|
final monthMapping = calendarManager.getMappingForList();
|
||||||
reverse: true,
|
|
||||||
slivers: [
|
return Consumer<Memories>(
|
||||||
SliverStickyHeader(
|
builder: (context, memories, _) => Scaffold(
|
||||||
header: Container(
|
body: Padding(
|
||||||
color: theme.canvasColor,
|
padding: const EdgeInsets.symmetric(vertical: MEDIUM_SPACE),
|
||||||
padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE),
|
child: CustomScrollView(
|
||||||
child: const DaysOfWeekStrip(),
|
reverse: true,
|
||||||
),
|
slivers: [
|
||||||
sliver: SliverList(
|
SliverStickyHeader(
|
||||||
delegate: SliverChildBuilderDelegate(
|
header: Container(
|
||||||
(context, index) => CalendarMonth(
|
color: theme.canvasColor,
|
||||||
year: sortedMonthMapping.keys.elementAt(index).year,
|
padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE),
|
||||||
month: sortedMonthMapping.keys.elementAt(index).month,
|
child: const DaysOfWeekStrip(),
|
||||||
dayAmountMap: sortedMonthMapping.values.elementAt(index),
|
),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
childCount: sortedMonthMapping.length,
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:share_location/extensions/date.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/models/timeline.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';
|
||||||
@ -26,7 +27,7 @@ class TimelineScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _TimelineScreenState extends State<TimelineScreen> with Loadable {
|
class _TimelineScreenState extends State<TimelineScreen> with Loadable {
|
||||||
final pageController = PageController();
|
final pageController = PageController();
|
||||||
final timeline = TimelineModel();
|
late final TimelineModel timeline;
|
||||||
bool _ignorePageChanges = false;
|
bool _ignorePageChanges = false;
|
||||||
|
|
||||||
Future<void> _goToPage(final int page) async {
|
Future<void> _goToPage(final int page) async {
|
||||||
@ -45,7 +46,13 @@ class _TimelineScreenState extends State<TimelineScreen> with Loadable {
|
|||||||
initState() {
|
initState() {
|
||||||
super.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
|
// Update page view
|
||||||
timeline.addListener(() async {
|
timeline.addListener(() async {
|
||||||
@ -86,22 +93,16 @@ class _TimelineScreenState extends State<TimelineScreen> with Loadable {
|
|||||||
|
|
||||||
return timeline.values.keys
|
return timeline.values.keys
|
||||||
.toList()
|
.toList()
|
||||||
.indexWhere((date) => DateTime.parse(date).isSameDay(widget.date!));
|
.indexWhere((date) => date.isSameDay(widget.date!));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (timeline.isInitializing) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
await Navigator.pushNamed(context, CalendarScreen.ID);
|
await Navigator.pushReplacementNamed(context, CalendarScreen.ID);
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: ChangeNotifierProvider.value(
|
body: ChangeNotifierProvider.value(
|
||||||
@ -124,7 +125,7 @@ class _TimelineScreenState extends State<TimelineScreen> with Loadable {
|
|||||||
},
|
},
|
||||||
itemBuilder: (_, index) => TimelinePage(
|
itemBuilder: (_, index) => TimelinePage(
|
||||||
date: timeline.dateAtIndex(index),
|
date: timeline.dateAtIndex(index),
|
||||||
memoryPack: timeline.atIndex(index),
|
memories: timeline.atIndex(index),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_calendar_widget/flutter_calendar_widget.dart';
|
import 'package:flutter_calendar_widget/flutter_calendar_widget.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:share_location/constants/spacing.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/screens/timeline_screen.dart';
|
||||||
import 'package:share_location/widgets/delay_render.dart';
|
import 'package:share_location/widgets/delay_render.dart';
|
||||||
import 'package:share_location/widgets/fade_and_move_in_animation.dart';
|
import 'package:share_location/widgets/fade_and_move_in_animation.dart';
|
||||||
@ -37,11 +38,18 @@ class MonthCalendarBuilder extends CalendarBuilder {
|
|||||||
return amount / highestAmountOfEvents;
|
return amount / highestAmountOfEvents;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
final duration = Duration(milliseconds: Random().nextInt(800));
|
final delay = Duration(
|
||||||
|
microseconds:
|
||||||
|
Random().nextInt(CALENDAR_DATE_IN_MAX_DELAY.inMicroseconds));
|
||||||
|
|
||||||
return DelayRender(
|
return DelayRender(
|
||||||
delay: duration,
|
delay: delay,
|
||||||
child: FadeAndMoveInAnimation(
|
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(
|
child: Opacity(
|
||||||
opacity: () {
|
opacity: () {
|
||||||
if (type.isOutSide) {
|
if (type.isOutSide) {
|
||||||
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.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.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_sheet.dart';
|
||||||
@ -11,12 +11,12 @@ import 'package:share_location/widgets/timeline_overlay.dart';
|
|||||||
|
|
||||||
class TimelinePage extends StatefulWidget {
|
class TimelinePage extends StatefulWidget {
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final MemoryPack memoryPack;
|
final List<Memory> memories;
|
||||||
|
|
||||||
const TimelinePage({
|
const TimelinePage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.memoryPack,
|
required this.memories,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -165,14 +165,14 @@ class _TimelinePageState extends State<TimelinePage> {
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: (_, index) => MemorySlide(
|
itemBuilder: (_, index) => MemorySlide(
|
||||||
key: Key(widget.memoryPack.memories[index].filename),
|
key: Key(widget.memories[index].filename),
|
||||||
memory: widget.memoryPack.memories[index],
|
memory: widget.memories[index],
|
||||||
),
|
),
|
||||||
itemCount: widget.memoryPack.memories.length,
|
itemCount: widget.memories.length,
|
||||||
),
|
),
|
||||||
TimelineOverlay(
|
TimelineOverlay(
|
||||||
date: widget.date,
|
date: widget.date,
|
||||||
memoriesAmount: widget.memoryPack.memories.length,
|
memoriesAmount: widget.memories.length,
|
||||||
memoryIndex: timeline.memoryIndex + 1,
|
memoryIndex: timeline.memoryIndex + 1,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user