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();
}
Future<bool> doesEmailExist() 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
Future<void> _signUp() async {
final response = await supabase.auth.signUp(
emailController.text.trim(),
passwordController.text,
@ -65,15 +37,45 @@ class _LoginScreenState extends AuthState<LoginScreen> with Loadable {
final error = response.error;
if (mounted) {
if (error != null) {
context.showErrorSnackBar(message: error.message);
} else {
throw Exception(error);
}
}
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);
}
}
}
}
@override
Widget build(BuildContext context) {

View File

@ -62,7 +62,7 @@ class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
),
const SizedBox(height: MEDIUM_SPACE),
const Text(
'Please grant permissions to use this app',
'Please grant the following permissions to use this app',
),
const SizedBox(height: LARGE_SPACE),
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/widgets/raw_memory_display.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:video_player/video_player.dart';
enum MemoryFetchStatus {
preparing,
@ -16,21 +17,27 @@ enum MemoryFetchStatus {
done,
}
class Memory extends StatefulWidget {
class MemoryView extends StatefulWidget {
final String location;
final DateTime creationDate;
final bool loopVideo;
final void Function(VideoPlayerController)? onVideoControllerInitialized;
final VoidCallback? onFileDownloaded;
const Memory({
const MemoryView({
Key? key,
required this.location,
required this.creationDate,
this.loopVideo = false,
this.onVideoControllerInitialized,
this.onFileDownloaded,
}) : super(key: key);
@override
State<Memory> createState() => _MemoryState();
State<MemoryView> createState() => _MemoryViewState();
}
class _MemoryState extends AuthRequiredState<Memory> {
class _MemoryViewState extends AuthRequiredState<MemoryView> {
late final User _user;
MemoryFetchStatus status = MemoryFetchStatus.preparing;
Uint8List? data;
@ -67,6 +74,10 @@ class _MemoryState extends AuthRequiredState<Memory> {
.single()
.execute();
if (!mounted) {
return;
}
if (response.data == null) {
setState(() {
status = MemoryFetchStatus.error;
@ -86,12 +97,24 @@ class _MemoryState extends AuthRequiredState<Memory> {
try {
final fileData = await FileManager.downloadFile('memories', location);
if (!mounted) {
return;
}
setState(() {
status = MemoryFetchStatus.done;
data = fileData;
type = memoryType;
});
if (widget.onFileDownloaded != null) {
widget.onFileDownloaded!();
}
} catch (error) {
if (!mounted) {
return;
}
setState(() {
status = MemoryFetchStatus.error;
});
@ -111,6 +134,8 @@ class _MemoryState extends AuthRequiredState<Memory> {
return RawMemoryDisplay(
data: data!,
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 bool loopVideo;
final String? filename;
final void Function(VideoPlayerController)? onVideoControllerInitialized;
const RawMemoryDisplay({
Key? key,
@ -18,6 +19,7 @@ class RawMemoryDisplay extends StatefulWidget {
required this.type,
this.loopVideo = false,
this.filename,
this.onVideoControllerInitialized,
}) : super(key: key);
@override
@ -60,6 +62,10 @@ class _RawMemoryDisplayState extends State<RawMemoryDisplay> {
setState(() {});
videoController!.setLooping(widget.loopVideo);
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:intl/intl.dart';
import 'package:share_location/foreign_types/memory.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';
final supabase = Supabase.instance.client;
@ -30,12 +32,32 @@ class _TimelineScrollState extends State<TimelineScroll> with Loadable {
.select()
.order('created_at', ascending: false)
.execute();
final memories = List<Memory>.from(
List<Map<String, dynamic>>.from(response.data).map(Memory.parse));
final timelineMapped = convertMemoriesToTimeline(memories);
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
Widget build(BuildContext context) {
if (timeline == null) {
@ -49,13 +71,17 @@ class _TimelineScrollState extends State<TimelineScroll> with Loadable {
controller: pageController,
scrollDirection: Axis.vertical,
itemCount: timeline.length,
itemBuilder: (_, index) => Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Memory(
location: timeline[index]['location'],
creationDate: DateTime.parse(timeline[index]['created_at']),
),
itemBuilder: (_, index) => MemoryPage(
date: DateTime.parse(timeline.keys.toList()[index]),
memories: timeline.values.toList()[index],
onNextTimeline: () {
pageController.nextPage(
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"
source: hosted
version: "4.0.1"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
js:
dependency: transitive
description:
@ -443,6 +450,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:

View File

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