Merge pull request #26 from krawieck/community-page

This commit is contained in:
Marcin Wojnarowski 2020-09-07 23:31:57 +02:00 committed by GitHub
commit b214c13827
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 603 additions and 37 deletions

482
lib/pages/community.dart Normal file
View File

@ -0,0 +1,482 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import '../util/intl.dart';
import '../util/text_color.dart';
import '../widgets/badge.dart';
import '../widgets/markdown_text.dart';
class CommunityPage extends HookWidget {
final Future<FullCommunityView> _fullCommunityFuture;
final CommunityView _community;
final String instanceUrl;
CommunityPage.fromName(
{@required String communityName, @required this.instanceUrl})
: assert(communityName != null),
assert(instanceUrl != null),
_fullCommunityFuture =
LemmyApi(instanceUrl).v1.getCommunity(name: communityName),
_community = null;
CommunityPage.fromId({@required int communityId, @required this.instanceUrl})
: assert(communityId != null),
assert(instanceUrl != null),
_fullCommunityFuture =
LemmyApi(instanceUrl).v1.getCommunity(id: communityId),
_community = null;
CommunityPage.fromCommunityView(this._community)
: instanceUrl = _community.actorId.split('/')[2],
_fullCommunityFuture = LemmyApi(_community.actorId.split('/')[2])
.v1
.getCommunity(name: _community.name);
void _goToInstance() {
print('GO TO INSTANCE');
}
void _subscribe() {
print('SUBSCRIBE');
}
void _share() {
print('SHARE');
}
void _openMoreMenu() {
print('OPEN MORE MENU');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
var fullCommunitySnap = useFuture(_fullCommunityFuture);
final colorOnCard = textColorBasedOnBackground(theme.cardColor);
final community = () {
if (fullCommunitySnap.hasData) {
return fullCommunitySnap.data.community;
} else if (_community != null) {
return _community;
} else {
return null;
}
}();
if (community == null) {
return Scaffold(
appBar: AppBar(
iconTheme: theme.iconTheme,
backgroundColor: theme.cardColor,
elevation: 0,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (fullCommunitySnap.hasError) ...[
Icon(Icons.error),
Padding(
padding: const EdgeInsets.all(8),
child: Text('ERROR: ${fullCommunitySnap.error}'),
)
] else
CircularProgressIndicator(semanticsLabel: 'loading')
],
),
),
);
}
return Scaffold(
body: DefaultTabController(
length: 3,
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
// TODO: change top section to be more flexible
SliverAppBar(
expandedHeight: 300,
floating: false,
pinned: true,
elevation: 0,
backgroundColor: theme.cardColor,
iconTheme: theme.iconTheme,
title: Text('!${community.name}',
style: TextStyle(color: colorOnCard)),
actions: [
IconButton(icon: Icon(Icons.share), onPressed: _share),
IconButton(
icon: Icon(Icons.more_vert), onPressed: _openMoreMenu),
],
flexibleSpace: FlexibleSpaceBar(
background: _CommunityOverview(
community,
instanceUrl: instanceUrl,
goToInstance: _goToInstance,
subscribe: _subscribe,
),
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
labelColor: theme.textTheme.bodyText1.color,
unselectedLabelColor: Colors.grey,
tabs: [
Tab(text: 'Posts'),
Tab(text: 'Comments'),
Tab(text: 'About'),
],
),
),
pinned: true,
),
],
body: TabBarView(
children: [
ListView(
children: [
Center(child: Text('posts go here')),
],
),
ListView(
children: [
Center(child: Text('comments go here')),
],
),
_AboutTab(
community: community,
moderators: fullCommunitySnap.data?.moderators,
),
],
),
),
),
);
}
}
class _CommunityOverview extends StatelessWidget {
final CommunityView community;
final String instanceUrl;
final void Function() goToInstance;
final void Function() subscribe;
_CommunityOverview(
this.community, {
@required this.instanceUrl,
@required this.goToInstance,
@required this.subscribe,
}) : assert(instanceUrl != null),
assert(goToInstance != null),
assert(subscribe != null);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorOnTopOfAccent = textColorBasedOnBackground(theme.accentColor);
final shadow = BoxShadow(color: theme.canvasColor, blurRadius: 5);
final icon = community.icon != null
? Stack(
alignment: Alignment.center,
children: [
Container(
width: 90,
height: 90,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.7), blurRadius: 3)
]),
),
Container(
width: 83,
height: 83,
child: CachedNetworkImage(
imageUrl: community.icon,
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover,
image: imageProvider,
),
),
),
),
),
],
)
: null;
final subscribed = community.subscribed ?? false;
return Stack(children: [
if (community.banner != null)
CachedNetworkImage(imageUrl: community.banner),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 45),
child: Column(children: [
if (community.icon != null)
Center(
child: Padding(
padding: const EdgeInsets.only(top: 0),
child: Center(child: icon),
),
),
// NAME
Center(
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: RichText(
overflow: TextOverflow.ellipsis, // TODO: fix overflowing
text: TextSpan(
style: theme.textTheme.subtitle1.copyWith(shadows: [shadow]),
children: [
TextSpan(
text: '!',
style: TextStyle(fontWeight: FontWeight.w200)),
TextSpan(
text: community.name,
style: TextStyle(fontWeight: FontWeight.w600)),
TextSpan(
text: '@',
style: TextStyle(fontWeight: FontWeight.w200)),
TextSpan(
text: instanceUrl,
style: TextStyle(fontWeight: FontWeight.w600),
recognizer: TapGestureRecognizer()
..onTap = goToInstance),
],
),
),
)),
// TITLE/MOTTO
Center(
child: Padding(
padding: const EdgeInsets.only(top: 8, left: 20, right: 20),
child: Text(
community.title,
textAlign: TextAlign.center,
style:
TextStyle(fontWeight: FontWeight.w300, shadows: [shadow]),
),
)),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Stack(
children: [
// INFO ICONS
Padding(
padding: const EdgeInsets.only(top: 5),
child: Row(
children: [
Spacer(),
Padding(
padding: const EdgeInsets.only(right: 3),
child: Icon(Icons.people, size: 20),
),
Text(compactNumber(community.numberOfSubscribers)),
Spacer(
flex: 4,
),
Padding(
padding: const EdgeInsets.only(right: 3),
child: Icon(Icons.record_voice_over, size: 20),
),
Text('xx'), // TODO: display online users
Spacer(),
],
),
),
// SUBSCRIBE BUTTON
Center(
child: SizedBox(
height: 27,
child: RaisedButton.icon(
padding:
EdgeInsets.symmetric(vertical: 0, horizontal: 20),
onPressed: subscribe,
icon: subscribed
? Icon(Icons.remove,
size: 18, color: colorOnTopOfAccent)
: Icon(Icons.add,
size: 18, color: colorOnTopOfAccent),
color: theme.accentColor,
label: Text(
'${subscribed ? 'un' : ''}subscribe',
style: TextStyle(
color: colorOnTopOfAccent,
fontSize: theme.textTheme.subtitle1.fontSize),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
),
],
),
),
]),
),
),
]);
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar);
final TabBar _tabBar;
@override
double get minExtent => _tabBar.preferredSize.height;
@override
double get maxExtent => _tabBar.preferredSize.height;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final theme = Theme.of(context);
return Container(child: _tabBar, color: theme.cardColor);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) => false;
}
class _AboutTab extends StatelessWidget {
final CommunityView community;
final List<CommunityModeratorView> moderators;
const _AboutTab({
Key key,
@required this.community,
@required this.moderators,
}) : super(key: key);
void goToUser(int id) {
print('GO TO USER $id');
}
void goToModlog() {
print('GO TO MODLOG');
}
void goToCategories() {
print('GO TO CATEGORIES');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListView(
padding: EdgeInsets.only(top: 20),
children: [
if (community.description != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: MarkdownText(community.description),
),
_Divider(),
],
SizedBox(
height: 25,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
// TODO: consider using Chips
Padding(
padding: const EdgeInsets.only(left: 7),
child: _Badge('X users online'),
),
_Badge(
'''${community.numberOfSubscribers} subscriber${pluralS(community.numberOfSubscribers)}'''),
_Badge(
'''${community.numberOfPosts} post${pluralS(community.numberOfPosts)}'''),
Padding(
padding: const EdgeInsets.only(right: 15),
child: _Badge(
'''${community.numberOfComments} comment${pluralS(community.numberOfComments)}'''),
),
],
),
),
_Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 0),
child: OutlineButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Text('${community.categoryName}'),
onPressed: goToCategories,
),
),
_Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: OutlineButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Text('Modlog'),
onPressed: goToModlog,
),
),
_Divider(),
if (moderators != null && moderators.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Text('Mods:', style: theme.textTheme.subtitle2),
),
for (final mod in moderators)
ListTile(
title: Text(mod.userPreferredUsername ?? '@${mod.userName}'),
onTap: () => goToUser(mod.id),
),
]
],
);
}
}
class _Badge extends StatelessWidget {
final String text;
final bool noPad;
_Badge(this.text, {this.noPad = false});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: noPad ? const EdgeInsets.all(0) : const EdgeInsets.only(left: 8),
child: Badge(
child: Text(
text,
style:
TextStyle(color: textColorBasedOnBackground(theme.accentColor)),
),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
);
}
}
class _Divider extends StatelessWidget {
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Divider(),
);
}

