Extract user.dart from common.dart and move classes

This commit is contained in:
Remi Rousselet
2022-07-31 13:28:41 +02:00
parent a2a6f8e5ba
commit 77530346bd
3 changed files with 275 additions and 256 deletions

View File

@ -1,14 +1,8 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part 'common.freezed.dart';
part 'common.g.dart';
final client = Provider((ref) => Dio());
/// A Provider that exposes the current theme.
@ -36,248 +30,3 @@ class TimestampParser implements JsonConverter<DateTime, int> {
@override
int toJson(DateTime object) => object.millisecondsSinceEpoch;
}
@freezed
abstract class Owner with _$Owner {
@JsonSerializable(fieldRename: FieldRename.snake)
factory Owner({
required int reputation,
required int userId,
BadgeCount? badgeCounts,
required String displayName,
required String profileImage,
required String link,
}) = _Owner;
factory Owner.fromJson(Map<String, Object> json) => _$OwnerFromJson(json);
}
@freezed
abstract class BadgeCount with _$BadgeCount {
factory BadgeCount({
required int bronze,
required int silver,
required int gold,
}) = _BadgeCount;
factory BadgeCount.fromJson(Map<String, Object> json) =>
_$BadgeCountFromJson(json);
}
class AnswersCount extends StatelessWidget {
const AnswersCount(
this.answerCount, {
Key? key,
required this.accepted,
}) : super(key: key);
final int answerCount;
final bool accepted;
@override
Widget build(BuildContext context) {
final textStyle = accepted
? null
: answerCount == 0
? const TextStyle(color: Color(0xffacb2b8))
: const TextStyle(color: Color(0xff5a9e6f));
return Container(
decoration: answerCount > 0
? BoxDecoration(
color: accepted ? const Color(0xff5a9e6f) : null,
border: Border.all(color: const Color(0xff5a9e6f)),
borderRadius: BorderRadius.circular(3),
)
: null,
padding: const EdgeInsets.all(7),
child: Column(
children: [
Text(answerCount.toString(), style: textStyle),
Text('answers', style: textStyle)
],
),
);
}
}
class UpvoteCount extends StatelessWidget {
const UpvoteCount(this.upvoteCount, {Key? key}) : super(key: key);
final int upvoteCount;
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(color: Color(0xffacb2b8));
return Padding(
padding: const EdgeInsets.all(7),
child: Column(
children: [
Text(upvoteCount.toString(), style: textStyle),
const Text('votes', style: textStyle)
],
),
);
}
}
String _useCreatedSince(DateTime creationDate) {
final label = useState('');
useEffect(() {
void setLabel() {
final now = DateTime.now();
final diff = now.difference(creationDate);
String value;
if (diff.inDays > 1) {
value = '${diff.inDays} days';
} else if (diff.inHours > 0) {
value = '${diff.inHours} hours';
} else if (diff.inMinutes > 0) {
value = '${diff.inMinutes} mins';
} else {
value = '${diff.inSeconds} seconds';
}
label.value = 'asked $value ago';
}
setLabel();
final timer = Timer.periodic(const Duration(minutes: 1), (_) => setLabel());
return timer.cancel;
}, [creationDate]);
return label.value;
}
class PostInfo extends HookConsumerWidget {
const PostInfo({
Key? key,
required this.owner,
required this.creationDate,
}) : super(key: key);
final Owner owner;
final DateTime creationDate;
@override
Widget build(BuildContext context, WidgetRef ref) {
final label = _useCreatedSince(creationDate);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
color: Color(0xFF9fa6ad),
fontSize: 12,
),
),
const SizedBox(height: 3),
Row(
children: [
Container(
height: 32,
width: 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
),
clipBehavior: Clip.hardEdge,
child: Image.network(owner.profileImage),
),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
owner.displayName,
style: const TextStyle(
color: Color(0xff3ca4ff),
fontSize: 12,
),
),
Row(
children: [
Text(
'${owner.reputation}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
if (owner.badgeCounts != null) ...[
if (owner.badgeCounts!.gold > 0) ...[
const SizedBox(width: 4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: const Color(0xffffcc00),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 2),
Text(
'${owner.badgeCounts!.gold}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
if (owner.badgeCounts!.silver > 0) ...[
const SizedBox(width: 4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: const Color(0xffb4b8bc),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 2),
Text(
'${owner.badgeCounts!.silver}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
if (owner.badgeCounts!.bronze > 0) ...[
const SizedBox(width: 4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: const Color(0xffd1a784),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 2),
Text(
'${owner.badgeCounts!.bronze}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
],
],
),
],
),
],
),
],
);
}
}

View File

@ -1,11 +1,15 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:html/parser.dart';
import 'common.dart';
import 'tag.dart';
import 'user.dart';
part 'question.g.dart';
part 'question.freezed.dart';
@ -30,7 +34,7 @@ class Question with _$Question {
required int score,
int? bountyAmount,
int? acceptedAnswerId,
required Owner owner,
required User owner,
required int answerCount,
@TimestampParser() required DateTime creationDate,
required int questionId,
@ -75,11 +79,10 @@ final paginatedQuestionsProvider = FutureProvider.autoDispose
}).toList(),
);
ref.keepAlive();
return page;
});
/// A provider exposing the total count of questions
final questionsCountProvider = Provider.autoDispose((ref) {
return ref
.watch(paginatedQuestionsProvider(0))
@ -170,8 +173,8 @@ class QuestionItem extends HookConsumerWidget {
children: [
Expanded(
child: PostInfo(
owner: question.owner,
creationDate: question.creationDate,
originalPoster: question.owner,
postCreationDate: question.creationDate,
),
),
UpvoteCount(question.score),
@ -191,3 +194,127 @@ class QuestionItem extends HookConsumerWidget {
);
}
}
/// A "hook" (see flutter_hooks) for rendering how far a [DateTime] is from now
/// ("5 seconds ago", ...).
///
/// This hook also takes care of refreshing the UI when the label changes.
/// More particularly, it will check every minute if the label has changed.
String _useAskedHowLongAgo(DateTime creationDate) {
final label = useState('');
useEffect(() {
void setLabel() {
// TODO use package:clock to make mock this value inside tests
final now = DateTime.now();
final diff = now.difference(creationDate);
String value;
if (diff.inDays > 1) {
value = '${diff.inDays} days';
} else if (diff.inHours > 0) {
value = '${diff.inHours} hours';
} else if (diff.inMinutes > 0) {
value = '${diff.inMinutes} mins';
} else {
value = '${diff.inSeconds} seconds';
}
label.value = 'asked $value ago';
}
setLabel();
final timer = Timer.periodic(const Duration(minutes: 1), (_) => setLabel());
return timer.cancel;
}, [creationDate]);
return label.value;
}
class PostInfo extends HookConsumerWidget {
const PostInfo({
super.key,
required this.originalPoster,
required this.postCreationDate,
});
final User originalPoster;
final DateTime postCreationDate;
@override
Widget build(BuildContext context, WidgetRef ref) {
final askedHowLongAgoLabel = _useAskedHowLongAgo(postCreationDate);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
askedHowLongAgoLabel,
style: const TextStyle(color: Color(0xFF9fa6ad), fontSize: 12),
),
const SizedBox(height: 3),
UserAvatar(owner: originalPoster),
],
);
}
}
/// A UI component for showing the answer count on a question
class AnswersCount extends StatelessWidget {
const AnswersCount(
this.answerCount, {
super.key,
required this.accepted,
});
final int answerCount;
final bool accepted;
@override
Widget build(BuildContext context) {
final textStyle = accepted
? null
: answerCount == 0
? const TextStyle(color: Color(0xffacb2b8))
: const TextStyle(color: Color(0xff5a9e6f));
return Container(
decoration: answerCount > 0
? BoxDecoration(
color: accepted ? const Color(0xff5a9e6f) : null,
border: Border.all(color: const Color(0xff5a9e6f)),
borderRadius: BorderRadius.circular(3),
)
: null,
padding: const EdgeInsets.all(7),
child: Column(
children: [
Text(answerCount.toString(), style: textStyle),
Text('answers', style: textStyle)
],
),
);
}
}
/// A UI component for showing the upvotes count on a question
class UpvoteCount extends StatelessWidget {
const UpvoteCount(this.upvoteCount, {super.key});
final int upvoteCount;
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(color: Color(0xffacb2b8));
return Padding(
padding: const EdgeInsets.all(7),
child: Column(
children: [
Text(upvoteCount.toString(), style: textStyle),
const Text('votes', style: textStyle)
],
),
);
}
}

View File

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.g.dart';
part 'user.freezed.dart';
@freezed
class User with _$User {
@JsonSerializable(fieldRename: FieldRename.snake)
factory User({
required int reputation,
required int userId,
BadgeCount? badgeCounts,
required String displayName,
required String profileImage,
required String link,
}) = _User;
factory User.fromJson(Map<String, Object> json) => _$UserFromJson(json);
}
@freezed
class BadgeCount with _$BadgeCount {
factory BadgeCount({
required int bronze,
required int silver,
required int gold,
}) = _BadgeCount;
factory BadgeCount.fromJson(Map<String, Object> json) =>
_$BadgeCountFromJson(json);
}
/// Renders the profile picture of a user and their badges.
class UserAvatar extends StatelessWidget {
const UserAvatar({super.key, required this.owner});
final User owner;
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
height: 32,
width: 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
),
clipBehavior: Clip.hardEdge,
child: Image.network(owner.profileImage),
),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
owner.displayName,
style: const TextStyle(
color: Color(0xff3ca4ff),
fontSize: 12,
),
),
Row(
children: [
Text(
'${owner.reputation}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
if (owner.badgeCounts != null) ...[
if (owner.badgeCounts!.gold > 0) ...[
const SizedBox(width: 4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: const Color(0xffffcc00),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 2),
Text(
'${owner.badgeCounts!.gold}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
if (owner.badgeCounts!.silver > 0) ...[
const SizedBox(width: 4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: const Color(0xffb4b8bc),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 2),
Text(
'${owner.badgeCounts!.silver}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
if (owner.badgeCounts!.bronze > 0) ...[
const SizedBox(width: 4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: const Color(0xffd1a784),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 2),
Text(
'${owner.badgeCounts!.bronze}',
style: const TextStyle(
color: Color(0xff9fa6ad),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
],
],
),
],
),
],
);
}
}