Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 120 additions & 94 deletions lib/community/widgets/community_header.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:thunder/feed/bloc/feed_bloc.dart';
import 'package:thunder/feed/utils/utils.dart';

import 'package:thunder/shared/avatars/community_avatar.dart';
import 'package:thunder/shared/full_name_widgets.dart';
import 'package:thunder/shared/icon_text.dart';
import 'package:thunder/utils/instance.dart';
import 'package:thunder/utils/colors.dart';
import 'package:thunder/utils/numbers.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class CommunityHeader extends StatefulWidget {
final bool showCommunitySidebar;
Expand All @@ -28,11 +28,22 @@ class CommunityHeader extends StatefulWidget {
State<CommunityHeader> createState() => _CommunityHeaderState();
}

class _CommunityHeaderState extends State<CommunityHeader> {
class _CommunityHeaderState extends State<CommunityHeader> with SingleTickerProviderStateMixin {
late AnimationController _bannerImageFadeInController;
late bool _hasBanner;

@override
void initState() {
_bannerImageFadeInController = AnimationController(vsync: this, duration: const Duration(milliseconds: 250), lowerBound: 0.0, upperBound: 1.0);
_hasBanner = widget.getCommunityResponse.communityView.community.banner?.isNotEmpty == true;

super.initState();
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final FeedBloc feedBloc = context.watch<FeedBloc>();
final AppLocalizations l10n = AppLocalizations.of(context)!;

return Material(
elevation: widget.showCommunitySidebar ? 5.0 : 0,
Expand All @@ -47,112 +58,127 @@ class _CommunityHeaderState extends State<CommunityHeader> {
},
child: Stack(
children: [
if (widget.getCommunityResponse.communityView.community.banner == null) Positioned.fill(child: Container(color: theme.colorScheme.background)),
if (widget.getCommunityResponse.communityView.community.banner != null)
Positioned.fill(
child: Row(
children: [
Expanded(flex: 1, child: Container()),
Expanded(
flex: 3,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: CachedNetworkImageProvider(widget.getCommunityResponse.communityView.community.banner!),
fit: BoxFit.cover,
),
),
),
),
],
Positioned.fill(child: Container(color: getBackgroundColor(context))),
if (_hasBanner)
SizedBox(
height: 100,
width: MediaQuery.sizeOf(context).width,
child: ExtendedImage.network(
widget.getCommunityResponse.communityView.community.banner!,
fit: BoxFit.cover,
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading:
_bannerImageFadeInController.reset();
return const SizedBox.shrink();
case LoadState.failed:
_bannerImageFadeInController.reset();
return const SizedBox.shrink();
case LoadState.completed:
if (state.wasSynchronouslyLoaded) return state.completedWidget;

_bannerImageFadeInController.forward();

return FadeTransition(
opacity: _bannerImageFadeInController,
child: state.completedWidget,
);
}
},
),
),
if (widget.getCommunityResponse.communityView.community.banner != null)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
theme.colorScheme.background,
theme.colorScheme.background,
theme.colorScheme.background.withOpacity(0.9),
theme.colorScheme.background.withOpacity(0.6),
theme.colorScheme.background.withOpacity(0.3),
],
),
Positioned(
left: 25,
top: _hasBanner ? 60 : 10,
child: Column(
children: [
CommunityAvatar(
community: widget.getCommunityResponse.communityView.community,
radius: 25,
showCommunityStatus: true,
showBorder: true,
),
),
],
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 16.0, left: 24.0, right: 24.0, bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
SizedBox(height: _hasBanner ? 125 : 15),
ConstrainedBox(
constraints: BoxConstraints(minHeight: _hasBanner ? 0 : 45),
child: Row(
children: [
Row(
children: [
CommunityAvatar(
community: widget.getCommunityResponse.communityView.community,
radius: 45.0,
showCommunityStatus: true,
),
const SizedBox(width: 20.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.getCommunityResponse.communityView.community.title,
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w600),
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.75,
child: Padding(
padding: EdgeInsets.only(left: _hasBanner ? 25 : 100),
child: Wrap(
runSpacing: 10,
children: [
Container(
decoration: BoxDecoration(
color: getBackgroundColorAlt(context),
borderRadius: BorderRadius.circular(6),
),
CommunityFullNameWidget(
context,
widget.getCommunityResponse.communityView.community.name,
widget.getCommunityResponse.communityView.community.title,
fetchInstanceNameFromUrl(widget.getCommunityResponse.communityView.community.actorId) ?? 'N/A',
// Override because we're showing right above
useDisplayName: false,
padding: const EdgeInsets.only(left: 4, right: 4),
child: IconText(
icon: const Icon(Icons.people_rounded, size: 15),
text: formatNumberToK(widget.getCommunityResponse.communityView.counts.subscribers),
),
const SizedBox(height: 8.0),
Wrap(
children: [
IconText(
icon: const Icon(Icons.people_rounded),
text: formatNumberToK(widget.getCommunityResponse.communityView.counts.subscribers),
),
const SizedBox(width: 8.0),
IconText(
icon: const Icon(Icons.calendar_month_rounded),
text: formatNumberToK(widget.getCommunityResponse.communityView.counts.usersActiveMonth),
),
const SizedBox(width: 8.0),
IconText(
icon: Icon(getSortIcon(feedBloc.state)),
text: getSortName(feedBloc.state),
),
],
),
const SizedBox(width: 8.0),
Container(
decoration: BoxDecoration(
color: getBackgroundColorAlt(context),
borderRadius: BorderRadius.circular(6),
),
],
),
padding: const EdgeInsets.only(left: 4, right: 4),
child: IconText(
icon: const Icon(Icons.calendar_month_rounded, size: 15),
text: formatNumberToK(widget.getCommunityResponse.communityView.counts.usersActiveMonth),
),
),
const SizedBox(width: 8.0),
Container(
decoration: BoxDecoration(
color: getBackgroundColorAlt(context),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.only(left: 4, right: 4),
child: IconText(
icon: Icon(getSortIcon(feedBloc.state), size: 15),
text: getSortName(feedBloc.state),
),
),
],
),
Padding(
padding: const EdgeInsets.all(9.0),
child: Icon(
Icons.info_outline_rounded,
size: 25,
shadows: <Shadow>[Shadow(color: theme.colorScheme.background, blurRadius: 10.0), Shadow(color: theme.colorScheme.background, blurRadius: 20.0)],
),
),
const Spacer(),
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () => widget.onToggle(!widget.showCommunitySidebar),
child: Container(
decoration: BoxDecoration(
color: getBackgroundColorAlt(context),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.only(left: 4, right: 4),
child: IconText(
icon: const Icon(Icons.info_outline_rounded, size: 15),
text: l10n.about,
),
),
],
),
),
const SizedBox(width: 25),
],
),
),
const SizedBox(height: 15),
],
),
],
Expand Down
59 changes: 59 additions & 0 deletions lib/feed/utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,41 @@ import 'package:thunder/community/bloc/community_bloc.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/feed/feed.dart';
import 'package:thunder/instance/bloc/instance_bloc.dart';
import 'package:thunder/shared/avatars/community_avatar.dart';
import 'package:thunder/shared/avatars/user_avatar.dart';
import 'package:thunder/shared/full_name_widgets.dart';
import 'package:thunder/shared/pages/loading_page.dart';
import 'package:thunder/shared/sort_picker.dart';
import 'package:thunder/community/widgets/community_drawer.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
import 'package:thunder/utils/instance.dart';
import 'package:thunder/utils/swipe.dart';

Widget getAppBarAvatar(FeedState state) {
if (state.status == FeedStatus.initial) {
return const SizedBox.shrink();
}

if ((state.communityId != null || state.communityName != null) && state.fullCommunityView!.communityView.community.icon?.isNotEmpty == true) {
return CommunityAvatar(
community: state.fullCommunityView!.communityView.community,
radius: 15,
showCommunityStatus: true,
showBorder: true,
);
}

if ((state.userId != null || state.username != null) && state.fullPersonView!.personView.person.avatar!.isNotEmpty == true) {
return UserAvatar(
person: state.fullPersonView!.personView.person,
radius: 15,
showBorder: true,
);
}

return const SizedBox.shrink();
}

String getAppBarTitle(FeedState state) {
if (state.status == FeedStatus.initial) {
return '';
Expand All @@ -33,6 +62,36 @@ String getAppBarTitle(FeedState state) {
return (state.postListingType != null) ? (destinations.firstWhere((destination) => destination.listingType == state.postListingType).label) : '';
}

Widget getAppBarSubtitle(BuildContext context, FeedState state) {
if (state.status == FeedStatus.initial) {
return const SizedBox.shrink();
}

if (state.communityId != null || state.communityName != null) {
return CommunityFullNameWidget(
context,
state.fullCommunityView!.communityView.community.name,
state.fullCommunityView!.communityView.community.title,
fetchInstanceNameFromUrl(state.fullCommunityView!.communityView.community.actorId) ?? '',
// Override because we're showing right above
useDisplayName: false,
);
}

if (state.userId != null || state.username != null) {
return UserFullNameWidget(
context,
state.fullPersonView!.personView.person.name,
state.fullPersonView!.personView.person.displayName,
fetchInstanceNameFromUrl(state.fullPersonView!.personView.person.actorId) ?? '',
// Override because we're showing right above
useDisplayName: false,
);
}

return const SizedBox.shrink();
}

String getSortName(FeedState state) {
if (state.status == FeedStatus.initial) {
return '';
Expand Down
17 changes: 9 additions & 8 deletions lib/feed/view/feed_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,9 @@ class FeedView extends StatefulWidget {
class _FeedViewState extends State<FeedView> {
final ScrollController _scrollController = ScrollController();

/// Boolean which indicates whether the title on the app bar should be shown
bool showAppBarTitle = false;
/// Boolean which indicates whether the page has been scrolled up
/// This is used to determine what to show on the app bar
bool isPageScrolled = false;

/// Boolean which indicates whether the community sidebar should be shown
bool showCommunitySidebar = false;
Expand Down Expand Up @@ -199,10 +200,10 @@ class _FeedViewState extends State<FeedView> {

_scrollController.addListener(() {
// Updates the [showAppBarTitle] value when the user has scrolled past a given threshold
if (_scrollController.position.pixels > 100.0 && showAppBarTitle == false) {
setState(() => showAppBarTitle = true);
} else if (_scrollController.position.pixels < 100.0 && showAppBarTitle == true) {
setState(() => showAppBarTitle = false);
if (_scrollController.position.pixels > 100.0 && isPageScrolled == false) {
setState(() => isPageScrolled = true);
} else if (_scrollController.position.pixels < 100.0 && isPageScrolled == true) {
setState(() => isPageScrolled = false);
}

// Fetches new posts when the user has scrolled past 70% list
Expand Down Expand Up @@ -325,7 +326,7 @@ class _FeedViewState extends State<FeedView> {
top: hideTopBarOnScroll, // Don't apply to top of screen to allow for the status bar colour to extend
child: BlocConsumer<FeedBloc, FeedState>(
listenWhen: (previous, current) {
if (current.status == FeedStatus.initial) setState(() => showAppBarTitle = false);
if (current.status == FeedStatus.initial) setState(() => isPageScrolled = false);
if (previous.scrollId != current.scrollId) _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
if (previous.dismissReadId != current.dismissReadId) dismissRead();
if (current.dismissBlockedUserId != null || current.dismissBlockedCommunityId != null) dismissBlockedUsersAndCommunities(current.dismissBlockedUserId, current.dismissBlockedCommunityId);
Expand Down Expand Up @@ -368,7 +369,7 @@ class _FeedViewState extends State<FeedView> {
controller: _scrollController,
slivers: <Widget>[
FeedPageAppBar(
showAppBarTitle: (state.feedType == FeedType.general && state.status != FeedStatus.initial) ? true : showAppBarTitle,
showSecondaryTitle: (state.feedType == FeedType.general && state.status != FeedStatus.initial) ? true : isPageScrolled,
scaffoldStateKey: widget.scaffoldStateKey,
),
// Display loading indicator until the feed is fetched
Expand Down
Loading