View File

@ -7,7 +7,7 @@ import '../widgets/comment_section.dart';
import '../widgets/post.dart';
class FullPostPage extends HookWidget {
final Future<FullPost> fullPost;
final Future<FullPostView> fullPost;
final PostView post;
FullPostPage({@required int id, @required String instanceUrl})

View File

@ -2,8 +2,12 @@ import 'package:flutter/material.dart';
class Badge extends StatelessWidget {
final Widget child;
final BorderRadiusGeometry borderRadius;
Badge({@required this.child});
Badge({
@required this.child,
this.borderRadius = const BorderRadius.all(Radius.circular(5)),
});
@override
Widget build(BuildContext context) {
@ -13,7 +17,7 @@ class Badge extends StatelessWidget {
height: 25,
decoration: BoxDecoration(
color: theme.accentColor,
borderRadius: BorderRadius.all(Radius.circular(5)),
borderRadius: borderRadius,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),

View File

@ -13,29 +13,34 @@ class BottomModal extends StatelessWidget {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
borderRadius: BorderRadius.all(const Radius.circular(10.0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) ...[
Padding(
padding: const EdgeInsets.only(left: 70),
child: Text(
'account',
style: theme.textTheme.subtitle2,
textAlign: TextAlign.left,
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
borderRadius: BorderRadius.all(const Radius.circular(10.0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) ...[
Padding(
padding: const EdgeInsets.only(left: 70),
child: Text(
title,
style: theme.textTheme.subtitle2,
textAlign: TextAlign.left,
),
),
),
Divider()
Divider(
indent: 20,
endIndent: 20,
)
],
child,
],
child,
],
),
),
),
),

View File

@ -0,0 +1,75 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import 'bottom_modal.dart';
class PostListOptions extends HookWidget {
final void Function(SortType sort) onChange;
final SortType defaultSort;
PostListOptions({
@required this.onChange,
this.defaultSort = SortType.active,
});
@override
Widget build(BuildContext context) {
var sort = useState(defaultSort);
void selectSortType(BuildContext context) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => BottomModal(
title: 'sort by',
child: Column(
children: [
for (var x in SortType.values)
RadioListTile<SortType>(
value: x,
groupValue: sort.value,
// TODO: use something more robust and user-friendly
// than describeEnum
title: Text(describeEnum(x)),
onChanged: (val) {
sort.value = val;
onChange(val);
Navigator.of(context).pop();
},
),
],
)),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Row(
children: [
OutlineButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onPressed: () => selectSortType(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(describeEnum(sort.value)),
const SizedBox(width: 8),
Icon(Icons.arrow_drop_down),
],
),
),
Spacer(),
IconButton(
icon: Icon(Icons.view_stream),
onPressed: () => print('TBD'),
)
],
),
);
}
}

