diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a0cb926..a04c0da5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,6 +45,8 @@ jobs: runs-on: ubuntu-latest needs: lint steps: + - uses: subosito/flutter-action@v2.10.0 + - uses: actions/checkout@v2 - uses: actions/setup-java@v1 @@ -85,53 +87,7 @@ jobs: - uses: actions/upload-artifact@v2 with: - name: android-debug-apk + name: android-build path: build/app/outputs/flutter-apk/ - ios: - name: iOS - runs-on: macos-latest - needs: lint - steps: - - uses: actions/checkout@v2 - - - uses: subosito/flutter-action@v2 - with: - channel: "stable" - - - run: flutter build ios --no-codesign --release --target lib/main_prod.dart --flavor prod - - linux: - name: Linux - runs-on: ubuntu-latest - needs: lint - steps: - - uses: actions/checkout@v2 - - - uses: subosito/flutter-action@v1 - with: - channel: "stable" - - - name: Get additional dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev - - - name: Build - run: | - flutter build linux --release --target lib/main_prod.dart - - windows: - name: Windows - runs-on: windows-latest - needs: lint - steps: - - uses: actions/checkout@v2 - - - uses: subosito/flutter-action@v1 - with: - channel: "stable" - - - name: Build - run: | - flutter build windows --release --target lib/main_prod.dart + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3f578b5..fc2a9e77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ From issues to wikis: everything is on [Lifttof GitHub Repo](https://github.com/ ## Linting / Formatting -Everything is formatted with `dart format` (no flags) and linted with `dart analyze` ([see rules](analysis_options.yaml)). Both are enforced by the CI. +Everything is formatted with `dart format` (no flags) and linted with `flutter analyze` ([see rules](analysis_options.yaml)). Both are enforced by the CI. ## Translations diff --git a/analysis_options.yaml b/analysis_options.yaml index a058d539..8972e12b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,127 +1,60 @@ +include: package:flutter_lints/flutter.yaml + linter: rules: - - annotate_overrides - avoid_bool_literals_in_conditional_expressions - avoid_catching_errors - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes - avoid_final_parameters - - avoid_function_literals_in_foreach_calls - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - avoid_positional_boolean_parameters - - avoid_print - avoid_private_typedef_functions - avoid_redundant_argument_values - - avoid_relative_lib_imports - - avoid_return_types_on_setters - avoid_returning_null - - avoid_returning_null_for_void - avoid_returning_this - avoid_setters_without_getters - - avoid_single_cascade_in_expression_statements - avoid_type_to_string - - avoid_unnecessary_containers - avoid_unused_constructor_parameters - avoid_void_async - - await_only_futures - - camel_case_extensions - - camel_case_types - cascade_invocations - cast_nullable_to_non_nullable - - constant_identifier_names - conditional_uri_does_not_exist - - curly_braces_in_flow_control_structures - directives_ordering - - empty_catches - - empty_constructor_bodies - eol_at_end_of_file - - exhaustive_cases - - file_names - - hash_and_equals - - implementation_imports - - library_names - - library_prefixes - literal_only_boolean_expressions - - non_constant_identifier_names - noop_primitive_operations - - no_leading_underscores_for_library_prefixes - - no_leading_underscores_for_local_identifiers - - null_check_on_nullable_type_parameter - omit_local_variable_types - one_member_abstracts - package_api_docs - parameter_assignments - - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - prefer_constructors_over_static_methods - - prefer_contains - - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals - - prefer_for_elements_to_map_fromIterable - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - prefer_int_literals - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_is_not_operator - - prefer_iterable_whereType - prefer_mixin - - prefer_null_aware_operators - prefer_relative_imports - prefer_single_quotes - - prefer_spread_collections - - prefer_typing_uninitialized_variables - - recursive_getters - secure_pubspec_urls - - sized_box_for_whitespace - sized_box_shrink_expand - - slash_for_doc_comments - - sort_child_properties_last - sort_unnamed_constructors_first - tighten_type_of_initializing_formals - type_annotate_public_apis - - type_init_formals - unawaited_futures - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_constructor_name - - unnecessary_getters_setters - unnecessary_lambdas - - unnecessary_late - - unnecessary_new - - unnecessary_null_aware_assignments - unnecessary_null_checks - - unnecessary_null_in_if_null_operators - unnecessary_parenthesis - unnecessary_raw_strings - - unnecessary_string_escapes - - unnecessary_string_interpolations - - unnecessary_this - - unrelated_type_equality_checks - use_colored_box - use_enums - - use_full_hex_values_for_flutter_colors - use_is_even_rather_than_modulo - use_named_constants - use_raw_strings - - use_rethrow_when_possible - use_setters_to_change_properties - use_super_parameters - use_test_throws_matchers - use_to_and_as_if_applicable - - void_checks analyzer: exclude: diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 1b65443b..2b67dada 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -186,6 +186,10 @@ "@post_view": {}, "show_scores": "Show Scores", "@show_scores": {}, + "auto_play_video": "Auto Play Video", + "@auto_play_video": {}, + "auto_mute_video": "Auto Mute Video", + "@auto_mute_video": {}, "font": "Font", "@font": {}, "preview": "Preview", @@ -218,9 +222,8 @@ "@federated_post_info": {}, "not_found": "Not found", "@not_found": {}, - "error": "Error: {error_text}", - "@error" :{ + "@error": { "placeholders": { "error_text": { "type": "String" @@ -416,10 +419,10 @@ } }, "set_as_default": "Set as default", - "@set_as_default" : {}, - "import_settings" :"Import settings to Liftoff", + "@set_as_default": {}, + "import_settings": "Import settings to Liftoff", "@import_settings": {}, - "import_successful" : "Import successful", + "import_successful": "Import successful", "@import_successful": {}, "instance_url": "instance url", "@instance_url": {}, @@ -441,26 +444,22 @@ } } }, - "block": "Block", - "@block":{}, + "@block": {}, "block_user": "Block user", - "@block_user":{}, + "@block_user": {}, "no_users_blocked": "No users blocked", - "@no_users_blocked":{}, + "@no_users_blocked": {}, "block_community": "Block community", - "@block_community":{}, + "@block_community": {}, "no_communities_blocked": "No communities blocked", - "@no_communities_blocked":{}, + "@no_communities_blocked": {}, "unblock": "Unblock", - "@unblock":{}, + "@unblock": {}, "unblock_user": "Unblock user", - "@unblock_user":{}, + "@unblock_user": {}, "unblock_community": "Unblock community", - "@unblock_community":{}, - - - + "@unblock_community": {}, "remove_account_confirm": "This will delete your account and data from this app. Your account and data will remain on the instance.", "@remove_account_confirm": {}, "delete_account_confirm": "Warning: this will permanently delete your account and all of your data from the app AND instance. Your data may not be deleted on other, existing instances. Enter your password to confirm.", @@ -540,32 +539,31 @@ "open_in_browser": "Open in browser", "@open_in_browser": {}, "share_text": "Share text", - "@share_text":{}, + "@share_text": {}, "share_url": "Share URL", - "@share_url":{}, + "@share_url": {}, "translate": "Translate", - "@translate":{}, + "@translate": {}, "make_text_selectable": "Make text selectable", - "@make_text_selectable":{}, + "@make_text_selectable": {}, "make_text_unselectable": "Make text unselectable", - "@make_text_unselectable":{}, + "@make_text_unselectable": {}, "show_fancy_text": "Show fancy text", - "@make_fancy_text":{}, + "@make_fancy_text": {}, "show_raw_text": "Show raw text", - "@make_raw_text":{}, + "@make_raw_text": {}, "delete_comment": "Delete comment", - "@delete_comment":{}, + "@delete_comment": {}, "restore_comment": "Restore comment", - "@restore_comment":{}, + "@restore_comment": {}, "report_comment": "Report comment", - "@report_comment":{}, + "@report_comment": {}, "delete_post": "Delete post", - "@delete_post":{}, + "@delete_post": {}, "restore_post": "Restore post", - "@restore_post":{}, + "@restore_post": {}, "report_post": "Report post", - "@report_post":{}, - + "@report_post": {}, "cannot_open_in_browser": "Can't open in browser", "@cannot_open_in_browser": {}, "about": "About", @@ -687,36 +685,25 @@ "description": "shows up on a snackbar when the image upload failed" }, "instance_error": "Hmm... it seems like your instance is having trouble responding.", - "@instance_error": { - }, + "@instance_error": {}, "instance_record_notfound": "Hmm... it seems like your instance couldn't find what you were looking for.", - "@instance_record_notfound": { - }, + "@instance_record_notfound": {}, "try_again": "Try Again", - "@try_again": { - }, + "@try_again": {}, "kbin_instances_not_supported": "Please note that kbin instances are not supported at present.", - "@kbin_instances_not_supported": { - }, + "@kbin_instances_not_supported": {}, "pick_a_photo": "Pick a photo", - "@pick_a_photo": { - }, + "@pick_a_photo": {}, "use_this_image": "Use this image", - "@use_this_image": { - }, + "@use_this_image": {}, "photo_picker_explanation": "Pick an image, then adjust the crop box to the right shape.\n\nYou can then drag the image until it's in the right place. \n\nResizing the crop box will zoom the selection in and out.", - "@photo_picker_explanation": { - }, + "@photo_picker_explanation": {}, "accounts_explanation": "The first instance on this screen will be treated as your default in other screens, and the marked account will be the default on that instance.\n\nPress and hold on an account to make it your default on that instance.\n\nPress and hold an instance or account to remove it from this app. You can add it back later. You may lose some local settings but your bookmarks, subscriptions etc will still be held on your instance.", - "@accounts_explanation": { - }, + "@accounts_explanation": {}, "comment_tag_op": "OP", - "@comment_tag_op": { - }, + "@comment_tag_op": {}, "comment_tag_you": "YOU", - "@comment_tag_you": { - }, + "@comment_tag_you": {}, "code_of_conduct_clickthrough": "By accessing the Lemmy network using Liftoff! you agree to adhere to our Code of Conduct", - "@code_of_conduct_clickthrough": { - } -} + "@code_of_conduct_clickthrough": {} +} \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart index 4e5288c9..5f598899 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -18,7 +18,7 @@ final l10nDelegates = [ ]; class MyApp extends StatelessWidget { - const MyApp(); + const MyApp({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/app_link_handler.dart b/lib/app_link_handler.dart index 5f5f84e5..4ae9232e 100644 --- a/lib/app_link_handler.dart +++ b/lib/app_link_handler.dart @@ -17,7 +17,7 @@ import './util/goto.dart'; /// - liftoff://programming.dev/c/programmer_humor /// - liftoff://lemmy.world/u/zachatrocity class AppLinkHandler extends HookWidget { - AppLinkHandler(this.child); + AppLinkHandler(this.child, {super.key}); final Widget child; final _appLinks = AppLinks(); diff --git a/lib/comment_tree.dart b/lib/comment_tree.dart index e518f60b..23ceb1e1 100644 --- a/lib/comment_tree.dart +++ b/lib/comment_tree.dart @@ -74,7 +74,7 @@ class CommentTree { } /// recursive sorter - void _sort(int compare(CommentTree a, CommentTree b)) { + void _sort(int Function(CommentTree a, CommentTree b) compare) { children.sort(compare); for (final el in children) { el._sort(compare); diff --git a/lib/hooks/stores.dart b/lib/hooks/stores.dart index 9038dd93..74363ea4 100644 --- a/lib/hooks/stores.dart +++ b/lib/hooks/stores.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import '../stores/accounts_store.dart'; AccountsStore useAccountsStore() => useContext().watch(); -T useAccountsStoreSelect(T selector(AccountsStore store)) => +T useAccountsStoreSelect(T Function(AccountsStore store) selector) => useContext().select(selector); V useStore(V Function(S value) selector) { diff --git a/lib/pages/communities_list.dart b/lib/pages/communities_list.dart index 5640e46d..3c087c46 100644 --- a/lib/pages/communities_list.dart +++ b/lib/pages/communities_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lemmy_api_client/v3.dart'; import '../stores/config_store.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import '../util/observer_consumers.dart'; import '../widgets/avatar.dart'; @@ -61,7 +62,8 @@ class CommunitiesListItem extends StatelessWidget { final bodyFontSize = context.read().postBodySize; return ListTile( - title: Text(community.community.name), + title: Text('${community.community.name}' + '@${community.community.originInstanceHost}'), subtitle: community.community.description != null ? SizedBox( height: bodyFontSize * 2 + 5, // 2 lines + padding diff --git a/lib/pages/communities_tab.dart b/lib/pages/communities_tab.dart index bffe82bd..28c9063e 100644 --- a/lib/pages/communities_tab.dart +++ b/lib/pages/communities_tab.dart @@ -22,7 +22,7 @@ import 'instance/instance.dart'; /// List of subscribed communities per instance class CommunitiesTab extends HookWidget { - const CommunitiesTab(); + const CommunitiesTab({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/community/community.dart b/lib/pages/community/community.dart index ce234c3d..9a99aff7 100644 --- a/lib/pages/community/community.dart +++ b/lib/pages/community/community.dart @@ -15,6 +15,7 @@ import '../../util/mobx_provider.dart'; import '../../util/observer_consumers.dart'; import '../../util/share.dart'; import '../../widgets/failed_to_load.dart'; +import '../../widgets/post/post_store.dart'; import '../../widgets/reveal_after_scroll.dart'; import '../../widgets/sortable_infinite_list.dart'; import '../create_post/create_post_fab.dart'; @@ -26,7 +27,7 @@ import 'community_store.dart'; /// Displays posts, comments, and general info about the given community class CommunityPage extends HookWidget { - const CommunityPage(); + const CommunityPage({super.key}); @override Widget build(BuildContext context) { @@ -128,18 +129,20 @@ class CommunityPage extends HookWidget { children: [ InfinitePostList( fetcher: (page, batchSize, sort) => - LemmyApiV3(community.instanceHost).run(GetPosts( - type: PostListingType.local, - sort: sort, - communityId: community.community.id, - page: page, - limit: batchSize, - savedOnly: false, - auth: accountsStore - .defaultUserDataFor(community.instanceHost) - ?.jwt - .raw, - )), + LemmyApiV3(community.instanceHost) + .run(GetPosts( + type: PostListingType.local, + sort: sort, + communityId: community.community.id, + page: page, + limit: batchSize, + savedOnly: false, + auth: accountsStore + .defaultUserDataFor(community.instanceHost) + ?.jwt + .raw, + )) + .toPostStores(), ), InfiniteCommentList( fetcher: (page, batchSize, sortType) => diff --git a/lib/pages/community/community_follow_button.dart b/lib/pages/community/community_follow_button.dart index 2d327f50..2a2425d9 100644 --- a/lib/pages/community/community_follow_button.dart +++ b/lib/pages/community/community_follow_button.dart @@ -11,7 +11,7 @@ import 'community_store.dart'; class CommunityFollowButton extends HookWidget { final CommunityView communityView; - const CommunityFollowButton(this.communityView); + const CommunityFollowButton(this.communityView, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/community/community_overview.dart b/lib/pages/community/community_overview.dart index 7931372f..262d584c 100644 --- a/lib/pages/community/community_overview.dart +++ b/lib/pages/community/community_overview.dart @@ -13,7 +13,7 @@ import 'community_follow_button.dart'; class CommunityOverview extends StatelessWidget { final FullCommunityView fullCommunityView; - const CommunityOverview(this.fullCommunityView); + const CommunityOverview(this.fullCommunityView, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/community/community_store.dart b/lib/pages/community/community_store.dart index baf7ad8b..bbba8ace 100644 --- a/lib/pages/community/community_store.dart +++ b/lib/pages/community/community_store.dart @@ -6,6 +6,8 @@ import '../../util/async_store.dart'; part 'community_store.g.dart'; +// Temp ignore until mobx stores are updated to satisfy linting rules +// ignore: library_private_types_in_public_api class CommunityStore = _CommunityStore with _$CommunityStore; abstract class _CommunityStore with Store { @@ -18,6 +20,7 @@ abstract class _CommunityStore with Store { required String this.communityName, required this.instanceHost, }) : id = null; + // ignore: unused_element _CommunityStore.fromId({required this.id, required this.instanceHost}) : communityName = null; diff --git a/lib/pages/create_post/create_post.dart b/lib/pages/create_post/create_post.dart index a55a91ee..c4da8f42 100644 --- a/lib/pages/create_post/create_post.dart +++ b/lib/pages/create_post/create_post.dart @@ -22,7 +22,7 @@ import 'create_post_url_field.dart'; /// Modal for creating a post to some community in some instance /// Pops the navigator stack with a [PostView] class CreatePostPage extends HookWidget { - const CreatePostPage(); + const CreatePostPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/create_post/create_post_fab.dart b/lib/pages/create_post/create_post_fab.dart index 26c190f7..09c88836 100644 --- a/lib/pages/create_post/create_post_fab.dart +++ b/lib/pages/create_post/create_post_fab.dart @@ -11,7 +11,7 @@ import 'create_post.dart'; class CreatePostFab extends HookWidget { final CommunityView? community; - const CreatePostFab({this.community}); + const CreatePostFab({super.key, this.community}); @override Widget build(BuildContext context) { @@ -25,7 +25,7 @@ class CreatePostFab extends HookWidget { : CreatePostPage.toCommunityRoute(community!), ); - if (postView != null) { + if (postView != null && context.mounted) { await Navigator.of(context) .push(FullPostPage.fromPostViewRoute(postView)); } diff --git a/lib/pages/create_post/create_post_store.dart b/lib/pages/create_post/create_post_store.dart index 872a12ca..1dbd7a7a 100644 --- a/lib/pages/create_post/create_post_store.dart +++ b/lib/pages/create_post/create_post_store.dart @@ -8,7 +8,10 @@ import '../../util/pictrs.dart'; part 'create_post_store.g.dart'; -class CreatePostStore = _CreatePostStore with _$CreatePostStore; +class CreatePostStore extends _CreatePostStore with _$CreatePostStore { + CreatePostStore( + {required super.instanceHost, super.postToEdit, super.selectedCommunity}); +} abstract class _CreatePostStore with Store { final Post? postToEdit; diff --git a/lib/pages/create_post/create_post_url_field.dart b/lib/pages/create_post/create_post_url_field.dart index 0c70ccd7..9316b740 100644 --- a/lib/pages/create_post/create_post_url_field.dart +++ b/lib/pages/create_post/create_post_url_field.dart @@ -12,7 +12,7 @@ import 'create_post_store.dart'; class CreatePostUrlField extends HookWidget { final FocusNode titleFocusNode; - const CreatePostUrlField(this.titleFocusNode); + const CreatePostUrlField(this.titleFocusNode, {super.key}); @override Widget build(BuildContext context) { @@ -29,7 +29,7 @@ class CreatePostUrlField extends HookWidget { ); // pic is null when the picker was cancelled - if (pic != null) { + if (pic != null && context.mounted) { await context.read().uploadImage(pic.path, userData); } } diff --git a/lib/pages/display_document.dart b/lib/pages/display_document.dart index a95fc3c0..ed1830ae 100644 --- a/lib/pages/display_document.dart +++ b/lib/pages/display_document.dart @@ -27,7 +27,7 @@ class DisplayDocumentPage extends StatelessWidget { link: href!, context: context, ); - if (didLaunch) { + if (didLaunch && context.mounted) { Navigator.of(context).pop(); } }, diff --git a/lib/pages/federation_resolver.dart b/lib/pages/federation_resolver.dart index 4173fdb5..ddbefcc5 100644 --- a/lib/pages/federation_resolver.dart +++ b/lib/pages/federation_resolver.dart @@ -14,6 +14,7 @@ class FederationResolver extends HookWidget { builder; const FederationResolver({ + super.key, required this.userData, required this.query, required this.loadingMessage, diff --git a/lib/pages/full_post/full_post.dart b/lib/pages/full_post/full_post.dart index c37918a9..9563c1a6 100644 --- a/lib/pages/full_post/full_post.dart +++ b/lib/pages/full_post/full_post.dart @@ -133,7 +133,7 @@ class FullPostPage extends HookWidget { physics: const AlwaysScrollableScrollPhysics(), children: [ const SizedBox(height: 15), - PostTile.fromPostStore(postStore), + PostTile.fromPostStore(postStore, fullPost: true), ...CommentSection.buildComments(context, store), ], ), diff --git a/lib/pages/full_post/full_post_store.dart b/lib/pages/full_post/full_post_store.dart index 6d91ddaa..fbf0feb8 100644 --- a/lib/pages/full_post/full_post_store.dart +++ b/lib/pages/full_post/full_post_store.dart @@ -10,6 +10,8 @@ import '../../widgets/post/post_store.dart'; part 'full_post_store.g.dart'; +// Temp ignore until mobx stores are updated to satisfy linting rules +// ignore: library_private_types_in_public_api class FullPostStore = _FullPostStore with _$FullPostStore; abstract class _FullPostStore with Store { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index c04ee4b6..2d50f2d8 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -15,7 +15,7 @@ import 'profile_tab.dart'; import 'search_tab.dart'; class HomePage extends HookWidget { - const HomePage(); + const HomePage({super.key}); static const List pages = [ HomeTab(), diff --git a/lib/pages/home_tab.dart b/lib/pages/home_tab.dart index 231d7cde..3c2a5457 100644 --- a/lib/pages/home_tab.dart +++ b/lib/pages/home_tab.dart @@ -15,6 +15,7 @@ import '../util/goto.dart'; import '../widgets/bottom_modal.dart'; import '../widgets/cached_network_image.dart'; import '../widgets/infinite_scroll.dart'; +import '../widgets/post/post_store.dart'; import '../widgets/sortable_infinite_list.dart'; import 'create_post/create_post.dart'; import 'full_post/full_post.dart'; @@ -26,7 +27,7 @@ import 'settings/settings.dart'; /// First thing users sees when opening the app /// Shows list of posts from all or just specific instances class HomeTab extends HookWidget { - const HomeTab(); + const HomeTab({super.key}); @override Widget build(BuildContext context) { @@ -38,7 +39,7 @@ class HomeTab extends HookWidget { final showEverythingFeed = useStore((ConfigStore store) => store.showEverythingFeed); - final selectedList = useState(_SelectedList( + final selectedList = useState(SelectedList( instanceHost: accStore.defaultInstanceHost, listingType: accStore.hasNoAccount && defaultListingType == PostListingType.subscribed @@ -68,7 +69,7 @@ class HomeTab extends HookWidget { accStore.isAnonymousFor(selectedList.value.instanceHost!)) && selectedList.value.listingType == PostListingType.subscribed || !accStore.instances.contains(selectedList.value.instanceHost)) { - selectedList.value = _SelectedList( + selectedList.value = SelectedList( listingType: accStore.hasNoAccount && defaultListingType == PostListingType.subscribed ? PostListingType.all @@ -85,10 +86,10 @@ class HomeTab extends HookWidget { ]); handleListChange() async { - final val = await showBottomModal<_SelectedList>( + final val = await showBottomModal( context: context, builder: (context) { - pop(_SelectedList thing) => Navigator.of(context).pop(thing); + pop(SelectedList thing) => Navigator.of(context).pop(thing); final everythingChoices = [ const ListTile( @@ -111,7 +112,7 @@ class HomeTab extends HookWidget { onTap: accStore.hasNoAccount ? null : () => pop( - const _SelectedList( + const SelectedList( listingType: PostListingType.subscribed, ), ), @@ -124,7 +125,7 @@ class HomeTab extends HookWidget { ListTile( title: Text(listingType.value), leading: const SizedBox(width: 20, height: 20), - onTap: () => pop(_SelectedList(listingType: listingType)), + onTap: () => pop(SelectedList(listingType: listingType)), ), ]; return Column( @@ -177,7 +178,7 @@ class HomeTab extends HookWidget { onTap: accStore.isAnonymousFor(instance) ? () => Navigator.of(context) .push(AddAccountPage.route(instance)) - : () => pop(_SelectedList( + : () => pop(SelectedList( listingType: PostListingType.subscribed, instanceHost: instance, )), @@ -185,7 +186,7 @@ class HomeTab extends HookWidget { ), ListTile( title: Text(L10n.of(context).local), - onTap: () => pop(_SelectedList( + onTap: () => pop(SelectedList( listingType: PostListingType.local, instanceHost: instance, )), @@ -193,7 +194,7 @@ class HomeTab extends HookWidget { ), ListTile( title: Text(L10n.of(context).all), - onTap: () => pop(_SelectedList( + onTap: () => pop(SelectedList( listingType: PostListingType.all, instanceHost: instance, )), @@ -206,7 +207,9 @@ class HomeTab extends HookWidget { ); if (val != null) { selectedList.value = val; - isc.clear(); + // This will be cleared automatically by the fetcher changing, + // since we set `refreshOnFetcherUpdate` to true. + // isc.clear(); } } @@ -276,7 +279,7 @@ class HomeTab extends HookWidget { CreatePostPage.route(), ); - if (postView != null) { + if (postView != null && context.mounted) { await Navigator.of(context) .push(FullPostPage.fromPostViewRoute(postView)); } @@ -327,10 +330,7 @@ class HomeTab extends HookWidget { ]; }, onSelected: (value) { if (value == 0) { - // unable to tie a controller to the infinite scroll - // table, for now just reload - // isc.scrollToTop(); - isc.clear(); + isc.scrollToTop(); } else if (value == 1) { isc.clear(); } else if (value == 2) { @@ -355,9 +355,10 @@ class HomeTab extends HookWidget { /// Infinite list of posts class InfiniteHomeList extends HookWidget { final InfiniteScrollController controller; - final _SelectedList selectedList; + final SelectedList selectedList; const InfiniteHomeList({ + super.key, required this.selectedList, required this.controller, }); @@ -370,7 +371,7 @@ class InfiniteHomeList extends HookWidget { /// list /// /// Process of combining them works sort of like zip function in python - Future> generalFetcher( + Future> generalFetcher( int page, int limit, SortType sort, @@ -386,14 +387,16 @@ class InfiniteHomeList extends HookWidget { final futures = [ for (final instanceHost in instances) - LemmyApiV3(instanceHost).run(GetPosts( - type: listingType, - sort: sort, - page: page, - limit: limit, - savedOnly: false, - auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw, - )) + LemmyApiV3(instanceHost) + .run(GetPosts( + type: listingType, + sort: sort, + page: page, + limit: limit, + savedOnly: false, + auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw, + )) + .toPostStores() ]; final instancePosts = await Future.wait(futures); final longest = instancePosts.map((e) => e.length).reduce(max); @@ -407,34 +410,45 @@ class InfiniteHomeList extends HookWidget { return newPosts; } - FetcherWithSorting fetcherFromInstance( + FetcherWithSorting fetcherFromInstance( String instanceHost, PostListingType listingType) => - (page, batchSize, sort) => LemmyApiV3(instanceHost).run(GetPosts( + (page, batchSize, sort) => LemmyApiV3(instanceHost) + .run(GetPosts( type: listingType, sort: sort, page: page, limit: batchSize, savedOnly: false, auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw, - )); + )) + .toPostStores(); + + final memoizedFetcher = useMemoized( + () { + final selectedInstanceHost = selectedList.instanceHost; + return selectedInstanceHost == null + ? (page, limit, sort) => + generalFetcher(page, limit, sort, selectedList.listingType) + : fetcherFromInstance( + selectedInstanceHost, selectedList.listingType); + }, + [selectedList], + ); return InfinitePostList( - fetcher: selectedList.instanceHost == null - ? (page, limit, sort) => - generalFetcher(page, limit, sort, selectedList.listingType) - : fetcherFromInstance( - selectedList.instanceHost!, selectedList.listingType), + fetcher: memoizedFetcher, + refreshOnFetcherUpdate: true, controller: controller, ); } } -class _SelectedList { +class SelectedList { /// when null it implies the 'EVERYTHING' mode final String? instanceHost; final PostListingType listingType; - const _SelectedList({ + const SelectedList({ required this.listingType, this.instanceHost, }); diff --git a/lib/pages/inbox.dart b/lib/pages/inbox.dart index 34d5e4fc..21c8563e 100644 --- a/lib/pages/inbox.dart +++ b/lib/pages/inbox.dart @@ -26,7 +26,7 @@ import '../widgets/tile_action.dart'; import 'write_message.dart'; class InboxPage extends HookWidget { - const InboxPage(); + const InboxPage({super.key}); @override Widget build(BuildContext context) { @@ -229,6 +229,7 @@ class PrivateMessageTile extends HookWidget { final bool hideOnRead; const PrivateMessageTile({ + super.key, required this.privateMessageView, this.hideOnRead = false, }); diff --git a/lib/pages/instance/instance.dart b/lib/pages/instance/instance.dart index f90c0b31..5ca62315 100644 --- a/lib/pages/instance/instance.dart +++ b/lib/pages/instance/instance.dart @@ -12,6 +12,7 @@ import '../../util/text_color.dart'; import '../../widgets/cached_network_image.dart'; import '../../widgets/failed_to_load.dart'; import '../../widgets/fullscreenable_image.dart'; +import '../../widgets/post/post_store.dart'; import '../../widgets/reveal_after_scroll.dart'; import '../../widgets/sortable_infinite_list.dart'; import 'instance_about_tab.dart'; @@ -20,7 +21,7 @@ import 'instance_store.dart'; /// Displays posts, comments, and general info about the given instance class InstancePage extends HookWidget { - const InstancePage(); + const InstancePage({super.key}); @override Widget build(BuildContext context) { @@ -155,18 +156,20 @@ class InstancePage extends HookWidget { 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 - .defaultUserData(store.instanceHost) - ?.jwt - .raw, - )), + LemmyApiV3(store.instanceHost) + .run(GetPosts( + // TODO: switch between all and subscribed + type: PostListingType.all, + sort: sort, + limit: batchSize, + page: page, + savedOnly: false, + auth: context + .defaultUserData(store.instanceHost) + ?.jwt + .raw, + )) + .toPostStores(), ), InfiniteCommentList( fetcher: (page, batchSize, sort) => diff --git a/lib/pages/instance/instance_about_tab.dart b/lib/pages/instance/instance_about_tab.dart index af07f64f..83d275c0 100644 --- a/lib/pages/instance/instance_about_tab.dart +++ b/lib/pages/instance/instance_about_tab.dart @@ -21,7 +21,8 @@ class InstanceAboutTab extends HookWidget { final FullSiteView site; final SiteView siteView; - const InstanceAboutTab({required this.site, required this.siteView}); + const InstanceAboutTab( + {super.key, required this.site, required this.siteView}); @override Widget build(BuildContext context) { diff --git a/lib/pages/instance/instance_store.dart b/lib/pages/instance/instance_store.dart index 20475537..61909bbb 100644 --- a/lib/pages/instance/instance_store.dart +++ b/lib/pages/instance/instance_store.dart @@ -6,7 +6,9 @@ import '../../util/async_store.dart'; part 'instance_store.g.dart'; -class InstanceStore = _InstanceStore with _$InstanceStore; +class InstanceStore extends _InstanceStore with _$InstanceStore { + InstanceStore(super.instanceHost); +} abstract class _InstanceStore with Store { final String instanceHost; diff --git a/lib/pages/log_console/log_console.dart b/lib/pages/log_console/log_console.dart index dceea4b7..62d50e80 100644 --- a/lib/pages/log_console/log_console.dart +++ b/lib/pages/log_console/log_console.dart @@ -7,7 +7,7 @@ import '../../widgets/bottom_safe.dart'; import 'log_console_page_store.dart'; class LogConsolePage extends StatelessWidget { - const LogConsolePage(); + const LogConsolePage({super.key}); @override Widget build(BuildContext context) { @@ -46,11 +46,14 @@ class LogConsolePage extends StatelessWidget { await Clipboard.setData(ClipboardData(text: data.join('\n'))); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - const SnackBar(content: Text('all logs copied to the clipboard')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + const SnackBar( + content: Text('all logs copied to the clipboard')), + ); + } }, tooltip: 'Copy to clipboard', child: const Icon(Icons.copy), diff --git a/lib/pages/log_console/log_console_page_store.dart b/lib/pages/log_console/log_console_page_store.dart index b39cf27f..fbd9ae32 100644 --- a/lib/pages/log_console/log_console_page_store.dart +++ b/lib/pages/log_console/log_console_page_store.dart @@ -3,7 +3,8 @@ import 'package:mobx/mobx.dart'; part 'log_console_page_store.g.dart'; -class LogConsolePageStore = _LogConsolePageStore with _$LogConsolePageStore; +class LogConsolePageStore extends _LogConsolePageStore + with _$LogConsolePageStore {} abstract class _LogConsolePageStore with Store { // TODO: implement as an ObservableDeque diff --git a/lib/pages/manage_account.dart b/lib/pages/manage_account.dart index a848593b..e6a27cfb 100644 --- a/lib/pages/manage_account.dart +++ b/lib/pages/manage_account.dart @@ -15,6 +15,10 @@ import '../widgets/cached_network_image.dart'; import '../widgets/editor/editor.dart'; import 'pick_image.dart'; +// FIXME: Remove this when linting fix for this rule is in Flutter SDK +// See: https://github.com/dart-lang/linter/issues/4007 +// ignore_for_file: use_build_context_synchronously + /// Page for managing things like username, email, avatar etc /// This page will assume the manage account is logged in and /// its token is in AccountsStore @@ -22,7 +26,8 @@ class ManageAccountPage extends HookWidget { final String instanceHost; final String username; - const ManageAccountPage({required this.instanceHost, required this.username}); + const ManageAccountPage( + {super.key, required this.instanceHost, required this.username}); @override Widget build(BuildContext context) { diff --git a/lib/pages/media_view.dart b/lib/pages/media_view.dart index c51e73e5..65add8ef 100644 --- a/lib/pages/media_view.dart +++ b/lib/pages/media_view.dart @@ -21,6 +21,10 @@ import '../util/icons.dart'; import '../util/share.dart'; import '../widgets/bottom_modal.dart'; +// FIXME: Remove this when linting fix for this rule is in Flutter SDK +// See: https://github.com/dart-lang/linter/issues/4007 +// ignore_for_file: use_build_context_synchronously + /// View to interact with a media object. Zoom in/out, download, share, etc. class MediaViewPage extends HookWidget { final String url; @@ -28,7 +32,7 @@ class MediaViewPage extends HookWidget { static const yThreshold = 150; static const speedThreshold = 45; - const MediaViewPage(this.url, {this.heroTag}); + const MediaViewPage(this.url, {super.key, this.heroTag}); @override Widget build(BuildContext context) { diff --git a/lib/pages/modlog/modlog.dart b/lib/pages/modlog/modlog.dart index 21bc4a80..0fd638e7 100644 --- a/lib/pages/modlog/modlog.dart +++ b/lib/pages/modlog/modlog.dart @@ -107,7 +107,8 @@ class ModlogPage extends StatelessWidget { return MaterialPageRoute( builder: (context) => MobxProvider( create: (context) => - ModlogPageStore(instanceHost, communityId)..fetchPage(), + ModlogPageStore(instanceHost, communityId: communityId) + ..fetchPage(), child: ModlogPage._(name: '!$communityName'), ), ); diff --git a/lib/pages/modlog/modlog_page_store.dart b/lib/pages/modlog/modlog_page_store.dart index b623d261..db7a2f24 100644 --- a/lib/pages/modlog/modlog_page_store.dart +++ b/lib/pages/modlog/modlog_page_store.dart @@ -6,14 +6,16 @@ import '../../util/mobx_provider.dart'; part 'modlog_page_store.g.dart'; -class ModlogPageStore = _ModlogPageStore with _$ModlogPageStore; +class ModlogPageStore extends _ModlogPageStore with _$ModlogPageStore { + ModlogPageStore(super.instanceHost, {super.communityId}); +} abstract class _ModlogPageStore with Store, DisposableStore { final String instanceHost; final int? communityId; // ignore: unused_element - _ModlogPageStore(this.instanceHost, [this.communityId]) { + _ModlogPageStore(this.instanceHost, {this.communityId}) { addReaction(reaction((_) => page, (_) => fetchPage())); } diff --git a/lib/pages/pick_image.dart b/lib/pages/pick_image.dart index 46f674c1..e2bde68b 100644 --- a/lib/pages/pick_image.dart +++ b/lib/pages/pick_image.dart @@ -14,7 +14,7 @@ import '../util/files.dart'; /// Modal for picking and editing a photo from the OS. /// Pops the navigator stack with a [XFile] class PickImagePage extends HookWidget { - const PickImagePage(); + const PickImagePage({super.key}); /// read the supplied XFile, check orientation and /// provide the corrected XFile. @@ -94,22 +94,24 @@ class PickImagePage extends HookWidget { ), ) else - ClipRect( - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 500), - child: ExtendedImage.file( - File(xfile.value!.path), - fit: BoxFit.contain, - mode: ExtendedImageMode.editor, - enableLoadState: true, - extendedImageEditorKey: editorKey, - cacheRawData: true, - initEditorConfigHandler: (ExtendedImageState? state) { - return EditorConfig( - maxScale: 4, - initCropRectType: InitCropRectType.layoutRect, - ); - }, + Flexible( + child: ClipRect( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 500), + child: ExtendedImage.file( + File(xfile.value!.path), + fit: BoxFit.contain, + mode: ExtendedImageMode.editor, + enableLoadState: true, + extendedImageEditorKey: editorKey, + cacheRawData: true, + initEditorConfigHandler: (ExtendedImageState? state) { + return EditorConfig( + maxScale: 4, + initCropRectType: InitCropRectType.layoutRect, + ); + }, + ), ), ), ), diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index 0d576f40..1c992982 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -11,7 +11,7 @@ import 'settings/settings.dart'; /// Profile page for a logged in user. The difference between this and /// UserPage is that here you have access to settings class UserProfileTab extends HookWidget { - const UserProfileTab(); + const UserProfileTab({super.key}); @override Widget build(BuildContext context) { @@ -20,7 +20,7 @@ class UserProfileTab extends HookWidget { final actions = [ IconButton( - onPressed: () => goTo(context, (context) => SavedPage()), + onPressed: () => goTo(context, (context) => const SavedPage()), icon: const Icon(Icons.bookmark), ), IconButton( diff --git a/lib/pages/saved_page.dart b/lib/pages/saved_page.dart index 76c0f8a2..b265b655 100644 --- a/lib/pages/saved_page.dart +++ b/lib/pages/saved_page.dart @@ -4,11 +4,14 @@ import 'package:lemmy_api_client/v3.dart'; import '../hooks/stores.dart'; import '../l10n/l10n.dart'; +import '../widgets/post/post_store.dart'; import '../widgets/sortable_infinite_list.dart'; /// Page with saved posts/comments. Fetches such saved data from the default user /// Assumes there is at least one logged in user class SavedPage extends HookWidget { + const SavedPage({super.key}); + @override Widget build(BuildContext context) { final accountStore = useAccountsStore(); @@ -39,16 +42,18 @@ class SavedPage extends HookWidget { children: [ InfinitePostList( fetcher: (page, batchSize, sortType) => - LemmyApiV3(accountStore.defaultInstanceHost!).run( - GetPosts( - type: PostListingType.all, - sort: sortType, - savedOnly: true, - page: page, - limit: batchSize, - auth: accountStore.defaultUserData!.jwt.raw, - ), - ), + LemmyApiV3(accountStore.defaultInstanceHost!) + .run( + GetPosts( + type: PostListingType.all, + sort: sortType, + savedOnly: true, + page: page, + limit: batchSize, + auth: accountStore.defaultUserData!.jwt.raw, + ), + ) + .toPostStores(), ), InfiniteCommentList( fetcher: (page, batchSize, sortType) => diff --git a/lib/pages/search_results.dart b/lib/pages/search_results.dart index 662da63b..06157564 100644 --- a/lib/pages/search_results.dart +++ b/lib/pages/search_results.dart @@ -15,6 +15,7 @@ class SearchResultsPage extends HookWidget { final String query; SearchResultsPage({ + super.key, required this.instanceHost, required this.query, }) : assert(instanceHost.isNotEmpty), diff --git a/lib/pages/search_tab.dart b/lib/pages/search_tab.dart index ca949005..1ba9084b 100644 --- a/lib/pages/search_tab.dart +++ b/lib/pages/search_tab.dart @@ -9,7 +9,7 @@ import '../widgets/radio_picker.dart'; import 'search_results.dart'; class SearchTab extends HookWidget { - const SearchTab(); + const SearchTab({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings/add_account_page.dart b/lib/pages/settings/add_account_page.dart index 1995947c..86649b54 100644 --- a/lib/pages/settings/add_account_page.dart +++ b/lib/pages/settings/add_account_page.dart @@ -21,7 +21,7 @@ import 'add_instance_page.dart'; class AddAccountPage extends HookWidget { final String instanceHost; - const AddAccountPage({required this.instanceHost}); + const AddAccountPage({super.key, required this.instanceHost}); @override Widget build(BuildContext context) { @@ -79,16 +79,19 @@ class AddAccountPage extends HookWidget { // if first account try to import settings if (isFirstAccount) { try { - await context.read().importLemmyUserSettings( - accountsStore - .userDataFor( - selectedInstance.value, usernameController.text)! - .jwt); + if (context.mounted) { + await context.read().importLemmyUserSettings( + accountsStore + .userDataFor( + selectedInstance.value, usernameController.text)! + .jwt); + } // ignore: empty_catches } catch (e) {} } - - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } } on VerifyEmailException { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(L10n.of(context).verification_email_sent), @@ -167,7 +170,9 @@ class AddAccountPage extends HookWidget { onTap: () async { final value = await Navigator.of(context).push(AddInstancePage.route()); - Navigator.of(context).pop(value); + if (context.mounted) { + Navigator.of(context).pop(value); + } }, ), ), diff --git a/lib/pages/settings/add_instance_page.dart b/lib/pages/settings/add_instance_page.dart index b3a15a51..914073a9 100644 --- a/lib/pages/settings/add_instance_page.dart +++ b/lib/pages/settings/add_instance_page.dart @@ -11,7 +11,7 @@ import '../../widgets/fullscreenable_image.dart'; /// A page that let's user add a new instance. Pops a url of the added instance class AddInstancePage extends HookWidget { - const AddInstancePage(); + const AddInstancePage({super.key}); @override Widget build(BuildContext context) { @@ -52,7 +52,9 @@ class AddInstancePage extends HookWidget { handleOnAdd() async { try { await accountsStore.addInstance(inst, assumeValid: true); - Navigator.of(context).pop(inst); + if (context.mounted) { + Navigator.of(context).pop(inst); + } } on Exception catch (err) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(err.toString()), diff --git a/lib/pages/settings/blocks/block_dialog.dart b/lib/pages/settings/blocks/block_dialog.dart index fdc6a4ca..7018049b 100644 --- a/lib/pages/settings/blocks/block_dialog.dart +++ b/lib/pages/settings/blocks/block_dialog.dart @@ -11,7 +11,7 @@ import 'blocks_store.dart'; class BlockPersonDialog extends StatelessWidget { final BlocksStore store; - const BlockPersonDialog(this.store); + const BlockPersonDialog(this.store, {super.key}); @override Widget build(BuildContext context) { @@ -69,7 +69,7 @@ class BlockPersonDialog extends StatelessWidget { class BlockCommunityDialog extends StatelessWidget { final BlocksStore store; - const BlockCommunityDialog(this.store); + const BlockCommunityDialog(this.store, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings/blocks/blocks_store.dart b/lib/pages/settings/blocks/blocks_store.dart index 01b79706..ffa17ffb 100644 --- a/lib/pages/settings/blocks/blocks_store.dart +++ b/lib/pages/settings/blocks/blocks_store.dart @@ -8,7 +8,9 @@ import 'user_block_store.dart'; part 'blocks_store.g.dart'; -class BlocksStore = _BlocksStore with _$BlocksStore; +class BlocksStore extends _BlocksStore with _$BlocksStore { + BlocksStore({required super.instanceHost, required super.token}); +} abstract class _BlocksStore with Store { final String instanceHost; diff --git a/lib/pages/settings/blocks/community_block_store.dart b/lib/pages/settings/blocks/community_block_store.dart index 0e535349..e69a5cc4 100644 --- a/lib/pages/settings/blocks/community_block_store.dart +++ b/lib/pages/settings/blocks/community_block_store.dart @@ -5,7 +5,13 @@ import '../../../util/async_store.dart'; part 'community_block_store.g.dart'; -class CommunityBlockStore = _CommunityBlockStore with _$CommunityBlockStore; +class CommunityBlockStore extends _CommunityBlockStore + with _$CommunityBlockStore { + CommunityBlockStore( + {required super.instanceHost, + required super.token, + required super.community}); +} abstract class _CommunityBlockStore with Store { final String instanceHost; diff --git a/lib/pages/settings/blocks/user_block_store.dart b/lib/pages/settings/blocks/user_block_store.dart index 651f89b5..69daa74b 100644 --- a/lib/pages/settings/blocks/user_block_store.dart +++ b/lib/pages/settings/blocks/user_block_store.dart @@ -5,7 +5,12 @@ import '../../../util/async_store.dart'; part 'user_block_store.g.dart'; -class UserBlockStore = _UserBlockStore with _$UserBlockStore; +class UserBlockStore extends _UserBlockStore with _$UserBlockStore { + UserBlockStore( + {required super.instanceHost, + required super.token, + required super.person}); +} abstract class _UserBlockStore with Store { final String instanceHost; diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 68b20cb4..6ea4a13a 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -28,7 +28,7 @@ import 'mock_post.dart'; /// Page with a list of different settings sections class SettingsPage extends HookWidget { - const SettingsPage(); + const SettingsPage({super.key}); @override Widget build(BuildContext context) { @@ -92,7 +92,7 @@ class SettingsPage extends HookWidget { /// Settings for theme color, AMOLED switch class AppearanceConfigPage extends StatelessWidget { - const AppearanceConfigPage(); + const AppearanceConfigPage({super.key}); @override Widget build(BuildContext context) { @@ -174,7 +174,7 @@ class AppearanceConfigPage extends StatelessWidget { /// Settings for theme color, AMOLED switch class PostStyleConfigPage extends StatelessWidget { - const PostStyleConfigPage(); + const PostStyleConfigPage({super.key}); @override Widget build(BuildContext context) { @@ -245,6 +245,20 @@ class PostStyleConfigPage extends StatelessWidget { store.showScores = checked; }, ), + SwitchListTile.adaptive( + title: Text(L10n.of(context).auto_play_video), + value: store.autoPlayVideo, + onChanged: (checked) { + store.autoPlayVideo = checked; + }, + ), + SwitchListTile.adaptive( + title: Text(L10n.of(context).auto_mute_video), + value: store.autoMuteVideo, + onChanged: (checked) { + store.autoMuteVideo = checked; + }, + ), const SizedBox(height: 12), _SectionHeading(L10n.of(context).font), ListTile( @@ -385,7 +399,7 @@ class PostStyleConfigPage extends StatelessWidget { } class CommentStyleConfigPage extends HookWidget { - const CommentStyleConfigPage(); + const CommentStyleConfigPage({super.key}); @override Widget build(BuildContext context) { @@ -598,7 +612,7 @@ class CommentStyleConfigPage extends HookWidget { /// General settings class GeneralConfigPage extends StatelessWidget { - const GeneralConfigPage(); + const GeneralConfigPage({super.key}); @override Widget build(BuildContext context) { @@ -788,7 +802,9 @@ class _AccountOptions extends HookWidget { ) ?? false) { await accountsStore.removeAccount(instanceHost, username); - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } } } @@ -826,7 +842,9 @@ class _AccountOptions extends HookWidget { await context.read().importLemmyUserSettings( accountsStore.userDataFor(instanceHost, username)!.jwt, ); - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, ), ), @@ -840,6 +858,8 @@ class _AccountOptions extends HookWidget { class AccountsConfigPage extends HookWidget { final GlobalKey _scaffoldKey = GlobalKey(); + AccountsConfigPage({super.key}); + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -866,7 +886,9 @@ class AccountsConfigPage extends HookWidget { ) ?? false) { await accountsStore.removeInstance(instanceHost); - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } } } diff --git a/lib/pages/user.dart b/lib/pages/user.dart index da897544..33483c4c 100644 --- a/lib/pages/user.dart +++ b/lib/pages/user.dart @@ -14,11 +14,12 @@ class UserPage extends HookWidget { final String instanceHost; final Future _userDetails; - UserPage({required this.userId, required this.instanceHost}) + UserPage({super.key, required this.userId, required this.instanceHost}) : _userDetails = LemmyApiV3(instanceHost).run(GetPersonDetails( personId: userId, savedOnly: true, sort: SortType.active)); - UserPage.fromName({required this.instanceHost, required String username}) + UserPage.fromName( + {super.key, required this.instanceHost, required String username}) : userId = null, _userDetails = LemmyApiV3(instanceHost).run(GetPersonDetails( username: username, savedOnly: true, sort: SortType.active)); @@ -72,7 +73,7 @@ class UserPage extends HookWidget { class SendMessageButton extends HookWidget { final PersonSafe user; - const SendMessageButton(this.user); + const SendMessageButton(this.user, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/write_message.dart b/lib/pages/write_message.dart index 40979fd2..a9dec93e 100644 --- a/lib/pages/write_message.dart +++ b/lib/pages/write_message.dart @@ -21,12 +21,13 @@ class WriteMessagePage extends HookWidget { final bool _isEdit; const WriteMessagePage.send({ + super.key, required this.recipient, required this.instanceHost, }) : privateMessage = null, _isEdit = false; - WriteMessagePage.edit(PrivateMessageView pmv) + WriteMessagePage.edit(PrivateMessageView pmv, {super.key}) : privateMessage = pmv.privateMessage, recipient = pmv.recipient, instanceHost = pmv.instanceHost, @@ -51,7 +52,9 @@ class WriteMessagePage extends HookWidget { privateMessageId: privateMessage!.id, content: bodyController.text, )); - Navigator.of(context).pop(msg); + if (context.mounted) { + Navigator.of(context).pop(msg); + } } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(e.toString()), @@ -66,7 +69,9 @@ class WriteMessagePage extends HookWidget { content: bodyController.text, recipientId: recipient.id, )); - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } // TODO: maybe send notification so that infinite list // containing this widget adds new message? } catch (e) { diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index e366f7bf..9ebd64d0 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -86,6 +86,14 @@ abstract class _ConfigStore with Store { @JsonKey(defaultValue: true) bool showThumbnail = true; + @observable + @JsonKey(defaultValue: true) + bool autoPlayVideo = true; + + @observable + @JsonKey(defaultValue: true) + bool autoMuteVideo = true; + // font size @observable @JsonKey(defaultValue: 16) diff --git a/lib/stores/config_store.g.dart b/lib/stores/config_store.g.dart index 3ed6b2ee..3c4b2382 100644 --- a/lib/stores/config_store.g.dart +++ b/lib/stores/config_store.g.dart @@ -18,6 +18,8 @@ ConfigStore _$ConfigStoreFromJson(Map json) => ConfigStore() ..showScores = json['showScores'] as bool? ?? true ..blurNsfw = json['blurNsfw'] as bool? ?? true ..showThumbnail = json['showThumbnail'] as bool? ?? true + ..autoPlayVideo = json['autoPlayVideo'] as bool? ?? true + ..autoMuteVideo = json['autoMuteVideo'] as bool? ?? true ..titleFontSize = (json['titleFontSize'] as num?)?.toDouble() ?? 16 ..postHeaderFontSize = (json['postHeaderFontSize'] as num?)?.toDouble() ?? 15 ..postBodySize = (json['postBodySize'] as num?)?.toDouble() ?? 15 @@ -47,6 +49,8 @@ Map _$ConfigStoreToJson(ConfigStore instance) => 'showScores': instance.showScores, 'blurNsfw': instance.blurNsfw, 'showThumbnail': instance.showThumbnail, + 'autoPlayVideo': instance.autoPlayVideo, + 'autoMuteVideo': instance.autoMuteVideo, 'titleFontSize': instance.titleFontSize, 'postHeaderFontSize': instance.postHeaderFontSize, 'postBodySize': instance.postBodySize, @@ -243,6 +247,38 @@ mixin _$ConfigStore on _ConfigStore, Store { }); } + late final _$autoPlayVideoAtom = + Atom(name: '_ConfigStore.autoPlayVideo', context: context); + + @override + bool get autoPlayVideo { + _$autoPlayVideoAtom.reportRead(); + return super.autoPlayVideo; + } + + @override + set autoPlayVideo(bool value) { + _$autoPlayVideoAtom.reportWrite(value, super.autoPlayVideo, () { + super.autoPlayVideo = value; + }); + } + + late final _$autoMuteVideoAtom = + Atom(name: '_ConfigStore.autoMuteVideo', context: context); + + @override + bool get autoMuteVideo { + _$autoMuteVideoAtom.reportRead(); + return super.autoMuteVideo; + } + + @override + set autoMuteVideo(bool value) { + _$autoMuteVideoAtom.reportWrite(value, super.autoMuteVideo, () { + super.autoMuteVideo = value; + }); + } + late final _$titleFontSizeAtom = Atom(name: '_ConfigStore.titleFontSize', context: context); @@ -473,6 +509,8 @@ showAvatars: ${showAvatars}, showScores: ${showScores}, blurNsfw: ${blurNsfw}, showThumbnail: ${showThumbnail}, +autoPlayVideo: ${autoPlayVideo}, +autoMuteVideo: ${autoMuteVideo}, titleFontSize: ${titleFontSize}, postHeaderFontSize: ${postHeaderFontSize}, postBodySize: ${postBodySize}, diff --git a/lib/util/async_store.dart b/lib/util/async_store.dart index 2e31ad4e..db008383 100644 --- a/lib/util/async_store.dart +++ b/lib/util/async_store.dart @@ -11,7 +11,7 @@ part 'async_store.freezed.dart'; part 'async_store.g.dart'; /// [AsyncState] but observable with helper methods/getters -class AsyncStore = _AsyncStore with _$AsyncStore; +class AsyncStore extends _AsyncStore with _$AsyncStore {} abstract class _AsyncStore with Store { @observable diff --git a/lib/util/extensions/iterators.dart b/lib/util/extensions/iterators.dart index d9feb1a7..6daf69cf 100644 --- a/lib/util/extensions/iterators.dart +++ b/lib/util/extensions/iterators.dart @@ -1,6 +1,6 @@ extension ExtraIterators on Iterable { /// A `.map` but with an index as the second argument - Iterable mapWithIndex(T f(E e, int i)) { + Iterable mapWithIndex(T Function(E e, int i) f) { var i = 0; return map((e) => f(e, i++)); } diff --git a/lib/util/share.dart b/lib/util/share.dart index a749f6f7..08b216d0 100644 --- a/lib/util/share.dart +++ b/lib/util/share.dart @@ -33,9 +33,11 @@ Future _fallbackShare( required BuildContext context, }) async { await Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Copied data to clipboard!')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied data to clipboard!')), + ); + } } /// Constructs an instance-agnostic link to the given target, diff --git a/lib/widgets/about_tile.dart b/lib/widgets/about_tile.dart index ecf40b60..b3dd519a 100644 --- a/lib/widgets/about_tile.dart +++ b/lib/widgets/about_tile.dart @@ -11,7 +11,7 @@ import '../url_launcher.dart'; /// Title that opens a dialog with information about Liftoff. /// Licenses, changelog, version etc. class AboutTile extends HookWidget { - const AboutTile(); + const AboutTile({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/bottom_modal.dart b/lib/widgets/bottom_modal.dart index 63f4e2ab..d60cdb83 100644 --- a/lib/widgets/bottom_modal.dart +++ b/lib/widgets/bottom_modal.dart @@ -8,6 +8,7 @@ class BottomModal extends StatelessWidget { final Widget child; const BottomModal({ + super.key, this.title, this.padding = EdgeInsets.zero, required this.child, diff --git a/lib/widgets/bottom_safe.dart b/lib/widgets/bottom_safe.dart index 451f8fef..319f5a34 100644 --- a/lib/widgets/bottom_safe.dart +++ b/lib/widgets/bottom_safe.dart @@ -7,8 +7,8 @@ class BottomSafe extends StatelessWidget { // FAB size + FAB margin, 56 is as per https://material.io/components/buttons-floating-action-button#anatomy 56 + kFloatingActionButtonMargin; - const BottomSafe([this.additionalPadding = 0]); - const BottomSafe.fab() : this(fabPadding); + const BottomSafe({this.additionalPadding = 0, super.key}); + const BottomSafe.fab({key}) : this(additionalPadding: fabPadding, key: key); @override Widget build(BuildContext context) { diff --git a/lib/widgets/comment/comment_actions.dart b/lib/widgets/comment/comment_actions.dart index 7bc0f0b8..0a130a5b 100644 --- a/lib/widgets/comment/comment_actions.dart +++ b/lib/widgets/comment/comment_actions.dart @@ -48,11 +48,13 @@ class CommentActions extends HookWidget { tooltip: 'copy', onPressed: () async { await Clipboard.setData(ClipboardData(text: comment.content)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('comment copied to clipboard'), - ), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('comment copied to clipboard'), + ), + ); + } }, ), const Spacer(), diff --git a/lib/widgets/comment/comment_more_menu_button.dart b/lib/widgets/comment/comment_more_menu_button.dart index 77d152d7..023d9806 100644 --- a/lib/widgets/comment/comment_more_menu_button.dart +++ b/lib/widgets/comment/comment_more_menu_button.dart @@ -80,7 +80,9 @@ class _CommentMoreMenuPopup extends HookWidget { if (editedComment != null) { store.comment = editedComment; - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } } } @@ -100,8 +102,9 @@ class _CommentMoreMenuPopup extends HookWidget { title: Text(L10n.of(context).open_in_browser), onTap: () async { await launchLink(link: comment.link, context: context); - - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, ), ListTile( @@ -142,8 +145,9 @@ class _CommentMoreMenuPopup extends HookWidget { link: 'https://translate.google.com/?tl=$targetLanguage&text=$sourceText', context: context); - - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, ), ListTile( diff --git a/lib/widgets/comment/comment_store.dart b/lib/widgets/comment/comment_store.dart index be89184f..7eb38f75 100644 --- a/lib/widgets/comment/comment_store.dart +++ b/lib/widgets/comment/comment_store.dart @@ -7,7 +7,15 @@ import '../../util/async_store.dart'; part 'comment_store.g.dart'; -class CommentStore = _CommentStore with _$CommentStore; +class CommentStore extends _CommentStore with _$CommentStore { + CommentStore(super.accountsStore, + {required super.commentTree, + required super.depth, + required super.canBeMarkedAsRead, + required super.detached, + required super.hideOnRead, + super.userMentionId}); +} abstract class _CommentStore with Store { @observable diff --git a/lib/widgets/comment_list_options.dart b/lib/widgets/comment_list_options.dart index a06fe20d..ccb566d8 100644 --- a/lib/widgets/comment_list_options.dart +++ b/lib/widgets/comment_list_options.dart @@ -27,6 +27,7 @@ class CommentListOptions extends StatelessWidget { }; const CommentListOptions({ + super.key, required this.onSortChanged, required this.sortValue, this.styleButton = true, diff --git a/lib/widgets/editor/editor_toolbar.dart b/lib/widgets/editor/editor_toolbar.dart index 067143c0..70b3d071 100644 --- a/lib/widgets/editor/editor_toolbar.dart +++ b/lib/widgets/editor/editor_toolbar.dart @@ -17,11 +17,11 @@ import 'editor.dart'; import 'editor_picking_dialog.dart'; import 'editor_toolbar_store.dart'; -class _Reformat { +class Reformat { final String text; final int selectionBeginningShift; final int selectionEndingShift; - _Reformat({ + Reformat({ required this.text, this.selectionBeginningShift = 0, this.selectionEndingShift = 0, @@ -45,7 +45,7 @@ class EditorToolbar extends HookWidget { static const _height = 50.0; - const EditorToolbar(this.controller); + const EditorToolbar(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -97,7 +97,7 @@ class EditorToolbar extends HookWidget { class BottomSticky extends StatelessWidget { final Widget child; - const BottomSticky({required this.child}); + const BottomSticky({super.key, required this.child}); @override Widget build(BuildContext context) => Positioned( @@ -297,7 +297,7 @@ class _ToolbarBody extends HookWidget { final textMid = selection.isNotEmpty ? selection : '___'; const textEnd = '\n:::\n'; - return _Reformat( + return Reformat( text: textBeg + textMid + textEnd, selectionBeginningShift: textBeg.length, selectionEndingShift: @@ -327,7 +327,7 @@ class AddLinkDialog extends HookWidget { static final _websiteRegex = RegExp(r'https?:\/\/', caseSensitive: false); - AddLinkDialog(this.selection) + AddLinkDialog(this.selection, {super.key}) : label = selection.startsWith(_websiteRegex) ? '' : selection, url = selection.startsWith(_websiteRegex) ? selection : ''; @@ -345,7 +345,7 @@ class AddLinkDialog extends HookWidget { } }(); final finalString = '[${labelController.text}]($link)'; - Navigator.of(context).pop(_Reformat( + Navigator.of(context).pop(Reformat( text: finalString, selectionBeginningShift: finalString.length, selectionEndingShift: finalString.length - selection.length, @@ -384,7 +384,7 @@ class AddLinkDialog extends HookWidget { ); } - static Future<_Reformat?> show(BuildContext context, String selection) async { + static Future show(BuildContext context, String selection) async { return showDialog( context: context, builder: (context) => AddLinkDialog(selection), @@ -486,7 +486,7 @@ extension on TextEditingController { ); } - void reformat(_Reformat Function(String selection) reformatter) { + void reformat(Reformat Function(String selection) reformatter) { final beg = beforeSelectionText; final mid = selectionText; final end = afterSelectionText; @@ -502,5 +502,5 @@ extension on TextEditingController { } void reformatSimple(String text) => - reformat((selection) => _Reformat(text: text)); + reformat((selection) => Reformat(text: text)); } diff --git a/lib/widgets/editor/editor_toolbar_store.dart b/lib/widgets/editor/editor_toolbar_store.dart index 3822fd83..63e385ef 100644 --- a/lib/widgets/editor/editor_toolbar_store.dart +++ b/lib/widgets/editor/editor_toolbar_store.dart @@ -7,7 +7,9 @@ import '../../util/pictrs.dart'; part 'editor_toolbar_store.g.dart'; -class EditorToolbarStore = _EditorToolbarStore with _$EditorToolbarStore; +class EditorToolbarStore extends _EditorToolbarStore with _$EditorToolbarStore { + EditorToolbarStore(super.instanceHost); +} abstract class _EditorToolbarStore with Store { final String instanceHost; diff --git a/lib/widgets/failed_to_load.dart b/lib/widgets/failed_to_load.dart index 93430829..10901231 100644 --- a/lib/widgets/failed_to_load.dart +++ b/lib/widgets/failed_to_load.dart @@ -6,7 +6,7 @@ class FailedToLoad extends StatelessWidget { final String message; final VoidCallback refresh; - const FailedToLoad({required this.refresh, required this.message}); + const FailedToLoad({super.key, required this.refresh, required this.message}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart index 578506a2..6469752c 100644 --- a/lib/widgets/infinite_scroll.dart +++ b/lib/widgets/infinite_scroll.dart @@ -1,7 +1,6 @@ -import 'dart:collection'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'bottom_safe.dart'; import 'pull_to_refresh.dart'; @@ -52,7 +51,15 @@ class InfiniteScroll extends HookWidget { /// duplicates thus perfoming deduplication final Object Function(T item)? uniqueProp; + /// If true, all content will be discarded and refetched when value + /// of [fetcher] changes. + /// + /// NOTE: [fetcher] MUST be memoized if this is set to true. + /// Otherwise, it will cause an infinite loop. + final bool refreshOnFetcherUpdate; + const InfiniteScroll({ + super.key, this.batchSize = 10, this.leading = const SizedBox.shrink(), this.padding, @@ -62,97 +69,76 @@ class InfiniteScroll extends HookWidget { required this.fetcher, this.controller, this.noItems = const SizedBox.shrink(), + this.refreshOnFetcherUpdate = false, this.uniqueProp, }) : assert(batchSize > 0); @override Widget build(BuildContext context) { - final data = useState>([]); - // holds unique props of the data - final dataSet = useRef(HashSet()); - final hasMore = useRef(true); - final page = useRef(1); - final isFetching = useRef(false); - - final uniquePropFunc = uniqueProp ?? (e) => e as Object; + final pagingController = + useMemoized(() => PagingController(firstPageKey: 1), []); useEffect(() { - if (controller != null) { - controller?.clear = () { - data.value = []; - hasMore.value = true; - page.value = 1; - dataSet.value.clear(); - }; - controller?.scrollToTop = () { - // to be implemented - }; - } - + controller?.clear = pagingController.refresh; + controller?.scrollToTop = () => PrimaryScrollController.of(context) + .animateTo(0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut); return null; }, []); + // Need to memoize the callback so we get a single instance + // that we can add/remove from the controller. + final pageRequestListener = useCallback((pageKey) async { + try { + final items = await fetcher(pageKey, batchSize); + // TODO: check if deduplication is needed + // final uniqueItems = + // uniqueProp == null ? items : LinkedHashSet.from(items).toList(); + final isLastPage = items.length < batchSize; + if (isLastPage) { + pagingController.appendLastPage(items); + } else { + pagingController.appendPage(items, pageKey + 1); + } + } catch (error) { + pagingController.error = error; + } + }, [fetcher]); + + // Because of the way closures and bindings work in dart, a lambda + // function we create here will always call the instance of `fetcher` + // it was created with, even if the `fetcher` variable changes. + // + // As such, we have to remove the listener and add a new one every + // time `fetcher` changes. The `useCallback` hook above will ensure + // we get a new `pageRequstListener` every time `fetcher` changes. + useEffect(() { + pagingController.addPageRequestListener(pageRequestListener); + return () { + pagingController.removePageRequestListener(pageRequestListener); + if (this.refreshOnFetcherUpdate) { + pagingController.refresh(); + } + }; + }, [pageRequestListener]); + return PullToRefresh( - onRefresh: () async { - data.value = []; - hasMore.value = true; - page.value = 1; - dataSet.value.clear(); - - await Future.delayed(const Duration(seconds: 1)); - }, - child: ListView.builder( - padding: padding, - // +2 for the loading widget and leading widget - itemCount: data.value.length + 2, - itemBuilder: (_, i) { - if (i == 0) { - return leading; - } - i -= 1; - - // if we are done but we have no data it means the list is empty - if (!hasMore.value && data.value.isEmpty) { - return Center(child: noItems); - } - - // reached the bottom, fetch more - if (i == data.value.length) { - // if there are no more, skip - if (!hasMore.value) { - return const BottomSafe(); - } - - // if it's already fetching more, skip - if (!isFetching.value) { - isFetching.value = true; - fetcher(page.value, batchSize).then((incoming) { - // if got less than the batchSize, mark the list as done - if (incoming.length < batchSize) { - hasMore.value = false; - } - - final newData = incoming.where( - (e) => !dataSet.value.contains(uniquePropFunc(e)), - ); - - // append new data - data.value = [...data.value, ...newData]; - dataSet.value.addAll(newData.map(uniquePropFunc)); - page.value += 1; - }).whenComplete(() => isFetching.value = false); - } - - return SafeArea( - top: false, - child: loadingWidget, - ); - } - - // not last element, render list item - return itemBuilder(data.value[i]); + onRefresh: () async { + pagingController.refresh(); }, - ), - ); + child: CustomScrollView(slivers: [ + SliverToBoxAdapter(child: leading), + PagedSliverList( + pagingController: pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => itemBuilder(item), + noItemsFoundIndicatorBuilder: (context) => Center(child: noItems), + noMoreItemsIndicatorBuilder: (context) => const BottomSafe(), + firstPageProgressIndicatorBuilder: (context) => loadingWidget, + newPageProgressIndicatorBuilder: (context) => loadingWidget, + ), + ) + ])); } } diff --git a/lib/widgets/markdown_text.dart b/lib/widgets/markdown_text.dart index 0b287dd5..99d3bfbc 100644 --- a/lib/widgets/markdown_text.dart +++ b/lib/widgets/markdown_text.dart @@ -44,7 +44,8 @@ class MarkdownText extends StatelessWidget { final double fontSize; const MarkdownText(this.text, - {required this.instanceHost, + {super.key, + required this.instanceHost, this.selectable = false, this.fontSize = 15}); diff --git a/lib/widgets/nsfw_hider.dart b/lib/widgets/nsfw_hider.dart index 0c7f59f4..2c6dbdfe 100644 --- a/lib/widgets/nsfw_hider.dart +++ b/lib/widgets/nsfw_hider.dart @@ -9,6 +9,7 @@ class NSFWHider extends HookWidget { final Widget child; const NSFWHider({ + super.key, required this.child, }); diff --git a/lib/widgets/post/post.dart b/lib/widgets/post/post.dart index a22ed20b..56cadd73 100644 --- a/lib/widgets/post/post.dart +++ b/lib/widgets/post/post.dart @@ -29,8 +29,9 @@ class PostTile extends StatelessWidget { static const double rounding = 10; - const PostTile.fromPostStore(this.postStore, {this.fullPost = true}); - PostTile.fromPostView(PostView post, {this.fullPost = false}) + const PostTile.fromPostStore(this.postStore, + {super.key, required this.fullPost}); + PostTile.fromPostView(PostView post, {super.key, this.fullPost = false}) : postStore = PostStore(post); @override diff --git a/lib/widgets/post/post_actions.dart b/lib/widgets/post/post_actions.dart index ef685b4c..32b77925 100644 --- a/lib/widgets/post/post_actions.dart +++ b/lib/widgets/post/post_actions.dart @@ -13,7 +13,7 @@ import 'post_voting.dart'; import 'save_post_button.dart'; class PostActions extends HookWidget { - const PostActions(); + const PostActions({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post/post_body.dart b/lib/widgets/post/post_body.dart index 57fe497d..0107a38e 100644 --- a/lib/widgets/post/post_body.dart +++ b/lib/widgets/post/post_body.dart @@ -7,7 +7,7 @@ import 'post_status.dart'; import 'post_store.dart'; class PostBody extends StatelessWidget { - const PostBody(); + const PostBody({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post/post_info_section.dart b/lib/widgets/post/post_info_section.dart index 7fb2cee7..7d140e24 100644 --- a/lib/widgets/post/post_info_section.dart +++ b/lib/widgets/post/post_info_section.dart @@ -13,7 +13,7 @@ import '../../util/observer_consumers.dart'; import 'post_store.dart'; class PostInfoSection extends HookWidget { - const PostInfoSection(); + const PostInfoSection({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post/post_link_preview.dart b/lib/widgets/post/post_link_preview.dart index ea0f7691..1b4fbbce 100644 --- a/lib/widgets/post/post_link_preview.dart +++ b/lib/widgets/post/post_link_preview.dart @@ -5,7 +5,7 @@ import '../../util/observer_consumers.dart'; import 'post_store.dart'; class PostLinkPreview extends StatelessWidget { - const PostLinkPreview(); + const PostLinkPreview({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post/post_media.dart b/lib/widgets/post/post_media.dart index 6fe01dd9..ab1ad594 100644 --- a/lib/widgets/post/post_media.dart +++ b/lib/widgets/post/post_media.dart @@ -16,7 +16,7 @@ final _logger = Logger('postMedia'); /// assembles image class PostMedia extends HookWidget { - const PostMedia(); + const PostMedia({super.key}); static const List providers = [ RedgifProvider(), MP4MediaProvider() @@ -44,7 +44,7 @@ class PostMedia extends HookWidget { future: provider.getMediaUrl(url), builder: (context, snapshot) { if (snapshot.hasData) { - return video.PostVideo(snapshot.data!); + return video.PostVideo(url: snapshot.data!); } else if (snapshot.hasError) { return const Text('Unable to fetch video'); } else { diff --git a/lib/widgets/post/post_more_menu.dart b/lib/widgets/post/post_more_menu.dart index b80964ca..7eabb4cf 100644 --- a/lib/widgets/post/post_more_menu.dart +++ b/lib/widgets/post/post_more_menu.dart @@ -18,7 +18,7 @@ import '../report_dialog.dart'; import 'post_store.dart'; class PostMoreMenuButton extends StatelessWidget { - const PostMoreMenuButton(); + const PostMoreMenuButton({super.key}); static void show({ required BuildContext context, @@ -52,6 +52,7 @@ class PostMoreMenu extends HookWidget { final PostStore postStore; final FullPostStore? fullPostStore; const PostMoreMenu({ + super.key, required this.postStore, required this.fullPostStore, }); @@ -169,8 +170,9 @@ class PostMoreMenu extends HookWidget { link: 'https://translate.google.com/?tl=$targetLanguage&text=$sourceText', context: context); - - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, ), ListTile( diff --git a/lib/widgets/post/post_store.dart b/lib/widgets/post/post_store.dart index b3879c10..eba52637 100644 --- a/lib/widgets/post/post_store.dart +++ b/lib/widgets/post/post_store.dart @@ -7,7 +7,9 @@ import '../../util/cleanup_url.dart'; part 'post_store.g.dart'; -class PostStore = _PostStore with _$PostStore; +class PostStore extends _PostStore with _$PostStore { + PostStore(super.postView); +} abstract class _PostStore with Store { _PostStore(this.postView); @@ -148,3 +150,8 @@ abstract class _PostStore with Store { void addComment(CommentView commentView) => newComments.insert(0, commentView); } + +extension PostStoreBuilder on Future> { + Future> toPostStores() => + then((value) => value.map(PostStore.new).toList()); +} diff --git a/lib/widgets/post/post_thumbnail.dart b/lib/widgets/post/post_thumbnail.dart index 4dd12285..737fbfc9 100644 --- a/lib/widgets/post/post_thumbnail.dart +++ b/lib/widgets/post/post_thumbnail.dart @@ -10,7 +10,7 @@ import '../fullscreenable_image.dart'; import 'post_store.dart'; class PostThumbnail extends HookWidget { - const PostThumbnail(); + const PostThumbnail({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post/post_title.dart b/lib/widgets/post/post_title.dart index 897824a6..fab32743 100644 --- a/lib/widgets/post/post_title.dart +++ b/lib/widgets/post/post_title.dart @@ -7,7 +7,7 @@ import '../../util/observer_consumers.dart'; import 'post_store.dart'; class PostTitle extends HookWidget { - const PostTitle(); + const PostTitle({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post/post_video.dart b/lib/widgets/post/post_video.dart index fce1450e..1ff7d48e 100644 --- a/lib/widgets/post/post_video.dart +++ b/lib/widgets/post/post_video.dart @@ -3,21 +3,25 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; +import '../../stores/config_store.dart'; +import '../../util/observer_consumers.dart'; + //TODO Support for full screen video class PostVideo extends StatefulWidget { final Uri url; - const PostVideo(this.url, {super.key}); + const PostVideo({super.key, required this.url}); @override - State createState() => _PostVideoState(url); + State createState() => _PostVideoState(); } class _PostVideoState extends State { late VideoPlayerController _controller; late Future _initializeVideoPlayerFuture; - final Uri url; - _PostVideoState(this.url); + late Uri url = widget.url; + bool? isPlaying; + bool? isMute; @override void initState() { @@ -28,10 +32,7 @@ class _PostVideoState extends State { Platform.isAndroid ? 'ExoPlayer' : 'Liftoff/1.0' }); - _controller - ..play() - ..setLooping(true) - ..setVolume(0); + _controller.setLooping(true); _initializeVideoPlayerFuture = _controller.initialize(); } @@ -42,39 +43,35 @@ class _PostVideoState extends State { super.dispose(); } - void togglePlay() { - if (_controller.value.isPlaying) { - _controller.pause(); - } else { - _controller.play(); - } - } + @override + Widget build(BuildContext context) { + isMute ??= context.read().autoMuteVideo; + isPlaying ??= context.read().autoPlayVideo; - void toggleMute() { - if (_controller.value.volume == 0) { - _controller.setVolume(1); + if (isPlaying!) { + _controller.play(); } else { - _controller.setVolume(0); + _controller.pause(); } - } - @override - Widget build(BuildContext context) { + _controller.setVolume(isMute! ? 0 : 1); + return Column(children: [ ButtonBar(children: [ IconButton( onPressed: () { - setState(togglePlay); + setState(() { + isPlaying = !isPlaying!; + }); }, - icon: Icon( - _controller.value.isPlaying ? Icons.pause : Icons.play_arrow)), + icon: Icon(isPlaying! ? Icons.pause : Icons.play_arrow)), IconButton( onPressed: () { - setState(toggleMute); + setState(() { + isMute = !isMute!; + }); }, - icon: Icon(_controller.value.volume == 0.0 - ? Icons.volume_off - : Icons.volume_up)) + icon: Icon(isMute! ? Icons.volume_off : Icons.volume_up)) ]), FutureBuilder( future: _initializeVideoPlayerFuture, @@ -82,7 +79,9 @@ class _PostVideoState extends State { if (snapshot.connectionState == ConnectionState.done) { return GestureDetector( onTap: () { - setState(togglePlay); + setState(() { + isPlaying = !isPlaying!; + }); }, child: AspectRatio( aspectRatio: _controller.value.aspectRatio, diff --git a/lib/widgets/post/post_voting.dart b/lib/widgets/post/post_voting.dart index c503b6cd..bf970ae7 100644 --- a/lib/widgets/post/post_voting.dart +++ b/lib/widgets/post/post_voting.dart @@ -11,7 +11,7 @@ import '../tile_toggle.dart'; import 'post_store.dart'; class PostVoting extends HookWidget { - const PostVoting(); + const PostVoting({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post/save_post_button.dart b/lib/widgets/post/save_post_button.dart index ce6fd923..a21cb9a0 100644 --- a/lib/widgets/post/save_post_button.dart +++ b/lib/widgets/post/save_post_button.dart @@ -6,7 +6,7 @@ import '../../util/observer_consumers.dart'; import 'post_store.dart'; class SavePostButton extends HookWidget { - const SavePostButton(); + const SavePostButton({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/post_list_options.dart b/lib/widgets/post_list_options.dart index 7ae5bb8d..148a36ce 100644 --- a/lib/widgets/post_list_options.dart +++ b/lib/widgets/post_list_options.dart @@ -12,6 +12,7 @@ class PostListOptions extends StatelessWidget { final bool styleButton; const PostListOptions({ + super.key, required this.onSortChanged, required this.sortValue, this.styleButton = true, diff --git a/lib/widgets/report_dialog.dart b/lib/widgets/report_dialog.dart index 697e5e0d..c7f6e328 100644 --- a/lib/widgets/report_dialog.dart +++ b/lib/widgets/report_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; class ReportDialog extends HookWidget { - const ReportDialog(); + const ReportDialog({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/reveal_after_scroll.dart b/lib/widgets/reveal_after_scroll.dart index 6d2c371b..190229fe 100644 --- a/lib/widgets/reveal_after_scroll.dart +++ b/lib/widgets/reveal_after_scroll.dart @@ -14,6 +14,7 @@ class RevealAfterScroll extends HookWidget { final bool fade; const RevealAfterScroll({ + super.key, required this.scrollController, required this.child, required this.after, diff --git a/lib/widgets/sortable_infinite_list.dart b/lib/widgets/sortable_infinite_list.dart index 053a4843..eb701724 100644 --- a/lib/widgets/sortable_infinite_list.dart +++ b/lib/widgets/sortable_infinite_list.dart @@ -12,6 +12,7 @@ import 'comment/comment.dart'; import 'comment_list_options.dart'; import 'infinite_scroll.dart'; import 'post/post.dart'; +import 'post/post_store.dart'; import 'post_list_options.dart'; typedef FetcherWithSorting = Future> Function( @@ -24,18 +25,21 @@ class SortableInfiniteList extends HookWidget { final InfiniteScrollController? controller; final Function? onStyleChange; final Widget noItems; + final bool refreshOnFetcherUpdate; /// if no defaultSort is provided, the defaultSortType /// from the configStore will be used final SortType? defaultSort; final Object Function(T item)? uniqueProp; const SortableInfiniteList({ + super.key, required this.fetcher, required this.itemBuilder, this.controller, this.onStyleChange, this.noItems = const SizedBox.shrink(), this.defaultSort, + this.refreshOnFetcherUpdate = false, this.uniqueProp, }); @@ -64,6 +68,7 @@ class SortableInfiniteList extends HookWidget { controller: isc, batchSize: 20, noItems: noItems, + refreshOnFetcherUpdate: refreshOnFetcherUpdate, uniqueProp: uniqueProp, ); } @@ -85,6 +90,7 @@ class SortableInfiniteCommentList extends HookWidget { final dynamic defaultSort; final Object Function(T item)? uniqueProp; const SortableInfiniteCommentList({ + super.key, required this.fetcher, required this.itemBuilder, this.controller, @@ -123,15 +129,17 @@ class SortableInfiniteCommentList extends HookWidget { } } -class InfinitePostList extends SortableInfiniteList { +class InfinitePostList extends SortableInfiniteList { InfinitePostList({ + super.key, required super.fetcher, super.controller, + super.refreshOnFetcherUpdate = false, }) : super( itemBuilder: (post) => Consumer( builder: (context, state, child) => Column( children: [ - PostTile.fromPostView(post), + PostTile.fromPostStore(post, fullPost: false), if (state.useAmoled) SizedBox( width: 250, @@ -156,12 +164,13 @@ class InfinitePostList extends SortableInfiniteList { ], )), noItems: const Text('there are no posts'), - uniqueProp: (item) => item.post.apId, + uniqueProp: (item) => item.postView.post.apId, ); } class InfiniteCommentList extends SortableInfiniteCommentList { InfiniteCommentList({ + super.key, required super.fetcher, super.controller, }) : super( diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index e5e35cf5..210f329c 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -15,6 +15,7 @@ import 'avatar.dart'; import 'cached_network_image.dart'; import 'fullscreenable_image.dart'; import 'markdown_text.dart'; +import 'post/post_store.dart'; import 'sortable_infinite_list.dart'; /// Shared widget of UserPage and ProfileTab @@ -24,10 +25,11 @@ class UserProfile extends HookWidget { final FullPersonView? _fullUserView; - const UserProfile({required this.userId, required this.instanceHost}) + const UserProfile( + {super.key, required this.userId, required this.instanceHost}) : _fullUserView = null; - UserProfile.fromFullPersonView(FullPersonView this._fullUserView) + UserProfile.fromFullPersonView(FullPersonView this._fullUserView, {super.key}) : userId = _fullUserView.personView.person.id, instanceHost = _fullUserView.instanceHost; @@ -108,7 +110,8 @@ class UserProfile extends HookWidget { ?.jwt .raw, )) - .then((val) => val.posts), + .then((val) => val.posts) + .toPostStores(), ), Center( child: ConstrainedBox( diff --git a/lib/widgets/write_comment.dart b/lib/widgets/write_comment.dart index df725141..9e0ec402 100644 --- a/lib/widgets/write_comment.dart +++ b/lib/widgets/write_comment.dart @@ -19,14 +19,16 @@ class WriteComment extends HookWidget { final Comment? comment; final bool _isEdit; - const WriteComment.toPost(this.post) + const WriteComment.toPost(this.post, {super.key}) : comment = null, _isEdit = false; const WriteComment.toComment({ + super.key, required Comment this.comment, required this.post, }) : _isEdit = false; const WriteComment.edit({ + super.key, required Comment this.comment, required this.post, }) : _isEdit = true; @@ -86,7 +88,9 @@ class WriteComment extends HookWidget { )); } }(); - Navigator.of(context).pop(res.commentView); + if (context.mounted) { + Navigator.of(context).pop(res.commentView); + } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to post comment'))); diff --git a/pubspec.lock b/pubspec.lock index fb3ef19e..4b6b91f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -430,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + url: "https://pub.dev" + source: hosted + version: "2.0.2" flutter_localizations: dependency: "direct main" description: flutter @@ -677,6 +685,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + infinite_scroll_pagination: + dependency: "direct main" + description: + name: infinite_scroll_pagination + sha256: "9517328f4e373f08f57dbb11c5aac5b05554142024d6b60c903f3b73476d52db" + url: "https://pub.dev" + source: hosted + version: "3.2.0" intl: dependency: "direct main" description: @@ -750,6 +766,14 @@ packages: url: "https://github.com/liftoff-app/lemmy_api_client.git" source: git version: "0.21.0" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" list_diff: dependency: transitive description: @@ -1163,6 +1187,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: ccdc502098a8bfa07b3ec582c282620031481300035584e1bb3aca296a505e8c + url: "https://pub.dev" + source: hosted + version: "0.2.10" source_gen: dependency: transitive description: @@ -1356,7 +1388,7 @@ packages: source: hosted version: "3.0.6" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" diff --git a/pubspec.yaml b/pubspec.yaml index 8d5b9953..0cab343b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: flutter_typeahead: ^4.0.0 modal_bottom_sheet: ^3.0.0-pre swipeable_page_route: ^0.4.0 + infinite_scroll_pagination: ^3.2.0 # native share_plus: ^7.0.2 @@ -65,6 +66,7 @@ dependencies: logging: ^1.0.1 nested: ^1.0.0 l10n_esperanto: ^2.0.5 + uuid: ^3.0.7 flutter: sdk: flutter @@ -96,6 +98,7 @@ dev_dependencies: build_runner: ^2.1.2 mobx_codegen: ^2.0.2 freezed: ^2.0.2 + flutter_lints: ^2.0.2 # To refresh: `pub run flutter_launcher_icons` flutter_launcher_icons: