Migrate instance page to mobx + l10 strings (#316)

This commit is contained in:
Marcin Wojnarowski 2022-01-20 11:50:24 +01:00 committed by GitHub
parent 56bba4d6af
commit 88608ea9e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 708 additions and 2295 deletions

View File

@ -34,7 +34,7 @@
"L10n string": { "L10n string": {
"scope": "dart", "scope": "dart",
"prefix": "l10n", "prefix": "l10n",
"body": ["L10n.of(context)!.$0"] "body": ["L10n.of(context).$0"]
}, },
"Mobx store": { "Mobx store": {
"prefix": "mobxstore", "prefix": "mobxstore",

View File

@ -1,3 +1,9 @@
## Unreleased
### Added
- You can now add an instance from the three dots menu on the instance page
## v0.8.0 - 2022-01-14 ## v0.8.0 - 2022-01-14
### Added ### Added

View File

@ -215,7 +215,10 @@
"number_of_users_online": "{formattedCount,plural, =1{{formattedCount} user online} other{{formattedCount} users online}}", "number_of_users_online": "{formattedCount,plural, =1{{formattedCount} user online} other{{formattedCount} users online}}",
"@number_of_users_online": { "@number_of_users_online": {
"placeholders": { "placeholders": {
"formattedCount": {} "formattedCount": {
"type": "int",
"format": "compact"
}
} }
}, },
"number_of_comments": "{formattedCount,plural, =1{{formattedCount} comment} other{{formattedCount} comments}}", "number_of_comments": "{formattedCount,plural, =1{{formattedCount} comment} other{{formattedCount} comments}}",
@ -239,13 +242,28 @@
"number_of_subscribers": "{formattedCount,plural, =1{{formattedCount} subscriber} other{{formattedCount} subscribers}}", "number_of_subscribers": "{formattedCount,plural, =1{{formattedCount} subscriber} other{{formattedCount} subscribers}}",
"@number_of_subscribers": { "@number_of_subscribers": {
"placeholders": { "placeholders": {
"formattedCount": {} "formattedCount": {
"type": "int",
"format": "compact"
}
} }
}, },
"number_of_users": "{formattedCount,plural, =1{{formattedCount} user} other{{formattedCount} users}}", "number_of_users": "{formattedCount,plural, =1{{formattedCount} user} other{{formattedCount} users}}",
"@number_of_users": { "@number_of_users": {
"placeholders": { "placeholders": {
"formattedCount": {} "formattedCount": {
"type": "int",
"format": "compact"
}
}
},
"number_of_communities": "{formattedCount,plural, =1{{formattedCount} community} other{{formattedCount} communities}}",
"@number_of_communities": {
"placeholders": {
"formattedCount": {
"type": "int",
"format": "compact"
}
} }
}, },
"unsubscribe": "unsubscribe", "unsubscribe": "unsubscribe",
@ -291,5 +309,41 @@
"show_bot_accounts": "Show Bot Accounts", "show_bot_accounts": "Show Bot Accounts",
"@show_bot_accounts": {}, "@show_bot_accounts": {},
"show_read_posts": "Show Read Posts", "show_read_posts": "Show Read Posts",
"@show_read_posts": {} "@show_read_posts": {},
"site_not_set_up": "This site has not yet been set up",
"@site_not_set_up": {},
"nerd_stuff": "Nerd stuff",
"@nerd_stuff": {},
"open_in_browser": "Open in browser",
"@open_in_browser": {},
"cannot_open_in_browser": "Can't open in browser",
"@cannot_open_in_browser": {},
"about": "About",
"@about": {},
"see_all": "See all",
"@see_all": {},
"admins": "Admins",
"@admins": {},
"trending_communities": "Trending communities",
"@trending_communities": {},
"communities_of_instance": "Communities of {instance}",
"@communities_of_instance": {
"placeholders": {
"instance": {
"type": "String"
}
}
},
"day": "day",
"@day": {},
"week": "week",
"@week": {},
"month": "month",
"@month": {},
"six_months": "6 months",
"@six_months": {},
"add_instance": "Add instance",
"@add_instance": {},
"instance_added": "Instance successfully added",
"@instance_added": {}
} }

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,5 @@ template-arb-file: intl_en.arb
output-localization-file: l10n.dart output-localization-file: l10n.dart
preferred-supported-locales: [en] preferred-supported-locales: [en]
output-class: L10n output-class: L10n
untranslated-messages-file: assets/l10n/untranslated.json
synthetic-package: false synthetic-package: false
nullable-getter: false nullable-getter: false

View File

@ -9,11 +9,7 @@ import '../widgets/sortable_infinite_list.dart';
/// Infinite list of Communities fetched by the given fetcher /// Infinite list of Communities fetched by the given fetcher
class CommunitiesListPage extends StatelessWidget { class CommunitiesListPage extends StatelessWidget {
final String title; final String title;
final Future<List<CommunityView>> Function( final FetcherWithSorting<CommunityView> fetcher;
int page,
int batchSize,
SortType sortType,
) fetcher;
const CommunitiesListPage({Key? key, required this.fetcher, this.title = ''}) const CommunitiesListPage({Key? key, required this.fetcher, this.title = ''})
: super(key: key); : super(key: key);
@ -21,6 +17,7 @@ class CommunitiesListPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.cardColor, backgroundColor: theme.cardColor,

View File

@ -15,6 +15,7 @@ import '../util/goto.dart';
import '../util/text_color.dart'; import '../util/text_color.dart';
import '../widgets/avatar.dart'; import '../widgets/avatar.dart';
import '../widgets/pull_to_refresh.dart'; import '../widgets/pull_to_refresh.dart';
import 'instance/instance.dart';
/// List of subscribed communities per instance /// List of subscribed communities per instance
class CommunitiesTab extends HookWidget { class CommunitiesTab extends HookWidget {
@ -171,8 +172,11 @@ class CommunitiesTab extends HookWidget {
Column( Column(
children: [ children: [
ListTile( ListTile(
onTap: () => goToInstance(context, onTap: () => Navigator.of(context).push(
accountsStore.loggedInInstances.elementAt(i)), InstancePage.route(
accountsStore.loggedInInstances.elementAt(i),
),
),
onLongPress: () => toggleCollapse(i), onLongPress: () => toggleCollapse(i),
leading: Avatar( leading: Avatar(
url: instances[i].icon, url: instances[i].icon,

View File

@ -4,10 +4,10 @@ import 'package:lemmy_api_client/v3.dart';
import '../../l10n/l10n.dart'; import '../../l10n/l10n.dart';
import '../../util/extensions/api.dart'; import '../../util/extensions/api.dart';
import '../../util/goto.dart';
import '../../widgets/avatar.dart'; import '../../widgets/avatar.dart';
import '../../widgets/cached_network_image.dart'; import '../../widgets/cached_network_image.dart';
import '../../widgets/fullscreenable_image.dart'; import '../../widgets/fullscreenable_image.dart';
import '../instance/instance.dart';
import 'community_follow_button.dart'; import 'community_follow_button.dart';
class CommunityOverview extends StatelessWidget { class CommunityOverview extends StatelessWidget {
@ -93,11 +93,12 @@ class CommunityOverview extends StatelessWidget {
text: community.community.originInstanceHost, text: community.community.originInstanceHost,
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => goToInstance( ..onTap = () => Navigator.of(context).push(
context, InstancePage.route(
community.community.originInstanceHost, community.community.originInstanceHost,
), ),
), ),
),
], ],
), ),
), ),

View File

@ -15,6 +15,7 @@ import '../widgets/cached_network_image.dart';
import '../widgets/infinite_scroll.dart'; import '../widgets/infinite_scroll.dart';
import '../widgets/sortable_infinite_list.dart'; import '../widgets/sortable_infinite_list.dart';
import 'inbox.dart'; import 'inbox.dart';
import 'instance/instance.dart';
import 'settings/add_account_page.dart'; import 'settings/add_account_page.dart';
/// First thing users sees when opening the app /// First thing users sees when opening the app
@ -128,7 +129,9 @@ class HomeTab extends HookWidget {
color: color:
theme.textTheme.bodyText1?.color?.withOpacity(0.7)), theme.textTheme.bodyText1?.color?.withOpacity(0.7)),
), ),
onTap: () => goToInstance(context, instance), onTap: () => Navigator.of(context).push(
InstancePage.route(instance),
),
dense: true, dense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
visualDensity: const VisualDensity( visualDensity: const VisualDensity(

View File

@ -1,387 +0,0 @@
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<FullSiteView> siteFuture;
final Future<List<CommunityView>> 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) => <Widget>[
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<List<CommunityView>> 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(),
);
}

View File

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v3.dart';
import '../../l10n/l10n.dart';
import '../../util/extensions/context.dart';
import '../../util/icons.dart';
import '../../util/mobx_provider.dart';
import '../../util/observer_consumers.dart';
import '../../util/share.dart';
import '../../util/text_color.dart';
import '../../widgets/cached_network_image.dart';
import '../../widgets/failed_to_load.dart';
import '../../widgets/fullscreenable_image.dart';
import '../../widgets/reveal_after_scroll.dart';
import '../../widgets/sortable_infinite_list.dart';
import 'instance_about_tab.dart';
import 'instance_more_menu.dart';
import 'instance_store.dart';
/// Displays posts, comments, and general info about the given instance
class InstancePage extends HookWidget {
const InstancePage();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorOnCard = textColorBasedOnBackground(theme.cardColor);
final scrollController = useScrollController();
return ObserverBuilder<InstanceStore>(
builder: (context, store) {
final instanceUrl = 'https://${store.instanceHost}';
return store.siteState.map(
loading: () => Scaffold(
appBar: AppBar(),
body: const Center(child: CircularProgressIndicator.adaptive()),
),
error: (errorTerm) => Scaffold(
appBar: AppBar(),
body: Center(
child: FailedToLoad(
refresh: () => store.fetch(
context.defaultJwt(store.instanceHost),
),
message: errorTerm.tr(context),
),
),
),
data: (site) {
final siteView = site.siteView;
if (siteView == null) {
return Scaffold(
appBar: AppBar(),
body: Center(child: Text(L10n.of(context).site_not_set_up)),
);
}
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(instanceUrl, context: context),
),
IconButton(
icon: Icon(moreIcon),
onPressed: () => InstanceMoreMenu.open(context, site),
),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
children: [
if (siteView.site.banner != null)
FullscreenableImage(
url: siteView.site.banner!,
child: CachedNetworkImage(
imageUrl: siteView.site.banner!,
errorBuilder: (_, ___) => const SizedBox(),
),
),
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(
store.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),
Tab(text: L10n.of(context).about),
],
),
),
),
),
],
body: TabBarView(
children: [
InfinitePostList(
fetcher: (page, batchSize, sort) =>
LemmyApiV3(store.instanceHost).run(GetPosts(
// TODO: switch between all and subscribed
type: PostListingType.all,
sort: sort,
limit: batchSize,
page: page,
savedOnly: false,
auth: context.defaultJwt(store.instanceHost)?.raw,
)),
),
InfiniteCommentList(
fetcher: (page, batchSize, sort) =>
LemmyApiV3(store.instanceHost).run(GetComments(
type: CommentListingType.all,
sort: sort,
limit: batchSize,
page: page,
savedOnly: false,
auth: context.defaultJwt(store.instanceHost)?.raw,
)),
),
InstanceAboutTab(
site: site,
siteView: siteView,
),
],
),
),
),
);
},
);
},
);
}
static Route route(String instanceHost) {
return MaterialPageRoute(
builder: (context) {
return MobxProvider(
create: (context) => InstanceStore(instanceHost)
..fetch(context.defaultJwt(instanceHost)),
child: const InstancePage(),
);
},
);
}
}

View File

@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v3.dart';
import '../../l10n/l10n.dart';
import '../../util/extensions/context.dart';
import '../../util/extensions/spaced.dart';
import '../../util/goto.dart';
import '../../util/observer_consumers.dart';
import '../../widgets/avatar.dart';
import '../../widgets/failed_to_load.dart';
import '../../widgets/markdown_text.dart';
import '../../widgets/pull_to_refresh.dart';
import '../../widgets/user_tile.dart';
import '../communities_list.dart';
import '../community/community.dart';
import '../modlog/modlog.dart';
import 'instance_store.dart';
class InstanceAboutTab extends HookWidget {
final FullSiteView site;
final SiteView siteView;
const InstanceAboutTab({required this.site, required this.siteView});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = L10n.of(context);
void goToCommunities() {
goTo(
context,
(_) => CommunitiesListPage(
fetcher: (page, batchSize, sortType) =>
LemmyApiV3(site.instanceHost).run(
ListCommunities(
type: PostListingType.local,
sort: sortType,
limit: batchSize,
page: page,
auth: context.defaultJwt(site.instanceHost)?.raw,
),
),
title: l10n.communities_of_instance(siteView.site.name),
),
);
}
return PullToRefresh(
onRefresh: () => context
.read<InstanceStore>()
.fetch(context.defaultJwt(site.instanceHost), refresh: true),
child: 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: site.instanceHost,
),
),
const _Divider(),
],
SizedBox(
height: 32,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 15),
children: [
Chip(
label: Text(
l10n.number_of_users_online(site.online),
),
),
Chip(
label: Text(
'${l10n.number_of_users(siteView.counts.usersActiveDay)} / ${l10n.day}',
),
),
Chip(
label: Text(
'${l10n.number_of_users(siteView.counts.usersActiveWeek)} / ${l10n.week}',
),
),
Chip(
label: Text(
'${l10n.number_of_users(siteView.counts.usersActiveMonth)} / ${l10n.month}',
),
),
Chip(
label: Text(
'${l10n.number_of_users(siteView.counts.usersActiveHalfYear)} / ${l10n.six_months}',
),
),
Chip(
label: Text(
l10n.number_of_users(siteView.counts.users),
),
),
Chip(
label: Text(
l10n.number_of_communities(siteView.counts.communities),
),
),
Chip(
label: Text(
l10n.number_of_posts(siteView.counts.posts),
),
),
Chip(
label: Text(
l10n.number_of_comments(siteView.counts.comments),
),
),
].spaced(8),
),
),
const _Divider(),
ListTile(
title: Center(
child: Text(
l10n.trending_communities,
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
),
),
),
ObserverBuilder<InstanceStore>(
builder: (context, store) => store.communitiesState.map(
loading: () => const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: CircularProgressIndicator.adaptive(),
),
error: (errorTerm) => FailedToLoad(
refresh: () => store.fetchCommunites(
context.defaultJwt(store.instanceHost)),
message: errorTerm.tr(context),
),
data: (communities) => Column(
children: [
for (final c in communities)
ListTile(
onTap: () => Navigator.of(context).push(
CommunityPage.fromIdRoute(
store.instanceHost,
c.community.id,
),
),
title: Text(c.community.name),
leading: Avatar(url: c.community.icon),
)
],
),
),
),
ListTile(
title: Center(child: Text(l10n.see_all)),
onTap: goToCommunities,
),
const _Divider(),
ListTile(
title: Center(
child: Text(
l10n.admins,
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
),
),
),
for (final u in site.admins)
PersonTile(
u.person,
expanded: true,
),
const _Divider(),
ListTile(
title: Center(child: Text(l10n.modlog)),
onTap: () => Navigator.of(context).push(
ModlogPage.forInstanceRoute(site.instanceHost),
),
),
],
),
),
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider();
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Divider(),
);
}
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:url_launcher/url_launcher.dart' as ul;
import '../../l10n/l10n.dart';
import '../../stores/accounts_store.dart';
import '../../util/observer_consumers.dart';
import '../../widgets/bottom_modal.dart';
import '../../widgets/info_table_popup.dart';
class InstanceMoreMenu extends StatelessWidget {
final FullSiteView site;
const InstanceMoreMenu({Key? key, required this.site}) : super(key: key);
@override
Widget build(BuildContext context) {
final instanceUrl = 'https://${site.instanceHost}';
final accountsStore = context.watch<AccountsStore>();
return Column(
children: [
if (!accountsStore.instances.contains(site.instanceHost))
ListTile(
leading: const Icon(Icons.add),
title: Text(L10n.of(context).add_instance),
onTap: () {
accountsStore.addInstance(site.instanceHost, assumeValid: true);
Navigator.of(context).pop();
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(L10n.of(context).instance_added)),
);
},
),
ListTile(
leading: const Icon(Icons.open_in_browser),
title: Text(L10n.of(context).open_in_browser),
onTap: () async {
if (await ul.canLaunch(instanceUrl)) {
await ul.launch(instanceUrl);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context).cannot_open_in_browser),
),
);
}
},
),
ListTile(
leading: const Icon(Icons.info_outline),
title: Text(L10n.of(context).nerd_stuff),
onTap: () {
showInfoTablePopup(context: context, table: site.toJson());
},
),
],
);
}
static void open(BuildContext context, FullSiteView site) {
showBottomModal(
context: context,
builder: (context) => InstanceMoreMenu(site: site),
);
}
}

View File

@ -0,0 +1,43 @@
import 'package:lemmy_api_client/v3.dart';
import 'package:mobx/mobx.dart';
import '../../util/async_store.dart';
part 'instance_store.g.dart';
class InstanceStore = _InstanceStore with _$InstanceStore;
abstract class _InstanceStore with Store {
final String instanceHost;
_InstanceStore(this.instanceHost);
final siteState = AsyncStore<FullSiteView>();
final communitiesState = AsyncStore<List<CommunityView>>();
@action
Future<void> fetch(Jwt? token, {bool refresh = false}) async {
await Future.wait([
siteState.runLemmy(
instanceHost,
GetSite(auth: token?.raw),
refresh: refresh,
),
fetchCommunites(token, refresh: refresh),
]);
}
@action
Future<void> fetchCommunites(Jwt? token, {bool refresh = false}) async {
await communitiesState.runLemmy(
instanceHost,
ListCommunities(
type: PostListingType.local,
sort: SortType.hot,
limit: 6,
auth: token?.raw,
),
refresh: refresh,
);
}
}

View File

@ -0,0 +1,34 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'instance_store.dart';
// **************************************************************************
// StoreGenerator
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
mixin _$InstanceStore on _InstanceStore, Store {
final _$fetchAsyncAction = AsyncAction('_InstanceStore.fetch');
@override
Future<void> fetch(Jwt? token, {bool refresh = false}) {
return _$fetchAsyncAction.run(() => super.fetch(token, refresh: refresh));
}
final _$fetchCommunitesAsyncAction =
AsyncAction('_InstanceStore.fetchCommunites');
@override
Future<void> fetchCommunites(Jwt? token, {bool refresh = false}) {
return _$fetchCommunitesAsyncAction
.run(() => super.fetchCommunites(token, refresh: refresh));
}
@override
String toString() {
return '''
''';
}
}

View File

@ -4,29 +4,35 @@ import 'package:lemmy_api_client/v3.dart';
import '../util/extensions/api.dart'; import '../util/extensions/api.dart';
import '../util/goto.dart'; import '../util/goto.dart';
import '../widgets/avatar.dart'; import '../widgets/avatar.dart';
import '../widgets/infinite_scroll.dart';
import '../widgets/markdown_text.dart'; import '../widgets/markdown_text.dart';
/// Infinite list of Users fetched by the given fetcher /// Infinite list of Users fetched by the given fetcher
class UsersListPage extends StatelessWidget { class UsersListPage extends StatelessWidget {
final String title; final String title;
final List<PersonViewSafe> users; final Fetcher<PersonViewSafe> fetcher;
const UsersListPage({Key? key, required this.users, this.title = ''}) const UsersListPage({Key? key, required this.fetcher, this.title = ''})
: super(key: key); : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
// TODO: change to infinite scroll
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.cardColor, backgroundColor: theme.cardColor,
title: Text(title), title: Text(title),
), ),
body: ListView.builder( body: InfiniteScroll<PersonViewSafe>(
itemBuilder: (context, i) => UsersListItem(user: users[i]), fetcher: fetcher,
itemCount: users.length, itemBuilder: (user) => Column(
children: [
const Divider(),
UsersListItem(user: user),
],
),
uniqueProp: (user) => user.person.actorId,
), ),
); );
} }
@ -38,7 +44,8 @@ class UsersListItem extends StatelessWidget {
const UsersListItem({Key? key, required this.user}) : super(key: key); const UsersListItem({Key? key, required this.user}) : super(key: key);
@override @override
Widget build(BuildContext context) => ListTile( Widget build(BuildContext context) {
return ListTile(
title: Text(user.person.originPreferredName), title: Text(user.person.originPreferredName),
subtitle: user.person.bio != null subtitle: user.person.bio != null
? Opacity( ? Opacity(
@ -52,4 +59,5 @@ class UsersListItem extends StatelessWidget {
onTap: () => goToUser.fromPersonSafe(context, user.person), onTap: () => goToUser.fromPersonSafe(context, user.person),
leading: Avatar(url: user.person.avatar), leading: Avatar(url: user.person.avatar),
); );
}
} }

View File

@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart' as ul; import 'package:url_launcher/url_launcher.dart' as ul;
import 'pages/community/community.dart'; import 'pages/community/community.dart';
import 'pages/instance.dart'; import 'pages/instance/instance.dart';
import 'pages/media_view.dart'; import 'pages/media_view.dart';
import 'pages/user.dart'; import 'pages/user.dart';
import 'stores/accounts_store.dart'; import 'stores/accounts_store.dart';
@ -48,7 +48,7 @@ Future<void> linkLauncher({
if (matchedInstance != null && instances.any((e) => e == match?.group(1))) { if (matchedInstance != null && instances.any((e) => e == match?.group(1))) {
if (rest == null || rest.isEmpty || rest == '/') { if (rest == null || rest.isEmpty || rest == '/') {
return push(() => InstancePage(instanceHost: matchedInstance)); return Navigator.of(context).push<void>(InstancePage.route(instanceHost));
} }
final split = rest.split('/'); final split = rest.split('/');
switch (split[1]) { switch (split[1]) {

View File

@ -76,8 +76,10 @@ abstract class _AsyncStore<T> with Store {
bool refresh = false, bool refresh = false,
}) async { }) async {
try { try {
return await run(() => LemmyApiV3(instanceHost).run(query), return await run(
refresh: refresh); () => LemmyApiV3(instanceHost).run(query),
refresh: refresh,
);
} on LemmyApiException catch (err) { } on LemmyApiException catch (err) {
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null; final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
if (data != null) { if (data != null) {

View File

@ -52,3 +52,14 @@ extension UserPreferredNames on PersonSafe {
extension CommentLink on Comment { extension CommentLink on Comment {
String get link => 'https://$instanceHost/post/$postId/comment/$id'; String get link => 'https://$instanceHost/post/$postId/comment/$id';
} }
// inspired by https://github.com/LemmyNet/lemmy-ui/blob/66c846ededef8c0afd5aaadca4aaedcbaeab3ee6/src/shared/utils.ts#L533
extension PersonSafeCakeDay on PersonSafe {
bool get isCakeDay {
final now = DateTime.now().toUtc();
return now.day == published.day &&
now.month == published.month &&
now.year != published.year;
}
}

View File

@ -1,12 +0,0 @@
import 'package:lemmy_api_client/v3.dart';
// inspired by https://github.com/LemmyNet/lemmy-ui/blob/66c846ededef8c0afd5aaadca4aaedcbaeab3ee6/src/shared/utils.ts#L533
extension PersonSafeCakeDay on PersonSafe {
bool get isCakeDay {
final now = DateTime.now().toUtc();
return now.day == published.day &&
now.month == published.month &&
now.year != published.year;
}
}

View File

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
import 'package:lemmy_api_client/v3.dart';
import '../../stores/accounts_store.dart';
import '../observer_consumers.dart';
extension BuildContextExtensions on BuildContext {
/// Get default [Jwt] for an instance
Jwt? defaultJwt(String instanceHost) =>
read<AccountsStore>().defaultUserDataFor(instanceHost)?.jwt;
}

View File

@ -3,7 +3,6 @@ import 'package:lemmy_api_client/v3.dart';
import '../pages/community/community.dart'; import '../pages/community/community.dart';
import '../pages/full_post/full_post.dart'; import '../pages/full_post/full_post.dart';
import '../pages/instance.dart';
import '../pages/media_view.dart'; import '../pages/media_view.dart';
import '../pages/user.dart'; import '../pages/user.dart';
@ -25,9 +24,6 @@ Future<dynamic> goToReplace(
builder: builder, builder: builder,
)); ));
void goToInstance(BuildContext context, String instanceHost) =>
goTo(context, (context) => InstancePage(instanceHost: instanceHost));
// ignore: camel_case_types // ignore: camel_case_types
abstract class goToCommunity { abstract class goToCommunity {
/// Navigates to `CommunityPage` /// Navigates to `CommunityPage`

View File

@ -9,7 +9,6 @@ import '../../l10n/l10n.dart';
import '../../stores/config_store.dart'; import '../../stores/config_store.dart';
import '../../util/async_store_listener.dart'; import '../../util/async_store_listener.dart';
import '../../util/extensions/api.dart'; import '../../util/extensions/api.dart';
import '../../util/extensions/cake_day.dart';
import '../../util/goto.dart'; import '../../util/goto.dart';
import '../../util/mobx_provider.dart'; import '../../util/mobx_provider.dart';
import '../../util/observer_consumers.dart'; import '../../util/observer_consumers.dart';

View File

@ -17,6 +17,8 @@ class InfiniteScrollController {
} }
} }
typedef Fetcher<T> = Future<List<T>> Function(int page, int batchSize);
/// `ListView.builder` with asynchronous data fetching and no `itemCount` /// `ListView.builder` with asynchronous data fetching and no `itemCount`
class InfiniteScroll<T> extends HookWidget { class InfiniteScroll<T> extends HookWidget {
/// How many items should be fetched per call /// How many items should be fetched per call
@ -31,7 +33,7 @@ class InfiniteScroll<T> extends HookWidget {
/// Fetches data to be displayed. It is important to respect `batchSize`, /// Fetches data to be displayed. It is important to respect `batchSize`,
/// if the returned list has less than `batchSize` then the InfiniteScroll /// if the returned list has less than `batchSize` then the InfiniteScroll
/// is considered finished /// is considered finished
final Future<List<T>> Function(int page, int batchSize) fetcher; final Fetcher<T> fetcher;
final InfiniteScrollController? controller; final InfiniteScrollController? controller;

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../../l10n/l10n.dart'; import '../../l10n/l10n.dart';
import '../../pages/community/community.dart'; import '../../pages/community/community.dart';
import '../../pages/instance/instance.dart';
import '../../util/extensions/api.dart'; import '../../util/extensions/api.dart';
import '../../util/goto.dart'; import '../../util/goto.dart';
import '../../util/observer_consumers.dart'; import '../../util/observer_consumers.dart';
@ -67,11 +68,12 @@ class PostInfoSection extends StatelessWidget {
text: post.post.originInstanceHost, text: post.post.originInstanceHost,
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => goToInstance( ..onTap = () => Navigator.of(context).push(
context, InstancePage.route(
post.post.originInstanceHost, post.post.originInstanceHost,
), ),
), ),
),
], ],
), ),
), ),

View File

@ -6,9 +6,9 @@ import 'package:lemmy_api_client/v3.dart';
import '../hooks/memo_future.dart'; import '../hooks/memo_future.dart';
import '../hooks/stores.dart'; import '../hooks/stores.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../pages/instance/instance.dart';
import '../pages/manage_account.dart'; import '../pages/manage_account.dart';
import '../util/extensions/api.dart'; import '../util/extensions/api.dart';
import '../util/extensions/cake_day.dart';
import '../util/goto.dart'; import '../util/goto.dart';
import '../util/text_color.dart'; import '../util/text_color.dart';
import 'avatar.dart'; import 'avatar.dart';
@ -226,8 +226,11 @@ class _UserOverview extends HookWidget {
style: theme.textTheme.caption, style: theme.textTheme.caption,
), ),
InkWell( InkWell(
onTap: () => goToInstance( onTap: () => Navigator.of(context).push(
context, userView.person.originInstanceHost), InstancePage.route(
userView.person.originInstanceHost,
),
),
child: Text( child: Text(
userView.person.originInstanceHost, userView.person.originInstanceHost,
style: theme.textTheme.caption, style: theme.textTheme.caption,