created basic timeline

This commit is contained in:
Myzel394 2022-08-14 14:02:33 +02:00
parent 3904c4dbef
commit 7558d1b86b
13 changed files with 521 additions and 47 deletions

View File

@ -0,0 +1,50 @@
import 'package:property_change_notifier/property_change_notifier.dart';
class MemorySlideController extends PropertyChangeNotifier<String> {
final int memoryLength;
MemorySlideController({
required this.memoryLength,
});
int _index = 0;
bool _paused = false;
bool _done = false;
bool _completed = false;
bool get paused => _paused;
bool get done => _done;
int get index => _index;
bool get completed => _completed;
void setPaused(bool paused) {
_paused = paused;
notifyListeners('paused');
}
void setDone() {
_done = true;
notifyListeners('done');
}
void next() {
if (_index == memoryLength - 1) {
_completed = true;
notifyListeners('completed');
} else {
_paused = false;
_done = false;
_index++;
notifyListeners();
}
}
void reset() {
_completed = false;
_paused = false;
_done = false;
_index = 0;
notifyListeners();
}
}

View File

@ -0,0 +1,29 @@
import 'package:property_change_notifier/property_change_notifier.dart';
class StatusController extends PropertyChangeNotifier<String> {
final Duration duration;
bool _isForwarding = false;
bool _done = false;
bool get isForwarding => _isForwarding;
bool get done => _done;
StatusController({
this.duration = const Duration(seconds: 4),
});
void start() {
_isForwarding = true;
notifyListeners('isForwarding');
}
void stop() {
_isForwarding = false;
notifyListeners('isForwarding');
}
void setDone() {
_done = true;
notifyListeners('done');
}
}

View File

@ -0,0 +1,30 @@
import 'package:share_location/enums.dart';
class Memory {
final String id;
final DateTime creationDate;
final String location;
final bool isPublic;
final String userID;
const Memory({
required this.id,
required this.creationDate,
required this.location,
required this.isPublic,
required this.userID,
});
static parse(Map<String, dynamic> jsonData) {
return Memory(
id: jsonData['id'],
creationDate: DateTime.parse(jsonData['created_at']),
location: jsonData['location'],
isPublic: jsonData['is_public'],
userID: jsonData['user'],
);
}
MemoryType get type =>
location.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video;
}

View File

@ -29,35 +29,7 @@ class _LoginScreenState extends AuthState<LoginScreen> with Loadable {
super.dispose(); super.dispose();
} }
Future<bool> doesEmailExist() async { Future<void> _signUp() async {
// TODO: SECURE PROFILE READ ACCESS TO ONLY ALLOW ACCESS TO EMAIL ADDRESSES
final response = await supabase
.from('profiles')
.select()
.match({'username': emailController.text.trim()}).execute();
return response.data.isNotEmpty;
}
Future<void> signIn() async {
if (await doesEmailExist()) {
// Login User
final response = await supabase.auth.signIn(
email: emailController.text.trim(),
password: passwordController.text,
);
final error = response.error;
if (mounted) {
if (error != null) {
context.showErrorSnackBar(message: error.message);
} else {
emailController.clear();
passwordController.clear();
}
}
} else {
// Sign up User
final response = await supabase.auth.signUp( final response = await supabase.auth.signUp(
emailController.text.trim(), emailController.text.trim(),
passwordController.text, passwordController.text,
@ -65,15 +37,45 @@ class _LoginScreenState extends AuthState<LoginScreen> with Loadable {
final error = response.error; final error = response.error;
if (mounted) {
if (error != null) { if (error != null) {
context.showErrorSnackBar(message: error.message); throw Exception(error);
} else { }
}
Future<void> _signIn() async {
final response = await supabase.auth.signIn(
email: emailController.text.trim(),
password: passwordController.text,
);
final error = response.error;
if (error != null) {
throw Exception(error);
}
}
Future<void> signIn() async {
try {
await _signUp();
} catch (error) {
try {
await _signIn();
} catch (error) {
if (mounted) {
context.showErrorSnackBar(message: error.toString());
emailController.clear();
passwordController.clear();
}
return;
}
}
if (mounted) {
Navigator.pushReplacementNamed(context, MainScreen.ID); Navigator.pushReplacementNamed(context, MainScreen.ID);
} }
} }
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -62,7 +62,7 @@ class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
), ),
const SizedBox(height: MEDIUM_SPACE), const SizedBox(height: MEDIUM_SPACE),
const Text( const Text(
'Please grant permissions to use this app', 'Please grant the following permissions to use this app',
), ),
const SizedBox(height: LARGE_SPACE), const SizedBox(height: LARGE_SPACE),
if (hasDeniedForever) ...[ if (hasDeniedForever) ...[

View File

@ -7,6 +7,7 @@ import 'package:share_location/managers/file_manager.dart';
import 'package:share_location/utils/auth_required.dart'; import 'package:share_location/utils/auth_required.dart';
import 'package:share_location/widgets/raw_memory_display.dart'; import 'package:share_location/widgets/raw_memory_display.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:video_player/video_player.dart';
enum MemoryFetchStatus { enum MemoryFetchStatus {
preparing, preparing,
@ -16,21 +17,27 @@ enum MemoryFetchStatus {
done, done,
} }
class Memory extends StatefulWidget { class MemoryView extends StatefulWidget {
final String location; final String location;
final DateTime creationDate; final DateTime creationDate;
final bool loopVideo;
final void Function(VideoPlayerController)? onVideoControllerInitialized;
final VoidCallback? onFileDownloaded;
const Memory({ const MemoryView({
Key? key, Key? key,
required this.location, required this.location,
required this.creationDate, required this.creationDate,
this.loopVideo = false,
this.onVideoControllerInitialized,
this.onFileDownloaded,
}) : super(key: key); }) : super(key: key);
@override @override
State<Memory> createState() => _MemoryState(); State<MemoryView> createState() => _MemoryViewState();
} }
class _MemoryState extends AuthRequiredState<Memory> { class _MemoryViewState extends AuthRequiredState<MemoryView> {
late final User _user; late final User _user;
MemoryFetchStatus status = MemoryFetchStatus.preparing; MemoryFetchStatus status = MemoryFetchStatus.preparing;
Uint8List? data; Uint8List? data;
@ -67,6 +74,10 @@ class _MemoryState extends AuthRequiredState<Memory> {
.single() .single()
.execute(); .execute();
if (!mounted) {
return;
}
if (response.data == null) { if (response.data == null) {
setState(() { setState(() {
status = MemoryFetchStatus.error; status = MemoryFetchStatus.error;
@ -86,12 +97,24 @@ class _MemoryState extends AuthRequiredState<Memory> {
try { try {
final fileData = await FileManager.downloadFile('memories', location); final fileData = await FileManager.downloadFile('memories', location);
if (!mounted) {
return;
}
setState(() { setState(() {
status = MemoryFetchStatus.done; status = MemoryFetchStatus.done;
data = fileData; data = fileData;
type = memoryType; type = memoryType;
}); });
if (widget.onFileDownloaded != null) {
widget.onFileDownloaded!();
}
} catch (error) { } catch (error) {
if (!mounted) {
return;
}
setState(() { setState(() {
status = MemoryFetchStatus.error; status = MemoryFetchStatus.error;
}); });
@ -111,6 +134,8 @@ class _MemoryState extends AuthRequiredState<Memory> {
return RawMemoryDisplay( return RawMemoryDisplay(
data: data!, data: data!,
type: type!, type: type!,
loopVideo: widget.loopVideo,
onVideoControllerInitialized: widget.onVideoControllerInitialized,
); );
} }

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.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/widgets/memory_slide.dart';
class MemoryPage extends StatefulWidget {
final DateTime date;
final List<Memory> memories;
final VoidCallback onPreviousTimeline;
final VoidCallback onNextTimeline;
const MemoryPage({
Key? key,
required this.date,
required this.memories,
required this.onPreviousTimeline,
required this.onNextTimeline,
}) : super(key: key);
@override
State<MemoryPage> createState() => _MemoryPageState();
}
class _MemoryPageState extends State<MemoryPage> {
late final MemorySlideController controller;
@override
void initState() {
super.initState();
controller = MemorySlideController(memoryLength: widget.memories.length);
controller.addListener(() {
if (controller.done) {
controller.next();
// Force UI update
setState(() {});
}
}, ['done']);
controller.addListener(() {
if (controller.completed) {
widget.onNextTimeline();
}
}, ['completed']);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) {
controller.setPaused(true);
},
onTapUp: (_) {
controller.setPaused(false);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
MemorySlide(
key: Key(controller.index.toString()),
controller: controller,
memory: widget.memories[controller.index],
),
Padding(
padding: const EdgeInsets.only(
top: LARGE_SPACE, left: MEDIUM_SPACE, right: MEDIUM_SPACE),
child: Text(
DateFormat('dd. MMMM yyyy').format(widget.date),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline1,
),
),
],
),
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.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';
import 'package:share_location/widgets/status.dart';
import 'memory.dart';
const BAR_HEIGHT = 4.0;
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
State<MemorySlide> createState() => _MemorySlideState();
}
class _MemorySlideState extends State<MemorySlide>
with TickerProviderStateMixin {
StatusController? controller;
Duration? duration;
@override
void initState() {
super.initState();
widget.controller.addListener(() {
if (!mounted) {
return;
}
if (widget.controller.paused) {
controller?.stop();
} else {
controller?.start();
}
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
void initializeAnimation(final Duration duration) {
this.duration = duration;
controller = StatusController(
duration: duration,
)..addListener(widget.controller.setDone, ['done']);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Status(
controller: controller,
child: MemoryView(
creationDate: widget.memory.creationDate,
location: widget.memory.location,
loopVideo: false,
onFileDownloaded: () {
if (widget.memory.type == MemoryType.photo) {
initializeAnimation(DEFAULT_IMAGE_DURATION);
}
},
onVideoControllerInitialized: (controller) {
if (mounted) {
initializeAnimation(controller.value.duration);
widget.controller.addListener(() {
if (!mounted) {
return;
}
if (widget.controller.paused) {
controller.pause();
} else {
controller.play();
}
});
}
},
),
);
}
}

View File

@ -11,6 +11,7 @@ class RawMemoryDisplay extends StatefulWidget {
final MemoryType type; final MemoryType type;
final bool loopVideo; final bool loopVideo;
final String? filename; final String? filename;
final void Function(VideoPlayerController)? onVideoControllerInitialized;
const RawMemoryDisplay({ const RawMemoryDisplay({
Key? key, Key? key,
@ -18,6 +19,7 @@ class RawMemoryDisplay extends StatefulWidget {
required this.type, required this.type,
this.loopVideo = false, this.loopVideo = false,
this.filename, this.filename,
this.onVideoControllerInitialized,
}) : super(key: key); }) : super(key: key);
@override @override
@ -60,6 +62,10 @@ class _RawMemoryDisplayState extends State<RawMemoryDisplay> {
setState(() {}); setState(() {});
videoController!.setLooping(widget.loopVideo); videoController!.setLooping(widget.loopVideo);
videoController!.play(); videoController!.play();
if (widget.onVideoControllerInitialized != null) {
widget.onVideoControllerInitialized!(videoController!);
}
}); });
} }

105
lib/widgets/status.dart Normal file
View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/controllers/status_controller.dart';
const BAR_HEIGHT = 4.0;
class Status extends StatefulWidget {
final StatusController? controller;
final Widget child;
const Status({
Key? key,
required this.child,
this.controller,
}) : super(key: key);
@override
State<Status> createState() => _StatusState();
}
class _StatusState extends State<Status> with TickerProviderStateMixin {
late final Animation<double> animation;
AnimationController? animationController;
@override
void didUpdateWidget(covariant Status oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != null && animationController == null) {
initializeAnimation();
}
}
@override
void dispose() {
animationController?.dispose();
super.dispose();
}
void initializeAnimation() {
animationController = AnimationController(
duration: widget.controller!.duration,
vsync: this,
);
animation =
Tween<double>(begin: 0.0, end: 1.0).animate(animationController!)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
widget.controller!.setDone();
}
});
animationController!.forward();
widget.controller!.addListener(() {
if (widget.controller!.isForwarding) {
animationController!.forward();
} else {
animationController!.stop();
}
});
}
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: <Widget>[
widget.child,
Positioned(
left: 0,
bottom: SMALL_SPACE,
width: MediaQuery.of(context).size.width,
height: BAR_HEIGHT,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: SMALL_SPACE),
child: (widget.controller == null)
? ClipRRect(
borderRadius: BorderRadius.circular(HUGE_SPACE),
child: LinearProgressIndicator(
value: null,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white.withOpacity(.3)),
backgroundColor: Colors.white.withOpacity(0.1),
),
)
: AnimatedBuilder(
animation: animation,
builder: (_, __) => ClipRRect(
borderRadius: BorderRadius.circular(HUGE_SPACE),
child: LinearProgressIndicator(
value: animation.value,
valueColor:
const AlwaysStoppedAnimation<Color>(Colors.white),
backgroundColor: Colors.white.withOpacity(0.1),
),
),
),
),
),
],
);
}
}

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:share_location/foreign_types/memory.dart';
import 'package:share_location/utils/loadable.dart'; import 'package:share_location/utils/loadable.dart';
import 'package:share_location/widgets/memory.dart'; import 'package:share_location/widgets/memory_page.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;
@ -30,12 +32,32 @@ class _TimelineScrollState extends State<TimelineScroll> with Loadable {
.select() .select()
.order('created_at', ascending: false) .order('created_at', ascending: false)
.execute(); .execute();
final memories = List<Memory>.from(
List<Map<String, dynamic>>.from(response.data).map(Memory.parse));
final timelineMapped = convertMemoriesToTimeline(memories);
setState(() { setState(() {
timeline = response.data; timeline = timelineMapped;
}); });
} }
static Map<String, List<Memory>> convertMemoriesToTimeline(
final List<Memory> memories,
) {
final map = <String, List<Memory>>{};
for (final memory in memories) {
final date = DateFormat('yyyy-MM-dd').format(memory.creationDate);
if (map.containsKey(date)) {
map[date]!.add(memory);
} else {
map[date] = [memory];
}
}
return map;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (timeline == null) { if (timeline == null) {
@ -49,13 +71,17 @@ class _TimelineScrollState extends State<TimelineScroll> with Loadable {
controller: pageController, controller: pageController,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
itemCount: timeline.length, itemCount: timeline.length,
itemBuilder: (_, index) => Container( itemBuilder: (_, index) => MemoryPage(
width: MediaQuery.of(context).size.width, date: DateTime.parse(timeline.keys.toList()[index]),
height: MediaQuery.of(context).size.height, memories: timeline.values.toList()[index],
child: Memory( onNextTimeline: () {
location: timeline[index]['location'], pageController.nextPage(
creationDate: DateTime.parse(timeline[index]['created_at']), duration: Duration(milliseconds: 500), curve: Curves.ease);
), },
onPreviousTimeline: () {
pageController.previousPage(
duration: Duration(milliseconds: 500), curve: Curves.ease);
},
), ),
), ),
); );

View File

@ -254,6 +254,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" version: "4.0.1"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -443,6 +450,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
property_change_notifier:
dependency: "direct main"
description:
name: property_change_notifier
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:

View File

@ -42,6 +42,8 @@ dependencies:
uuid: ^3.0.6 uuid: ^3.0.6
video_player: ^2.4.6 video_player: ^2.4.6
path_provider: ^2.0.11 path_provider: ^2.0.11
intl: ^0.17.0
property_change_notifier: ^0.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: