mirror of
https://github.com/Myzel394/quid_faciam_hodie.git
synced 2025-06-19 07:35:26 +02:00
created first prototype of timeline screen
This commit is contained in:
parent
fbcc2982c6
commit
b0d0d709dd
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:share_location/constants/apis.dart';
|
import 'package:share_location/constants/apis.dart';
|
||||||
import 'package:share_location/screens/login_screen.dart';
|
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/welcome_screen.dart';
|
import 'package:share_location/screens/welcome_screen.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ class MyApp extends StatelessWidget {
|
|||||||
WelcomeScreen.ID: (context) => const WelcomeScreen(),
|
WelcomeScreen.ID: (context) => const WelcomeScreen(),
|
||||||
MainScreen.ID: (context) => const MainScreen(),
|
MainScreen.ID: (context) => const MainScreen(),
|
||||||
LoginScreen.ID: (context) => const LoginScreen(),
|
LoginScreen.ID: (context) => const LoginScreen(),
|
||||||
|
TimelineScreen.ID: (context) => const TimelineScreen(),
|
||||||
},
|
},
|
||||||
initialRoute: initialPage,
|
initialRoute: initialPage,
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:share_location/enums.dart';
|
import 'package:share_location/enums.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
@ -9,6 +10,8 @@ const uuid = Uuid();
|
|||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
class FileManager {
|
class FileManager {
|
||||||
|
static Map<String, Uint8List> fileCache = {};
|
||||||
|
|
||||||
static Future<User> getUser(final String userID) async {
|
static Future<User> getUser(final String userID) async {
|
||||||
final response = await supabase
|
final response = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
@ -68,4 +71,27 @@ class FileManager {
|
|||||||
|
|
||||||
return [file.data!, memoryType];
|
return [file.data!, memoryType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Uint8List> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
lib/screens/timeline_screen.dart
Normal file
13
lib/screens/timeline_screen.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
139
lib/widgets/memory.dart
Normal file
139
lib/widgets/memory.dart
Normal file
@ -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<Memory> createState() => _MemoryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MemoryState extends AuthRequiredState<Memory> {
|
||||||
|
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<void> 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: <Widget>[
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
92
lib/widgets/raw_memory_display.dart
Normal file
92
lib/widgets/raw_memory_display.dart
Normal file
@ -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<RawMemoryDisplay> createState() => _RawMemoryDisplayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RawMemoryDisplayState extends State<RawMemoryDisplay> {
|
||||||
|
VideoPlayerController? videoController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (widget.type == MemoryType.video) {
|
||||||
|
initializeVideo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> 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<void> 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}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
lib/widgets/timeline_scroll.dart
Normal file
63
lib/widgets/timeline_scroll.dart
Normal file
@ -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<TimelineScroll> createState() => _TimelineScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineScrollState extends State<TimelineScroll> with Loadable {
|
||||||
|
final pageController = PageController();
|
||||||
|
dynamic timeline;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
loadTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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']),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:share_location/constants/spacing.dart';
|
import 'package:share_location/constants/spacing.dart';
|
||||||
import 'package:share_location/enums.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 Uint8List? data;
|
||||||
final MemoryType? type;
|
final MemoryType? type;
|
||||||
|
|
||||||
@ -17,45 +17,12 @@ class TodayPhotoButton extends StatefulWidget {
|
|||||||
this.type,
|
this.type,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
State<TodayPhotoButton> createState() => _TodayPhotoButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TodayPhotoButtonState extends State<TodayPhotoButton> {
|
|
||||||
VideoPlayerController? videoController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
if (widget.type == MemoryType.video) {
|
|
||||||
initializeVideo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pushNamed(context, TimelineScreen.ID);
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 45,
|
width: 45,
|
||||||
height: 45,
|
height: 45,
|
||||||
@ -68,31 +35,14 @@ class _TodayPhotoButtonState extends State<TodayPhotoButton> {
|
|||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(SMALL_SPACE),
|
borderRadius: BorderRadius.circular(SMALL_SPACE),
|
||||||
child: () {
|
child: (data == null || type == null)
|
||||||
if (widget.data == null) {
|
? const SizedBox()
|
||||||
return SizedBox();
|
: RawMemoryDisplay(
|
||||||
}
|
data: data!,
|
||||||
|
type: type!,
|
||||||
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();
|
|
||||||
}
|
|
||||||
}()),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user