mirror of
https://github.com/git-touch/git-touch
synced 2025-02-14 18:40:38 +01:00
refactor: extract long list scaffold for issue and pull request
This commit is contained in:
parent
73907f51b4
commit
650af30838
BIN
images/progressive-disclosure-line.png
Normal file
BIN
images/progressive-disclosure-line.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 333 B |
@ -7,6 +7,8 @@ import 'screens/notifications.dart';
|
|||||||
import 'screens/search.dart';
|
import 'screens/search.dart';
|
||||||
import 'screens/profile.dart';
|
import 'screens/profile.dart';
|
||||||
import 'screens/login.dart';
|
import 'screens/login.dart';
|
||||||
|
import 'screens/pull_request.dart';
|
||||||
|
import 'screens/issue.dart';
|
||||||
|
|
||||||
class Home extends StatefulWidget {
|
class Home extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -58,6 +60,7 @@ class _HomeState extends State<Home> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildScreen(int index) {
|
_buildScreen(int index) {
|
||||||
|
// return IssueScreen(number: 29, owner: 'reactjs', name: 'rfcs');
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
return NewsScreen();
|
return NewsScreen();
|
||||||
|
@ -1,26 +1,102 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import '../utils/utils.dart';
|
import '../utils/utils.dart';
|
||||||
import '../widgets/list_scaffold.dart';
|
import '../widgets/long_list_scaffold.dart';
|
||||||
import '../widgets/timeline_item.dart';
|
import '../widgets/timeline_item.dart';
|
||||||
import '../widgets/comment_item.dart';
|
import '../widgets/comment_item.dart';
|
||||||
import '../providers/settings.dart';
|
import '../providers/settings.dart';
|
||||||
|
|
||||||
class IssueScreen extends StatefulWidget {
|
class IssueScreen extends StatefulWidget {
|
||||||
final int id;
|
final int number;
|
||||||
final String owner;
|
final String owner;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
IssueScreen(this.id, this.owner, this.name);
|
IssueScreen({
|
||||||
|
@required this.number,
|
||||||
|
@required this.owner,
|
||||||
|
@required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
IssueScreen.fromFullName({@required this.number, @required String fullName})
|
||||||
|
: this.owner = fullName.split('/')[0],
|
||||||
|
this.name = fullName.split('/')[1];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_IssueScreenState createState() => _IssueScreenState();
|
_IssueScreenState createState() => _IssueScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _IssueScreenState extends State<IssueScreen> {
|
class _IssueScreenState extends State<IssueScreen> {
|
||||||
Map<String, dynamic> payload;
|
get _fullName => widget.owner + '/' + widget.name;
|
||||||
|
get owner => widget.owner;
|
||||||
|
get id => widget.number;
|
||||||
|
get name => widget.name;
|
||||||
|
|
||||||
Widget _buildHeader() {
|
Future queryIssue() async {
|
||||||
|
var data = await SettingsProvider.of(context).query('''
|
||||||
|
{
|
||||||
|
repository(owner: "$owner", name: "$name") {
|
||||||
|
issue(number: $id) {
|
||||||
|
$graphqlChunk1
|
||||||
|
timeline(first: $pageSize) {
|
||||||
|
totalCount
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
$graghqlChunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
return data['repository']['issue'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future queryMore(String cursor) async {
|
||||||
|
var data = await SettingsProvider.of(context).query('''
|
||||||
|
{
|
||||||
|
repository(owner: "$owner", name: "$name") {
|
||||||
|
issue(number: $id) {
|
||||||
|
timeline(first: $pageSize, after: $cursor) {
|
||||||
|
totalCount
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
$graghqlChunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
return data['repository']['issue'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List> queryTrailing() async {
|
||||||
|
var data = await SettingsProvider.of(context).query('''
|
||||||
|
{
|
||||||
|
repository(owner: "$owner", name: "$name") {
|
||||||
|
issue(number: $id) {
|
||||||
|
timeline(last: $pageSize) {
|
||||||
|
nodes {
|
||||||
|
$graghqlChunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
return data['repository']['issue']['timeline']['nodes'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LongListScaffold(
|
||||||
|
title: Text(_fullName + ' #' + widget.number.toString()),
|
||||||
|
headerBuilder: (payload) {
|
||||||
return Column(children: <Widget>[
|
return Column(children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
// padding: EdgeInsets.all(10),
|
// padding: EdgeInsets.all(10),
|
||||||
@ -40,52 +116,42 @@ class _IssueScreenState extends State<IssueScreen> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
get _fullName => widget.owner + '/' + widget.name;
|
|
||||||
|
|
||||||
List get _items => payload == null ? [] : payload['timeline']['nodes'];
|
|
||||||
|
|
||||||
Future queryIssue(
|
|
||||||
BuildContext context, int id, String owner, String name) async {
|
|
||||||
var data = await SettingsProvider.of(context).query('''
|
|
||||||
{
|
|
||||||
repository(owner: "$owner", name: "$name") {
|
|
||||||
issue(number: $id) {
|
|
||||||
$graphqlChunk1
|
|
||||||
timeline(first: $pageSize) {
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
nodes {
|
|
||||||
$graghqlChunk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''');
|
|
||||||
return data['repository']['issue'];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListScaffold(
|
|
||||||
title: Text(_fullName + ' #' + widget.id.toString()),
|
|
||||||
header: payload == null ? null : _buildHeader(),
|
|
||||||
itemCount: _items.length,
|
|
||||||
itemBuilder: (context, index) => TimelineItem(_items[index], payload),
|
|
||||||
onRefresh: () async {
|
|
||||||
var _payload =
|
|
||||||
await queryIssue(context, widget.id, widget.owner, widget.name);
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
payload = _payload;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// onLoadMore: () => ,
|
itemBuilder: (itemPayload) => TimelineItem(itemPayload),
|
||||||
|
onRefresh: () async {
|
||||||
|
var res = await queryIssue();
|
||||||
|
int totalCount = res['timeline']['totalCount'];
|
||||||
|
String cursor = res['timeline']['pageInfo']['endCursor'];
|
||||||
|
List leadingItems = res['timeline']['nodes'];
|
||||||
|
|
||||||
|
var payload = LongListPayload(
|
||||||
|
header: res,
|
||||||
|
totalCount: totalCount,
|
||||||
|
cursor: cursor,
|
||||||
|
leadingItems: leadingItems,
|
||||||
|
trailingItems: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (totalCount > 2 * pageSize) {
|
||||||
|
payload.trailingItems = await queryTrailing();
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
onLoadMore: (String _cursor) async {
|
||||||
|
var res = await queryMore(_cursor);
|
||||||
|
int totalCount = res['timeline']['totalCount'];
|
||||||
|
String cursor = res['timeline']['pageInfo']['endCursor'];
|
||||||
|
List leadingItems = res['timeline']['nodes'];
|
||||||
|
|
||||||
|
var payload = LongListPayload(
|
||||||
|
totalCount: totalCount,
|
||||||
|
cursor: cursor,
|
||||||
|
leadingItems: leadingItems,
|
||||||
|
);
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,76 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import '../providers/settings.dart';
|
import '../providers/settings.dart';
|
||||||
import '../utils/utils.dart';
|
import '../utils/utils.dart';
|
||||||
import '../widgets/list_scaffold.dart';
|
import '../widgets/long_list_scaffold.dart';
|
||||||
import '../widgets/timeline_item.dart';
|
import '../widgets/timeline_item.dart';
|
||||||
import '../widgets/comment_item.dart';
|
import '../widgets/comment_item.dart';
|
||||||
|
|
||||||
class PullRequestScreen extends StatefulWidget {
|
class PullRequestScreen extends StatefulWidget {
|
||||||
final int id;
|
final int number;
|
||||||
final String owner;
|
final String owner;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
PullRequestScreen(this.id, this.owner, this.name);
|
PullRequestScreen({
|
||||||
|
@required this.number,
|
||||||
|
@required this.owner,
|
||||||
|
@required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
PullRequestScreen.fromFullName(
|
||||||
|
{@required this.number, @required String fullName})
|
||||||
|
: this.owner = fullName.split('/')[0],
|
||||||
|
this.name = fullName.split('/')[1];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PullRequestScreenState createState() => _PullRequestScreenState();
|
_PullRequestScreenState createState() => _PullRequestScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var commonChunk = '''
|
||||||
|
$graghqlChunk
|
||||||
|
... on ReviewRequestedEvent {
|
||||||
|
createdAt
|
||||||
|
actor {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
requestedReviewer {
|
||||||
|
... on User {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on PullRequestReview {
|
||||||
|
createdAt
|
||||||
|
state
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on MergedEvent {
|
||||||
|
createdAt
|
||||||
|
mergeRefName
|
||||||
|
actor {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
commit {
|
||||||
|
oid
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on HeadRefDeletedEvent {
|
||||||
|
createdAt
|
||||||
|
actor {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
headRefName
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
class _PullRequestScreenState extends State<PullRequestScreen> {
|
class _PullRequestScreenState extends State<PullRequestScreen> {
|
||||||
Map<String, dynamic> payload;
|
get owner => widget.owner;
|
||||||
|
get id => widget.number;
|
||||||
Future queryPullRequest(BuildContext context) async {
|
get name => widget.name;
|
||||||
var owner = widget.owner;
|
|
||||||
var id = widget.id;
|
|
||||||
var name = widget.name;
|
|
||||||
|
|
||||||
|
Future queryPullRequest() async {
|
||||||
var data = await SettingsProvider.of(context).query('''
|
var data = await SettingsProvider.of(context).query('''
|
||||||
{
|
{
|
||||||
repository(owner: "$owner", name: "$name") {
|
repository(owner: "$owner", name: "$name") {
|
||||||
@ -38,48 +85,12 @@ class _PullRequestScreenState extends State<PullRequestScreen> {
|
|||||||
totalCount
|
totalCount
|
||||||
}
|
}
|
||||||
timeline(first: $pageSize) {
|
timeline(first: $pageSize) {
|
||||||
|
totalCount
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
|
||||||
endCursor
|
endCursor
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
$graghqlChunk
|
$commonChunk
|
||||||
... on ReviewRequestedEvent {
|
|
||||||
createdAt
|
|
||||||
actor {
|
|
||||||
login
|
|
||||||
}
|
|
||||||
requestedReviewer {
|
|
||||||
... on User {
|
|
||||||
login
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on PullRequestReview {
|
|
||||||
createdAt
|
|
||||||
state
|
|
||||||
author {
|
|
||||||
login
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on MergedEvent {
|
|
||||||
createdAt
|
|
||||||
mergeRefName
|
|
||||||
actor {
|
|
||||||
login
|
|
||||||
}
|
|
||||||
commit {
|
|
||||||
oid
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on HeadRefDeletedEvent {
|
|
||||||
createdAt
|
|
||||||
actor {
|
|
||||||
login
|
|
||||||
}
|
|
||||||
headRefName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,7 +100,45 @@ class _PullRequestScreenState extends State<PullRequestScreen> {
|
|||||||
return data['repository']['pullRequest'];
|
return data['repository']['pullRequest'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBadge() {
|
Future queryMore(String cursor) async {
|
||||||
|
var data = await SettingsProvider.of(context).query('''
|
||||||
|
{
|
||||||
|
repository(owner: "$owner", name: "$name") {
|
||||||
|
pullRequest(number: $id) {
|
||||||
|
timeline(first: $pageSize, after: $cursor) {
|
||||||
|
totalCount
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
$commonChunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
return data['repository']['pullRequest'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List> queryTrailing() async {
|
||||||
|
var data = await SettingsProvider.of(context).query('''
|
||||||
|
{
|
||||||
|
repository(owner: "$owner", name: "$name") {
|
||||||
|
pullRequest(number: $id) {
|
||||||
|
timeline(last: $pageSize) {
|
||||||
|
nodes {
|
||||||
|
$commonChunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
return data['repository']['pullRequest']['timeline']['nodes'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBadge(payload) {
|
||||||
bool merged = payload['merged'];
|
bool merged = payload['merged'];
|
||||||
Color bgColor = merged ? Palette.purple : Palette.green;
|
Color bgColor = merged ? Palette.purple : Palette.green;
|
||||||
IconData iconData = merged ? Octicons.git_merge : Octicons.git_pull_request;
|
IconData iconData = merged ? Octicons.git_merge : Octicons.git_pull_request;
|
||||||
@ -117,14 +166,18 @@ class _PullRequestScreenState extends State<PullRequestScreen> {
|
|||||||
|
|
||||||
get _fullName => widget.owner + '/' + widget.name;
|
get _fullName => widget.owner + '/' + widget.name;
|
||||||
|
|
||||||
Widget _buildHeader() {
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LongListScaffold(
|
||||||
|
title: Text(_fullName + ' #' + widget.number.toString()),
|
||||||
|
headerBuilder: (payload) {
|
||||||
return Column(children: <Widget>[
|
return Column(children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
// padding: EdgeInsets.all(10),
|
// padding: EdgeInsets.all(10),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildBadge(),
|
_buildBadge(payload),
|
||||||
Text(
|
Text(
|
||||||
payload['title'],
|
payload['title'],
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -138,26 +191,42 @@ class _PullRequestScreenState extends State<PullRequestScreen> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
List get _items => payload == null ? [] : payload['timeline']['nodes'];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListScaffold(
|
|
||||||
title: Text(_fullName + ' #' + widget.id.toString()),
|
|
||||||
header: payload == null ? null : _buildHeader(),
|
|
||||||
itemCount: _items.length,
|
|
||||||
itemBuilder: (context, index) => TimelineItem(_items[index], payload),
|
|
||||||
onRefresh: () async {
|
|
||||||
var _payload = await queryPullRequest(context);
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
payload = _payload;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// onLoadMore: () => ,
|
itemBuilder: (itemPayload) => TimelineItem(itemPayload),
|
||||||
|
onRefresh: () async {
|
||||||
|
var res = await queryPullRequest();
|
||||||
|
int totalCount = res['timeline']['totalCount'];
|
||||||
|
String cursor = res['timeline']['pageInfo']['endCursor'];
|
||||||
|
List leadingItems = res['timeline']['nodes'];
|
||||||
|
|
||||||
|
var payload = LongListPayload(
|
||||||
|
header: res,
|
||||||
|
totalCount: totalCount,
|
||||||
|
cursor: cursor,
|
||||||
|
leadingItems: leadingItems,
|
||||||
|
trailingItems: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (totalCount > 2 * pageSize) {
|
||||||
|
payload.trailingItems = await queryTrailing();
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
onLoadMore: (String _cursor) async {
|
||||||
|
var res = await queryMore(_cursor);
|
||||||
|
int totalCount = res['timeline']['totalCount'];
|
||||||
|
String cursor = res['timeline']['pageInfo']['endCursor'];
|
||||||
|
List leadingItems = res['timeline']['nodes'];
|
||||||
|
|
||||||
|
var payload = LongListPayload(
|
||||||
|
totalCount: totalCount,
|
||||||
|
cursor: cursor,
|
||||||
|
leadingItems: leadingItems,
|
||||||
|
);
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ class Palette {
|
|||||||
static const gray = Color(0xff959da5);
|
static const gray = Color(0xff959da5);
|
||||||
}
|
}
|
||||||
|
|
||||||
final pageSize = 20;
|
final pageSize = 5;
|
||||||
|
|
||||||
final graphqlChunk1 = '''
|
final graphqlChunk1 = '''
|
||||||
title
|
title
|
||||||
|
@ -19,17 +19,17 @@ class EventItem extends StatelessWidget {
|
|||||||
|
|
||||||
TextSpan _buildIssue(BuildContext context) {
|
TextSpan _buildIssue(BuildContext context) {
|
||||||
int id = event.payload['issue']['number'];
|
int id = event.payload['issue']['number'];
|
||||||
String name = event.repo.name;
|
return createLinkSpan(context, '#' + id.toString(),
|
||||||
var arr = name.split('/');
|
() => IssueScreen.fromFullName(number: id, fullName: event.repo.name));
|
||||||
return createLinkSpan(
|
|
||||||
context, '#' + id.toString(), () => IssueScreen(id, arr[0], arr[1]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextSpan _buildPullRequest(BuildContext context, int id) {
|
TextSpan _buildPullRequest(BuildContext context, int number) {
|
||||||
String name = event.repo.name;
|
return createLinkSpan(
|
||||||
var arr = name.split('/');
|
context,
|
||||||
return createLinkSpan(context, '#' + id.toString(),
|
'#' + number.toString(),
|
||||||
() => PullRequestScreen(id, arr[0], arr[1]));
|
() => PullRequestScreen.fromFullName(
|
||||||
|
number: number, fullName: event.repo.name),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildItem({
|
Widget _buildItem({
|
||||||
|
218
lib/widgets/long_list_scaffold.dart
Normal file
218
lib/widgets/long_list_scaffold.dart
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import '../providers/settings.dart';
|
||||||
|
import 'loading.dart';
|
||||||
|
import 'link.dart';
|
||||||
|
|
||||||
|
class LongListPayload<T, K> {
|
||||||
|
T header;
|
||||||
|
int totalCount;
|
||||||
|
String cursor;
|
||||||
|
List<K> leadingItems;
|
||||||
|
List<K> trailingItems;
|
||||||
|
|
||||||
|
LongListPayload({
|
||||||
|
this.header,
|
||||||
|
this.totalCount,
|
||||||
|
this.cursor,
|
||||||
|
this.leadingItems,
|
||||||
|
this.trailingItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a scaffold for issue and pull request
|
||||||
|
// Since the list could be very long, and some users may only want to to check trailing items
|
||||||
|
// We should load leading and trailing items at first fetching, and do load more in the middle
|
||||||
|
// e.g. https://github.com/reactjs/rfcs/pull/68
|
||||||
|
class LongListScaffold<T, K> extends StatefulWidget {
|
||||||
|
final Widget title;
|
||||||
|
final List<Widget> actions;
|
||||||
|
final Widget trailing;
|
||||||
|
final Widget Function(T headerPayload) headerBuilder;
|
||||||
|
final Widget Function(K itemPayload) itemBuilder;
|
||||||
|
final Future<LongListPayload<T, K>> Function() onRefresh;
|
||||||
|
final Future<LongListPayload<T, K>> Function(String cursor) onLoadMore;
|
||||||
|
|
||||||
|
LongListScaffold({
|
||||||
|
@required this.title,
|
||||||
|
this.actions,
|
||||||
|
this.trailing,
|
||||||
|
@required this.headerBuilder,
|
||||||
|
@required this.itemBuilder,
|
||||||
|
@required this.onRefresh,
|
||||||
|
@required this.onLoadMore,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LongListScaffoldState createState() => _LongListScaffoldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
|
||||||
|
bool loading;
|
||||||
|
bool loadingMore = false;
|
||||||
|
LongListPayload<T, K> payload;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refresh() async {
|
||||||
|
print('long list scaffold refresh');
|
||||||
|
setState(() {
|
||||||
|
loading = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
payload = await widget.onRefresh();
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadMore() async {
|
||||||
|
print('long list scaffold load more');
|
||||||
|
setState(() {
|
||||||
|
loadingMore = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
var _payload = await widget.onLoadMore(payload.cursor);
|
||||||
|
payload.totalCount = _payload.totalCount;
|
||||||
|
payload.cursor = _payload.cursor;
|
||||||
|
payload.leadingItems.addAll(_payload.leadingItems);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
loadingMore = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(BuildContext context, int index) {
|
||||||
|
if (index % 2 == 1) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(bottom: BorderSide(color: Colors.black12)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int realIndex = index ~/ 2;
|
||||||
|
|
||||||
|
if (realIndex < payload.leadingItems.length) {
|
||||||
|
return widget.itemBuilder(payload.leadingItems[realIndex]);
|
||||||
|
} else if (realIndex == payload.leadingItems.length) {
|
||||||
|
var count = payload.totalCount -
|
||||||
|
payload.leadingItems.length +
|
||||||
|
payload.trailingItems.length;
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: ExactAssetImage('images/progressive-disclosure-line.png',
|
||||||
|
scale: 2),
|
||||||
|
repeat: ImageRepeat.repeatX,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Link(
|
||||||
|
beforeRedirect: _loadMore,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.black12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Text('$count hidden items',
|
||||||
|
style: TextStyle(color: Colors.black87, fontSize: 15)),
|
||||||
|
Padding(padding: EdgeInsets.only(top: 4)),
|
||||||
|
loadingMore
|
||||||
|
? CupertinoActivityIndicator()
|
||||||
|
: Text(
|
||||||
|
'Load more...',
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.blueAccent, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return widget.itemBuilder(
|
||||||
|
payload.trailingItems[realIndex - payload.leadingItems.length - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get _itemCount {
|
||||||
|
int count = payload.leadingItems.length + payload.trailingItems.length;
|
||||||
|
if (payload.totalCount > count) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return 2 * count; // including bottom border
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSliver() {
|
||||||
|
if (loading) {
|
||||||
|
return SliverToBoxAdapter(child: Loading(more: false));
|
||||||
|
} else {
|
||||||
|
return SliverList(
|
||||||
|
delegate:
|
||||||
|
SliverChildBuilderDelegate(_buildItem, childCount: _itemCount),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
|
if (loading) {
|
||||||
|
return Loading(more: false);
|
||||||
|
} else {
|
||||||
|
return ListView.builder(itemCount: _itemCount, itemBuilder: _buildItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (SettingsProvider.of(context).theme) {
|
||||||
|
case ThemeMap.cupertino:
|
||||||
|
List<Widget> slivers = [
|
||||||
|
CupertinoSliverRefreshControl(onRefresh: widget.onRefresh)
|
||||||
|
];
|
||||||
|
if (payload != null) {
|
||||||
|
slivers.add(
|
||||||
|
SliverToBoxAdapter(child: widget.headerBuilder(payload.header)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
slivers.add(_buildSliver());
|
||||||
|
|
||||||
|
return CupertinoPageScaffold(
|
||||||
|
navigationBar: CupertinoNavigationBar(
|
||||||
|
middle: widget.title,
|
||||||
|
trailing: widget.trailing,
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: CustomScrollView(slivers: slivers),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: widget.title,
|
||||||
|
actions: widget.actions,
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
onRefresh: widget.onRefresh,
|
||||||
|
child: _buildBody(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -55,9 +55,17 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
Widget _buildRoute(BuildContext context) {
|
Widget _buildRoute(BuildContext context) {
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case 'Issue':
|
case 'Issue':
|
||||||
return IssueScreen(payload.number, payload.owner, payload.name);
|
return IssueScreen(
|
||||||
|
number: payload.number,
|
||||||
|
owner: payload.owner,
|
||||||
|
name: payload.name,
|
||||||
|
);
|
||||||
case 'PullRequest':
|
case 'PullRequest':
|
||||||
return PullRequestScreen(payload.number, payload.owner, payload.name);
|
return PullRequestScreen(
|
||||||
|
number: payload.number,
|
||||||
|
owner: payload.owner,
|
||||||
|
name: payload.name,
|
||||||
|
);
|
||||||
case 'Release':
|
case 'Release':
|
||||||
// return
|
// return
|
||||||
default:
|
default:
|
||||||
|
@ -7,9 +7,8 @@ import 'user_name.dart';
|
|||||||
|
|
||||||
class TimelineItem extends StatelessWidget {
|
class TimelineItem extends StatelessWidget {
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
final Map<String, dynamic> payload;
|
|
||||||
|
|
||||||
TimelineItem(this.item, this.payload);
|
TimelineItem(this.item);
|
||||||
|
|
||||||
TextSpan _buildReviewText(BuildContext context, item) {
|
TextSpan _buildReviewText(BuildContext context, item) {
|
||||||
switch (item['state']) {
|
switch (item['state']) {
|
||||||
@ -189,7 +188,9 @@ class TimelineItem extends StatelessWidget {
|
|||||||
case 'ReviewRequestedEvent':
|
case 'ReviewRequestedEvent':
|
||||||
return _buildItem(
|
return _buildItem(
|
||||||
iconData: Octicons.eye,
|
iconData: Octicons.eye,
|
||||||
actor: payload['author']['login'],
|
// actor: payload['author']['login'],
|
||||||
|
// TODO:
|
||||||
|
actor: 'test',
|
||||||
textSpan: TextSpan(children: [
|
textSpan: TextSpan(children: [
|
||||||
TextSpan(text: ' requested a review from '),
|
TextSpan(text: ' requested a review from '),
|
||||||
createUserSpan(item['requestedReviewer']['login']),
|
createUserSpan(item['requestedReviewer']['login']),
|
||||||
@ -247,10 +248,6 @@ class TimelineItem extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(10),
|
padding: EdgeInsets.all(10),
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom:
|
|
||||||
BorderSide(color: CupertinoColors.extraLightBackgroundGray))),
|
|
||||||
child: _buildByType(context),
|
child: _buildByType(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,10 @@ dependencies:
|
|||||||
github: ^4.1.0
|
github: ^4.1.0
|
||||||
intl: ^0.15.7
|
intl: ^0.15.7
|
||||||
url_launcher: ^4.2.0
|
url_launcher: ^4.2.0
|
||||||
|
uni_links: ^0.1.4
|
||||||
flutter_markdown: ^0.2.0
|
flutter_markdown: ^0.2.0
|
||||||
|
shared_preferences: ^0.5.0
|
||||||
|
nanoid: ^0.0.6
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
@ -48,6 +51,9 @@ flutter:
|
|||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- images/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.io/assets-and-images/#resolution-aware.
|
# https://flutter.io/assets-and-images/#resolution-aware.
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user