mirror of
https://github.com/rrousselGit/riverpod.git
synced 2025-08-26 18:40:22 +08:00
Extract user.dart from common.dart and move classes
This commit is contained in:
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
143
examples/stackoverflow/lib/user.dart
Normal file
143
examples/stackoverflow/lib/user.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user