View File

@ -63,7 +63,7 @@ packages:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.10"
version: "1.3.11"
build_runner:
dependency: "direct dev"
description:
@ -77,7 +77,7 @@ packages:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
version: "6.0.1"
built_collection:
dependency: transitive
description:
@ -140,7 +140,7 @@ packages:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.4.0"
version: "3.4.1"
collection:
dependency: transitive
description:
@ -161,7 +161,7 @@ packages:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "2.1.5"
csslib:
dependency: transitive
description:
@ -250,7 +250,7 @@ packages:
name: flutter_markdown
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.4.4"
flutter_mobx:
dependency: "direct main"
description:
@ -344,7 +344,7 @@ packages:
name: lemmy_api_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
version: "0.4.0"
logging:
dependency: transitive
description:
@ -379,7 +379,7 @@ packages:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6+3"
version: "0.9.7"
mobx:
dependency: "direct main"
description:
@ -512,7 +512,7 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2+1"
version: "4.3.2+2"
pub_semver:
dependency: transitive
description:
@ -582,7 +582,7 @@ packages:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.7"
version: "0.7.9"
shelf_web_socket:
dependency: transitive
description:
@ -699,7 +699,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "5.5.1"
version: "5.5.2"
url_launcher_linux:
dependency: transitive
description:
@ -727,14 +727,14 @@ packages:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
version: "0.1.3+1"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "2.2.2"
vector_math:
dependency: transitive
description:

View File

@ -28,7 +28,7 @@ dependencies:
flutter_hooks: ^0.13.2
cached_network_image: ^2.2.0+1
timeago: ^2.0.27
lemmy_api_client: ^0.3.0
lemmy_api_client: ^0.4.0
mobx: ^1.2.1
flutter_mobx: ^1.1.0