From 8e678a37d4a98c54989ba1d206bffef1173166a0 Mon Sep 17 00:00:00 2001 From: Rongjian Zhang Date: Tue, 5 Feb 2019 20:57:05 +0800 Subject: [PATCH] feat: inbox screen --- lib/main.dart | 13 ++++- lib/providers/settings.dart | 2 +- lib/screens/inbox.dart | 54 ++++++++++++++++++ lib/screens/issue.dart | 2 +- lib/screens/news.dart | 2 +- lib/screens/notifications.dart | 2 +- lib/screens/pull_request.dart | 2 +- lib/utils/timeago.dart | 4 +- lib/utils/utils.dart | 45 ++++++++++++++- lib/widgets/event_item.dart | 2 - lib/widgets/list_scaffold.dart | 22 +++++--- lib/widgets/notification_item.dart | 90 +++++++++++++++++++++--------- 12 files changed, 194 insertions(+), 46 deletions(-) create mode 100644 lib/screens/inbox.dart diff --git a/lib/main.dart b/lib/main.dart index 9c1906b..30d80e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'providers/providers.dart'; import 'providers/settings.dart'; import 'screens/screens.dart'; +import 'screens/inbox.dart'; class Home extends StatefulWidget { @override @@ -51,6 +52,10 @@ class _HomeState extends State { List _buildNavigationItems() { return [ + BottomNavigationBarItem( + icon: Icon(Icons.inbox), + title: Text('Inbox'), + ), BottomNavigationBarItem( icon: Icon(Icons.rss_feed), title: Text('News'), @@ -73,12 +78,14 @@ class _HomeState extends State { _buildScreen(int index) { switch (index) { case 0: - return NewsScreen(); + return InboxScreen(); case 1: - return SearchScreen(); + return NewsScreen(); case 2: - return NotificationScreen(); + return SearchScreen(); case 3: + return NotificationScreen(); + case 4: return ProfileScreen(); } } diff --git a/lib/providers/settings.dart b/lib/providers/settings.dart index 02d134e..e62646c 100644 --- a/lib/providers/settings.dart +++ b/lib/providers/settings.dart @@ -30,7 +30,7 @@ class _SettingsProviderState extends State { if (Platform.isIOS) { layout = LayoutMap.cupertino; } - // layout = LayoutMap.material; + layout = LayoutMap.material; } @override diff --git a/lib/screens/inbox.dart b/lib/screens/inbox.dart new file mode 100644 index 0000000..9a24c2a --- /dev/null +++ b/lib/screens/inbox.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import '../widgets/list_scaffold.dart'; +import '../widgets/notification_item.dart'; +import '../utils/utils.dart'; + +Future> fetchNotifications(int page) async { + List items = + await getWithCredentials('/notifications?page=$page&per_page=20'); + return items.map((item) => NotificationPayload.fromJson(item)).toList(); +} + +class InboxScreen extends StatefulWidget { + @override + _InboxScreenState createState() => _InboxScreenState(); +} + +class _InboxScreenState extends State { + // int active = 0; + int page = 0; + var payload; + List _items = []; + + final titleMap = { + 0: 'Unread', + 1: 'Participating', + 2: 'All', + }; + + @override + Widget build(BuildContext context) { + return ListScaffold( + title: Text('Inbox'), + onRefresh: () async { + page = 1; + var items = await fetchNotifications(page); + setState(() { + _items = items; + }); + }, + onLoadMore: () async { + page = page + 1; + var items = await fetchNotifications(page); + setState(() { + _items.addAll(items); + }); + }, + itemCount: _items.length, + itemBuilder: (context, index) { + return NotificationItem(payload: _items[index]); + }, + ); + } +} diff --git a/lib/screens/issue.dart b/lib/screens/issue.dart index f3cb292..e8219c4 100644 --- a/lib/screens/issue.dart +++ b/lib/screens/issue.dart @@ -51,7 +51,7 @@ class _IssueScreenState extends State { @override Widget build(BuildContext context) { return ListScaffold( - title: _fullName + ' #' + widget.id.toString(), + title: Text(_fullName + ' #' + widget.id.toString()), header: payload == null ? null : _buildHeader(), itemCount: _items.length, itemBuilder: (context, index) => TimelineItem(_items[index], payload), diff --git a/lib/screens/news.dart b/lib/screens/news.dart index fa241d9..e8ae396 100644 --- a/lib/screens/news.dart +++ b/lib/screens/news.dart @@ -16,7 +16,7 @@ class NewsScreenState extends State { @override Widget build(context) { return ListScaffold( - title: 'News', + title: Text('News'), itemCount: _events.length, itemBuilder: (context, index) => EventItem(_events[index]), onRefresh: () async { diff --git a/lib/screens/notifications.dart b/lib/screens/notifications.dart index 4ed7c1c..5d3db4a 100644 --- a/lib/screens/notifications.dart +++ b/lib/screens/notifications.dart @@ -45,7 +45,7 @@ class NotificationScreenState extends State { style: TextStyle(color: Colors.black, fontSize: 15), ), items: group.items, - itemBuilder: (item) => NotificationItem(item: item), + itemBuilder: (item) => NotificationItem(payload: item), ); } diff --git a/lib/screens/pull_request.dart b/lib/screens/pull_request.dart index d301098..3fccaa5 100644 --- a/lib/screens/pull_request.dart +++ b/lib/screens/pull_request.dart @@ -138,7 +138,7 @@ class _PullRequestScreenState extends State { @override Widget build(BuildContext context) { return ListScaffold( - title: _fullName + ' #' + widget.id.toString(), + title: Text(_fullName + ' #' + widget.id.toString()), header: payload == null ? null : _buildHeader(), itemCount: _items.length, itemBuilder: (context, index) => TimelineItem(_items[index], payload), diff --git a/lib/utils/timeago.dart b/lib/utils/timeago.dart index 0d73772..79945c1 100644 --- a/lib/utils/timeago.dart +++ b/lib/utils/timeago.dart @@ -18,7 +18,9 @@ class TimeAgo { double diff = (DateTime.now().millisecondsSinceEpoch - time.millisecondsSinceEpoch) / 1000; - if (diff < 3600) { + if (diff < 0) { + return 'in the future'; + } else if (diff < 3600) { return _pluralize(diff / 60, 'minute'); } else if (diff < 86400) { return _pluralize(diff / 3600, 'hour'); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index b10b7b1..6a11649 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; - +import '../providers/settings.dart'; import '../screens/screens.dart'; export 'github.dart'; export 'octicons.dart'; @@ -14,6 +14,49 @@ Color convertColor(String cssHex) { return Color(int.parse('ff' + cssHex, radix: 16)); } +class Option { + final T value; + final Widget widget; + Option({this.value, this.widget}); +} + +Future showOptions(BuildContext context, List> options) { + var builder = (BuildContext context) { + return CupertinoAlertDialog( + actions: options.map((option) { + return CupertinoDialogAction( + child: option.widget, + onPressed: () { + Navigator.pop(context, option.value); + }, + ); + }).toList(), + ); + }; + + switch (SettingsProvider.of(context).layout) { + case LayoutMap.cupertino: + return showCupertinoDialog( + context: context, + builder: builder, + ); + default: + return showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + child: Column( + children: [ + PopupMenuItem(child: Text('a')), + PopupMenuItem(child: Text('b')), + ], + ), + ); + }, + ); + } +} + TextSpan createLinkSpan(BuildContext context, String text, Function handle) { return TextSpan( text: text, diff --git a/lib/widgets/event_item.dart b/lib/widgets/event_item.dart index da5ad68..2fda868 100644 --- a/lib/widgets/event_item.dart +++ b/lib/widgets/event_item.dart @@ -142,8 +142,6 @@ class EventItem extends StatelessWidget { build(BuildContext context) { return Container( padding: EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.black12))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/widgets/list_scaffold.dart b/lib/widgets/list_scaffold.dart index 485f3b8..65028c0 100644 --- a/lib/widgets/list_scaffold.dart +++ b/lib/widgets/list_scaffold.dart @@ -7,7 +7,7 @@ import 'loading.dart'; typedef RefreshCallback = Future Function(); class ListScaffold extends StatefulWidget { - final String title; + final Widget title; final Widget header; final int itemCount; final IndexedWidgetBuilder itemBuilder; @@ -77,11 +77,19 @@ class _ListScaffoldState extends State { } Widget _buildItem(BuildContext context, int index) { - if (index == widget.itemCount) { + if (index == 2 * widget.itemCount) { return Loading(more: true); } - return widget.itemBuilder(context, index); + if (index % 2 == 1) { + return Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.black12)), + ), + ); + } + + return widget.itemBuilder(context, index ~/ 2); } Widget _buildSliver(BuildContext context) { @@ -91,7 +99,7 @@ class _ListScaffoldState extends State { return SliverList( delegate: SliverChildBuilderDelegate( _buildItem, - childCount: widget.itemCount + 1, + childCount: 2 * widget.itemCount + 1, ), ); } @@ -103,7 +111,7 @@ class _ListScaffoldState extends State { } else { return ListView.builder( controller: _controller, - itemCount: widget.itemCount + 1, + itemCount: 2 * widget.itemCount + 1, itemBuilder: _buildItem, ); } @@ -122,7 +130,7 @@ class _ListScaffoldState extends State { slivers.add(_buildSliver(context)); return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(middle: Text(widget.title)), + navigationBar: CupertinoNavigationBar(middle: widget.title), child: SafeArea( child: CustomScrollView( controller: _controller, @@ -132,7 +140,7 @@ class _ListScaffoldState extends State { ); default: return Scaffold( - appBar: AppBar(title: Text(widget.title)), + appBar: AppBar(title: widget.title), body: RefreshIndicator( onRefresh: widget.onRefresh, child: _buildBody(context), diff --git a/lib/widgets/notification_item.dart b/lib/widgets/notification_item.dart index 2e8a2a3..7864a73 100644 --- a/lib/widgets/notification_item.dart +++ b/lib/widgets/notification_item.dart @@ -2,30 +2,56 @@ import 'dart:core'; import 'package:flutter/material.dart' hide Notification; import 'package:flutter/cupertino.dart' hide Notification; import '../utils/utils.dart'; +import '../screens/issue.dart'; +import '../screens/pull_request.dart'; import 'link.dart'; +class NotificationPayload { + String type; + String owner; + String name; + int number; + String title; + String updateAt; + bool unread; + + NotificationPayload.fromJson(input) { + type = input['subject']['type']; + name = input['repository']['name']; + owner = input['repository']['owner']['login']; + + String url = input['subject']['url']; + String numberStr = url.split('/').lastWhere((_) => true); + number = int.parse(numberStr); + + title = input['subject']['title']; + updateAt = TimeAgo.formatFromString(input['updated_at']); + unread = input['unread']; + } +} + class NotificationItem extends StatelessWidget { const NotificationItem({ Key key, - @required this.item, + @required this.payload, }) : super(key: key); - final Notification item; + final NotificationPayload payload; - Widget _buildRoute(Notification item) { - String type = item.subject.type; - switch (type) { + Widget _buildRoute() { + switch (payload.type) { case 'Issue': + return IssueScreen(payload.number, payload.owner, payload.name); case 'PullRequest': - // return IssueScreen(item.repository.); + return PullRequestScreen(payload.number, payload.owner, payload.name); default: // throw new Exception('Unhandled notification type: $type'); return Text('test'); } } - IconData _buildIconData(String type) { - switch (type) { + IconData _buildIconData() { + switch (payload.type) { case 'Issue': return Octicons.issue_opened; // color: Color.fromRGBO(0x28, 0xa7, 0x45, 1), @@ -42,39 +68,49 @@ class NotificationItem extends StatelessWidget { return Link( onTap: () { Navigator.of(context).push( - CupertinoPageRoute(builder: (context) => _buildRoute(item)), + CupertinoPageRoute(builder: (context) => _buildRoute()), ); }, - child: Row( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Icon( - _buildIconData(item.subject.type), - color: Colors.black45, - ), - ), - Expanded( - child: Row( + child: Container( + padding: EdgeInsets.all(8), + color: payload.unread ? Colors.white : Colors.black12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + padding: EdgeInsets.only(right: 8, top: 20), + child: Icon(_buildIconData(), color: Colors.black45), + ), Expanded( child: Container( - padding: EdgeInsets.symmetric(vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - item.subject.title, + payload.owner + + '/' + + payload.name + + ' #' + + payload.number.toString(), + style: TextStyle(fontSize: 13, color: Colors.black54), + ), + Padding(padding: EdgeInsets.only(top: 4)), + Text( + payload.title, style: TextStyle(fontSize: 15), maxLines: 3, overflow: TextOverflow.ellipsis, ), - Padding(padding: EdgeInsets.only(top: 8)), + Padding(padding: EdgeInsets.only(top: 6)), Text( - TimeAgo.format(item.updatedAt), + payload.updateAt, style: TextStyle( fontSize: 12, - color: Colors.black87, + // fontWeight: FontWeight.w300, + color: Colors.black54, ), ) ], @@ -87,8 +123,8 @@ class NotificationItem extends StatelessWidget { ), ], ), - ), - ], + ], + ), ), ); }