From b0d0d709dd95edf33756392c92eb99a070ed79ea Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 13 Aug 2022 20:32:03 +0200 Subject: [PATCH] created first prototype of timeline screen --- lib/main.dart | 2 + lib/managers/file_manager.dart | 26 ++++++ lib/screens/timeline_screen.dart | 13 +++ lib/widgets/memory.dart | 139 ++++++++++++++++++++++++++++ lib/widgets/raw_memory_display.dart | 92 ++++++++++++++++++ lib/widgets/timeline_scroll.dart | 63 +++++++++++++ lib/widgets/today_photo_button.dart | 80 +++------------- 7 files changed, 350 insertions(+), 65 deletions(-) create mode 100644 lib/screens/timeline_screen.dart create mode 100644 lib/widgets/memory.dart create mode 100644 lib/widgets/raw_memory_display.dart create mode 100644 lib/widgets/timeline_scroll.dart diff --git a/lib/main.dart b/lib/main.dart index a4a516c..e2eb3b1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:share_location/constants/apis.dart'; 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:supabase_flutter/supabase_flutter.dart'; @@ -58,6 +59,7 @@ class MyApp extends StatelessWidget { WelcomeScreen.ID: (context) => const WelcomeScreen(), MainScreen.ID: (context) => const MainScreen(), LoginScreen.ID: (context) => const LoginScreen(), + TimelineScreen.ID: (context) => const TimelineScreen(), }, initialRoute: initialPage, ); diff --git a/lib/managers/file_manager.dart b/lib/managers/file_manager.dart index 577f2ee..3c38d60 100644 --- a/lib/managers/file_manager.dart +++ b/lib/managers/file_manager.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:share_location/enums.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -9,6 +10,8 @@ const uuid = Uuid(); final supabase = Supabase.instance.client; class FileManager { + static Map fileCache = {}; + static Future getUser(final String userID) async { final response = await supabase .from('users') @@ -68,4 +71,27 @@ class FileManager { return [file.data!, memoryType]; } + + static Future downloadFile( + final String table, + final String path, + ) async { + final key = '$table:$path'; + + if (fileCache.containsKey(key)) { + return fileCache[key]!; + } + + final response = await supabase.storage.from(table).download(path); + + if (response.error != null) { + throw Exception('Error downloading file: ${response.error!.message}'); + } + + final data = response.data!; + + fileCache[key] = data; + + return data; + } } diff --git a/lib/screens/timeline_screen.dart b/lib/screens/timeline_screen.dart new file mode 100644 index 0000000..7ffb656 --- /dev/null +++ b/lib/screens/timeline_screen.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:share_location/widgets/timeline_scroll.dart'; + +class TimelineScreen extends StatelessWidget { + static const ID = 'timeline'; + + const TimelineScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return TimelineScroll(); + } +} diff --git a/lib/widgets/memory.dart b/lib/widgets/memory.dart new file mode 100644 index 0000000..bd92e2b --- /dev/null +++ b/lib/widgets/memory.dart @@ -0,0 +1,139 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:share_location/constants/spacing.dart'; +import 'package:share_location/enums.dart'; +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'; + +enum MemoryFetchStatus { + preparing, + loadingMetadata, + downloading, + error, + done, +} + +class Memory extends StatefulWidget { + final String location; + final DateTime creationDate; + + const Memory({ + Key? key, + required this.location, + required this.creationDate, + }) : super(key: key); + + @override + State createState() => _MemoryState(); +} + +class _MemoryState extends AuthRequiredState { + late final User _user; + MemoryFetchStatus status = MemoryFetchStatus.preparing; + Uint8List? data; + MemoryType? type; + + @override + void initState() { + super.initState(); + + loadMemoryFile(); + } + + @override + void onAuthenticated(Session session) { + final user = session.user; + + if (user != null) { + _user = user; + } + } + + Future loadMemoryFile() async { + final filename = widget.location.split('/').last; + + setState(() { + status = MemoryFetchStatus.loadingMetadata; + }); + + final response = await supabase + .from('memories') + .select() + .eq('location', '${_user.id}/$filename') + .limit(1) + .single() + .execute(); + + if (response.data == null) { + setState(() { + status = MemoryFetchStatus.error; + }); + return; + } + + setState(() { + status = MemoryFetchStatus.downloading; + }); + + final memory = response.data; + final location = memory['location']; + final memoryType = + location.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video; + + try { + final fileData = await FileManager.downloadFile('memories', location); + + setState(() { + status = MemoryFetchStatus.done; + data = fileData; + type = memoryType; + }); + } catch (error) { + setState(() { + status = MemoryFetchStatus.error; + }); + return; + } + } + + @override + Widget build(BuildContext context) { + if (status == MemoryFetchStatus.error) { + return const Center( + child: Text('Memory could not be loaded.'), + ); + } + + if (status == MemoryFetchStatus.done) { + return RawMemoryDisplay( + data: data!, + type: type!, + ); + } + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: SMALL_SPACE), + () { + switch (status) { + // ADD dot loading text + case MemoryFetchStatus.preparing: + return const Text('Preparing to download memory'); + case MemoryFetchStatus.loadingMetadata: + return const Text('Loading memory metadata'); + case MemoryFetchStatus.downloading: + return const Text('Downloading memory'); + default: + return const SizedBox(); + } + }(), + ], + ); + } +} diff --git a/lib/widgets/raw_memory_display.dart b/lib/widgets/raw_memory_display.dart new file mode 100644 index 0000000..fef81d1 --- /dev/null +++ b/lib/widgets/raw_memory_display.dart @@ -0,0 +1,92 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_location/enums.dart'; +import 'package:video_player/video_player.dart'; + +class RawMemoryDisplay extends StatefulWidget { + final Uint8List data; + final MemoryType type; + final String? filename; + + const RawMemoryDisplay({ + Key? key, + required this.data, + required this.type, + this.filename, + }) : super(key: key); + + @override + State createState() => _RawMemoryDisplayState(); +} + +class _RawMemoryDisplayState extends State { + VideoPlayerController? videoController; + + @override + void initState() { + super.initState(); + + if (widget.type == MemoryType.video) { + initializeVideo(); + } + } + + Future createTempVideo() async { + final tempDirectory = await getTemporaryDirectory(); + final path = '${tempDirectory.path}/${widget.filename ?? 'video.mp4'}'; + + if (widget.filename != null) { + // File already exists, so just return the path + return File(path); + } + + // File needs to be created + final file = await File(path).create(); + await file.writeAsBytes(widget.data); + + return file; + } + + Future initializeVideo() async { + final file = await createTempVideo(); + + videoController = VideoPlayerController.file(file); + videoController!.initialize().then((value) { + setState(() {}); + videoController!.setLooping(true); + videoController!.play(); + }); + } + + @override + void dispose() { + videoController?.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + switch (widget.type) { + case MemoryType.photo: + return Image.memory( + widget.data, + fit: BoxFit.cover, + ); + case MemoryType.video: + if (videoController == null) { + return const SizedBox(); + } + + return AspectRatio( + aspectRatio: videoController!.value.aspectRatio, + child: VideoPlayer(videoController!), + ); + default: + throw Exception('Unknown memory type: ${widget.type}'); + } + } +} diff --git a/lib/widgets/timeline_scroll.dart b/lib/widgets/timeline_scroll.dart new file mode 100644 index 0000000..b00eb4f --- /dev/null +++ b/lib/widgets/timeline_scroll.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:share_location/utils/loadable.dart'; +import 'package:share_location/widgets/memory.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +final supabase = Supabase.instance.client; + +class TimelineScroll extends StatefulWidget { + TimelineScroll({ + Key? key, + }) : super(key: key); + + @override + State createState() => _TimelineScrollState(); +} + +class _TimelineScrollState extends State with Loadable { + final pageController = PageController(); + dynamic timeline; + + @override + initState() { + super.initState(); + loadTimeline(); + } + + Future loadTimeline() async { + final response = await supabase + .from('memories') + .select() + .order('created_at', ascending: false) + .execute(); + + setState(() { + timeline = response.data; + }); + } + + @override + Widget build(BuildContext context) { + if (timeline == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + return Scaffold( + body: PageView.builder( + 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']), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/today_photo_button.dart b/lib/widgets/today_photo_button.dart index f0dd94a..3b1fa59 100644 --- a/lib/widgets/today_photo_button.dart +++ b/lib/widgets/today_photo_button.dart @@ -1,13 +1,13 @@ -import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:share_location/constants/spacing.dart'; import 'package:share_location/enums.dart'; -import 'package:video_player/video_player.dart'; +import 'package:share_location/screens/timeline_screen.dart'; -class TodayPhotoButton extends StatefulWidget { +import 'raw_memory_display.dart'; + +class TodayPhotoButton extends StatelessWidget { final Uint8List? data; final MemoryType? type; @@ -17,45 +17,12 @@ class TodayPhotoButton extends StatefulWidget { this.type, }) : super(key: key); - @override - State createState() => _TodayPhotoButtonState(); -} - -class _TodayPhotoButtonState extends State { - VideoPlayerController? videoController; - - @override - void initState() { - super.initState(); - - if (widget.type == MemoryType.video) { - initializeVideo(); - } - } - - Future initializeVideo() async { - final tempDir = await getTemporaryDirectory(); - final file = await File('${tempDir.path}/video.mp4').create(); - file.writeAsBytesSync(widget.data!); - - videoController = VideoPlayerController.file(file); - videoController!.initialize().then((value) { - setState(() {}); - videoController!.setLooping(true); - videoController!.play(); - }); - } - - @override - void dispose() { - videoController?.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return InkWell( + onTap: () { + Navigator.pushNamed(context, TimelineScreen.ID); + }, child: Container( width: 45, height: 45, @@ -68,31 +35,14 @@ class _TodayPhotoButtonState extends State { color: Colors.grey, ), child: ClipRRect( - borderRadius: BorderRadius.circular(SMALL_SPACE), - child: () { - if (widget.data == null) { - return SizedBox(); - } - - switch (widget.type) { - case MemoryType.photo: - return Image.memory( - widget.data as Uint8List, - fit: BoxFit.cover, - ); - case MemoryType.video: - if (videoController == null) { - return const SizedBox(); - } - - return AspectRatio( - aspectRatio: videoController!.value.aspectRatio, - child: VideoPlayer(videoController!), - ); - default: - return const SizedBox(); - } - }()), + borderRadius: BorderRadius.circular(SMALL_SPACE), + child: (data == null || type == null) + ? const SizedBox() + : RawMemoryDisplay( + data: data!, + type: type!, + ), + ), ), ); }