mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +02:00
feat: buddy merge
This commit is contained in:
parent
c8cc325a95
commit
60747d30d8
7 changed files with 431 additions and 105 deletions
|
@ -14,16 +14,22 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
go_router: any
|
||||
flutter_timeline_view:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_view
|
||||
ref: 1.0.0
|
||||
# flutter_timeline_view:
|
||||
# git:
|
||||
# url: https://github.com/Iconica-Development/flutter_timeline
|
||||
# path: packages/flutter_timeline_view
|
||||
# ref: 1.0.0
|
||||
# flutter_timeline_interface:
|
||||
# git:
|
||||
# url: https://github.com/Iconica-Development/flutter_timeline
|
||||
# path: packages/flutter_timeline_interface
|
||||
# ref: 1.0.0
|
||||
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 1.0.0
|
||||
path: ../flutter_timeline_interface
|
||||
flutter_timeline_view:
|
||||
path: ../flutter_timeline_view
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
|
|
|
@ -19,10 +19,21 @@ class TimelineOptions {
|
|||
this.allowAllDeletion = false,
|
||||
this.sortCommentsAscending = true,
|
||||
this.sortPostsAscending = false,
|
||||
this.doubleTapTolike = false,
|
||||
this.iconsWithValues = false,
|
||||
this.likeAndDislikeIconsForDoubleTap = const (
|
||||
Icon(
|
||||
Icons.favorite_rounded,
|
||||
color: Color(0xFFC3007A),
|
||||
),
|
||||
null,
|
||||
),
|
||||
this.itemInfoBuilder,
|
||||
this.dateformat,
|
||||
this.timeFormat,
|
||||
this.buttonBuilder,
|
||||
this.textInputBuilder,
|
||||
this.dividerBuilder,
|
||||
this.userAvatarBuilder,
|
||||
this.anonymousAvatarBuilder,
|
||||
this.nameBuilder,
|
||||
|
@ -71,6 +82,21 @@ class TimelineOptions {
|
|||
/// ImagePickerConfig can be used to define the
|
||||
/// size and quality for the uploaded image.
|
||||
final ImagePickerConfig imagePickerConfig;
|
||||
|
||||
/// Whether to allow double tap to like
|
||||
final bool doubleTapTolike;
|
||||
|
||||
/// The icons to display when double tap to like is enabled
|
||||
final (Icon?, Icon?) likeAndDislikeIconsForDoubleTap;
|
||||
|
||||
/// Whether to display the icons with values
|
||||
final bool iconsWithValues;
|
||||
|
||||
/// The builder for the item info, all below the like and comment buttons
|
||||
final Widget Function({required TimelinePost post})? itemInfoBuilder;
|
||||
|
||||
/// The builder for the divider
|
||||
final Widget Function()? dividerBuilder;
|
||||
}
|
||||
|
||||
typedef ButtonBuilder = Widget Function(
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:flutter_image_picker/flutter_image_picker.dart';
|
|||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class TimelinePostScreen extends StatefulWidget {
|
||||
|
@ -223,7 +224,36 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: CachedNetworkImage(
|
||||
child: widget.options.doubleTapTolike
|
||||
? TappableImage(
|
||||
likeAndDislikeIcon: widget
|
||||
.options.likeAndDislikeIconsForDoubleTap,
|
||||
post: post,
|
||||
userId: widget.userId,
|
||||
onLike: ({required bool liked}) async {
|
||||
var userId = widget.userId;
|
||||
|
||||
late TimelinePost result;
|
||||
|
||||
if (!liked) {
|
||||
result = await widget.service.likePost(
|
||||
userId,
|
||||
post,
|
||||
);
|
||||
} else {
|
||||
result = await widget.service.unlikePost(
|
||||
userId,
|
||||
post,
|
||||
);
|
||||
}
|
||||
|
||||
await loadPostDetails();
|
||||
|
||||
return result.likedBy?.contains(userId) ??
|
||||
false;
|
||||
},
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
width: double.infinity,
|
||||
imageUrl: post.imageUrl!,
|
||||
fit: BoxFit.fitHeight,
|
||||
|
|
|
@ -100,6 +100,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
(post) => Padding(
|
||||
padding: widget.padding,
|
||||
child: TimelinePostWidget(
|
||||
service: widget.service,
|
||||
userId: widget.userId,
|
||||
options: widget.options,
|
||||
post: post,
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
|
||||
class TappableImage extends StatefulWidget {
|
||||
const TappableImage({
|
||||
required this.post,
|
||||
required this.onLike,
|
||||
required this.userId,
|
||||
required this.likeAndDislikeIcon,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TimelinePost post;
|
||||
final String userId;
|
||||
final Future<bool> Function({required bool liked}) onLike;
|
||||
final (Icon?, Icon?) likeAndDislikeIcon;
|
||||
|
||||
@override
|
||||
State<TappableImage> createState() => _TappableImageState();
|
||||
}
|
||||
|
||||
class _TappableImageState extends State<TappableImage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController animationController;
|
||||
late Animation<double> animation;
|
||||
bool loading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
|
||||
animation = CurvedAnimation(
|
||||
parent: animationController,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
|
||||
animationController.addListener(listener);
|
||||
}
|
||||
|
||||
void listener() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.removeListener(listener);
|
||||
animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void startAnimation() {
|
||||
animationController.forward();
|
||||
}
|
||||
|
||||
void reverseAnimation() {
|
||||
animationController.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InkWell(
|
||||
onDoubleTap: () async {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
await animationController.forward();
|
||||
|
||||
var liked = await widget.onLike(
|
||||
liked: widget.post.likedBy?.contains(
|
||||
widget.userId,
|
||||
) ??
|
||||
false,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
await showDialog(
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => HeartAnimation(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
liked: liked,
|
||||
likeAndDislikeIcon: widget.likeAndDislikeIcon,
|
||||
),
|
||||
);
|
||||
}
|
||||
await animationController.reverse();
|
||||
loading = false;
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, animation.value * -32),
|
||||
child: Transform.scale(
|
||||
scale: 1 + animation.value * 0.1,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.post.imageUrl ?? '',
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class HeartAnimation extends StatefulWidget {
|
||||
const HeartAnimation({
|
||||
required this.duration,
|
||||
required this.liked,
|
||||
required this.likeAndDislikeIcon,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Duration duration;
|
||||
final bool liked;
|
||||
final (Icon?, Icon?) likeAndDislikeIcon;
|
||||
|
||||
@override
|
||||
State<HeartAnimation> createState() => _HeartAnimationState();
|
||||
}
|
||||
|
||||
class _HeartAnimationState extends State<HeartAnimation> {
|
||||
late bool active;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
active = widget.liked;
|
||||
unawaited(
|
||||
Future.delayed(const Duration(milliseconds: 100)).then((value) async {
|
||||
active = widget.liked;
|
||||
var navigator = Navigator.of(context);
|
||||
await Future.delayed(widget.duration);
|
||||
navigator.pop();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedOpacity(
|
||||
opacity: widget.likeAndDislikeIcon.$1 != null &&
|
||||
widget.likeAndDislikeIcon.$2 != null
|
||||
? 1
|
||||
: active
|
||||
? 1
|
||||
: 0,
|
||||
duration: widget.duration,
|
||||
curve: Curves.decelerate,
|
||||
child: AnimatedScale(
|
||||
scale: widget.likeAndDislikeIcon.$1 != null &&
|
||||
widget.likeAndDislikeIcon.$2 != null
|
||||
? 10
|
||||
: active
|
||||
? 10
|
||||
: 1,
|
||||
duration: widget.duration,
|
||||
child: active
|
||||
? widget.likeAndDislikeIcon.$1
|
||||
: widget.likeAndDislikeIcon.$2,
|
||||
),
|
||||
);
|
||||
}
|
|
@ -6,8 +6,9 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
||||
|
||||
class TimelinePostWidget extends StatelessWidget {
|
||||
class TimelinePostWidget extends StatefulWidget {
|
||||
const TimelinePostWidget({
|
||||
required this.userId,
|
||||
required this.options,
|
||||
|
@ -17,6 +18,7 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
required this.onTapLike,
|
||||
required this.onTapUnlike,
|
||||
required this.onPostDelete,
|
||||
required this.service,
|
||||
this.onUserTap,
|
||||
super.key,
|
||||
});
|
||||
|
@ -33,44 +35,51 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
final VoidCallback onTapLike;
|
||||
final VoidCallback onTapUnlike;
|
||||
final VoidCallback onPostDelete;
|
||||
final TimelineService service;
|
||||
|
||||
/// If this is not null, the user can tap on the user avatar or name
|
||||
final Function(String userId)? onUserTap;
|
||||
|
||||
@override
|
||||
State<TimelinePostWidget> createState() => _TimelinePostWidgetState();
|
||||
}
|
||||
|
||||
class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
onTap: widget.onTap,
|
||||
child: SizedBox(
|
||||
height: post.imageUrl != null ? height : null,
|
||||
height: widget.post.imageUrl != null ? widget.height : null,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (post.creator != null)
|
||||
if (widget.post.creator != null)
|
||||
InkWell(
|
||||
onTap: onUserTap != null
|
||||
? () => onUserTap?.call(post.creator!.userId)
|
||||
onTap: widget.onUserTap != null
|
||||
? () =>
|
||||
widget.onUserTap?.call(widget.post.creator!.userId)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
if (post.creator!.imageUrl != null) ...[
|
||||
options.userAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
if (widget.post.creator!.imageUrl != null) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
40,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
post.creator!.imageUrl!,
|
||||
widget.post.creator!.imageUrl!,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
options.anonymousAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
widget.options.anonymousAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
40,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
|
@ -82,22 +91,24 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
],
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
options.nameBuilder?.call(post.creator) ??
|
||||
post.creator?.fullName ??
|
||||
options.translations.anonymousUser,
|
||||
style:
|
||||
options.theme.textStyles.postCreatorTitleStyle ??
|
||||
widget.options.nameBuilder
|
||||
?.call(widget.post.creator) ??
|
||||
widget.post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles
|
||||
.postCreatorTitleStyle ??
|
||||
theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (options.allowAllDeletion || post.creator?.userId == userId)
|
||||
if (widget.options.allowAllDeletion ||
|
||||
widget.post.creator?.userId == widget.userId)
|
||||
PopupMenuButton(
|
||||
onSelected: (value) {
|
||||
if (value == 'delete') {
|
||||
onPostDelete();
|
||||
widget.onPostDelete();
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
|
@ -107,38 +118,65 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
options.translations.deletePost,
|
||||
style: options.theme.textStyles.deletePostStyle ??
|
||||
widget.options.translations.deletePost,
|
||||
style: widget.options.theme.textStyles
|
||||
.deletePostStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
options.theme.deleteIcon ??
|
||||
widget.options.theme.deleteIcon ??
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: options.theme.iconColor,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: options.theme.moreIcon ??
|
||||
child: widget.options.theme.moreIcon ??
|
||||
Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
color: options.theme.iconColor,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// image of the post
|
||||
if (post.imageUrl != null) ...[
|
||||
if (widget.post.imageUrl != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Flexible(
|
||||
flex: height != null ? 1 : 0,
|
||||
flex: widget.height != null ? 1 : 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: CachedNetworkImage(
|
||||
child: widget.options.doubleTapTolike
|
||||
? TappableImage(
|
||||
likeAndDislikeIcon:
|
||||
widget.options.likeAndDislikeIconsForDoubleTap,
|
||||
post: widget.post,
|
||||
userId: widget.userId,
|
||||
onLike: ({required bool liked}) async {
|
||||
var userId = widget.userId;
|
||||
|
||||
late TimelinePost result;
|
||||
|
||||
if (!liked) {
|
||||
result = await widget.service.likePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
} else {
|
||||
result = await widget.service.unlikePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
}
|
||||
|
||||
return result.likedBy?.contains(userId) ?? false;
|
||||
},
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
width: double.infinity,
|
||||
imageUrl: post.imageUrl!,
|
||||
imageUrl: widget.post.imageUrl!,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
|
@ -148,58 +186,110 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
height: 8,
|
||||
),
|
||||
// post information
|
||||
if (widget.options.iconsWithValues)
|
||||
Row(
|
||||
children: [
|
||||
if (post.likedBy?.contains(userId) ?? false) ...[
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
var userId = widget.userId;
|
||||
|
||||
var liked =
|
||||
widget.post.likedBy?.contains(userId) ?? false;
|
||||
|
||||
if (!liked) {
|
||||
await widget.service.likePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
} else {
|
||||
await widget.service.unlikePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
widget.post.likedBy?.contains(widget.userId) ?? false
|
||||
? Icons.favorite
|
||||
: Icons.favorite_outline_outlined,
|
||||
),
|
||||
label: Text('${widget.post.likes}'),
|
||||
),
|
||||
if (widget.post.reactionEnabled)
|
||||
TextButton.icon(
|
||||
onPressed: widget.onTap,
|
||||
icon: widget.options.theme.commentIcon ??
|
||||
const Icon(
|
||||
Icons.chat_bubble_outline_outlined,
|
||||
),
|
||||
label: Text('${widget.post.reaction}'),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
if (widget.post.likedBy?.contains(widget.userId) ??
|
||||
false) ...[
|
||||
InkWell(
|
||||
onTap: onTapUnlike,
|
||||
child: options.theme.likedIcon ??
|
||||
onTap: widget.onTapUnlike,
|
||||
child: widget.options.theme.likedIcon ??
|
||||
Icon(
|
||||
Icons.thumb_up_rounded,
|
||||
color: options.theme.iconColor,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
InkWell(
|
||||
onTap: onTapLike,
|
||||
child: options.theme.likeIcon ??
|
||||
onTap: widget.onTapLike,
|
||||
child: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
Icons.thumb_up_alt_outlined,
|
||||
color: options.theme.iconColor,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
if (post.reactionEnabled)
|
||||
options.theme.commentIcon ??
|
||||
if (widget.post.reactionEnabled)
|
||||
widget.options.theme.commentIcon ??
|
||||
Icon(
|
||||
Icons.chat_bubble_outline_rounded,
|
||||
color: options.theme.iconColor,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
||||
if (widget.options.itemInfoBuilder != null) ...[
|
||||
widget.options.itemInfoBuilder!(
|
||||
post: widget.post,
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
'${post.likes} ${options.translations.likesTitle}',
|
||||
style: options.theme.textStyles.listPostLikeTitleAndAmount ??
|
||||
'${widget.post.likes} '
|
||||
'${widget.options.translations.likesTitle}',
|
||||
style: widget
|
||||
.options.theme.textStyles.listPostLikeTitleAndAmount ??
|
||||
theme.textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: options.nameBuilder?.call(post.creator) ??
|
||||
post.creator?.fullName ??
|
||||
options.translations.anonymousUser,
|
||||
style: options.theme.textStyles.listCreatorNameStyle ??
|
||||
text: widget.options.nameBuilder?.call(widget.post.creator) ??
|
||||
widget.post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles.listCreatorNameStyle ??
|
||||
theme.textTheme.titleSmall,
|
||||
children: [
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: post.title,
|
||||
style: options.theme.textStyles.listPostTitleStyle ??
|
||||
text: widget.post.title,
|
||||
style:
|
||||
widget.options.theme.textStyles.listPostTitleStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
|
@ -207,11 +297,14 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
options.translations.viewPost,
|
||||
style: options.theme.textStyles.viewPostStyle ??
|
||||
widget.options.translations.viewPost,
|
||||
style: widget.options.theme.textStyles.viewPostStyle ??
|
||||
theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
if (widget.options.dividerBuilder != null)
|
||||
widget.options.dividerBuilder!(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -19,11 +19,13 @@ dependencies:
|
|||
dotted_border: ^2.1.0
|
||||
flutter_html: ^3.0.0-beta.2
|
||||
|
||||
# flutter_timeline_interface:
|
||||
# git:
|
||||
# url: https://github.com/Iconica-Development/flutter_timeline
|
||||
# path: packages/flutter_timeline_interface
|
||||
# ref: 1.0.0
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 1.0.0
|
||||
path: ../flutter_timeline_interface
|
||||
flutter_image_picker:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||
|
|
Loading…
Reference in a new issue