Migrate instance page to mobx + l10 strings (#316)
This commit is contained in:
parent
56bba4d6af
commit
88608ea9e1
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,9 +93,10 @@ 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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 '''
|
||||||
|
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,18 +44,20 @@ 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) {
|
||||||
title: Text(user.person.originPreferredName),
|
return ListTile(
|
||||||
subtitle: user.person.bio != null
|
title: Text(user.person.originPreferredName),
|
||||||
? Opacity(
|
subtitle: user.person.bio != null
|
||||||
opacity: 0.7,
|
? Opacity(
|
||||||
child: MarkdownText(
|
opacity: 0.7,
|
||||||
user.person.bio!,
|
child: MarkdownText(
|
||||||
instanceHost: user.instanceHost,
|
user.person.bio!,
|
||||||
),
|
instanceHost: user.instanceHost,
|
||||||
)
|
),
|
||||||
: null,
|
)
|
||||||
onTap: () => goToUser.fromPersonSafe(context, user.person),
|
: null,
|
||||||
leading: Avatar(url: user.person.avatar),
|
onTap: () => goToUser.fromPersonSafe(context, user.person),
|
||||||
);
|
leading: Avatar(url: user.person.avatar),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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`
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,9 +68,10 @@ 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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue