mirror of
https://github.com/Myzel394/quid_faciam_hodie.git
synced 2025-06-19 07:35:26 +02:00
created basic timeline
This commit is contained in:
parent
3904c4dbef
commit
7558d1b86b
50
lib/controllers/memory_slide_controller.dart
Normal file
50
lib/controllers/memory_slide_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
29
lib/controllers/status_controller.dart
Normal file
29
lib/controllers/status_controller.dart
Normal 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');
|
||||
}
|
||||
}
|
30
lib/foreign_types/memory.dart
Normal file
30
lib/foreign_types/memory.dart
Normal 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;
|
||||
}
|
@ -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) {
|
||||
|
@ -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) ...[
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
85
lib/widgets/memory_page.dart
Normal file
85
lib/widgets/memory_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
100
lib/widgets/memory_slide.dart
Normal file
100
lib/widgets/memory_slide.dart
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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
105
lib/widgets/status.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
14
pubspec.lock
14
pubspec.lock
@ -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:
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user