Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish stories, comments, likes, likes on comments, add more users #4

Merged
merged 3 commits into from
May 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Instagram clone for Groot created using Flutter! I'm currently working on this w

## Current Status

![day 1 progress](status_updates/day1.gif)
![day 4 progress](status_updates/day4.gif)

## Picture Credits

Expand Down
Binary file added assets/images/gamora.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/nebula.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/rocket.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/starlord.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 102 additions & 0 deletions lib/avatar_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:instagroot/models.dart';

class AvatarWidget extends StatelessWidget {
final User user;
final VoidCallback onTap;
final EdgeInsetsGeometry padding;
final bool isLarge;
final bool isShowingUsernameLabel;
final bool isCurrentUserStory;

const AvatarWidget({
@required this.user,
this.onTap,
this.padding = const EdgeInsets.all(8.0),
this.isLarge = false,
this.isShowingUsernameLabel = false,
this.isCurrentUserStory = false,
});

static const _gradientBorderDecoration = BoxDecoration(
shape: BoxShape.circle,
// https://brandpalettes.com/instagram-color-codes/
gradient: SweepGradient(
colors: [
Color(0xFF833AB4), // Purple
Color(0xFFF77737), // Orange
Color(0xFFE1306C), // Red-pink
Color(0xFFC13584), // Red-purple
],
),
);
static const _whiteBorderDecoration = BoxDecoration(
shape: BoxShape.circle,
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 3.0)),
);
static const _greyBoxShadowDecoration = BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: Colors.grey, blurRadius: 1.0, spreadRadius: 1.0)
],
);

@override
Widget build(BuildContext context) {
final radius = isLarge ? 28.0 : 14.0;
final avatar = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: radius * 2 + 9.0,
width: radius * 2 + 9.0,
decoration: user.stories.isEmpty ? null : _gradientBorderDecoration,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
decoration: _whiteBorderDecoration,
child: Container(
decoration: _greyBoxShadowDecoration,
child: CircleAvatar(
radius: radius,
backgroundImage: AssetImage(user.imageUrl),
),
),
),
if (isCurrentUserStory && user.stories.isEmpty)
// Bottom right circular add icon
Positioned(
right: 2.0,
bottom: 2.0,
child: Container(
width: 18.0,
height: 18.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
border: Border.all(color: Colors.white),
),
child: Icon(Icons.add, size: 16.0, color: Colors.white),
),
),
],
),
),
if (isShowingUsernameLabel)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
isCurrentUserStory ? 'Your Story' : user.name,
textScaleFactor: 0.9,
),
),
],
);

return Padding(
padding: this.padding,
child: GestureDetector(child: avatar, onTap: onTap),
);
}
}
60 changes: 60 additions & 0 deletions lib/comment_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:instagroot/heart_icon_animator.dart';
import 'package:instagroot/models.dart';
import 'package:instagroot/ui_utils.dart';

class CommentWidget extends StatefulWidget {
final Comment comment;

CommentWidget(this.comment);

@override
_CommentWidgetState createState() => _CommentWidgetState();
}

class _CommentWidgetState extends State<CommentWidget> {
void _toggleIsLiked() {
setState(() => widget.comment.toggleLikeFor(currentUser));
}

Text _buildRichText() {
var currentTextData = StringBuffer();
var textSpans = <TextSpan>[
TextSpan(text: '${widget.comment.user.name} ', style: bold),
];
this.widget.comment.text.split(' ').forEach((word) {
if (word.startsWith('#') && word.length > 1) {
if (currentTextData.isNotEmpty) {
textSpans.add(TextSpan(text: currentTextData.toString()));
currentTextData.clear();
}
textSpans.add(TextSpan(text: '$word ', style: link));
} else {
currentTextData.write('$word ');
}
});
if (currentTextData.isNotEmpty) {
textSpans.add(TextSpan(text: currentTextData.toString()));
currentTextData.clear();
}
return Text.rich(TextSpan(children: textSpans));
}

@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
_buildRichText(),
Spacer(),
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: HeartIconAnimator(
isLiked: widget.comment.isLikedBy(currentUser),
size: 16.0,
onTap: _toggleIsLiked,
),
),
],
);
}
}
97 changes: 97 additions & 0 deletions lib/heart_icon_animator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';

class HeartIconAnimator extends StatefulWidget {
final bool isLiked;
final double size;
final VoidCallback onTap;
final Stream<void> triggerAnimationStream;

HeartIconAnimator({
@required this.isLiked,
this.size = 24.0,
@required this.onTap,
this.triggerAnimationStream,
});

@override
_HeartIconAnimatorState createState() => _HeartIconAnimatorState();
}

class _HeartIconAnimatorState extends State<HeartIconAnimator>
with SingleTickerProviderStateMixin {
AnimationController _likeController;
Animation<double> _likeAnimation;

@override
void initState() {
super.initState();
final quick = const Duration(milliseconds: 500);
final scaleTween = Tween(begin: 0.0, end: 1.0);
_likeController = AnimationController(duration: quick, vsync: this);
_likeAnimation = scaleTween.animate(
CurvedAnimation(
parent: _likeController,
curve: Curves.elasticOut,
),
);

// Ensure a full scale like button on init.
_likeController.animateTo(1.0, duration: Duration.zero);

if (widget.triggerAnimationStream != null) {
widget.triggerAnimationStream.listen((_) => _animate());
}
}

@override
void dispose() {
_likeController.dispose();
super.dispose();
}

void _animate() {
_likeController
..reset()
..forward();
}

@override
Widget build(BuildContext context) {
return _TapableHeart(
isLiked: widget.isLiked,
size: widget.size,
onTap: () {
_animate();
widget.onTap();
},
animation: _likeAnimation,
);
}
}

class _TapableHeart extends AnimatedWidget {
final bool isLiked;
final double size;
final VoidCallback onTap;

_TapableHeart({
Key key,
@required this.isLiked,
@required this.size,
@required this.onTap,
@required Animation<double> animation,
}) : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: listenable,
child: GestureDetector(
child: isLiked
? Icon(Icons.favorite, size: size, color: Colors.red)
: Icon(Icons.favorite_border, size: size),
onTap: onTap,
),
);
}
}
59 changes: 59 additions & 0 deletions lib/heart_overlay_animator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';

class HeartOverlayAnimator extends StatefulWidget {
final Stream<void> triggerAnimationStream;

HeartOverlayAnimator({@required this.triggerAnimationStream});

@override
_HeartOverlayAnimatorState createState() => _HeartOverlayAnimatorState();
}

class _HeartOverlayAnimatorState extends State<HeartOverlayAnimator>
with SingleTickerProviderStateMixin {
AnimationController _heartController;
Animation<double> _heartAnimation;

@override
void initState() {
super.initState();
final quick = const Duration(milliseconds: 500);
final scaleTween = Tween(begin: 0.0, end: 1.0);
_heartController = AnimationController(duration: quick, vsync: this);
_heartAnimation = scaleTween.animate(
CurvedAnimation(
parent: _heartController,
curve: Curves.elasticOut,
),
);
_heartController.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
_heartController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.decelerate,
);
}
});

widget.triggerAnimationStream.listen((_) {
_heartController
..reset()
..forward();
});
}

@override
void dispose() {
_heartController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _heartAnimation,
child: Icon(Icons.favorite, size: 80.0, color: Colors.white70),
);
}
}
Loading