added main screen & login screen

This commit is contained in:
Myzel394 2022-08-13 16:18:26 +02:00
parent 60d830aca4
commit fbcc2982c6
24 changed files with 1410 additions and 11 deletions

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {
@ -44,10 +44,10 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.share_location"
applicationId "floss.myzel394.quid_faciam_hodie"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion flutter.minSdkVersion
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@ -23,6 +23,10 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<data
android:scheme="floss.myzel394.quid_faciam_hodie"
android:host="login-callback" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
@ -31,4 +35,6 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -45,5 +45,31 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Taking photos is a crucial part of the app.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Recording videos is a crucial part of the app.</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>floss.myzel394.quid_faciam_hodie</string>
</array>
</dict>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

3
lib/constants/apis.dart Normal file
View File

@ -0,0 +1,3 @@
const SUPABASE_API_URL = 'https://gmqzelvauqziurlloawb.supabase.co';
const SUPABASE_API_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImdtcXplbHZhdXF6aXVybGxvYXdiIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjAzODE5MDcsImV4cCI6MTk3NTk1NzkwN30.D_964EIlD9WRFnG6MWtQtmIg04eMBbZhIEF7zl--bKw';

View File

@ -0,0 +1 @@
const APP_ID = 'floss.myzel394.quid_faciam_hodie';

View File

@ -1,6 +1,6 @@
const SPACE_MULTIPLIER = 2.0;
const SMALL_SPACE = SPACE_MULTIPLIER * 1;
const SMALL_SPACE = SPACE_MULTIPLIER * 5;
const MEDIUM_SPACE = SPACE_MULTIPLIER * 10;
const LARGE_SPACE = SPACE_MULTIPLIER * 20;
const HUGE_SPACE = SPACE_MULTIPLIER * 40;

4
lib/enums.dart Normal file
View File

@ -0,0 +1,4 @@
enum MemoryType {
photo,
video,
}

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
extension ShowSnackBar on BuildContext {
void showSnackBar({
required String message,
Color backgroundColor = Colors.white,
}) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
content: Text(message),
backgroundColor: backgroundColor,
));
}
void showErrorSnackBar({required String message}) {
showSnackBar(message: message, backgroundColor: Colors.red);
}
}

3
lib/global_values.dart Normal file
View File

@ -0,0 +1,3 @@
import 'package:camera/camera.dart';
List<CameraDescription> cameras = [];

View File

@ -1,9 +1,26 @@
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
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/welcome_screen.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'managers/global_values_manager.dart';
import 'managers/startup_page_manager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: SUPABASE_API_URL,
anonKey: SUPABASE_API_KEY,
debug: kDebugMode,
);
GlobalValuesManager.setCameras(await availableCameras());
final initialPage = await StartupPageManager.getPage();
runApp(MyApp(initialPage: initialPage));
@ -17,21 +34,30 @@ class MyApp extends StatelessWidget {
required this.initialPage,
}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark().copyWith(
textTheme: ThemeData.dark().textTheme.copyWith(
headline1: TextStyle(
headline1: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w500,
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
helperMaxLines: 10,
errorMaxLines: 10,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
routes: {
WelcomeScreen.ID: (context) => WelcomeScreen(),
WelcomeScreen.ID: (context) => const WelcomeScreen(),
MainScreen.ID: (context) => const MainScreen(),
LoginScreen.ID: (context) => const LoginScreen(),
},
initialRoute: initialPage,
);

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:share_location/extensions/snackbar.dart';
import 'package:share_location/screens/login_screen.dart';
import 'package:share_location/screens/main_screen.dart';
import 'package:supabase/supabase.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
@override
void onUnauthenticated() {
if (mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(LoginScreen.ID, (route) => false);
}
}
@override
void onAuthenticated(Session session) {
if (mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(MainScreen.ID, (route) => false);
}
}
@override
void onPasswordRecovery(Session session) {}
@override
void onErrorAuthenticating(String message) {
context.showErrorSnackBar(message: message);
}
}

View File

@ -0,0 +1,71 @@
import 'dart:io';
import 'package:share_location/enums.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
const uuid = Uuid();
final supabase = Supabase.instance.client;
class FileManager {
static Future<User> getUser(final String userID) async {
final response = await supabase
.from('users')
.select()
.eq('id', userID)
.single()
.execute();
return response.data;
}
static uploadFile(final User user, final File file) async {
final basename = uuid.v4();
final extension = file.path.split('.').last;
final filename = '$basename.$extension';
final path = '${user.id}/$filename';
final response = await supabase.storage.from('memories').upload(path, file);
if (response.error != null) {
throw Exception('Error uploading file: ${response.error!.message}');
}
final memoryResponse = await supabase.from('memories').insert({
'user': user.id,
'location': path,
}).execute();
if (memoryResponse.error != null) {
throw Exception('Error creating memory: ${response.error!.message}');
}
}
static Future<List?> getLastFile(final User user) async {
final response = await supabase
.from('memories')
.select()
.eq('user', user.id)
.order('created_at', ascending: false)
.limit(1)
.single()
.execute();
if (response.data == null) {
return null;
}
final memory = response.data;
final location = memory['location'];
final memoryType =
location.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video;
final file = await supabase.storage.from('memories').download(location);
if (file.error != null) {
return null;
}
return [file.data!, memoryType];
}
}

View File

@ -0,0 +1,15 @@
import 'package:camera/camera.dart';
class GlobalValuesManager {
static List<CameraDescription> _cameras = [];
static List<CameraDescription> get cameras => [..._cameras];
static void setCameras(List<CameraDescription> cameras) {
if (_cameras.isNotEmpty) {
throw Exception('Cameras already set');
}
_cameras = cameras;
}
}

View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/extensions/snackbar.dart';
import 'package:share_location/managers/authentication_manager.dart';
import 'package:share_location/screens/main_screen.dart';
import 'package:share_location/utils/loadable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
class LoginScreen extends StatefulWidget {
static const ID = 'login';
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends AuthState<LoginScreen> with Loadable {
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
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
final response = await supabase.auth.signUp(
emailController.text.trim(),
passwordController.text,
);
final error = response.error;
if (mounted) {
if (error != null) {
context.showErrorSnackBar(message: error.message);
} else {
Navigator.pushReplacementNamed(context, MainScreen.ID);
}
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(MEDIUM_SPACE),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'Login',
style: theme.textTheme.headline1,
),
const SizedBox(height: LARGE_SPACE),
const Text(
'Sign in to your account. If you do not have one already, we will automatically set up one for you.',
),
const SizedBox(height: MEDIUM_SPACE),
TextFormField(
controller: emailController,
autofocus: true,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: SMALL_SPACE),
TextFormField(
obscureText: true,
controller: passwordController,
decoration: const InputDecoration(
labelText: 'Password',
),
),
const SizedBox(height: MEDIUM_SPACE),
ElevatedButton.icon(
icon: const Icon(Icons.arrow_right),
label: const Text('Login'),
onPressed: isLoading ? null : () => callWithLoading(signIn),
)
],
),
),
);
}
}

View File

@ -0,0 +1,261 @@
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/extensions/snackbar.dart';
import 'package:share_location/managers/file_manager.dart';
import 'package:share_location/managers/global_values_manager.dart';
import 'package:share_location/screens/main_screen/permissions_required_page.dart';
import 'package:share_location/utils/auth_required.dart';
import 'package:share_location/utils/loadable.dart';
import 'package:share_location/widgets/camera_button.dart';
import 'package:share_location/widgets/change_camera_button.dart';
import 'package:share_location/widgets/today_photo_button.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class MainScreen extends StatefulWidget {
static const ID = 'main';
const MainScreen({Key? key}) : super(key: key);
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
bool isRecording = false;
bool hasGrantedPermissions = false;
List? lastPhoto;
late User _user;
CameraController? controller;
@override
bool get isLoading =>
super.isLoading || controller == null || !controller!.value.isInitialized;
@override
void initState() {
super.initState();
callWithLoading(getLastPhoto);
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final CameraController? cameraController = controller;
// App state changed before we got the chance to initialize.
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
onNewCameraSelected(cameraController.description);
}
}
@override
void onAuthenticated(Session session) {
final user = session.user;
if (user != null) {
_user = user;
}
}
Future<void> getLastPhoto() async {
final data = await FileManager.getLastFile(_user);
setState(() {
lastPhoto = data;
});
}
void onNewCameraSelected(final CameraDescription cameraDescription) async {
final previousCameraController = controller;
// Instantiating the camera controller
final CameraController cameraController = CameraController(
cameraDescription,
ResolutionPreset.high,
imageFormatGroup: ImageFormatGroup.jpeg,
);
cameraController.setFlashMode(FlashMode.off);
await previousCameraController?.dispose();
controller = cameraController;
// Update UI if controller updates
controller!.addListener(() {
if (mounted) setState(() {});
});
controller!.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
Future<void> takePhoto() async {
if (controller!.value.isTakingPicture) {
return;
}
controller!.setFlashMode(FlashMode.off);
final file = File((await controller!.takePicture()).path);
try {
await FileManager.uploadFile(_user, file);
} catch (error) {
if (mounted) {
context.showErrorSnackBar(message: error.toString());
}
return;
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Photo saved.'),
backgroundColor: Colors.green,
),
);
await getLastPhoto();
}
}
Future<void> takeVideo() async {
if (!controller!.value.isRecordingVideo) {
// Recording has already been stopped
return;
}
setState(() {
isRecording = false;
});
final file = File((await controller!.stopVideoRecording()).path);
try {
await FileManager.uploadFile(_user, file);
} catch (error) {
if (mounted) {
context.showErrorSnackBar(message: error.toString());
}
return;
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video saved.'),
backgroundColor: Colors.green,
),
);
await getLastPhoto();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: () {
if (!hasGrantedPermissions) {
return Center(
child: PermissionsRequiredPage(
onPermissionsGranted: () {
onNewCameraSelected(GlobalValuesManager.cameras[0]);
setState(() {
hasGrantedPermissions = true;
});
},
),
);
}
if (isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
controller!.buildPreview(),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(LARGE_SPACE),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
ChangeCameraButton(onChangeCamera: () {
final currentCameraIndex = GlobalValuesManager
.cameras
.indexOf(controller!.description);
final availableCameras =
GlobalValuesManager.cameras.length;
onNewCameraSelected(
GlobalValuesManager.cameras[
(currentCameraIndex + 1) % availableCameras],
);
}),
CameraButton(
active: isRecording,
onVideoBegin: () async {
if (controller!.value.isRecordingVideo) {
// A recording has already started, do nothing.
return;
}
setState(() {
isRecording = true;
});
await controller!.startVideoRecording();
},
onVideoEnd: takeVideo,
onPhotoShot: takePhoto,
),
lastPhoto == null
? TodayPhotoButton()
: TodayPhotoButton(
data: lastPhoto![0],
type: lastPhoto![1],
),
],
),
)
],
),
],
),
);
}(),
),
);
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_location/constants/spacing.dart';
class PermissionsRequiredPage extends StatefulWidget {
final VoidCallback onPermissionsGranted;
const PermissionsRequiredPage({
Key? key,
required this.onPermissionsGranted,
}) : super(key: key);
@override
State<PermissionsRequiredPage> createState() =>
_PermissionsRequiredPageState();
}
class _PermissionsRequiredPageState extends State<PermissionsRequiredPage> {
bool hasDeniedForever = false;
bool hasGrantedCameraPermission = false;
bool hasGrantedMicrophonePermission = false;
@override
void initState() {
super.initState();
checkPermissions();
}
Future<void> checkPermissions() async {
final cameraStatus = await Permission.camera.status;
final microphoneStatus = await Permission.microphone.status;
setState(() {
hasGrantedCameraPermission = cameraStatus.isGranted;
hasGrantedMicrophonePermission = microphoneStatus.isGranted;
});
if (cameraStatus.isPermanentlyDenied ||
microphoneStatus.isPermanentlyDenied) {
setState(() {
hasDeniedForever = true;
});
return;
}
if (cameraStatus.isGranted && microphoneStatus.isGranted) {
widget.onPermissionsGranted();
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'Permissions Required',
style: Theme.of(context).textTheme.headline1,
),
const SizedBox(height: MEDIUM_SPACE),
const Text(
'Please grant permissions to use this app',
),
const SizedBox(height: LARGE_SPACE),
if (hasDeniedForever) ...[
const Text(
'You have permanently denied permissions required to use this app. Please enable them in the settings.',
),
const SizedBox(height: LARGE_SPACE),
TextButton.icon(
onPressed: () => openAppSettings(),
icon: const Icon(Icons.settings),
label: const Text('Open Settings'),
),
] else ...[
TextButton.icon(
onPressed: hasGrantedCameraPermission
? null
: () async {
await Permission.camera.request();
await checkPermissions();
},
icon: const Icon(Icons.camera_alt),
label: Text(
'Grant camera permission${hasGrantedCameraPermission ? ' - Granted!' : ''}',
),
),
const SizedBox(height: MEDIUM_SPACE),
TextButton.icon(
onPressed: hasGrantedMicrophonePermission
? null
: () async {
await Permission.microphone.request();
await checkPermissions();
},
icon: const Icon(Icons.mic),
label: Text(
'Grant microphone permission ${hasGrantedMicrophonePermission ? ' - Granted!' : ''}',
),
),
],
],
);
}
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/managers/startup_page_manager.dart';
import 'package:share_location/screens/main_screen.dart';
import 'package:share_location/widgets/logo.dart';
class WelcomeScreen extends StatelessWidget {
@ -38,9 +40,11 @@ class WelcomeScreen extends StatelessWidget {
),
const SizedBox(height: LARGE_SPACE),
ElevatedButton.icon(
icon: Icon(Icons.arrow_right),
label: Text('Start'),
onPressed: () {},
icon: const Icon(Icons.arrow_right),
label: const Text('Start'),
onPressed: () {
StartupPageManager.navigateToNewPage(context, MainScreen.ID);
},
),
],
),

View File

@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:share_location/screens/login_screen.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthRequiredState<T extends StatefulWidget>
extends SupabaseAuthRequiredState<T> {
@override
void onUnauthenticated() {
if (mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(LoginScreen.ID, (route) => false);
}
}
}

21
lib/utils/loadable.dart Normal file
View File

@ -0,0 +1,21 @@
mixin Loadable {
bool _isLoading = false;
bool get isLoading => _isLoading;
void setState(void Function() callback);
Future<void> callWithLoading(Future<void> Function() callback) async {
setState(() {
_isLoading = true;
});
try {
await callback();
} finally {
setState(() {
_isLoading = false;
});
}
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CameraButton extends StatelessWidget {
final bool active;
final VoidCallback onPhotoShot;
final VoidCallback onVideoBegin;
final VoidCallback onVideoEnd;
const CameraButton({
Key? key,
required this.onPhotoShot,
required this.onVideoBegin,
required this.onVideoEnd,
this.active = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
HapticFeedback.heavyImpact();
if (active) {
onVideoEnd();
} else {
onPhotoShot();
}
},
onLongPress: () {
HapticFeedback.heavyImpact();
if (active) {
onVideoEnd();
} else {
onVideoBegin();
}
},
child: Stack(
alignment: Alignment.center,
children: active
? const <Widget>[
Icon(
Icons.circle,
size: 75,
color: Colors.white,
),
Icon(
Icons.circle,
size: 65,
color: Colors.red,
),
Icon(
Icons.stop,
size: 45,
color: Colors.white,
),
]
: <Widget>[
Icon(
Icons.circle,
size: 75,
color: Colors.white.withOpacity(.2),
),
const Icon(
Icons.circle,
size: 50,
color: Colors.white,
),
],
),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class ChangeCameraButton extends StatelessWidget {
final VoidCallback onChangeCamera;
const ChangeCameraButton({
Key? key,
required this.onChangeCamera,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
highlightColor: Colors.transparent,
onTap: () {
HapticFeedback.heavyImpact();
onChangeCamera();
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Icon(
Icons.circle,
size: 60,
color: Colors.white.withOpacity(.2),
),
Icon(
Icons.camera_alt,
size: 30,
color: Colors.white,
),
],
),
);
}
}

View File

@ -0,0 +1,99 @@
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';
class TodayPhotoButton extends StatefulWidget {
final Uint8List? data;
final MemoryType? type;
const TodayPhotoButton({
Key? key,
this.data,
this.type,
}) : 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
Widget build(BuildContext context) {
return InkWell(
child: Container(
width: 45,
height: 45,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 2,
),
borderRadius: BorderRadius.circular(SMALL_SPACE),
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();
}
}()),
),
);
}
}

View File

@ -15,6 +15,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
camera:
dependency: "direct main"
description:
name: camera
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0+1"
camera_android:
dependency: transitive
description:
name: camera_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0+1"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.8+3"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
camera_web:
dependency: transitive
description:
name: camera_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
characters:
dependency: transitive
description:
@ -43,6 +78,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.2"
cupertino_icons:
dependency: "direct main"
description:
@ -57,6 +113,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
flutter:
dependency: "direct main"
description: flutter
@ -69,6 +139,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
flutter_secure_storage:
dependency: "direct main"
description:
@ -128,6 +205,55 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
functions_client:
dependency: transitive
description:
name: functions_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1-dev.5"
gotrue:
dependency: transitive
description:
name: gotrue
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
hive:
dependency: transitive
description:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.3"
hive_flutter:
dependency: transitive
description:
name: hive_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.0"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.5"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
js:
dependency: transitive
description:
@ -135,6 +261,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
jwt_decode:
dependency: transitive
description:
name: jwt_decode
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
lints:
dependency: transitive
description:
@ -163,6 +296,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
path:
dependency: transitive
description:
@ -184,6 +324,90 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.19"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "10.0.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
version: "10.0.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.4"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.7.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
petitparser:
dependency: transitive
description:
@ -191,6 +415,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
plugin_platform_interface:
dependency: transitive
description:
@ -198,6 +429,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
postgrest:
dependency: transitive
description:
name: postgrest
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.11"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
realtime_client:
dependency: transitive
description:
name: realtime_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.15"
sky_engine:
dependency: transitive
description: flutter
@ -217,6 +476,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
storage_client:
dependency: transitive
description:
name: storage_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.6+2"
stream_channel:
dependency: transitive
description:
@ -224,6 +490,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
stream_transform:
dependency: transitive
description:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
string_scanner:
dependency: transitive
description:
@ -231,6 +504,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
supabase:
dependency: transitive
description:
name: supabase
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.6"
supabase_flutter:
dependency: "direct main"
description:
name: supabase_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
term_glyph:
dependency: transitive
description:
@ -245,6 +532,104 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
uni_links:
dependency: transitive
description:
name: uni_links
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1"
uni_links_platform_interface:
dependency: transitive
description:
name: uni_links_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
uni_links_web:
dependency: transitive
description:
name: uni_links_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
universal_io:
dependency: transitive
description:
name: universal_io
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
url_launcher:
dependency: transitive
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.5"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
uuid:
dependency: "direct main"
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
vector_math:
dependency: transitive
description:
@ -252,6 +637,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
video_player:
dependency: "direct main"
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.6"
video_player_android:
dependency: transitive
description:
name: video_player_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.8"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.5"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.4"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.7.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
xml:
dependency: transitive
description:
@ -261,4 +702,4 @@ packages:
version: "6.1.0"
sdks:
dart: ">=2.17.5 <3.0.0"
flutter: ">=2.11.0-0.1.pre"
flutter: ">=3.0.0"

View File

@ -36,6 +36,12 @@ dependencies:
cupertino_icons: ^1.0.2
flutter_svg: ^1.1.3
flutter_secure_storage: ^5.1.0
camera: ^0.10.0+1
permission_handler: ^10.0.0
supabase_flutter: ^0.3.3
uuid: ^3.0.6
video_player: ^2.4.6
path_provider: ^2.0.11
dev_dependencies:
flutter_test: