import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:url_launcher/url_launcher.dart' as ul; import '../hooks/stores.dart'; import '../l10n/l10n.dart'; import '../util/extensions/spaced.dart'; import '../util/goto.dart'; import '../util/icons.dart'; import '../util/share.dart'; import '../util/text_color.dart'; import '../widgets/avatar.dart'; import '../widgets/bottom_modal.dart'; import '../widgets/cached_network_image.dart'; import '../widgets/fullscreenable_image.dart'; import '../widgets/info_table_popup.dart'; import '../widgets/markdown_text.dart'; import '../widgets/reveal_after_scroll.dart'; import '../widgets/sortable_infinite_list.dart'; import '../widgets/user_tile.dart'; import 'communities_list.dart'; import 'modlog/modlog.dart'; /// Displays posts, comments, and general info about the given instance class InstancePage extends HookWidget { final String instanceHost; final Future siteFuture; final Future> communitiesFuture; InstancePage({required this.instanceHost}) : siteFuture = LemmyApiV3(instanceHost).run(const GetSite()), communitiesFuture = LemmyApiV3(instanceHost).run(const ListCommunities( type: PostListingType.local, sort: SortType.hot, limit: 6)); @override Widget build(BuildContext context) { final theme = Theme.of(context); final siteSnap = useFuture(siteFuture); final colorOnCard = textColorBasedOnBackground(theme.cardColor); final accStore = useAccountsStore(); final scrollController = useScrollController(); if (!siteSnap.hasData || siteSnap.data!.siteView == null) { return Scaffold( appBar: AppBar(), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (siteSnap.hasError) ...[ const Icon(Icons.error), Padding( padding: const EdgeInsets.all(8), child: Text('ERROR: ${siteSnap.error}'), ) ] else if (siteSnap.hasData && siteSnap.data!.siteView == null) const Text('ERROR') else const CircularProgressIndicator.adaptive( semanticsLabel: 'loading') ], ), ), ); } final site = siteSnap.data!; final siteView = site.siteView!; void _share() => share('https://$instanceHost', context: context); void _openMoreMenu() { showBottomModal( context: context, builder: (context) => Column( children: [ ListTile( leading: const Icon(Icons.open_in_browser), title: const Text('Open in browser'), onTap: () async => await ul .canLaunch('https://${site.instanceHost}') ? ul.launch('https://${site.instanceHost}') : ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("can't open in browser"))), ), ListTile( leading: const Icon(Icons.info_outline), title: const Text('Nerd stuff'), onTap: () { showInfoTablePopup(context: context, table: site.toJson()); }, ), ], ), ); } return Scaffold( body: DefaultTabController( length: 3, child: NestedScrollView( controller: scrollController, headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverAppBar( expandedHeight: 250, pinned: true, backgroundColor: theme.cardColor, title: RevealAfterScroll( after: 150, fade: true, scrollController: scrollController, child: Text( siteView.site.name, style: TextStyle(color: colorOnCard), ), ), actions: [ IconButton(icon: Icon(shareIcon), onPressed: _share), IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu), ], flexibleSpace: FlexibleSpaceBar( background: Stack(children: [ if (siteView.site.banner != null) FullscreenableImage( url: siteView.site.banner!, child: CachedNetworkImage( imageUrl: siteView.site.banner!, errorBuilder: (_, ___) => const SizedBox.shrink(), ), ), SafeArea( child: Center( child: Column( children: [ Padding( padding: const EdgeInsets.only(top: 40), child: siteView.site.icon == null ? const SizedBox(height: 100, width: 100) : FullscreenableImage( url: siteView.site.icon!, child: CachedNetworkImage( width: 100, height: 100, imageUrl: siteView.site.icon!, errorBuilder: (_, ___) => const Icon(Icons.warning), ), ), ), Text(siteView.site.name, style: theme.textTheme.headline6), Text(instanceHost, style: theme.textTheme.caption) ], ), ), ), ]), ), bottom: PreferredSize( preferredSize: const TabBar(tabs: []).preferredSize, child: Material( color: theme.cardColor, child: TabBar( tabs: [ Tab(text: L10n.of(context).posts), Tab(text: L10n.of(context).comments), const Tab(text: 'About'), ], ), ), ), ), ], body: TabBarView( children: [ InfinitePostList( fetcher: (page, batchSize, sort) => LemmyApiV3(instanceHost).run(GetPosts( // TODO: switch between all and subscribed type: PostListingType.all, sort: sort, limit: batchSize, page: page, savedOnly: false, auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw, ))), InfiniteCommentList( fetcher: (page, batchSize, sort) => LemmyApiV3(instanceHost).run(GetComments( type: CommentListingType.all, sort: sort, limit: batchSize, page: page, savedOnly: false, auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw, ))), _AboutTab(site, communitiesFuture: communitiesFuture, instanceHost: instanceHost), ], ), ), ), ); } } class _AboutTab extends HookWidget { final FullSiteView site; final Future> communitiesFuture; final String instanceHost; const _AboutTab( this.site, { required this.communitiesFuture, required this.instanceHost, }); // void goToBannedUsers(BuildContext context) { // goTo( // context, // (_) => UsersListPage( // users: site.banned.reversed.toList(), // title: L10n.of(context).banned_users, // ), // ); // } @override Widget build(BuildContext context) { final theme = Theme.of(context); final commSnap = useFuture(communitiesFuture); final accStore = useAccountsStore(); void goToCommunities() { goTo( context, (_) => CommunitiesListPage( fetcher: (page, batchSize, sortType) => LemmyApiV3(instanceHost).run( ListCommunities( type: PostListingType.local, sort: sortType, limit: batchSize, page: page, auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw, ), ), title: 'Communities of ${site.siteView?.site.name}', ), ); } final siteView = site.siteView; if (siteView == null) { return const SingleChildScrollView( child: Center( child: Padding( padding: EdgeInsets.all(16), child: Text('error'), ))); } return SingleChildScrollView( child: SafeArea( top: false, child: Column( children: [ if (siteView.site.description != null) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), child: MarkdownText( siteView.site.description!, instanceHost: instanceHost, ), ), const _Divider(), ], SizedBox( height: 32, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 15), children: [ Chip( label: Text(L10n.of(context) .number_of_users_online(site.online))), Chip( label: Text( '${siteView.counts.usersActiveDay} users / day')), Chip( label: Text( '${siteView.counts.usersActiveWeek} users / week')), Chip( label: Text( '${siteView.counts.usersActiveMonth} users / month')), Chip( label: Text( '${siteView.counts.usersActiveHalfYear} users / 6 months')), Chip( label: Text(L10n.of(context) .number_of_users(siteView.counts.users))), Chip( label: Text('${siteView.counts.communities} communities')), Chip(label: Text('${siteView.counts.posts} posts')), Chip(label: Text('${siteView.counts.comments} comments')), ].spaced(8), ), ), const _Divider(), ListTile( title: Center( child: Text( 'Trending communities:', style: theme.textTheme.headline6?.copyWith(fontSize: 18), ), ), ), if (commSnap.hasData) for (final c in commSnap.data!) ListTile( onTap: () => goToCommunity.byId( context, c.instanceHost, c.community.id), title: Text(c.community.name), leading: Avatar(url: c.community.icon), ) else if (commSnap.hasError) Padding( padding: const EdgeInsets.all(8), child: Text("Can't load communities, ${commSnap.error}"), ) else const Padding( padding: EdgeInsets.symmetric(vertical: 10), child: CircularProgressIndicator.adaptive(), ), ListTile( title: const Center(child: Text('See all')), onTap: goToCommunities, ), const _Divider(), ListTile( title: Center( child: Text( 'Admins:', style: theme.textTheme.headline6?.copyWith(fontSize: 18), ), ), ), for (final u in site.admins) PersonTile( u.person, expanded: true, ), const _Divider(), // TODO: transition to new API // ListTile( // title: Center(child: Text(L10n.of(context).banned_users)), // onTap: () => goToBannedUsers(context), // ), ListTile( title: Center(child: Text(L10n.of(context).modlog)), onTap: () => Navigator.of(context).push( ModlogPage.forInstanceRoute(instanceHost), ), ), ], ), ), ); } } class _Divider extends StatelessWidget { const _Divider(); @override Widget build(BuildContext context) => const Padding( padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10), child: Divider(), ); }