From 139c9a0cd3ff1db889de5acb2d1ac7ef91e59902 Mon Sep 17 00:00:00 2001 From: Jagandeep Brar Date: Thu, 1 Apr 2021 22:06:07 -0500 Subject: [PATCH] (ui): Removed all deprecated UI elements, added scroll-back support to entire UI, clean-up codebase (#396) * Added RadarrManualImportConfigure for manual import config options * Default bottom modal sheet to not expand * Shrinkwrap modal listviews around content * use keyboardDismissBehavior for all ListView * Ensure keyboard is unfocused when opening the drawer * Fetch and store quality definitions in global Radarr state * Ability to configure language and quality profile for manual import instances * Reworking all search bar implementations, integrated scroll back for Lidarr across module * (ui): [Dashboard] Clean up of dashboard code, integrated scroll to top support * (ui): [SnackBar] Remove all deprecated uses of LSSnackBar, add scrollback for NZBGet, improve error handling in queue * (ui): Removed usage of old StickyListView, ReorderableListView, integrated LunaScaffold into base routes * (flutter): Update packages * (flutter): Remove flutter_sticky_header package * (ui): Remove unused deprecated classes * (chore): Update changelog * (fix): [Dashboard/Calendar] Radarr releases were not considering digital release date * (ui): [Sonarr] Adapted for scroll back to top functionality, remove more deprecated UI elements * (ui): Refactored Tautulli, Removed ALL deprecated UI elements from LunaSea * (ui): Fixed inconsistent usage of material and luna colours across UI * (fix): [Sonarr/Radarr] Do not show "Search For..." button when the query is empty, but there are no results * (chore): Updated changelog * (fix): [Sonarr/Add] Fix search queries not gettting executed * (fix): [Tautulli] Remove unused global state elements * (feat): [UI/AppBar] Ability to hide the leading widget * (fix): [Radarr/Languages] Fix "Unknown" language not being shown in selection dialog * (feat): [Radarr/Import] Ability to search movies to select, toggle real and proper toggles * (chore): Updated packages --- CHANGELOG.md | 25 +- assets/images/services/bandsintown.png | Bin 20427 -> 12766 bytes assets/images/services/discogs.png | Bin 22435 -> 30149 bytes assets/images/services/lastfm.png | Bin 18639 -> 19341 bytes assets/localization/en.json | 7 +- lib/core/constants.dart | 20 - lib/core/dialogs.dart | 32 +- lib/core/extensions/double.dart | 2 +- lib/core/extensions/duration.dart | 2 +- lib/core/system/modules.dart | 2 +- lib/core/system/router.dart | 2 +- lib/core/ui.dart | 6 +- lib/core/ui/appbar.dart | 31 +- lib/core/ui/bottom_bar/action_bar.dart | 5 +- lib/core/ui/bottom_bar/navigation_bar.dart | 2 +- lib/core/ui/bottom_modal_sheet.dart | 2 +- lib/core/ui/deprecated.dart | 16 - lib/core/ui/deprecated/card.dart | 2 - lib/core/ui/deprecated/card/card.dart | 43 -- lib/core/ui/deprecated/card/card_tile.dart | 71 --- lib/core/ui/deprecated/containers.dart | 1 - lib/core/ui/deprecated/containers/row.dart | 32 - .../floating_action_button.dart | 28 - .../floating_action_button_animated.dart | 34 - .../floating_action_button_extended.dart | 41 -- lib/core/ui/deprecated/icon.dart | 1 - lib/core/ui/deprecated/icon/icon_button.dart | 51 -- lib/core/ui/deprecated/input_bar.dart | 119 ---- lib/core/ui/deprecated/listview.dart | 4 - lib/core/ui/deprecated/listview/listview.dart | 27 - .../deprecated/listview/listview_builder.dart | 32 - .../listview/listview_reorderable.dart | 594 ------------------ .../listview/listview_stickyheader.dart | 31 - lib/core/ui/deprecated/loader.dart | 26 - lib/core/ui/deprecated/messages.dart | 3 - .../ui/deprecated/messages/error_message.dart | 25 - lib/core/ui/deprecated/messages/generic.dart | 64 -- .../ui/deprecated/messages/not_enabled.dart | 21 - lib/core/ui/deprecated/navigation_bar.dart | 1 - .../navigation_bar/navigation_bar.dart | 59 -- lib/core/ui/deprecated/network_image.dart | 42 -- lib/core/ui/deprecated/refresh_indicator.dart | 28 - lib/core/ui/deprecated/snackbar.dart | 90 --- lib/core/ui/deprecated/table.dart | 2 - lib/core/ui/deprecated/table/block.dart | 35 -- lib/core/ui/deprecated/table/content.dart | 60 -- lib/core/ui/deprecated/text.dart | 4 - lib/core/ui/deprecated/text/header.dart | 55 -- .../ui/deprecated/text/text_highlighted.dart | 40 -- .../ui/deprecated/text/text_subtitle.dart | 36 -- lib/core/ui/deprecated/text/text_title.dart | 34 - .../{deprecated/dialogs.dart => dialog.dart} | 40 +- lib/core/ui/drawer/drawer.dart | 6 +- .../floating_action_button.dart | 1 - .../floating_action_button_animated.dart | 35 ++ .../floating_action_button_extended.dart | 40 ++ lib/core/ui/input_bar.dart | 16 +- lib/core/ui/list_view.dart | 3 + lib/core/ui/list_view/list_view.dart | 24 +- lib/core/ui/list_view/list_view_builder.dart | 26 +- lib/core/ui/list_view/list_view_modal.dart | 14 +- .../ui/list_view/list_view_modal_builder.dart | 42 ++ lib/core/ui/list_view/paged_list_view.dart | 77 ++- .../ui/list_view/reorderable_list_view.dart | 33 + .../reorderable_list_view_builder.dart | 39 ++ lib/core/ui/scaffold.dart | 52 ++ lib/core/ui/sliver_sticky_header.dart | 17 - lib/core/ui/snackbar/snackbar.dart | 8 +- lib/core/ui/snackbar/snackbar_error.dart | 2 - lib/core/ui/snackbar/snackbar_info.dart | 2 - lib/core/ui/snackbar/snackbar_success.dart | 2 - lib/core/ui/will_pop_scope.dart | 18 + lib/core/utilities/changelog.dart | 1 + lib/modules/dashboard.dart | 1 - lib/modules/dashboard/core.dart | 1 + .../core/adapters/calendar_starting_type.dart | 10 +- lib/modules/dashboard/core/api/api.dart | 192 +++--- lib/modules/dashboard/core/api/data.dart | 1 + .../dashboard/core/api/data/lidarr.dart | 10 +- .../dashboard/core/api/data/radarr.dart | 46 +- .../dashboard/core/api/data/sonarr.dart | 15 +- lib/modules/dashboard/core/dialogs.dart | 34 +- lib/modules/dashboard/core/router.dart | 33 + lib/modules/dashboard/core/state.dart | 39 +- lib/modules/dashboard/routes.dart | 4 +- lib/modules/dashboard/routes/calendar.dart | 71 +-- .../dashboard/routes/calendar/route.dart | 46 ++ .../dashboard/routes/calendar/widgets.dart | 1 + .../routes/calendar/widgets/calendar.dart | 229 +++++++ lib/modules/dashboard/routes/dashboard.dart | 2 + .../dashboard/routes/dashboard/route.dart | 65 ++ .../dashboard/routes/dashboard/widgets.dart | 2 + .../widgets/appbar_switch_view_action.dart | 20 + .../dashboard/widgets/navigation_bar.dart | 32 + lib/modules/dashboard/routes/home.dart | 105 ---- lib/modules/dashboard/routes/modules.dart | 1 + .../dashboard/routes/modules/route.dart | 74 +++ .../dashboard/routes/quick_access.dart | 64 -- lib/modules/dashboard/widgets.dart | 3 - lib/modules/dashboard/widgets/calendar.dart | 195 ------ .../dashboard/widgets/navigation_bar.dart | 51 -- .../dashboard/widgets/summary_tile.dart | 37 -- lib/modules/lidarr/core/api/api.dart | 9 + .../lidarr/core/api/data/catalogue.dart | 2 +- lib/modules/lidarr/core/api/data/history.dart | 40 +- lib/modules/lidarr/core/api/data/track.dart | 2 +- lib/modules/lidarr/core/constants.dart | 1 + lib/modules/lidarr/core/dialogs.dart | 66 +- lib/modules/lidarr/core/state.dart | 98 ++- lib/modules/lidarr/core/state/global.dart | 112 ---- lib/modules/lidarr/core/state/local.dart | 0 lib/modules/lidarr/routes/add_details.dart | 73 ++- lib/modules/lidarr/routes/add_search.dart | 7 +- lib/modules/lidarr/routes/catalogue.dart | 160 +++-- lib/modules/lidarr/routes/details_album.dart | 37 +- lib/modules/lidarr/routes/details_artist.dart | 14 +- lib/modules/lidarr/routes/edit_artist.dart | 63 +- lib/modules/lidarr/routes/history.dart | 53 +- lib/modules/lidarr/routes/lidarr.dart | 149 +++-- lib/modules/lidarr/routes/missing.dart | 17 +- lib/modules/lidarr/routes/search_results.dart | 146 ++--- lib/modules/lidarr/widgets.dart | 1 + .../lidarr/widgets/add_search_bar.dart | 15 +- .../widgets/add_search_result_tile.dart | 14 +- .../lidarr/widgets/artist_navigation_bar.dart | 49 +- .../lidarr/widgets/catalogue_hide_button.dart | 6 +- .../lidarr/widgets/catalogue_search_bar.dart | 50 +- .../widgets/catalogue_sorting_button.dart | 6 +- .../lidarr/widgets/catalogue_tile.dart | 43 +- .../lidarr/widgets}/description_block.dart | 19 +- .../lidarr/widgets/details_album_list.dart | 14 +- .../lidarr/widgets/details_album_tile.dart | 19 +- .../widgets/details_album_track_tile.dart | 11 +- .../lidarr/widgets/details_edit_button.dart | 6 +- .../lidarr/widgets/details_hide_button.dart | 2 +- .../lidarr/widgets/details_overview.dart | 135 ++-- .../widgets/details_settings_button.dart | 14 +- lib/modules/lidarr/widgets/history_tile.dart | 18 +- lib/modules/lidarr/widgets/missing_tile.dart | 21 +- .../lidarr/widgets/navigation_bar.dart | 45 +- .../lidarr/widgets/releases_hide_button.dart | 4 +- .../lidarr/widgets/releases_search_bar.dart | 51 +- .../widgets/releases_sorting_button.dart | 6 +- .../lidarr/widgets/search_result_tile.dart | 3 +- lib/modules/nzbget/core/dialogs.dart | 128 ++-- lib/modules/nzbget/core/state.dart | 84 ++- lib/modules/nzbget/core/state/global.dart | 90 --- lib/modules/nzbget/core/state/local.dart | 0 lib/modules/nzbget/routes/history.dart | 98 ++- lib/modules/nzbget/routes/nzbget.dart | 149 ++--- lib/modules/nzbget/routes/queue.dart | 77 +-- lib/modules/nzbget/routes/statistics.dart | 157 +---- lib/modules/nzbget/widgets/app_bar_stats.dart | 24 +- .../nzbget/widgets/history_hide_button.dart | 11 +- .../nzbget/widgets/history_search_bar.dart | 48 +- lib/modules/nzbget/widgets/history_tile.dart | 31 +- lib/modules/nzbget/widgets/log_tile.dart | 9 +- .../nzbget/widgets/navigation_bar.dart | 49 +- lib/modules/nzbget/widgets/queue_fab.dart | 74 +-- lib/modules/nzbget/widgets/queue_tile.dart | 154 ++--- lib/modules/radarr/core.dart | 1 + lib/modules/radarr/core/api_helper.dart | 13 +- .../radarr/core/bottom_modal_sheets.dart | 174 +++++ lib/modules/radarr/core/dialogs.dart | 182 ++++-- .../core/extensions/radarr_event_type.dart | 2 +- lib/modules/radarr/core/state.dart | 15 + .../radarr/routes/add_movie/state.dart | 6 +- .../routes/add_movie/widgets/page_search.dart | 10 +- .../add_movie/widgets/search_search_bar.dart | 5 +- .../routes/add_movie_details/route.dart | 2 +- .../routes/add_movie_details/state.dart | 4 +- .../widgets/tile_minimum_availability.dart | 2 +- .../widgets/tile_quality_profile.dart | 2 +- .../widgets/tile_root_folder.dart | 2 +- .../radarr/routes/catalogue/route.dart | 3 +- .../routes/catalogue/widgets/search_bar.dart | 5 +- .../widgets/search_bar_filter_button.dart | 2 +- .../widgets/search_bar_sort_button.dart | 2 +- .../routes/edit_movie/widgets/tile_path.dart | 2 +- .../widgets/tile_quality_profile.dart | 2 +- .../radarr/routes/manual_import/route.dart | 4 +- .../widgets/appbar_path_bar.dart | 8 + .../routes/manual_import_details/route.dart | 28 +- .../routes/manual_import_details/widgets.dart | 1 + .../widgets/appbar_configure_movies.dart | 37 ++ .../widgets/import_tile.dart | 117 +++- .../widgets/overview_movie_links_section.dart | 8 +- lib/modules/radarr/routes/radarr/route.dart | 25 +- .../routes/releases/widgets/search_bar.dart | 1 + .../widgets/search_bar_filter_button.dart | 2 +- .../widgets/search_bar_sort_button.dart | 4 +- .../tags/widgets/appbar_action_add_tag.dart | 2 +- .../radarr/routes/tags/widgets/tag_tile.dart | 3 +- lib/modules/sabnzbd/core/dialogs.dart | 138 ++-- lib/modules/sabnzbd/routes/history.dart | 96 ++- .../sabnzbd/routes/history_stages.dart | 19 +- lib/modules/sabnzbd/routes/queue.dart | 77 +-- lib/modules/sabnzbd/routes/sabnzbd.dart | 161 +++-- lib/modules/sabnzbd/routes/statistics.dart | 155 ++--- .../sabnzbd/widgets/app_bar_stats.dart | 24 +- .../sabnzbd/widgets/history_hide_button.dart | 11 +- .../sabnzbd/widgets/history_search_bar.dart | 48 +- lib/modules/sabnzbd/widgets/history_tile.dart | 22 +- .../sabnzbd/widgets/navigation_bar.dart | 41 +- lib/modules/sabnzbd/widgets/queue_fab.dart | 68 +- lib/modules/sabnzbd/widgets/queue_tile.dart | 113 ++-- lib/modules/search/core/dialogs.dart | 18 +- lib/modules/search/routes/indexers/route.dart | 24 +- lib/modules/search/routes/search/route.dart | 1 + .../routes/search/widgets/search_bar.dart | 3 + lib/modules/settings/core/dialogs.dart | 338 +++++----- .../routes/account/pages/password_reset.dart | 12 +- .../routes/account/pages/signed_in.dart | 4 +- .../routes/account/pages/signed_out.dart | 16 +- .../routes/account/widgets/backup_tile.dart | 5 +- .../routes/account/widgets/delete_tile.dart | 11 +- .../routes/account/widgets/restore_tile.dart | 3 - .../pages/calendar_settings.dart | 8 +- .../pages/connection_details.dart | 4 - .../pages/connection_details.dart | 9 +- .../pages/connection_details.dart | 4 - .../pages/connection_details.dart | 4 - .../pages/add_indexer.dart | 1 - .../routes/configuration_search/route.dart | 33 +- .../pages/connection_details.dart | 4 - .../pages/connection_details.dart | 4 - .../settings/routes/donations/route.dart | 1 - lib/modules/settings/routes/settings.dart | 19 +- lib/modules/settings/routes/system/route.dart | 1 - .../routes/system/widgets/backup_tile.dart | 1 - .../routes/system/widgets/restore_tile.dart | 4 - .../settings/routes/system_logs/route.dart | 2 - lib/modules/sonarr/core/dialogs.dart | 190 +++--- .../core/extensions/sonarr_calendar.dart | 2 +- .../sonarr/core/extensions/sonarr_series.dart | 6 +- lib/modules/sonarr/core/state.dart | 21 - .../sonarr/core/types/sorting_series.dart | 4 +- lib/modules/sonarr/routes/add_series.dart | 1 + .../sonarr/routes/add_series/route.dart | 27 +- .../sonarr/routes/add_series/state.dart | 26 + .../routes/add_series/widgets/appbar.dart | 67 +- .../add_series/widgets/search_results.dart | 118 ++-- .../widgets/search_results_tile.dart | 60 +- .../routes/add_series_details/route.dart | 100 +-- .../routes/add_series_details/state.dart | 6 +- .../widgets/bottom_action_bar.dart | 3 +- .../widgets/tile_language_profile.dart | 23 +- .../widgets/tile_monitor_status.dart | 31 +- .../widgets/tile_monitored.dart | 25 +- .../widgets/tile_quality_profile.dart | 23 +- .../widgets/tile_root_folder.dart | 23 +- .../widgets/tile_series_type.dart | 23 +- .../add_series_details/widgets/tile_tags.dart | 2 +- .../widgets/tile_use_season_folders.dart | 7 +- .../widgets/button_update_series.dart | 4 +- .../widgets/tile_language_profile.dart | 19 +- .../edit_series/widgets/tile_monitored.dart | 19 +- .../widgets/tile_quality_profile.dart | 19 +- .../edit_series/widgets/tile_series_path.dart | 15 +- .../edit_series/widgets/tile_series_type.dart | 19 +- .../routes/edit_series/widgets/tile_tags.dart | 21 +- .../widgets/tile_use_season_folders.dart | 19 +- lib/modules/sonarr/routes/history/route.dart | 72 ++- .../routes/history/widgets/history_tile.dart | 37 +- lib/modules/sonarr/routes/missing/route.dart | 83 ++- .../routes/missing/widgets/missing_tile.dart | 30 +- lib/modules/sonarr/routes/queue/route.dart | 74 ++- .../routes/queue/widgets/queue_tile.dart | 18 +- lib/modules/sonarr/routes/releases/route.dart | 112 ++-- .../routes/releases/widgets/appbar.dart | 38 +- .../widgets/appbar_filter_button.dart | 4 +- .../releases/widgets/appbar_sort_button.dart | 6 +- .../routes/releases/widgets/tile_release.dart | 9 +- .../sonarr/routes/season_details/route.dart | 98 +-- .../season_details/widgets/all_seasons.dart | 11 +- .../routes/season_details/widgets/season.dart | 31 +- .../season_details/widgets/season_header.dart | 30 +- .../season_details/widgets/tile_episode.dart | 8 +- lib/modules/sonarr/routes/series/route.dart | 157 ++--- .../routes/series/widgets/search_bar.dart | 26 +- .../widgets/search_bar_filter_button.dart | 4 +- .../widgets/search_bar_sort_button.dart | 6 +- .../routes/series/widgets/series_tile.dart | 25 +- .../sonarr/routes/series_details/route.dart | 31 +- .../widgets/appbar_edit_action.dart | 4 +- .../widgets/appbar_settings_action.dart | 34 +- .../widgets/navigation_bar.dart | 48 +- .../series_details/widgets/overview.dart | 258 ++++---- .../widgets/season_all_tile.dart | 68 +- .../series_details/widgets/season_list.dart | 5 +- .../series_details/widgets/season_tile.dart | 54 +- lib/modules/sonarr/routes/sonarr/route.dart | 32 +- .../widgets/appbar_add_series_action.dart | 18 +- .../appbar_global_settings_action.dart | 42 +- .../routes/sonarr/widgets/navigation_bar.dart | 47 +- lib/modules/sonarr/routes/tags/route.dart | 91 +-- .../tags/widgets/appbar_action_add_tag.dart | 32 +- .../sonarr/routes/tags/widgets/tag_tile.dart | 104 ++- lib/modules/sonarr/routes/upcoming/route.dart | 73 +-- .../upcoming/widgets/upcoming_tile.dart | 11 +- lib/modules/tautulli/core/dialogs.dart | 54 +- .../extensions/tautulli_history_record.dart | 2 +- .../core/extensions/tautulli_session.dart | 1 - .../extensions/tautulli_table_library.dart | 6 +- .../tautulli/core/graphs/bar_graph.dart | 9 +- lib/modules/tautulli/core/graphs/graph.dart | 125 ++-- .../tautulli/core/graphs/line_graph.dart | 7 +- lib/modules/tautulli/core/state.dart | 163 ----- .../core/types/media_details_switcher.dart | 1 - .../tautulli/routes/activity/route.dart | 1 - .../activity/widgets/activity_tile.dart | 1 - .../routes/activity_details/route.dart | 1 - .../widgets/appbar_metadata_action.dart | 1 - .../widgets/appbar_user_action.dart | 1 - .../widgets/bottom_action_bar.dart | 1 - .../tautulli/routes/check_for_updates.dart | 1 + .../routes/check_for_updates/route.dart | 104 ++- .../routes/check_for_updates/state.dart | 36 ++ .../check_for_updates/widgets/pms_tile.dart | 95 +-- .../widgets/tautulli_tile.dart | 106 ++-- lib/modules/tautulli/routes/graphs/route.dart | 56 +- .../routes/graphs/widgets/navigation_bar.dart | 47 +- .../routes/graphs/widgets/type_button.dart | 3 +- .../routes/graphs_play_by_period/route.dart | 123 ++-- .../widgets/daily_play_count_graph.dart | 16 +- .../play_count_by_day_of_week_graph.dart | 20 +- .../play_count_by_top_platforms_graph.dart | 20 +- .../play_count_by_top_users_graph.dart | 20 +- .../widgets/plays_by_month_graph.dart | 20 +- .../graphs_stream_information/route.dart | 123 ++-- .../daily_stream_type_breakdown_graph.dart | 16 +- ...y_count_by_platform_stream_type_graph.dart | 20 +- ...play_count_by_source_resolution_graph.dart | 20 +- ...play_count_by_stream_resolution_graph.dart | 20 +- .../play_count_by_user_stream_type_graph.dart | 26 +- .../tautulli/routes/history/route.dart | 83 ++- .../routes/history/widgets/history_tile.dart | 54 +- .../routes/history_details/route.dart | 102 +-- .../history_details/widgets/information.dart | 82 +-- .../history_details/widgets/metadata.dart | 9 +- .../routes/history_details/widgets/user.dart | 3 +- .../tautulli/routes/ipaddress_details.dart | 1 + .../routes/ipaddress_details/route.dart | 97 +-- .../routes/ipaddress_details/state.dart | 37 ++ .../widgets/geolocation_tile.dart | 27 +- .../ipaddress_details/widgets/whois_tile.dart | 43 +- .../tautulli/routes/libraries/route.dart | 95 +-- .../libraries/widgets/library_tile.dart | 66 +- .../routes/libraries_details/route.dart | 44 +- .../widgets/information.dart | 79 ++- .../widgets/information_details.dart | 30 +- .../widgets/information_global_stats.dart | 21 +- .../widgets/navigation_bar.dart | 48 +- .../libraries_details/widgets/user_stats.dart | 64 +- .../widgets/user_stats_tile.dart | 123 ++-- lib/modules/tautulli/routes/logs.dart | 1 - lib/modules/tautulli/routes/logs/route.dart | 98 ++- lib/modules/tautulli/routes/logs/widgets.dart | 6 - .../routes/logs/widgets/logins_tile.dart | 18 - .../routes/logs/widgets/newsletters_tile.dart | 18 - .../logs/widgets/notifications_tile.dart | 18 - .../logs/widgets/plex_media_scanner.dart | 18 - .../logs/widgets/plex_media_server_tile.dart | 18 - .../routes/logs/widgets/tautulli_tile.dart | 18 - lib/modules/tautulli/routes/logs_logins.dart | 1 + .../tautulli/routes/logs_logins/route.dart | 92 ++- .../tautulli/routes/logs_logins/state.dart | 20 + .../routes/logs_logins/widgets/log_tile.dart | 87 +-- .../tautulli/routes/logs_newsletters.dart | 1 + .../routes/logs_newsletters/route.dart | 92 ++- .../routes/logs_newsletters/state.dart | 20 + .../logs_newsletters/widgets/log_tile.dart | 29 +- .../tautulli/routes/logs_notifications.dart | 1 + .../routes/logs_notifications/route.dart | 92 ++- .../routes/logs_notifications/state.dart | 20 + .../logs_notifications/widgets/log_tile.dart | 29 +- .../routes/logs_plex_media_scanner.dart | 1 + .../routes/logs_plex_media_scanner/route.dart | 93 ++- .../routes/logs_plex_media_scanner/state.dart | 21 + .../widgets/log_tile.dart | 2 +- .../routes/logs_plex_media_server.dart | 1 + .../routes/logs_plex_media_server/route.dart | 93 ++- .../routes/logs_plex_media_server/state.dart | 21 + .../widgets/log_tile.dart | 2 +- .../tautulli/routes/logs_tautulli.dart | 1 + .../tautulli/routes/logs_tautulli/route.dart | 92 ++- .../tautulli/routes/logs_tautulli/state.dart | 21 + .../logs_tautulli/widgets/log_tile.dart | 2 +- .../tautulli/routes/media_details/route.dart | 66 +- .../routes/media_details/widgets/history.dart | 68 +- .../media_details/widgets/metadata.dart | 57 +- .../widgets/metadata_header.dart | 60 +- .../widgets/metadata_metadata.dart | 93 +-- .../media_details/widgets/navigation_bar.dart | 48 +- .../widgets/switcher_buttons.dart | 1 - lib/modules/tautulli/routes/more.dart | 1 - lib/modules/tautulli/routes/more/route.dart | 99 ++- lib/modules/tautulli/routes/more/widgets.dart | 8 - .../more/widgets/check_for_updates_tile.dart | 18 - .../routes/more/widgets/graphs_tile.dart | 18 - .../routes/more/widgets/libraries_tile.dart | 18 - .../routes/more/widgets/logs_tile.dart | 18 - .../more/widgets/recently_added_tile.dart | 18 - .../routes/more/widgets/search_tile.dart | 18 - .../routes/more/widgets/statistics_tile.dart | 18 - .../more/widgets/synced_items_tile.dart | 18 - .../tautulli/routes/recently_added/route.dart | 88 +-- .../recently_added/widgets/content_tile.dart | 68 +- lib/modules/tautulli/routes/search/route.dart | 6 +- .../routes/search/widgets/search_appbar.dart | 45 +- .../search/widgets/search_result_tile.dart | 54 +- .../routes/search/widgets/search_results.dart | 125 ++-- .../tautulli/routes/statistics/route.dart | 113 ++-- .../routes/statistics/widgets/media_tile.dart | 56 +- .../statistics/widgets/platform_tile.dart | 14 +- .../widgets/recently_watched_tile.dart | 18 +- .../statistics/widgets/stream_tile.dart | 13 +- .../statistics/widgets/time_range_button.dart | 2 +- .../statistics/widgets/type_button.dart | 3 +- .../routes/statistics/widgets/user_tile.dart | 22 +- .../tautulli/routes/synced_items/route.dart | 97 +-- .../widgets/synced_item_tile.dart | 92 +-- .../tautulli/routes/tautulli/route.dart | 25 +- .../tautulli/widgets/navigation_bar.dart | 1 - lib/modules/tautulli/routes/users/route.dart | 74 ++- .../routes/users/widgets/user_tile.dart | 16 +- .../tautulli/routes/users_details/route.dart | 77 ++- .../routes/users_details/widgets/history.dart | 65 +- .../users_details/widgets/ip_addresses.dart | 149 +++-- .../users_details/widgets/navigation_bar.dart | 50 +- .../routes/users_details/widgets/profile.dart | 135 ++-- .../users_details/widgets/synced_items.dart | 76 ++- localization/lunasea/en.json | 2 + localization/radarr/en.json | 5 +- pubspec.lock | 78 +-- pubspec.yaml | 25 +- 436 files changed, 7712 insertions(+), 9441 deletions(-) delete mode 100644 lib/core/ui/deprecated/card.dart delete mode 100644 lib/core/ui/deprecated/card/card.dart delete mode 100644 lib/core/ui/deprecated/card/card_tile.dart delete mode 100644 lib/core/ui/deprecated/containers.dart delete mode 100644 lib/core/ui/deprecated/containers/row.dart delete mode 100644 lib/core/ui/deprecated/floating_action_button/floating_action_button.dart delete mode 100644 lib/core/ui/deprecated/floating_action_button/floating_action_button_animated.dart delete mode 100644 lib/core/ui/deprecated/floating_action_button/floating_action_button_extended.dart delete mode 100644 lib/core/ui/deprecated/icon.dart delete mode 100644 lib/core/ui/deprecated/icon/icon_button.dart delete mode 100644 lib/core/ui/deprecated/input_bar.dart delete mode 100644 lib/core/ui/deprecated/listview.dart delete mode 100644 lib/core/ui/deprecated/listview/listview.dart delete mode 100644 lib/core/ui/deprecated/listview/listview_builder.dart delete mode 100644 lib/core/ui/deprecated/listview/listview_reorderable.dart delete mode 100644 lib/core/ui/deprecated/listview/listview_stickyheader.dart delete mode 100644 lib/core/ui/deprecated/loader.dart delete mode 100644 lib/core/ui/deprecated/messages.dart delete mode 100644 lib/core/ui/deprecated/messages/error_message.dart delete mode 100644 lib/core/ui/deprecated/messages/generic.dart delete mode 100644 lib/core/ui/deprecated/messages/not_enabled.dart delete mode 100644 lib/core/ui/deprecated/navigation_bar.dart delete mode 100644 lib/core/ui/deprecated/navigation_bar/navigation_bar.dart delete mode 100644 lib/core/ui/deprecated/network_image.dart delete mode 100644 lib/core/ui/deprecated/refresh_indicator.dart delete mode 100644 lib/core/ui/deprecated/snackbar.dart delete mode 100644 lib/core/ui/deprecated/table.dart delete mode 100644 lib/core/ui/deprecated/table/block.dart delete mode 100644 lib/core/ui/deprecated/table/content.dart delete mode 100644 lib/core/ui/deprecated/text.dart delete mode 100644 lib/core/ui/deprecated/text/header.dart delete mode 100644 lib/core/ui/deprecated/text/text_highlighted.dart delete mode 100644 lib/core/ui/deprecated/text/text_subtitle.dart delete mode 100644 lib/core/ui/deprecated/text/text_title.dart rename lib/core/ui/{deprecated/dialogs.dart => dialog.dart} (87%) rename lib/core/ui/{deprecated => }/floating_action_button.dart (69%) create mode 100644 lib/core/ui/floating_action_button/floating_action_button_animated.dart create mode 100644 lib/core/ui/floating_action_button/floating_action_button_extended.dart create mode 100644 lib/core/ui/list_view/list_view_modal_builder.dart create mode 100644 lib/core/ui/list_view/reorderable_list_view.dart create mode 100644 lib/core/ui/list_view/reorderable_list_view_builder.dart create mode 100644 lib/core/ui/scaffold.dart delete mode 100644 lib/core/ui/sliver_sticky_header.dart create mode 100644 lib/core/ui/will_pop_scope.dart create mode 100644 lib/modules/dashboard/core/router.dart create mode 100644 lib/modules/dashboard/routes/calendar/route.dart create mode 100644 lib/modules/dashboard/routes/calendar/widgets.dart create mode 100644 lib/modules/dashboard/routes/calendar/widgets/calendar.dart create mode 100644 lib/modules/dashboard/routes/dashboard.dart create mode 100644 lib/modules/dashboard/routes/dashboard/route.dart create mode 100644 lib/modules/dashboard/routes/dashboard/widgets.dart create mode 100644 lib/modules/dashboard/routes/dashboard/widgets/appbar_switch_view_action.dart create mode 100644 lib/modules/dashboard/routes/dashboard/widgets/navigation_bar.dart delete mode 100644 lib/modules/dashboard/routes/home.dart create mode 100644 lib/modules/dashboard/routes/modules.dart create mode 100644 lib/modules/dashboard/routes/modules/route.dart delete mode 100644 lib/modules/dashboard/routes/quick_access.dart delete mode 100644 lib/modules/dashboard/widgets.dart delete mode 100644 lib/modules/dashboard/widgets/calendar.dart delete mode 100644 lib/modules/dashboard/widgets/navigation_bar.dart delete mode 100644 lib/modules/dashboard/widgets/summary_tile.dart delete mode 100644 lib/modules/lidarr/core/state/global.dart delete mode 100644 lib/modules/lidarr/core/state/local.dart rename lib/{core/ui/deprecated => modules/lidarr/widgets}/description_block.dart (81%) delete mode 100644 lib/modules/nzbget/core/state/global.dart delete mode 100644 lib/modules/nzbget/core/state/local.dart create mode 100644 lib/modules/radarr/core/bottom_modal_sheets.dart create mode 100644 lib/modules/radarr/routes/manual_import_details/widgets/appbar_configure_movies.dart create mode 100644 lib/modules/sonarr/routes/add_series/state.dart create mode 100644 lib/modules/tautulli/routes/check_for_updates/state.dart create mode 100644 lib/modules/tautulli/routes/ipaddress_details/state.dart delete mode 100644 lib/modules/tautulli/routes/logs/widgets.dart delete mode 100644 lib/modules/tautulli/routes/logs/widgets/logins_tile.dart delete mode 100644 lib/modules/tautulli/routes/logs/widgets/newsletters_tile.dart delete mode 100644 lib/modules/tautulli/routes/logs/widgets/notifications_tile.dart delete mode 100644 lib/modules/tautulli/routes/logs/widgets/plex_media_scanner.dart delete mode 100644 lib/modules/tautulli/routes/logs/widgets/plex_media_server_tile.dart delete mode 100644 lib/modules/tautulli/routes/logs/widgets/tautulli_tile.dart create mode 100644 lib/modules/tautulli/routes/logs_logins/state.dart create mode 100644 lib/modules/tautulli/routes/logs_newsletters/state.dart create mode 100644 lib/modules/tautulli/routes/logs_notifications/state.dart create mode 100644 lib/modules/tautulli/routes/logs_plex_media_scanner/state.dart create mode 100644 lib/modules/tautulli/routes/logs_plex_media_server/state.dart create mode 100644 lib/modules/tautulli/routes/logs_tautulli/state.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/check_for_updates_tile.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/graphs_tile.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/libraries_tile.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/logs_tile.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/recently_added_tile.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/search_tile.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/statistics_tile.dart delete mode 100644 lib/modules/tautulli/routes/more/widgets/synced_items_tile.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 1834e36f..e94b6e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,25 +3,42 @@ ## GitHub #### NEW +- `[Everything]` All lists now have scroll-back support by tapping the AppBar - `[Radarr/Catalogue]` Ability to sort by in cinema, physical release, and digital release dates - `[Radarr/Details]` Show the monitor state of the movie in the overview table -- `[Radarr/Manual Import]` Ability to manually import content from the filesystem (quick) -- `[Sonarr/Add]` View a series' TheTVDB page by long-pressing the tile +- `[Radarr/Manual Import]` Ability to manually import content from the filesystem +- `[Sonarr/Add]` View a series' TheTVDB page by long-pressing the tile in the search list - `[Tautulli/Webhooks]` Support for deep-linking to content through notifications #### TWEAKS +- `[Dashboard]` Removed month header from calendar +- `[Dashboard]` Disable calendar days outside of the past and future date range +- `[Lidarr/Artist]` Overview is now displayed as a table, to match Radarr and Sonarr +- `[NZBGet/Status]` Status and statistics are now shown in a table +- `[SABnzbd/Status]` Status and statistics are now shown in a table - `[UI/UX]` Minor tweaks to the colour palette -- `[UI/UX]` Normalized the design and size of all action buttons -- `[UI/UX]` Sticky primary button actions to the bottom navigation bar area +- `[UI/UX]` Unify the design and size of all action buttons +- `[UI/UX]` Sticky primary action buttons to the bottom navigation bar area - `[UI/UX]` Unified the design of all expandable cards in their expanded state +- `[UI/UX]` Bottom modal sheets lists will now shrink wrap to match content height +- `[UI/UX]` All ListViews will now dismiss the keyboard on scroll gestures +- `[UI/UX]` Opening the drawer will now ensure that the keyboard gets dismissed +- `[UI/UX]` Removed all deprecated UI elements and usages #### FIXES - `[Backup & Restore]` Indexers would not get restored with backups +- `[Dashboard]` Radarr releases were not considering the digital release date - `[Dialogs]` Checkbox tiles were padded too aggressively in the X-axis +- `[Errors]` All error snackbars will now have a button to view the error - `[Networking]` Headers would not get attached in some networking configurations +- `[NZBGet/Queue]` Queue would not refresh in some cases when switching profiles +- `[Radarr/Catalogue]` "Search For..." would be shown when there were no results and the query was empty - `[Radarr/Details]` The overview table would not update after editing a movie +- `[SABnzbd]` Queue would not refresh in some cases when switching profiles - `[Sonarr/Add]` Series type would not be set correctly when adding a new series - `[Tautulli/Activity]` Activity header included some incorrectly localized strings +- `[UI/UX]` Remove all uses of the deprecated snackbar +- `[UI/UX]` Some elements were using the system colours instead of the LunaSea colours - `[Webhooks]` Push the module's home route when receiving malformed data blocks from webhooks --- diff --git a/assets/images/services/bandsintown.png b/assets/images/services/bandsintown.png index 8466993bfb9084442b20543cb57f836a38d7ceb6..f92d9c7aedd2d5e22f8e469bd39ee1c7430c9d01 100644 GIT binary patch literal 12766 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 G%l`vu<4{@v literal 20427 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 Oow4D0g9<&D=)VD~q;P=% diff --git a/assets/images/services/discogs.png b/assets/images/services/discogs.png index 17c3b891514552380316c3c35c9d334277d76091..353f14a2b825867f1df8614bd04b1d9848af4bbe 100644 GIT binary patch literal 30149 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 V+w>B~2#m*%s-niLN_q2;{{_u+Lw^7O literal 22435 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 GGywodoF~En literal 18639 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 diff --git a/assets/localization/en.json b/assets/localization/en.json index 7d599688..67028b2e 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -6,7 +6,9 @@ "lunasea.ComingSoon": "Coming Soon", "lunasea.Dashboard": "Dashboard", "lunasea.GoBack": "Go Back", + "lunasea.GoToSettings": "Go to Settings", "lunasea.ModuleIsNotEnabled": "{} Is Not Enabled", + "lunasea.NoModulesEnabled": "No Modules Enabled", "lunasea.Refresh": "Refresh", "lunasea.ReturnToDashboard": "Return to Dashboard", "lunasea.SearchTextBar": "Search…", @@ -45,7 +47,7 @@ "radarr.InCinemasIn": "In Cinemas in {}", "radarr.InCinemasToday": "In Cinemas Today", "radarr.Interactive": "Interactive", - "radarr.Language": "Language", + "radarr.Languages": "Languages", "radarr.ManualImport": "Manual Import", "radarr.ManualImportDescription": "Import Movies from the Filesystem", "radarr.MinimumAvailability": "Minimum Availability", @@ -87,6 +89,9 @@ "radarr.SearchAllMissing": "Search All Missing", "radarr.SearchFor": "Search for {}", "radarr.Seeders": "Seeders", + "radarr.SelectLanguage": "Select Languages", + "radarr.SelectMovie": "Select Movie", + "radarr.SelectQuality": "Select Quality", "radarr.Size": "Size", "radarr.SortCatalogue": "Sort Catalogue", "radarr.Studio": "Studio", diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 1ed4a8ad..21e0b32b 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -1,20 +1,6 @@ class Constants { Constants._(); - static const TEXT_EMDASH = '—'; - static const TEXT_BULLET = '•'; - static const TEXT_ELLIPSIS = '…'; - - static const UI_BORDER_RADIUS = 10.0; - static const UI_FONT_SIZE_HEADER = 18.0; - static const UI_FONT_SIZE_STICKYHEADER = 14.0; - static const UI_FONT_SIZE_SUBHEADER = 12.0; - static const UI_FONT_SIZE_TITLE = 16.0; - static const UI_FONT_SIZE_SUBTITLE = 13.0; - static const UI_FONT_SIZE_GRAPH_LEGEND = 10.0; - - static const NO_SERVICES_ENABLED = '<>'; - static const URL_CHANGELOG = 'https://github.com/CometTools/LunaSea/blob/master/CHANGELOG.md'; static const URL_CRASHLYTICS = 'https://firebase.google.com/products/crashlytics'; static const URL_DISCORD = 'https://www.lunasea.app/discord'; @@ -26,10 +12,4 @@ class Constants { static const URL_WEBLATE = 'https:/www.lunasea.app/translate'; static const URL_WEBSITE = 'https://www.lunasea.app'; static const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15'; - - static const Map historyReasonMessages = { - 'Upgrade': 'Upgraded File', - 'MissingFromDisk': 'Missing From Disk', - 'Manual': 'Manually Removed', - }; } diff --git a/lib/core/dialogs.dart b/lib/core/dialogs.dart index 261caf07..21a2405f 100644 --- a/lib/core/dialogs.dart +++ b/lib/core/dialogs.dart @@ -22,20 +22,20 @@ class LunaDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: dialogTitle, buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Save', onPressed: () => _setValues(true), ), ], content: [ - if((extraText?.length ?? 0) != 0) LSDialog.richText(children: extraText), + if((extraText?.length ?? 0) != 0) LunaDialog.richText(children: extraText), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: dialogTitle, onSubmitted: (_) => _setValues(true), @@ -43,7 +43,7 @@ class LunaDialogs { ), ), ], - contentPadding: (extraText?.length ?? 0) == 0 ? LSDialog.inputDialogContentPadding() : LSDialog.inputTextDialogContentPadding(), + contentPadding: (extraText?.length ?? 0) == 0 ? LunaDialog.inputDialogContentPadding() : LunaDialog.inputTextDialogContentPadding(), ); return Tuple2(_flag, _textController.text); } @@ -52,12 +52,12 @@ class LunaDialogs { /// /// Can pass in boolean [alignLeft] to left align the text in the dialog (useful for bulleted lists) Future textPreview(BuildContext context, String dialogTitle, String text, { bool alignLeft = false }) async { - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: dialogTitle, cancelButtonText: 'Close', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Copy', onPressed: () async { await Clipboard.setData(ClipboardData(text: text)); @@ -67,27 +67,27 @@ class LunaDialogs { ), ], content: [ - LSDialog.textContent(text: text), + LunaDialog.textContent(text: text), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); } Future showRejections(BuildContext context, List rejections) async { if((rejections ?? []).isEmpty) return textPreview(context, 'Rejection Reasons', 'No rejections found'); - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Rejection Reasons', cancelButtonText: 'Close', content: List.generate( rejections.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: rejections[index], icon: Icons.report_outlined, iconColor: LunaColours.red, ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); } @@ -102,20 +102,20 @@ class LunaDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete All Files', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete all the files and folders for $moduleTitle?'), + LunaDialog.textContent(text: 'Are you sure you want to delete all the files and folders for $moduleTitle?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } diff --git a/lib/core/extensions/double.dart b/lib/core/extensions/double.dart index ba622dba..5d732b25 100644 --- a/lib/core/extensions/double.dart +++ b/lib/core/extensions/double.dart @@ -5,7 +5,7 @@ extension DoubleExtension on double { /// /// Time greater than 48 hours => "x Days Ago" else "x Hours Ago" String lunaHoursToAge() { - if(this == null) return Constants.TEXT_EMDASH; + if(this == null) return LunaUI.TEXT_EMDASH; int days = (this/24).floor(); if(this > 48) return '$days Days Ago'; return this == 1 ? '1 Hour Ago' : '${this.toStringAsFixed(1)} Hours Ago'; diff --git a/lib/core/extensions/duration.dart b/lib/core/extensions/duration.dart index b542217e..d3e0f55f 100644 --- a/lib/core/extensions/duration.dart +++ b/lib/core/extensions/duration.dart @@ -25,7 +25,7 @@ extension DurationExtension on Duration { if(days != null) days, if(hours != null) hours, if(minutes != null) minutes, - if(minutes == null) Constants.TEXT_EMDASH, + if(minutes == null) LunaUI.TEXT_EMDASH, ].join(' '); } } diff --git a/lib/core/system/modules.dart b/lib/core/system/modules.dart index 246c12c9..a6af9bae 100644 --- a/lib/core/system/modules.dart +++ b/lib/core/system/modules.dart @@ -83,7 +83,7 @@ extension LunaModuleExtension on LunaModule { String get route { switch(this) { - case LunaModule.DASHBOARD: return Dashboard.ROUTE_NAME; + case LunaModule.DASHBOARD: return DashboardHomeRouter().route(); case LunaModule.LIDARR: return Lidarr.ROUTE_NAME; case LunaModule.NZBGET: return NZBGet.ROUTE_NAME; case LunaModule.RADARR: return RadarrHomeRouter().route(); diff --git a/lib/core/system/router.dart b/lib/core/system/router.dart index 6eed17c0..995e3871 100644 --- a/lib/core/system/router.dart +++ b/lib/core/system/router.dart @@ -10,6 +10,7 @@ class LunaRouter { /// Calls `defineAllRoutes()` on all module routers that implement [LunaModuleRouter]. void intialize() { router.notFoundHandler = Handler(handlerFunc: (context, params) => LunaInvalidRoute()); + DashboardRouter().defineAllRoutes(router); RadarrRouter().defineAllRoutes(router); SearchRouter().defineAllRoutes(router); SettingsRouter().defineAllRoutes(router); @@ -21,7 +22,6 @@ class LunaRouter { /// /// Returns a map of all module routes. Map get routes => { - Dashboard.ROUTE_NAME: (context) => Dashboard(), ..._lidarr, ..._sabnzbd, ..._nzbget, diff --git a/lib/core/ui.dart b/lib/core/ui.dart index b6e7ad84..7a8b5573 100644 --- a/lib/core/ui.dart +++ b/lib/core/ui.dart @@ -9,9 +9,10 @@ export 'ui/colors.dart'; export 'ui/containers.dart'; export 'ui/controllers.dart'; export 'ui/decoration.dart'; -export 'ui/deprecated.dart'; +export 'ui/dialog.dart'; export 'ui/divider.dart'; export 'ui/drawer.dart'; +export 'ui/floating_action_button.dart'; export 'ui/header.dart'; export 'ui/highlighted_node.dart'; export 'ui/icons.dart'; @@ -23,13 +24,14 @@ export 'ui/message.dart'; export 'ui/network_image.dart'; export 'ui/popup_menu_button.dart'; export 'ui/refresh_indicator.dart'; +export 'ui/scaffold.dart'; export 'ui/shape.dart'; -export 'ui/sliver_sticky_header.dart'; export 'ui/snackbar.dart'; export 'ui/switch.dart'; export 'ui/table.dart'; export 'ui/text.dart'; export 'ui/theme.dart'; +export 'ui/will_pop_scope.dart'; class LunaUI { // Text Constants diff --git a/lib/core/ui/appbar.dart b/lib/core/ui/appbar.dart index fff8127d..f3c6e42f 100644 --- a/lib/core/ui/appbar.dart +++ b/lib/core/ui/appbar.dart @@ -13,6 +13,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { final String title; final List actions; final PreferredSizeWidget bottom; + final bool hideLeading; final bool useDrawer; final Widget child; final double height; @@ -22,7 +23,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { @override Size get preferredSize { - double _size = kToolbarHeight+6.0; + double _size = (height ?? kToolbarHeight); if(bottom != null) _size += bottom.preferredSize.height; return new Size.fromHeight(_size); } @@ -38,6 +39,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { this.profiles, this.pageController, this.scrollControllers, + this.hideLeading = false, }); /// Create a new [AppBar] widget pre-styled for LunaSea. @@ -52,6 +54,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { List actions, PreferredSizeWidget bottom, bool useDrawer = false, + bool hideLeading = false, PageController pageController, List scrollControllers, }) { @@ -64,6 +67,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { useDrawer: useDrawer, pageController: pageController, scrollControllers: scrollControllers, + hideLeading: hideLeading, type: _APPBAR_TYPE.DEFAULT, ); } @@ -71,14 +75,23 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { /// Create a new, empty [LunaAppBar] which can be used to attach to a [Scaffold] in a [PageView] that is already wrapped in an [AppBar]. /// /// Example usages would be a [PageView] but a single page needs an [AppBar] bottom widget. + /// + /// The default padding is for a [LunaTextInputBar]. factory LunaAppBar.empty({ @required Widget child, @required double height, + EdgeInsets padding = LunaTextInputBar.appBarMargin, + Alignment alignment = Alignment.topCenter, }) { assert(child != null); assert(height != null); return LunaAppBar._internal( - child: child, + child: Container( + child: child, + height: height, + padding: padding, + alignment: alignment, + ), height: height, useDrawer: false, type: _APPBAR_TYPE.EMPTY, @@ -96,6 +109,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { @required String title, @required List profiles, bool useDrawer = true, + bool hideLeading = false, List actions, PageController pageController, List scrollControllers, @@ -107,6 +121,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { title: title, actions: actions, useDrawer: useDrawer, + hideLeading: hideLeading, pageController: pageController, scrollControllers: scrollControllers, type: _APPBAR_TYPE.DEFAULT, @@ -116,6 +131,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { profiles: profiles, actions: actions, useDrawer: useDrawer, + hideLeading: hideLeading, pageController: pageController, scrollControllers: scrollControllers, type: _APPBAR_TYPE.DROPDOWN, @@ -176,11 +192,15 @@ class _State extends State { } Widget _sharedLeading(BuildContext context) { + if(widget.hideLeading ?? false) return null; if(widget.useDrawer) return IconButton( icon: Icon(Icons.menu_rounded), onPressed: () async { HapticFeedback.lightImpact(); - if(Scaffold.of(context).hasDrawer) Scaffold.of(context).openDrawer(); + if(Scaffold.of(context).hasDrawer) { + Scaffold.of(context).openDrawer(); + FocusManager.instance.primaryFocus?.unfocus(); + } }, ); return InkWell( @@ -205,6 +225,7 @@ class _State extends State { style: TextStyle(fontSize: LunaUI.FONT_SIZE_APP_BAR), ), leading: _sharedLeading(context), + automaticallyImplyLeading: !(widget.hideLeading ?? false), centerTitle: false, elevation: 0, actions: widget.actions, @@ -233,7 +254,7 @@ class _State extends State { children: [ Text( widget.title, - style: TextStyle(fontSize: Constants.UI_FONT_SIZE_HEADER), + style: TextStyle(fontSize: LunaUI.FONT_SIZE_HEADER), ), Icon(Icons.arrow_drop_down), ], @@ -248,7 +269,7 @@ class _State extends State { child: Text( profile, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), )]; diff --git a/lib/core/ui/bottom_bar/action_bar.dart b/lib/core/ui/bottom_bar/action_bar.dart index 57a67105..4560cdf3 100644 --- a/lib/core/ui/bottom_bar/action_bar.dart +++ b/lib/core/ui/bottom_bar/action_bar.dart @@ -36,10 +36,7 @@ class LunaBottomActionBar extends StatelessWidget { padding: padding, ), ), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - ), + decoration: BoxDecoration(color: Theme.of(context).primaryColor), ); } - } \ No newline at end of file diff --git a/lib/core/ui/bottom_bar/navigation_bar.dart b/lib/core/ui/bottom_bar/navigation_bar.dart index 5ea432d4..df090e87 100644 --- a/lib/core/ui/bottom_bar/navigation_bar.dart +++ b/lib/core/ui/bottom_bar/navigation_bar.dart @@ -57,7 +57,7 @@ class _State extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if(widget.topActions != null && widget.topActions.length != 0) LunaBottomActionBar( + if((widget.topActions?.length ?? 0) != 0) LunaBottomActionBar( actions: widget.topActions, useSafeArea: false, ), diff --git a/lib/core/ui/bottom_modal_sheet.dart b/lib/core/ui/bottom_modal_sheet.dart index 2737873c..a22fda5c 100644 --- a/lib/core/ui/bottom_modal_sheet.dart +++ b/lib/core/ui/bottom_modal_sheet.dart @@ -7,7 +7,7 @@ class LunaBottomModalSheet { Future showModal({ @required BuildContext context, @required Widget Function(BuildContext) builder, - bool expand = true, + bool expand = false, }) async => showBarModalBottomSheet( context: context, expand: expand, diff --git a/lib/core/ui/deprecated.dart b/lib/core/ui/deprecated.dart index f9fbfec5..e69de29b 100644 --- a/lib/core/ui/deprecated.dart +++ b/lib/core/ui/deprecated.dart @@ -1,16 +0,0 @@ -export 'deprecated/card.dart'; -export 'deprecated/containers.dart'; -export 'deprecated/description_block.dart'; -export 'deprecated/dialogs.dart'; -export 'deprecated/floating_action_button.dart'; -export 'deprecated/icon.dart'; -export 'deprecated/input_bar.dart'; -export 'deprecated/listview.dart'; -export 'deprecated/loader.dart'; -export 'deprecated/messages.dart'; -export 'deprecated/navigation_bar.dart'; -export 'deprecated/network_image.dart'; -export 'deprecated/refresh_indicator.dart'; -export 'deprecated/snackbar.dart'; -export 'deprecated/table.dart'; -export 'deprecated/text.dart'; diff --git a/lib/core/ui/deprecated/card.dart b/lib/core/ui/deprecated/card.dart deleted file mode 100644 index 9502976c..00000000 --- a/lib/core/ui/deprecated/card.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'card/card.dart'; -export 'card/card_tile.dart'; diff --git a/lib/core/ui/deprecated/card/card.dart b/lib/core/ui/deprecated/card/card.dart deleted file mode 100644 index 196a4ccd..00000000 --- a/lib/core/ui/deprecated/card/card.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSCard extends StatelessWidget { - final Widget child; - final EdgeInsets margin; - final bool reducedMargin; - final Color color; - final Decoration decoration; - final double height; - - LSCard({ - @required this.child, - this.margin = LunaUI.MARGIN_CARD, - this.reducedMargin = false, - this.color, - this.decoration, - this.height, - }); - - @override - Widget build(BuildContext context) => ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ - LunaDatabaseValue.THEME_AMOLED.key, - LunaDatabaseValue.THEME_AMOLED_BORDER.key, - ]), - builder: (context, box, widget) => Card( - child: Container( - child: child, - decoration: decoration, - height: height, - ), - elevation: LunaUI.ELEVATION, - margin: reducedMargin - ? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0) - : margin, - color: color == null - ? Theme.of(context).primaryColor - : color, - shape: LunaUI.shapeBorder, - ), - ); -} diff --git a/lib/core/ui/deprecated/card/card_tile.dart b/lib/core/ui/deprecated/card/card_tile.dart deleted file mode 100644 index 5e56ec3d..00000000 --- a/lib/core/ui/deprecated/card/card_tile.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSCardTile extends StatelessWidget { - final Widget title; - final Widget subtitle; - final Widget trailing; - final Widget leading; - final Function onTap; - final Function onLongPress; - final bool padContent; - final bool reducedMargin; - final EdgeInsets customPadding; - final EdgeInsets customMargin; - final Decoration decoration; - final Color color; - - LSCardTile({ - @required this.title, - this.subtitle, - this.trailing, - this.leading, - this.onTap, - this.onLongPress, - this.padContent = false, - this.decoration, - this.customMargin, - this.reducedMargin = false, - this.customPadding, - this.color, - }); - - @override - Widget build(BuildContext context) => ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ - LunaDatabaseValue.THEME_AMOLED.key, - LunaDatabaseValue.THEME_AMOLED_BORDER.key, - ]), - builder: (context, box, widget) => Card( - child: Container( - child: InkWell( - child: ListTile( - title: title, - subtitle: subtitle, - trailing: trailing, - leading: leading, - contentPadding: customPadding == null - ? padContent - ? EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0) - : null - : customPadding, - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: onTap, - onLongPress: onLongPress, - ), - decoration: decoration, - ), - margin: customMargin == null - ? reducedMargin - ? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0) - : LunaUI.MARGIN_CARD - : customMargin, - elevation: LunaUI.ELEVATION, - shape: LunaUI.shapeBorder, - color: color == null - ? Theme.of(context).primaryColor - : color, - ), - ); -} diff --git a/lib/core/ui/deprecated/containers.dart b/lib/core/ui/deprecated/containers.dart deleted file mode 100644 index f4b62f99..00000000 --- a/lib/core/ui/deprecated/containers.dart +++ /dev/null @@ -1 +0,0 @@ -export 'containers/row.dart'; diff --git a/lib/core/ui/deprecated/containers/row.dart b/lib/core/ui/deprecated/containers/row.dart deleted file mode 100644 index 04f5da16..00000000 --- a/lib/core/ui/deprecated/containers/row.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; - -class LSContainerRow extends StatelessWidget { - final List children; - final EdgeInsets padding; - final Color backgroundColor; - final MainAxisAlignment mainAxisAlignment; - final CrossAxisAlignment crossAxisAlignment; - - LSContainerRow({ - @required this.children, - this.backgroundColor, - this.padding = const EdgeInsets.symmetric(horizontal: 6.0), - this.mainAxisAlignment = MainAxisAlignment.start, - this.crossAxisAlignment = CrossAxisAlignment.start, - }); - - @override - Widget build(BuildContext context) => Padding( - child: Container( - child: Row( - mainAxisAlignment: mainAxisAlignment, - crossAxisAlignment: crossAxisAlignment, - children: children, - ), - decoration: BoxDecoration( - color: backgroundColor, - ), - ), - padding: padding, - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/floating_action_button/floating_action_button.dart b/lib/core/ui/deprecated/floating_action_button/floating_action_button.dart deleted file mode 100644 index 51f78471..00000000 --- a/lib/core/ui/deprecated/floating_action_button/floating_action_button.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSFloatingActionButton extends StatelessWidget { - final Object heroTag; - final IconData icon; - final Color iconColor; - final Color backgroundColor; - final Function onPressed; - - LSFloatingActionButton({ - @required this.onPressed, - @required this.icon, - this.backgroundColor, - this.iconColor = Colors.white, - this.heroTag, - }); - - @override - Widget build(BuildContext context) => FloatingActionButton( - child: Icon(icon, color: iconColor), - heroTag: heroTag, - onPressed: onPressed, - backgroundColor: backgroundColor == null - ? LunaColours.accent - : backgroundColor, - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/floating_action_button/floating_action_button_animated.dart b/lib/core/ui/deprecated/floating_action_button/floating_action_button_animated.dart deleted file mode 100644 index 2a267abf..00000000 --- a/lib/core/ui/deprecated/floating_action_button/floating_action_button_animated.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSFloatingActionButtonAnimated extends StatelessWidget { - final Object heroTag; - final AnimatedIconData icon; - final AnimationController controller; - final Color iconColor; - final Color backgroundColor; - final Function onPressed; - - LSFloatingActionButtonAnimated({ - @required this.onPressed, - @required this.icon, - @required this.controller, - this.backgroundColor, - this.iconColor = Colors.white, - this.heroTag, - }); - - @override - Widget build(BuildContext context) => FloatingActionButton( - child: AnimatedIcon( - icon: icon, - color: iconColor, - progress: controller, - ), - heroTag: heroTag, - onPressed: onPressed, - backgroundColor: backgroundColor == null - ? LunaColours.accent - : backgroundColor, - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/floating_action_button/floating_action_button_extended.dart b/lib/core/ui/deprecated/floating_action_button/floating_action_button_extended.dart deleted file mode 100644 index 0ea1b2c2..00000000 --- a/lib/core/ui/deprecated/floating_action_button/floating_action_button_extended.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSFloatingActionButtonExtended extends StatelessWidget { - final String label; - final Object heroTag; - final IconData icon; - final Color iconColor; - final Color labelColor; - final Color backgroundColor; - final Function onPressed; - - LSFloatingActionButtonExtended({ - @required this.label, - @required this.onPressed, - @required this.icon, - this.backgroundColor, - this.labelColor = Colors.white, - this.iconColor = Colors.white, - this.heroTag, - }); - - @override - Widget build(BuildContext context) => FloatingActionButton.extended( - icon: Icon(icon, color: iconColor), - label: Text( - label, - style: TextStyle( - color: labelColor, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - letterSpacing: 0.25, - ), - ), - heroTag: heroTag, - onPressed: onPressed, - backgroundColor: backgroundColor == null - ? LunaColours.accent - : backgroundColor, - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/icon.dart b/lib/core/ui/deprecated/icon.dart deleted file mode 100644 index c9879f8e..00000000 --- a/lib/core/ui/deprecated/icon.dart +++ /dev/null @@ -1 +0,0 @@ -export 'icon/icon_button.dart'; diff --git a/lib/core/ui/deprecated/icon/icon_button.dart b/lib/core/ui/deprecated/icon/icon_button.dart deleted file mode 100644 index b76ea87d..00000000 --- a/lib/core/ui/deprecated/icon/icon_button.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:lunasea/core.dart'; - -class LSIconButton extends StatelessWidget { - final IconData icon; - final String text; - final Color color; - final Function onPressed; - final Function onLongPress; - final double iconSize; - final double textSize; - - LSIconButton({ - this.icon, - this.text, - this.color = Colors.white, - this.onPressed, - this.onLongPress, - this.iconSize = 24.0, - this.textSize = 10.0, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - child: IconButton( - icon: text != null - ? Text( - text, - textAlign: TextAlign.center, - style: TextStyle( - color: color, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: textSize, - ), - ) - : Icon(icon, color: color), - iconSize: iconSize, - onPressed: onPressed == null ? null : () async { - HapticFeedback.lightImpact(); - onPressed(); - } - ), - onLongPress: onLongPress == null ? null : () async { - HapticFeedback.heavyImpact(); - onLongPress(); - }, - ); - } -} diff --git a/lib/core/ui/deprecated/input_bar.dart b/lib/core/ui/deprecated/input_bar.dart deleted file mode 100644 index 741b6448..00000000 --- a/lib/core/ui/deprecated/input_bar.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSTextInputBar extends StatefulWidget { - final TextEditingController controller; - final TextInputAction action; - final TextInputType keyboardType; - final Iterable autofillHints; - final String labelText; - final IconData labelIcon; - final bool autofocus; - final bool obscureText; - final bool isFormField; - final void Function(String, bool) onChanged; - final void Function(String) onSubmitted; - final String Function(String) validator; - final EdgeInsets margin; - - LSTextInputBar({ - @required this.controller, - this.onChanged, - this.onSubmitted, - this.validator, - this.autofillHints, - this.action = TextInputAction.search, - this.keyboardType = TextInputType.text, - this.labelText = 'Search...', - this.labelIcon = Icons.search, - this.margin = LunaUI.MARGIN_CARD, - this.autofocus = false, - this.obscureText = false, - this.isFormField = false, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - bool _isFocused = false; - - @override - Widget build(BuildContext context) => LSCard( - margin: widget.margin, - child: FocusScope( - child: Focus( - onFocusChange: (value) => setState(() => _isFocused = value), - child: widget.isFormField ? _isForm : _isNotForm, - ), - ), - color: Theme.of(context).canvasColor, - ); - - TextStyle get _sharedTextStyle => TextStyle( - color: Colors.white, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ); - - InputDecoration get _sharedInputDecoration => InputDecoration( - labelText: widget.labelText, - labelStyle: TextStyle( - color: Colors.white54, - decoration: TextDecoration.none, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - suffixIcon: AnimatedOpacity( - child: GestureDetector( - child: Icon( - Icons.close, - color: LunaColours.accent, - size: 24.0, - ), - onTap: () => widget?.onChanged('', true), - ), - opacity: !_isFocused || widget.controller.text == '' ? 0.0 : 1.0, - duration: Duration(milliseconds: 200), - ), - icon: Padding( - child: Icon( - widget.labelIcon, - color: LunaColours.accent, - ), - padding: EdgeInsets.only(left: 16.0), - ), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 8.0), - ); - - TextFormField get _isForm => TextFormField( - autofillHints: widget.autofillHints, - autofocus: widget.autofocus, - controller: widget.controller, - decoration: _sharedInputDecoration, - style: _sharedTextStyle, - cursorColor: LunaColours.accent, - textInputAction: widget.action, - obscureText: widget.obscureText, - autocorrect: false, - keyboardType: widget.keyboardType, - validator: widget?.validator, - onChanged: widget.onChanged == null ? null : (value) => widget.onChanged(value, false), - onFieldSubmitted: widget?.onSubmitted, - ); - - TextField get _isNotForm => TextField( - autofillHints: widget.autofillHints, - autofocus: widget.autofocus, - controller: widget.controller, - decoration: _sharedInputDecoration, - style: _sharedTextStyle, - cursorColor: LunaColours.accent, - textInputAction: widget.action, - obscureText: widget.obscureText, - autocorrect: false, - keyboardType: widget.keyboardType, - onChanged: widget.onChanged == null ? null : (value) => widget.onChanged(value, false), - onSubmitted: widget?.onSubmitted, - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/listview.dart b/lib/core/ui/deprecated/listview.dart deleted file mode 100644 index 931efd73..00000000 --- a/lib/core/ui/deprecated/listview.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'listview/listview.dart'; -export 'listview/listview_builder.dart'; -export 'listview/listview_reorderable.dart'; -export 'listview/listview_stickyheader.dart'; diff --git a/lib/core/ui/deprecated/listview/listview.dart b/lib/core/ui/deprecated/listview/listview.dart deleted file mode 100644 index 8628645c..00000000 --- a/lib/core/ui/deprecated/listview/listview.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class LSListView extends StatelessWidget { - final List children; - final EdgeInsetsGeometry customPadding; - final ScrollController controller; - - LSListView({ - @required this.children, - this.controller, - this.customPadding, - }); - - @override - Widget build(BuildContext context) => Scrollbar( - controller: controller == null ? ScrollController() : controller, - child: ListView( - controller: controller, - children: children, - padding: customPadding != null ? customPadding : EdgeInsets.only( - top: 8.0, - bottom: 8.0+(MediaQuery.of(context).padding.bottom), - ), - physics: AlwaysScrollableScrollPhysics(), - ), - ); -} diff --git a/lib/core/ui/deprecated/listview/listview_builder.dart b/lib/core/ui/deprecated/listview/listview_builder.dart deleted file mode 100644 index e10ee662..00000000 --- a/lib/core/ui/deprecated/listview/listview_builder.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; - -class LSListViewBuilder extends StatelessWidget { - final int itemCount; - final bool reverse; - final Function(BuildContext context, int index) itemBuilder; - final EdgeInsetsGeometry customPadding; - final ScrollController controller = ScrollController(); - - LSListViewBuilder({ - @required this.itemCount, - @required this.itemBuilder, - this.reverse = false, - this.customPadding, - }); - - @override - Widget build(BuildContext context) => Scrollbar( - controller: controller, - child: ListView.builder( - controller: controller, - itemCount: itemCount, - itemBuilder: itemBuilder, - padding: customPadding != null ? customPadding : EdgeInsets.only( - top: 8.0, - bottom: 8.0+(MediaQuery.of(context).padding.bottom), - ), - reverse: reverse, - physics: AlwaysScrollableScrollPhysics(), - ), - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/listview/listview_reorderable.dart b/lib/core/ui/deprecated/listview/listview_reorderable.dart deleted file mode 100644 index 1cb1c6c7..00000000 --- a/lib/core/ui/deprecated/listview/listview_reorderable.dart +++ /dev/null @@ -1,594 +0,0 @@ -/* - * Modification of a Flutter-core file, adjusted Line 197 - * static const double _dropAreaMargin = 8.0; //Used to be 100.0 - */ - -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:math'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/material.dart'; - -// Examples can assume: -// class MyDataObject { } - -/// The callback used by [ReorderableListView] to move an item to a new -/// position in a list. -/// -/// Implementations should remove the corresponding list item at [oldIndex] -/// and reinsert it at [newIndex]. -/// -/// If [oldIndex] is before [newIndex], removing the item at [oldIndex] from the -/// list will reduce the list's length by one. Implementations used by -/// [ReorderableListView] will need to account for this when inserting before -/// [newIndex]. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE} -/// -/// {@tool snippet} -/// -/// ```dart -/// final List backingList = [/* ... */]; -/// -/// void handleReorder(int oldIndex, int newIndex) { -/// if (oldIndex < newIndex) { -/// // removing the item at oldIndex will shorten the list by 1. -/// newIndex -= 1; -/// } -/// final MyDataObject element = backingList.removeAt(oldIndex); -/// backingList.insert(newIndex, element); -/// } -/// ``` -/// {@end-tool} -typedef ReorderCallback = void Function(int oldIndex, int newIndex); - -/// A list whose items the user can interactively reorder by dragging. -/// -/// This class is appropriate for views with a small number of -/// children because constructing the [List] requires doing work for every -/// child that could possibly be displayed in the list view instead of just -/// those children that are actually visible. -/// -/// All [children] must have a key. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE} -class LSReorderableListView extends StatefulWidget { - - /// Creates a reorderable list. - LSReorderableListView({ - Key key, - this.header, - @required this.children, - @required this.onReorder, - this.scrollController, - this.scrollDirection = Axis.vertical, - this.padding, - this.reverse = false, - }) : assert(scrollDirection != null), - assert(onReorder != null), - assert(children != null), - assert( - children.every((Widget w) => w.key != null), - 'All children of this widget must have a key.', - ), - super(key: key); - - /// A non-reorderable header widget to show before the list. - /// - /// If null, no header will appear before the list. - final Widget header; - - /// The widgets to display. - final List children; - - /// The [Axis] along which the list scrolls. - /// - /// List [children] can only drag along this [Axis]. - final Axis scrollDirection; - - /// Creates a [ScrollPosition] to manage and determine which portion - /// of the content is visible in the scroll view. - /// - /// This can be used in many ways, such as setting an initial scroll offset, - /// (via [ScrollController.initialScrollOffset]), reading the current scroll position - /// (via [ScrollController.offset]), or changing it (via [ScrollController.jumpTo] or - /// [ScrollController.animateTo]). - final ScrollController scrollController; - - /// The amount of space by which to inset the [children]. - final EdgeInsets padding; - - /// Whether the scroll view scrolls in the reading direction. - /// - /// For example, if the reading direction is left-to-right and - /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from - /// left to right when [reverse] is false and from right to left when - /// [reverse] is true. - /// - /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view - /// scrolls from top to bottom when [reverse] is false and from bottom to top - /// when [reverse] is true. - /// - /// Defaults to false. - final bool reverse; - - /// Called when a list child is dropped into a new position to shuffle the - /// underlying list. - /// - /// This [ReorderableListView] calls [onReorder] after a list child is dropped - /// into a new position. - final ReorderCallback onReorder; - - @override - _ReorderableListViewState createState() => _ReorderableListViewState(); -} - -// This top-level state manages an Overlay that contains the list and -// also any Draggables it creates. -// -// _ReorderableListContent manages the list itself and reorder operations. -// -// The Overlay doesn't properly keep state by building new overlay entries, -// and so we cache a single OverlayEntry for use as the list layer. -// That overlay entry then builds a _ReorderableListContent which may -// insert Draggables into the Overlay above itself. -class _ReorderableListViewState extends State { - // We use an inner overlay so that the dragging list item doesn't draw outside of the list itself. - final GlobalKey _overlayKey = GlobalKey(debugLabel: 'modded $ReorderableListView overlay key'); - - // This entry contains the scrolling list itself. - OverlayEntry _listOverlayEntry; - - @override - void initState() { - super.initState(); - _listOverlayEntry = OverlayEntry( - opaque: true, - builder: (BuildContext context) { - return _ReorderableListContent( - header: widget.header, - children: widget.children, - scrollController: widget.scrollController, - scrollDirection: widget.scrollDirection, - onReorder: widget.onReorder, - padding: widget.padding, - reverse: widget.reverse, - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return Overlay( - key: _overlayKey, - initialEntries: [ - _listOverlayEntry, - ]); - } -} - -// This widget is responsible for the inside of the Overlay in the -// ReorderableListView. -class _ReorderableListContent extends StatefulWidget { - const _ReorderableListContent({ - @required this.header, - @required this.children, - @required this.scrollController, - @required this.scrollDirection, - @required this.padding, - @required this.onReorder, - @required this.reverse, - }); - - final Widget header; - final List children; - final ScrollController scrollController; - final Axis scrollDirection; - final EdgeInsets padding; - final ReorderCallback onReorder; - final bool reverse; - - @override - _ReorderableListContentState createState() => _ReorderableListContentState(); -} - -class _ReorderableListContentState extends State<_ReorderableListContent> with TickerProviderStateMixin<_ReorderableListContent> { - - // The extent along the [widget.scrollDirection] axis to allow a child to - // drop into when the user reorders list children. - // - // This value is used when the extents haven't yet been calculated from - // the currently dragging widget, such as when it first builds. - static const double _defaultDropAreaExtent = 8.0; - - // The additional margin to place around a computed drop area. - static const double _dropAreaMargin = 8.0; - - // How long an animation to reorder an element in the list takes. - static const Duration _reorderAnimationDuration = Duration(milliseconds: 200); - - // How long an animation to scroll to an off-screen element in the - // list takes. - static const Duration _scrollAnimationDuration = Duration(milliseconds: 200); - - // Controls scrolls and measures scroll progress. - ScrollController _scrollController; - - // This controls the entrance of the dragging widget into a new place. - AnimationController _entranceController; - - // This controls the 'ghost' of the dragging widget, which is left behind - // where the widget used to be. - AnimationController _ghostController; - - // The member of widget.children currently being dragged. - // - // Null if no drag is underway. - Key _dragging; - - // The last computed size of the feedback widget being dragged. - Size _draggingFeedbackSize; - - // The location that the dragging widget occupied before it started to drag. - int _dragStartIndex = 0; - - // The index that the dragging widget most recently left. - // This is used to show an animation of the widget's position. - int _ghostIndex = 0; - - // The index that the dragging widget currently occupies. - int _currentIndex = 0; - - // The widget to move the dragging widget too after the current index. - int _nextIndex = 0; - - // Whether or not we are currently scrolling this view to show a widget. - bool _scrolling = false; - - double get _dropAreaExtent { - if (_draggingFeedbackSize == null) { - return _defaultDropAreaExtent; - } - double dropAreaWithoutMargin; - switch (widget.scrollDirection) { - case Axis.horizontal: - dropAreaWithoutMargin = _draggingFeedbackSize.width; - break; - case Axis.vertical: - default: - dropAreaWithoutMargin = _draggingFeedbackSize.height; - break; - } - return dropAreaWithoutMargin + _dropAreaMargin; - } - - @override - void initState() { - super.initState(); - _entranceController = AnimationController(vsync: this, duration: _reorderAnimationDuration); - _ghostController = AnimationController(vsync: this, duration: _reorderAnimationDuration); - _entranceController.addStatusListener(_onEntranceStatusChanged); - } - - @override - void didChangeDependencies() { - _scrollController = widget.scrollController ?? PrimaryScrollController.of(context) ?? ScrollController(); - super.didChangeDependencies(); - } - - @override - void dispose() { - _entranceController.dispose(); - _ghostController.dispose(); - super.dispose(); - } - - // Animates the droppable space from _currentIndex to _nextIndex. - void _requestAnimationToNextIndex() { - if (_entranceController.isCompleted) { - _ghostIndex = _currentIndex; - if (_nextIndex == _currentIndex) { - return; - } - _currentIndex = _nextIndex; - _ghostController.reverse(from: 1.0); - _entranceController.forward(from: 0.0); - } - } - - // Requests animation to the latest next index if it changes during an animation. - void _onEntranceStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.completed) { - setState(() { - _requestAnimationToNextIndex(); - }); - } - } - - // Scrolls to a target context if that context is not on the screen. - void _scrollTo(BuildContext context) { - if (_scrolling) - return; - final RenderObject contextObject = context.findRenderObject(); - final RenderAbstractViewport viewport = RenderAbstractViewport.of(contextObject); - assert(viewport != null); - // If and only if the current scroll offset falls in-between the offsets - // necessary to reveal the selected context at the top or bottom of the - // screen, then it is already on-screen. - final double margin = _dropAreaExtent; - final double scrollOffset = _scrollController.offset; - final double topOffset = max( - _scrollController.position.minScrollExtent, - viewport.getOffsetToReveal(contextObject, 0.0).offset - margin, - ); - final double bottomOffset = min( - _scrollController.position.maxScrollExtent, - viewport.getOffsetToReveal(contextObject, 1.0).offset + margin, - ); - final bool onScreen = scrollOffset <= topOffset && scrollOffset >= bottomOffset; - - // If the context is off screen, then we request a scroll to make it visible. - if (!onScreen) { - _scrolling = true; - _scrollController.position.animateTo( - scrollOffset < bottomOffset ? bottomOffset : topOffset, - duration: _scrollAnimationDuration, - curve: Curves.easeInOut, - ).then((void value) { - setState(() { - _scrolling = false; - }); - }); - } - } - - // Wraps children in Row or Column, so that the children flow in - // the widget's scrollDirection. - Widget _buildContainerForScrollDirection({ List children }) { - switch (widget.scrollDirection) { - case Axis.horizontal: - return Row(children: children); - case Axis.vertical: - default: - return Column(children: children); - } - } - - // Wraps one of the widget's children in a DragTarget and Draggable. - // Handles up the logic for dragging and reordering items in the list. - Widget _wrap(Widget toWrap, int index, BoxConstraints constraints) { - assert(toWrap.key != null); - final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap.key); - // We pass the toWrapWithGlobalKey into the Draggable so that when a list - // item gets dragged, the accessibility framework can preserve the selected - // state of the dragging item. - - // Starts dragging toWrap. - void onDragStarted() { - setState(() { - _dragging = toWrap.key; - _dragStartIndex = index; - _ghostIndex = index; - _currentIndex = index; - _entranceController.value = 1.0; - _draggingFeedbackSize = keyIndexGlobalKey.currentContext.size; - }); - } - - // Places the value from startIndex one space before the element at endIndex. - void reorder(int startIndex, int endIndex) { - setState(() { - if (startIndex != endIndex) - widget.onReorder(startIndex, endIndex); - // Animates leftover space in the drop area closed. - _ghostController.reverse(from: 0.1); - _entranceController.reverse(from: 0.1); - _dragging = null; - }); - } - - // Drops toWrap into the last position it was hovering over. - void onDragEnded() { - reorder(_dragStartIndex, _currentIndex); - } - - Widget wrapWithSemantics() { - // First, determine which semantics actions apply. - final Map semanticsActions = {}; - - // Create the appropriate semantics actions. - void moveToStart() => reorder(index, 0); - void moveToEnd() => reorder(index, widget.children.length); - void moveBefore() => reorder(index, index - 1); - // To move after, we go to index+2 because we are moving it to the space - // before index+2, which is after the space at index+1. - void moveAfter() => reorder(index, index + 2); - - final MaterialLocalizations localizations = MaterialLocalizations.of(context); - - // If the item can move to before its current position in the list. - if (index > 0) { - semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToStart)] = moveToStart; - String reorderItemBefore = localizations.reorderItemUp; - if (widget.scrollDirection == Axis.horizontal) { - reorderItemBefore = Directionality.of(context) == TextDirection.ltr - ? localizations.reorderItemLeft - : localizations.reorderItemRight; - } - semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = moveBefore; - } - - // If the item can move to after its current position in the list. - if (index < widget.children.length - 1) { - String reorderItemAfter = localizations.reorderItemDown; - if (widget.scrollDirection == Axis.horizontal) { - reorderItemAfter = Directionality.of(context) == TextDirection.ltr - ? localizations.reorderItemRight - : localizations.reorderItemLeft; - } - semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = moveAfter; - semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToEnd)] = moveToEnd; - } - - // We pass toWrap with a GlobalKey into the Draggable so that when a list - // item gets dragged, the accessibility framework can preserve the selected - // state of the dragging item. - // - // We also apply the relevant custom accessibility actions for moving the item - // up, down, to the start, and to the end of the list. - return KeyedSubtree( - key: keyIndexGlobalKey, - child: MergeSemantics( - child: Semantics( - customSemanticsActions: semanticsActions, - child: toWrap, - ), - ), - ); - } - - Widget buildDragTarget(BuildContext context, List acceptedCandidates, List rejectedCandidates) { - final Widget toWrapWithSemantics = wrapWithSemantics(); - - // We build the draggable inside of a layout builder so that we can - // constrain the size of the feedback dragging widget. - Widget child = LongPressDraggable( - maxSimultaneousDrags: 1, - axis: widget.scrollDirection, - data: toWrap.key, - ignoringFeedbackSemantics: false, - feedback: Container( - alignment: Alignment.topLeft, - // These constraints will limit the cross axis of the drawn widget. - constraints: constraints, - child: Material( - elevation: 6.0, - child: toWrapWithSemantics, - ), - ), - child: _dragging == toWrap.key ? const SizedBox() : toWrapWithSemantics, - childWhenDragging: const SizedBox(), - dragAnchor: DragAnchor.child, - onDragStarted: onDragStarted, - // When the drag ends inside a DragTarget widget, the drag - // succeeds, and we reorder the widget into position appropriately. - onDragCompleted: onDragEnded, - // When the drag does not end inside a DragTarget widget, the - // drag fails, but we still reorder the widget to the last position it - // had been dragged to. - onDraggableCanceled: (Velocity velocity, Offset offset) { - onDragEnded(); - }, - ); - - // The target for dropping at the end of the list doesn't need to be - // draggable. - if (index >= widget.children.length) { - child = toWrap; - } - - // Determine the size of the drop area to show under the dragging widget. - Widget spacing; - switch (widget.scrollDirection) { - case Axis.horizontal: - spacing = SizedBox(width: _dropAreaExtent); - break; - case Axis.vertical: - default: - spacing = SizedBox(height: _dropAreaExtent); - break; - } - - // We open up a space under where the dragging widget currently is to - // show it can be dropped. - if (_currentIndex == index) { - return _buildContainerForScrollDirection(children: [ - SizeTransition( - sizeFactor: _entranceController, - axis: widget.scrollDirection, - child: spacing, - ), - child, - ]); - } - // We close up the space under where the dragging widget previously was - // with the ghostController animation. - if (_ghostIndex == index) { - return _buildContainerForScrollDirection(children: [ - SizeTransition( - sizeFactor: _ghostController, - axis: widget.scrollDirection, - child: spacing, - ), - child, - ]); - } - return child; - } - - // We wrap the drag target in a Builder so that we can scroll to its specific context. - return Builder(builder: (BuildContext context) { - return DragTarget( - builder: buildDragTarget, - onWillAccept: (Key toAccept) { - setState(() { - _nextIndex = index; - _requestAnimationToNextIndex(); - }); - _scrollTo(context); - // If the target is not the original starting point, then we will accept the drop. - return _dragging == toAccept && toAccept != toWrap.key; - }, - onAccept: (Key accepted) { }, - onLeave: (Object leaving) { }, - ); - }); - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterialLocalizations(context)); - // We use the layout builder to constrain the cross-axis size of dragging child widgets. - return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - const Key endWidgetKey = Key('DraggableList - End Widget'); - Widget finalDropArea; - switch (widget.scrollDirection) { - case Axis.horizontal: - finalDropArea = SizedBox( - key: endWidgetKey, - width: _defaultDropAreaExtent, - height: constraints.maxHeight, - ); - break; - case Axis.vertical: - default: - finalDropArea = SizedBox( - key: endWidgetKey, - height: _defaultDropAreaExtent, - width: constraints.maxWidth, - ); - break; - } - return SingleChildScrollView( - scrollDirection: widget.scrollDirection, - padding: widget.padding, - controller: _scrollController, - reverse: widget.reverse, - physics: AlwaysScrollableScrollPhysics(), - child: _buildContainerForScrollDirection( - children: [ - if (widget.reverse) _wrap(finalDropArea, widget.children.length, constraints), - if (widget.header != null) widget.header, - for (int i = 0; i < widget.children.length; i += 1) _wrap(widget.children[i], i, constraints), - if (!widget.reverse) _wrap(finalDropArea, widget.children.length, constraints), - ], - ), - ); - }); - } -} diff --git a/lib/core/ui/deprecated/listview/listview_stickyheader.dart b/lib/core/ui/deprecated/listview/listview_stickyheader.dart deleted file mode 100644 index a879ee24..00000000 --- a/lib/core/ui/deprecated/listview/listview_stickyheader.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class LSListViewStickyHeader extends StatelessWidget { - final List slivers; - final ScrollController controller; - final EdgeInsets padding; - - LSListViewStickyHeader({ - @required this.slivers, - @required this.controller, - this.padding, - }); - - @override - Widget build(BuildContext context) => Scrollbar( - controller: controller, - child: Container( - child: CustomScrollView( - controller: controller, - slivers: [ - ...slivers, - ], - physics: AlwaysScrollableScrollPhysics(), - ), - padding: padding != null ? padding : EdgeInsets.only( - top: 0.0, - bottom: (MediaQuery.of(context).padding.bottom), - ), - ), - ); -} diff --git a/lib/core/ui/deprecated/loader.dart b/lib/core/ui/deprecated/loader.dart deleted file mode 100644 index 63d0e569..00000000 --- a/lib/core/ui/deprecated/loader.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; -import 'package:lunasea/core.dart'; - -class LSLoader extends StatelessWidget { - final double size; - final Color color; - - LSLoader({ - Key key, - this.size = 25.0, - this.color, - }): super(key: key); - - @override - Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SpinKitThreeBounce( - color: color != null ? color : LunaColours.accent, - size: size, - ), - ], - ); -} diff --git a/lib/core/ui/deprecated/messages.dart b/lib/core/ui/deprecated/messages.dart deleted file mode 100644 index 80c6bf0f..00000000 --- a/lib/core/ui/deprecated/messages.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'messages/error_message.dart'; -export 'messages/generic.dart'; -export 'messages/not_enabled.dart'; diff --git a/lib/core/ui/deprecated/messages/error_message.dart b/lib/core/ui/deprecated/messages/error_message.dart deleted file mode 100644 index 93e6f1c8..00000000 --- a/lib/core/ui/deprecated/messages/error_message.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; -import './generic.dart'; - -class LSErrorMessage extends StatelessWidget { - final Function onTapHandler; - final bool hideButton; - final String message; - final String btnMessage; - - LSErrorMessage({ - Key key, - @required this.onTapHandler, - this.message = 'An Error Has Occurred', - this.btnMessage = 'Try Again', - this.hideButton = false, - }) : super(key: key); - - @override - Widget build(BuildContext context) => LSGenericMessage( - text: message, - showButton: !hideButton, - buttonText: btnMessage, - onTapHandler: onTapHandler, - ); -} diff --git a/lib/core/ui/deprecated/messages/generic.dart b/lib/core/ui/deprecated/messages/generic.dart deleted file mode 100644 index e10e9b45..00000000 --- a/lib/core/ui/deprecated/messages/generic.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSGenericMessage extends StatelessWidget { - final String text; - final String buttonText; - final bool showButton; - final Function onTapHandler; - - LSGenericMessage({ - Key key, - @required this.text, - this.showButton = false, - this.buttonText = '', - this.onTapHandler, - }) : super(key: key); - - @override - Widget build(BuildContext context) => ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ - LunaDatabaseValue.THEME_AMOLED.key, - LunaDatabaseValue.THEME_AMOLED_BORDER.key, - ]), - builder: (context, box, child) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Card( - child: child, - margin: LunaUI.MARGIN_CARD, - elevation: LunaUI.ELEVATION, - shape: LunaUI.shapeBorder, - ), - if(showButton) LunaButtonContainer( - children: [ - LunaButton.text( - text: buttonText, - onTap: onTapHandler, - backgroundColor: LunaColours.accent, - ), - ], - ), - ], - ), - child: Row( - children: [ - Expanded( - child: Container( - child: Text( - text, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_TITLE, - ), - ), - margin: EdgeInsets.symmetric(vertical: 24.0, horizontal: 12.0), - ), - ), - ], - ), - ); -} diff --git a/lib/core/ui/deprecated/messages/not_enabled.dart b/lib/core/ui/deprecated/messages/not_enabled.dart deleted file mode 100644 index 6e45cc06..00000000 --- a/lib/core/ui/deprecated/messages/not_enabled.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSNotEnabled extends StatelessWidget { - final String service; - final bool showButton; - - LSNotEnabled(this.service, { - this.showButton = true, - }); - - @override - Widget build(BuildContext context) => LSGenericMessage( - text: service == Constants.NO_SERVICES_ENABLED - ? 'No Modules Enabled' - : '$service Is Not Enabled', - showButton: showButton, - buttonText: 'Return to Dashboard', - onTapHandler: () async => await Navigator.of(context).pushNamedAndRemoveUntil('/', (Route route) => false), - ); -} diff --git a/lib/core/ui/deprecated/navigation_bar.dart b/lib/core/ui/deprecated/navigation_bar.dart deleted file mode 100644 index 874b8b6b..00000000 --- a/lib/core/ui/deprecated/navigation_bar.dart +++ /dev/null @@ -1 +0,0 @@ -export 'navigation_bar/navigation_bar.dart'; diff --git a/lib/core/ui/deprecated/navigation_bar/navigation_bar.dart b/lib/core/ui/deprecated/navigation_bar/navigation_bar.dart deleted file mode 100644 index d6bfe921..00000000 --- a/lib/core/ui/deprecated/navigation_bar/navigation_bar.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:google_nav_bar/google_nav_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSBottomNavigationBar extends StatelessWidget { - final int index; - final Function onTap; - final List icons; - final List titles; - final List leadingOnGButton; - - LSBottomNavigationBar({ - @required this.index, - @required this.onTap, - @required this.icons, - @required this.titles, - this.leadingOnGButton, - }); - - @override - Widget build(BuildContext context) => Container( - child: SafeArea( - top: false, - child: Padding( - child: GNav( - gap: 8.0, - iconSize: 24.0, - padding: EdgeInsets.fromLTRB(18.0, 10.0, 12.0, 10.0), - duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), - tabBackgroundColor: Theme.of(context).canvasColor, - activeColor: LunaColours.accent, - tabs: List.generate( - icons.length, - (index) => GButton( - icon: icons[index], - text: titles[index], - iconSize: 22.0, - haptic: true, - iconColor: Colors.white, - textStyle: TextStyle( - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: LunaColours.accent, - ), - leading: leadingOnGButton == null ? null : leadingOnGButton[index], - ) - ).toList(), - tabActiveBorder: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data ? Border.all(color: Colors.white12) : null, - selectedIndex: index, - onTabChange: onTap, - ), - padding: EdgeInsets.all(12.0), - ), - ), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - ), - ); -} diff --git a/lib/core/ui/deprecated/network_image.dart b/lib/core/ui/deprecated/network_image.dart deleted file mode 100644 index e517614d..00000000 --- a/lib/core/ui/deprecated/network_image.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -// ignore: non_constant_identifier_names -Widget LSNetworkImage({ - @required double height, - @required double width, - @required String url, - @required String placeholder, - Map headers, - bool roundCorners = true, -}) => ClipRRect( - child: Container( - child: Stack( - fit: StackFit.expand, - children: [ - Image.asset(placeholder), - if(url != null && url.isNotEmpty) FadeInImage( - height: height, - width: width, - fadeInDuration: Duration(milliseconds: LunaUI.ANIMATION_IMAGE_FADE_IN_SPEED), - fadeOutDuration: Duration(milliseconds: 1), - placeholder: AssetImage(placeholder), - fit: BoxFit.cover, - image: NetworkImage( - url, - headers: headers, - ), - imageErrorBuilder: (context, error, stack) => Container( - height: height, - width: width, - ), - ), - ], - ), - height: height, - width: width, - ), - borderRadius: roundCorners - ? BorderRadius.circular(Constants.UI_BORDER_RADIUS) - : null, -); \ No newline at end of file diff --git a/lib/core/ui/deprecated/refresh_indicator.dart b/lib/core/ui/deprecated/refresh_indicator.dart deleted file mode 100644 index a9f344ce..00000000 --- a/lib/core/ui/deprecated/refresh_indicator.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -class LSRefreshIndicator extends StatefulWidget { - final Widget child; - final Function onRefresh; - final GlobalKey refreshKey; - - LSRefreshIndicator({ - @required this.child, - @required this.onRefresh, - @required this.refreshKey, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) { - return RefreshIndicator( - key: widget.refreshKey, - backgroundColor: Theme.of(context).primaryColor, - onRefresh: widget.onRefresh, - child: widget.child, - ); - } -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/snackbar.dart b/lib/core/ui/deprecated/snackbar.dart deleted file mode 100644 index cf1d5b6c..00000000 --- a/lib/core/ui/deprecated/snackbar.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flash/flash.dart'; -import 'package:lunasea/core.dart'; - -enum SNACKBAR_TYPE { - success, - failure, - info, -} - -const _LEFT_PADDING = 8.0; - -// ignore: non_constant_identifier_names -Future LSSnackBar({ - @required BuildContext context, - @required String title, - String message, - Duration duration, - SNACKBAR_TYPE type = SNACKBAR_TYPE.info, - bool showButton = false, - String buttonText = 'view', - Function buttonOnPressed, -}) async { - if(showButton) assert(buttonOnPressed != null); - Color color; - IconData icon; - switch(type) { - case SNACKBAR_TYPE.failure: color = LunaColours.red; icon = Icons.error_outline; break; - case SNACKBAR_TYPE.success: color = LunaColours.accent; icon = Icons.check_circle_outline; break; - case SNACKBAR_TYPE.info: color = LunaColours.blue; icon = Icons.info_outline; break; - } - showFlash( - context: context, - duration: duration != null - ? duration - : showButton - ? Duration(seconds: 4) - : Duration(seconds: 2), - builder: (context, controller) => Flash( - backgroundColor: Theme.of(context).primaryColor, - controller: controller, - style: FlashStyle.floating, - boxShadows: [BoxShadow(blurRadius: 6.0, spreadRadius: 4.0, color: Colors.black.withOpacity(0.10))], - horizontalDismissDirection: HorizontalDismissDirection.horizontal, - margin: EdgeInsets.all(8.0), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - borderColor: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? Colors.white12 - : Colors.transparent, - child: FlashBar( - title: Text( - title, - style: TextStyle( - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_TITLE, - ), - ), - message: Text( - message ?? LunaLogger.checkLogsMessage, - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - icon: Padding( - child: LSIconButton( - icon: icon, - color: color, - ), - padding: EdgeInsets.only(left: _LEFT_PADDING), - ), - primaryAction: showButton - ? TextButton( - child: Text( - buttonText.toUpperCase(), - style: TextStyle( - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - color: LunaColours.accent, - ), - ), - onPressed: () { - controller.dismiss(); - buttonOnPressed(); - }, - ) - : null, - ), - ), - ); -} diff --git a/lib/core/ui/deprecated/table.dart b/lib/core/ui/deprecated/table.dart deleted file mode 100644 index 59e9bf2b..00000000 --- a/lib/core/ui/deprecated/table.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'table/block.dart'; -export 'table/content.dart'; diff --git a/lib/core/ui/deprecated/table/block.dart b/lib/core/ui/deprecated/table/block.dart deleted file mode 100644 index e09c418a..00000000 --- a/lib/core/ui/deprecated/table/block.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSTableBlock extends StatelessWidget { - final String title; - final List children; - - LSTableBlock({ - Key key, - this.title, - @required this.children, - }) : super(key: key); - - List _block({ - String title, - @required List children, - }) => [ - if(title != null) LSHeader(text: title), - LSCard( - child: Padding( - child: Column( - children: children, - ), - padding: EdgeInsets.symmetric(vertical: 8.0), - ), - ), - ]; - - @override - Widget build(BuildContext context) => Column( - children: _block(title: title, children: children), - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - ); -} diff --git a/lib/core/ui/deprecated/table/content.dart b/lib/core/ui/deprecated/table/content.dart deleted file mode 100644 index de2eef67..00000000 --- a/lib/core/ui/deprecated/table/content.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSTableContent extends StatelessWidget { - final String title; - final String body; - final TextAlign titleAlign; - final TextAlign bodyAlign; - final int titleFlex; - final int bodyFlex; - final EdgeInsets padding; - final Color titleColour; - final Color bodyColour; - - LSTableContent({ - Key key, - @required this.title, - @required this.body, - this.titleAlign = TextAlign.end, - this.bodyAlign = TextAlign.start, - this.titleFlex = 2, - this.bodyFlex = 5, - this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - this.titleColour = Colors.white70, - this.bodyColour = Colors.white, - }) : super(key: key); - - @override - Widget build(BuildContext context) => Padding( - child: Row( - children: [ - Expanded( - child: Text( - title.toUpperCase(), - textAlign: titleAlign, - style: TextStyle( - color: titleColour, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - flex: titleFlex, - ), - Container(width: 16.0, height: 0.0), - Expanded( - child: Text( - body, - textAlign: bodyAlign, - style: TextStyle( - color: bodyColour, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - flex: bodyFlex, - ), - ], - crossAxisAlignment: CrossAxisAlignment.start, - ), - padding: padding, - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/text.dart b/lib/core/ui/deprecated/text.dart deleted file mode 100644 index 271a8863..00000000 --- a/lib/core/ui/deprecated/text.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'text/header.dart'; -export 'text/text_highlighted.dart'; -export 'text/text_title.dart'; -export 'text/text_subtitle.dart'; diff --git a/lib/core/ui/deprecated/text/header.dart b/lib/core/ui/deprecated/text/header.dart deleted file mode 100644 index 07ca0df0..00000000 --- a/lib/core/ui/deprecated/text/header.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSHeader extends StatelessWidget { - final String text; - final String subtitle; - - LSHeader({ - @required this.text, - this.subtitle, - }); - - @override - Widget build(BuildContext context) => Padding( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - text.toUpperCase(), - style: TextStyle( - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_HEADER, - color: Colors.white, - ), - ), - Padding( - child: Container( - height: 2.0, - width: 48.0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - color: LunaColours.accent, - ), - ), - padding: EdgeInsets.only( - top: 4.0, - left: 1.0, - bottom: subtitle != null - ? 6.0 - : 0.0, - ), - ), - if(subtitle != null) Text( - subtitle, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBHEADER, - color: Colors.white70, - fontWeight: FontWeight.w300, - ), - ), - ], - ), - padding: EdgeInsets.fromLTRB(12.0, 6.0, 12.0, 8.0), - ); -} diff --git a/lib/core/ui/deprecated/text/text_highlighted.dart b/lib/core/ui/deprecated/text/text_highlighted.dart deleted file mode 100644 index 77c0802f..00000000 --- a/lib/core/ui/deprecated/text/text_highlighted.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSTextHighlighted extends StatelessWidget { - final Color bgColor; - final Color fgColor; - final String text; - final EdgeInsets margin; - - LSTextHighlighted({ - @required this.text, - this.bgColor = LunaColours.accent, - this.fgColor = Colors.white, - this.margin = const EdgeInsets.only(right: 8.0), - }); - - @override - Widget build(BuildContext context) => Padding( - child: Container( - child: Padding( - child: Text( - text, - maxLines: 1, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBHEADER, - color: fgColor, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - ), - ), - padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - ), - - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - ), - ), - padding: margin, - ); -} diff --git a/lib/core/ui/deprecated/text/text_subtitle.dart b/lib/core/ui/deprecated/text/text_subtitle.dart deleted file mode 100644 index 91cf3baa..00000000 --- a/lib/core/ui/deprecated/text/text_subtitle.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSSubtitle extends StatelessWidget { - final String text; - final int maxLines; - final bool darken; - final bool centerText; - final bool softWrap; - final TextOverflow overflow; - - LSSubtitle({ - @required this.text, - this.maxLines = 1, - this.darken = false, - this.centerText = false, - this.softWrap = false, - this.overflow = TextOverflow.fade, - }); - - - @override - Widget build(BuildContext context) => Text( - text, - overflow: overflow, - softWrap: softWrap, - maxLines: maxLines, - textAlign: centerText - ? TextAlign.center - : TextAlign.start, - style: TextStyle( - color: darken ? Colors.white30 : Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ); -} diff --git a/lib/core/ui/deprecated/text/text_title.dart b/lib/core/ui/deprecated/text/text_title.dart deleted file mode 100644 index 1562f2dc..00000000 --- a/lib/core/ui/deprecated/text/text_title.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LSTitle extends StatelessWidget { - final String text; - final int maxLines; - final bool darken; - final bool centerText; - final bool softWrap; - - LSTitle({ - @required this.text, - this.maxLines = 1, - this.softWrap = false, - this.darken = false, - this.centerText = false, - }); - - @override - Widget build(BuildContext context) => Text( - text, - overflow: TextOverflow.fade, - softWrap: softWrap, - maxLines: maxLines, - textAlign: centerText - ? TextAlign.center - : TextAlign.start, - style: TextStyle( - color: darken ? Colors.white30 : Colors.white, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_TITLE, - ), - ); -} \ No newline at end of file diff --git a/lib/core/ui/deprecated/dialogs.dart b/lib/core/ui/dialog.dart similarity index 87% rename from lib/core/ui/deprecated/dialogs.dart rename to lib/core/ui/dialog.dart index 78e35ada..a99ddf3b 100644 --- a/lib/core/ui/deprecated/dialogs.dart +++ b/lib/core/ui/dialog.dart @@ -2,22 +2,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; -abstract class LSDialog { - static const HEADER_SIZE = Constants.UI_FONT_SIZE_HEADER; - static const BODY_SIZE = Constants.UI_FONT_SIZE_SUBTITLE; - static const SUBBODY_SIZE = Constants.UI_FONT_SIZE_SUBHEADER; - static const BUTTON_SIZE = Constants.UI_FONT_SIZE_SUBHEADER; +abstract class LunaDialog { + static const HEADER_SIZE = LunaUI.FONT_SIZE_HEADER; + static const BODY_SIZE = LunaUI.FONT_SIZE_SUBTITLE; + static const SUBBODY_SIZE = LunaUI.FONT_SIZE_SUBHEADER; + static const BUTTON_SIZE = LunaUI.FONT_SIZE_SUBHEADER; static Widget title({ @required String text }) => Text( text, textAlign: TextAlign.center, style: TextStyle( - fontSize: LSDialog.HEADER_SIZE, + fontSize: LunaDialog.HEADER_SIZE, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ); - static TextSpan bolded({ @required String text, double fontSize = LSDialog.BODY_SIZE, Color color }) => TextSpan( + static TextSpan bolded({ @required String text, double fontSize = LunaDialog.BODY_SIZE, Color color }) => TextSpan( text: text, style: TextStyle( color: color == null @@ -31,7 +31,7 @@ abstract class LSDialog { static Widget richText({ @required List children, TextAlign alignment = TextAlign.start }) => RichText( text: TextSpan( style: TextStyle( - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, ), children: children, ), @@ -45,7 +45,7 @@ abstract class LSDialog { color: textColor == null ? LunaColours.accent : textColor, - fontSize: LSDialog.BUTTON_SIZE, + fontSize: LunaDialog.BUTTON_SIZE, ), ), onPressed: onPressed == null ? null : () async { @@ -59,7 +59,7 @@ abstract class LSDialog { text ?? 'lunasea.Cancel'.tr(), style: TextStyle( color: textColor, - fontSize: LSDialog.BUTTON_SIZE, + fontSize: LunaDialog.BUTTON_SIZE, ), ), onPressed: () { @@ -79,7 +79,7 @@ abstract class LSDialog { text, style: TextStyle( color: Colors.white, - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, ), textAlign: textAlign, ); @@ -88,7 +88,7 @@ abstract class LSDialog { text: text, style: TextStyle( color: Colors.white, - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, ), ); @@ -105,7 +105,7 @@ abstract class LSDialog { labelStyle: TextStyle( color: Colors.white54, decoration: TextDecoration.none, - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, ), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: LunaColours.accent), @@ -116,7 +116,7 @@ abstract class LSDialog { ), style: TextStyle( color: Colors.white, - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, ), cursorColor: LunaColours.accent, textInputAction: TextInputAction.done, @@ -141,7 +141,7 @@ abstract class LSDialog { labelStyle: TextStyle( color: Colors.white54, decoration: TextDecoration.none, - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, ), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: LunaColours.accent), @@ -152,7 +152,7 @@ abstract class LSDialog { ), style: TextStyle( color: Colors.white, - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, ), cursorColor: LunaColours.accent, textInputAction: TextInputAction.done, @@ -179,7 +179,7 @@ abstract class LSDialog { title: Text( text, style: TextStyle( - fontSize: LSDialog.BODY_SIZE, + fontSize: LunaDialog.BODY_SIZE, color: Colors.white, ), ), @@ -231,15 +231,15 @@ abstract class LSDialog { context: context, builder: (context) => AlertDialog( actions: [ - if(showCancelButton) LSDialog.cancel( + if(showCancelButton) LunaDialog.cancel( context, text: cancelButtonText, textColor: buttons != null ? Colors.white : LunaColours.accent, ), if(buttons != null) ...buttons, ], - title: LSDialog.title(text: title), - content: customContent ?? LSDialog.content(children: content), + title: LunaDialog.title(text: title), + content: customContent ?? LunaDialog.content(children: content), contentPadding: contentPadding, shape: LunaUI.shapeBorder, ), diff --git a/lib/core/ui/drawer/drawer.dart b/lib/core/ui/drawer/drawer.dart index 2ceface8..9dfc207c 100644 --- a/lib/core/ui/drawer/drawer.dart +++ b/lib/core/ui/drawer/drawer.dart @@ -68,7 +68,7 @@ class LunaDrawer extends StatelessWidget { color: currentPage ? LunaColours.accent : Colors.white, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), onTap: () async { @@ -86,21 +86,19 @@ class LunaDrawer extends StatelessWidget { LunaModule.WAKE_ON_LAN.name, style: TextStyle( color: Colors.white, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), onTap: () async { WakeOnLANAPI api = WakeOnLANAPI.fromProfile(); await api.wake() .then((_) => showLunaSuccessSnackBar( - context: context, title: 'Machine is Waking Up...', message: 'Magic packet successfully sent', )) .catchError((error, stack) { LunaLogger().error('Failed to wake machine', error, stack); return showLunaErrorSnackBar( - context: context, title: 'Failed to Wake Machine', error: error, ); diff --git a/lib/core/ui/deprecated/floating_action_button.dart b/lib/core/ui/floating_action_button.dart similarity index 69% rename from lib/core/ui/deprecated/floating_action_button.dart rename to lib/core/ui/floating_action_button.dart index c19a3b46..2c7cfc41 100644 --- a/lib/core/ui/deprecated/floating_action_button.dart +++ b/lib/core/ui/floating_action_button.dart @@ -1,3 +1,2 @@ -export 'floating_action_button/floating_action_button.dart'; export 'floating_action_button/floating_action_button_animated.dart'; export 'floating_action_button/floating_action_button_extended.dart'; diff --git a/lib/core/ui/floating_action_button/floating_action_button_animated.dart b/lib/core/ui/floating_action_button/floating_action_button_animated.dart new file mode 100644 index 00000000..31419f13 --- /dev/null +++ b/lib/core/ui/floating_action_button/floating_action_button_animated.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaFloatingActionButtonAnimated extends StatelessWidget { + final Object heroTag; + final AnimatedIconData icon; + final AnimationController controller; + final Color color; + final Color backgroundColor; + final Function onPressed; + + LunaFloatingActionButtonAnimated({ + Key key, + @required this.onPressed, + @required this.icon, + @required this.controller, + this.backgroundColor = LunaColours.accent, + this.color = Colors.white, + this.heroTag, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + child: AnimatedIcon( + icon: icon, + color: color, + progress: controller, + ), + heroTag: heroTag, + onPressed: onPressed, + backgroundColor: backgroundColor, + ); + } +} \ No newline at end of file diff --git a/lib/core/ui/floating_action_button/floating_action_button_extended.dart b/lib/core/ui/floating_action_button/floating_action_button_extended.dart new file mode 100644 index 00000000..d7097eb4 --- /dev/null +++ b/lib/core/ui/floating_action_button/floating_action_button_extended.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaFloatingActionButtonExtended extends StatelessWidget { + final Color color; + final Color backgroundColor; + final IconData icon; + final String label; + final void Function() onPressed; + final Object heroTag; + + LunaFloatingActionButtonExtended({ + Key key, + @required this.icon, + @required this.label, + @required this.onPressed, + this.backgroundColor = LunaColours.accent, + this.color = Colors.white, + this.heroTag, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FloatingActionButton.extended( + icon: Icon(icon, color: color), + onPressed: onPressed, + label: Text( + label, + style: TextStyle( + color: color, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + letterSpacing: 0.35, + ), + ), + backgroundColor: backgroundColor, + heroTag: heroTag, + ); + } +} diff --git a/lib/core/ui/input_bar.dart b/lib/core/ui/input_bar.dart index a6510b4a..3610464c 100644 --- a/lib/core/ui/input_bar.dart +++ b/lib/core/ui/input_bar.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; class LunaTextInputBar extends StatefulWidget { - static const EdgeInsets preferredSizeWidgetMargin = const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0); + static const double appBarHeight = 62.0; + static const EdgeInsets appBarMargin = const EdgeInsets.fromLTRB(12.0, 1.0, 12.0, 12.0); + final TextEditingController controller; + final ScrollController scrollController; final TextInputAction action; final TextInputType keyboardType; final Iterable autofillHints; @@ -19,6 +22,7 @@ class LunaTextInputBar extends StatefulWidget { LunaTextInputBar({ @required this.controller, + this.scrollController, this.onChanged, this.onSubmitted, this.validator, @@ -43,7 +47,8 @@ class _State extends State { bool _isFocused = false; @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, margin: widget.margin, child: FocusScope( child: Focus( @@ -56,7 +61,7 @@ class _State extends State { TextStyle get _sharedTextStyle => TextStyle( color: Colors.white, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ); InputDecoration get _sharedInputDecoration => InputDecoration( @@ -64,7 +69,7 @@ class _State extends State { labelStyle: TextStyle( color: Colors.white54, decoration: TextDecoration.none, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), suffixIcon: AnimatedOpacity( child: GestureDetector( @@ -74,6 +79,7 @@ class _State extends State { size: 24.0, ), onTap: widget.onChanged == null ? null : () { + widget.scrollController?.lunaAnimateToStart(); widget.controller.text = ''; widget.onChanged(''); } @@ -104,6 +110,7 @@ class _State extends State { autocorrect: false, keyboardType: widget.keyboardType, validator: widget?.validator, + onTap: widget.scrollController?.lunaAnimateToStart, onChanged: widget.onChanged == null ? null : widget.onChanged, onFieldSubmitted: widget?.onSubmitted, ); @@ -119,6 +126,7 @@ class _State extends State { obscureText: widget.obscureText, autocorrect: false, keyboardType: widget.keyboardType, + onTap: widget.scrollController?.lunaAnimateToStart, onChanged: widget.onChanged == null ? null : widget.onChanged, onSubmitted: widget?.onSubmitted, ); diff --git a/lib/core/ui/list_view.dart b/lib/core/ui/list_view.dart index 58da96dc..d69e9519 100644 --- a/lib/core/ui/list_view.dart +++ b/lib/core/ui/list_view.dart @@ -1,4 +1,7 @@ export 'list_view/list_view.dart'; export 'list_view/list_view_builder.dart'; export 'list_view/list_view_modal.dart'; +export 'list_view/list_view_modal_builder.dart'; export 'list_view/paged_list_view.dart'; +export 'list_view/reorderable_list_view.dart'; +export 'list_view/reorderable_list_view_builder.dart'; diff --git a/lib/core/ui/list_view/list_view.dart b/lib/core/ui/list_view/list_view.dart index ce020d0b..95d8c323 100644 --- a/lib/core/ui/list_view/list_view.dart +++ b/lib/core/ui/list_view/list_view.dart @@ -18,23 +18,15 @@ class LunaListView extends StatelessWidget { @override Widget build(BuildContext context) { - return NotificationListener( - onNotification: (notification) { - if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus(); - return null; - }, - child: Scrollbar( + return Scrollbar( + controller: controller, + child: ListView( controller: controller, - child: ListView( - controller: controller, - children: children, - itemExtent: itemExtent, - padding: padding != null ? padding : EdgeInsets.only( - top: 8.0, - bottom: 8.0+(MediaQuery.of(context).padding.bottom), - ), - physics: physics, - ), + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + children: children, + itemExtent: itemExtent, + padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)), + physics: physics, ), ); } diff --git a/lib/core/ui/list_view/list_view_builder.dart b/lib/core/ui/list_view/list_view_builder.dart index f2368c88..3c1c7df8 100644 --- a/lib/core/ui/list_view/list_view_builder.dart +++ b/lib/core/ui/list_view/list_view_builder.dart @@ -23,24 +23,16 @@ class LunaListViewBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return NotificationListener( - onNotification: (notification) { - if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus(); - return null; - }, - child: Scrollbar( + return Scrollbar( + controller: controller, + child: ListView.builder( controller: controller, - child: ListView.builder( - controller: controller, - padding: padding != null ? padding : EdgeInsets.only( - top: 8.0, - bottom: 8.0+(MediaQuery.of(context).padding.bottom), - ), - physics: physics, - itemCount: itemCount, - itemBuilder: itemBuilder, - itemExtent: itemExtent, - ), + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)), + physics: physics, + itemCount: itemCount, + itemBuilder: itemBuilder, + itemExtent: itemExtent, ), ); } diff --git a/lib/core/ui/list_view/list_view_modal.dart b/lib/core/ui/list_view/list_view_modal.dart index 10ecc262..85703878 100644 --- a/lib/core/ui/list_view/list_view_modal.dart +++ b/lib/core/ui/list_view/list_view_modal.dart @@ -1,14 +1,21 @@ import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +/// [ListView] with a [Scrollbar] that is intended to be used with a modal bottom sheet. +/// +/// The [ScrollController] is pulled from the context from the sheet, and cannot be defined. +/// +/// By default, the list is shrink-wrapped. class LunaListViewModal extends StatelessWidget { final List children; final double itemExtent; + final bool shrinkWrap; LunaListViewModal({ Key key, @required this.children, this.itemExtent, + this.shrinkWrap = true, }) : super(key: key); @override @@ -17,12 +24,11 @@ class LunaListViewModal extends StatelessWidget { controller: ModalScrollController.of(context), child: ListView( controller: ModalScrollController.of(context), + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, children: children, itemExtent: itemExtent, - padding: EdgeInsets.only( - top: 8.0, - bottom: 8.0+(MediaQuery.of(context).padding.bottom), - ), + shrinkWrap: shrinkWrap, + padding: MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)), physics: AlwaysScrollableScrollPhysics(), ), ); diff --git a/lib/core/ui/list_view/list_view_modal_builder.dart b/lib/core/ui/list_view/list_view_modal_builder.dart new file mode 100644 index 00000000..7e5c876b --- /dev/null +++ b/lib/core/ui/list_view/list_view_modal_builder.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +/// [ListView] builder with a [Scrollbar] that is intended to be used with a modal bottom sheet. +/// +/// The [ScrollController] is pulled from the context from the sheet, and cannot be defined. +/// +/// By default, the list is shrink-wrapped. +class LunaListViewModalBuilder extends StatelessWidget { + final int itemCount; + final Widget Function(BuildContext, int) itemBuilder; + final double itemExtent; + final bool shrinkWrap; + + LunaListViewModalBuilder({ + Key key, + @required this.itemCount, + @required this.itemBuilder, + this.itemExtent, + this.shrinkWrap = true, + }) : super(key: key) { + assert(itemCount != null); + assert(itemBuilder != null); + } + + @override + Widget build(BuildContext context) { + return Scrollbar( + controller: ModalScrollController.of(context), + child: ListView.builder( + controller: ModalScrollController.of(context), + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + padding: MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)), + physics: AlwaysScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: itemBuilder, + itemExtent: itemExtent, + shrinkWrap: shrinkWrap, + ), + ); + } +} diff --git a/lib/core/ui/list_view/paged_list_view.dart b/lib/core/ui/list_view/paged_list_view.dart index e09b45a2..38eebf00 100644 --- a/lib/core/ui/list_view/paged_list_view.dart +++ b/lib/core/ui/list_view/paged_list_view.dart @@ -42,50 +42,45 @@ class _State extends State> { @override Widget build(BuildContext context) { - return NotificationListener( - onNotification: (notification) { - if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus(); - return null; - }, - child: LunaRefreshIndicator( - key: widget.refreshKey, - context: context, - onRefresh: () => Future.sync(() => widget.pagingController.refresh()), - child: Scrollbar( - controller: widget.scrollController, - child: PagedListView( - pagingController: widget.pagingController, - scrollController: widget.scrollController, - itemExtent: widget.itemExtent, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: widget.itemBuilder, - firstPageErrorIndicatorBuilder: (context) => LunaMessage.error(onTap: () => Future.sync(() => widget.pagingController.refresh())), - firstPageProgressIndicatorBuilder: (context) => LunaLoader(), - newPageProgressIndicatorBuilder: (context) => Padding( - child: Container( - alignment: Alignment.center, - height: 40.0, - child: LunaLoader(size: 16.0, useSafeArea: false), - ), - padding: EdgeInsets.only(bottom: 16.0), - ), - newPageErrorIndicatorBuilder: (context) => LunaIconButton(icon: Icons.error, color: LunaColours.red), - noItemsFoundIndicatorBuilder: (context) => LunaMessage( - text: widget.noItemsFoundMessage, - buttonText: 'lunasea.Refresh'.tr(), - onTap: () => Future.sync(() => widget.pagingController.refresh()), - ), - noMoreItemsIndicatorBuilder: (context) => Padding( - child: LunaIconButton( - icon: Icons.check, - color: LunaColours.accent, - ), - padding: EdgeInsets.only(bottom: 16.0), + return LunaRefreshIndicator( + key: widget.refreshKey, + context: context, + onRefresh: () => Future.sync(() => widget.pagingController.refresh()), + child: Scrollbar( + controller: widget.scrollController, + child: PagedListView( + pagingController: widget.pagingController, + scrollController: widget.scrollController, + itemExtent: widget.itemExtent, + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: widget.itemBuilder, + firstPageErrorIndicatorBuilder: (context) => LunaMessage.error(onTap: () => Future.sync(() => widget.pagingController.refresh())), + firstPageProgressIndicatorBuilder: (context) => LunaLoader(), + newPageProgressIndicatorBuilder: (context) => Padding( + child: Container( + alignment: Alignment.center, + height: 40.0, + child: LunaLoader(size: 16.0, useSafeArea: false), ), + padding: EdgeInsets.only(bottom: 16.0), + ), + newPageErrorIndicatorBuilder: (context) => LunaIconButton(icon: Icons.error, color: LunaColours.red), + noItemsFoundIndicatorBuilder: (context) => LunaMessage( + text: widget.noItemsFoundMessage, + buttonText: 'lunasea.Refresh'.tr(), + onTap: () => Future.sync(() => widget.pagingController.refresh()), + ), + noMoreItemsIndicatorBuilder: (context) => Padding( + child: LunaIconButton( + icon: Icons.check, + color: LunaColours.accent, + ), + padding: EdgeInsets.only(bottom: 16.0), ), - padding: widget.padding != null ? widget.padding : EdgeInsets.only(top: 8.0), - physics: AlwaysScrollableScrollPhysics(), ), + padding: widget.padding ?? EdgeInsets.only(top: 8.0), + physics: AlwaysScrollableScrollPhysics(), ), ), ); diff --git a/lib/core/ui/list_view/reorderable_list_view.dart b/lib/core/ui/list_view/reorderable_list_view.dart new file mode 100644 index 00000000..0280ca3f --- /dev/null +++ b/lib/core/ui/list_view/reorderable_list_view.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class LunaReorderableListView extends StatelessWidget { + final List children; + final EdgeInsetsGeometry padding; + final ScrollPhysics physics; + final ScrollController controller; + final void Function(int, int) onReorder; + + LunaReorderableListView({ + Key key, + @required this.children, + @required this.controller, + @required this.onReorder, + this.padding, + this.physics = const AlwaysScrollableScrollPhysics(), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scrollbar( + controller: controller, + child: ReorderableListView( + scrollController: controller, + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + children: children, + padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)), + physics: physics, + onReorder: onReorder, + ), + ); + } +} \ No newline at end of file diff --git a/lib/core/ui/list_view/reorderable_list_view_builder.dart b/lib/core/ui/list_view/reorderable_list_view_builder.dart new file mode 100644 index 00000000..afedafc9 --- /dev/null +++ b/lib/core/ui/list_view/reorderable_list_view_builder.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class LunaReorderableListViewBuilder extends StatelessWidget { + final int itemCount; + final Widget Function(BuildContext, int) itemBuilder; + final EdgeInsetsGeometry padding; + final ScrollPhysics physics; + final ScrollController controller; + final void Function(int, int) onReorder; + + LunaReorderableListViewBuilder({ + Key key, + @required this.itemCount, + @required this.itemBuilder, + @required this.controller, + @required this.onReorder, + this.padding, + this.physics = const AlwaysScrollableScrollPhysics(), + }) : super(key: key) { + assert(itemCount != null); + assert(itemBuilder != null); + } + + @override + Widget build(BuildContext context) { + return Scrollbar( + controller: controller, + child: ReorderableListView.builder( + scrollController: controller, + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)), + physics: physics, + itemCount: itemCount, + itemBuilder: itemBuilder, + onReorder: onReorder, + ), + ); + } +} diff --git a/lib/core/ui/scaffold.dart b/lib/core/ui/scaffold.dart new file mode 100644 index 00000000..a5f7e9fa --- /dev/null +++ b/lib/core/ui/scaffold.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaScaffold extends StatelessWidget { + final GlobalKey scaffoldKey; + final PreferredSizeWidget appBar; + final Widget body; + final Widget drawer; + final Widget bottomNavigationBar; + final Widget floatingActionButton; + final bool extendBody; + final bool extendBodyBehindAppBar; + /// Called when [LunaDatabaseValue.ENABLED_PROFILE] has changed. Triggered within the build function. + final void Function(BuildContext) onProfileChange; + + LunaScaffold({ + Key key, + @required this.scaffoldKey, + this.appBar, + this.body, + this.drawer, + this.bottomNavigationBar, + this.floatingActionButton, + this.extendBody = false, + this.extendBodyBehindAppBar = false, + this.onProfileChange, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaWillPopScope( + scaffoldKey: scaffoldKey, + child: ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]), + builder: (context, _, __) { + if(onProfileChange != null) onProfileChange(context); + return Scaffold( + key: scaffoldKey, + appBar: appBar, + body: body, + drawer: drawer, + bottomNavigationBar: bottomNavigationBar, + floatingActionButton: floatingActionButton, + extendBody: extendBody, + extendBodyBehindAppBar: extendBodyBehindAppBar, + onDrawerChanged: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ); + } + ), + ); + } +} diff --git a/lib/core/ui/sliver_sticky_header.dart b/lib/core/ui/sliver_sticky_header.dart deleted file mode 100644 index 02f459c6..00000000 --- a/lib/core/ui/sliver_sticky_header.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_sticky_header/flutter_sticky_header.dart'; - -class LunaSliverStickyHeader extends SliverStickyHeader { - LunaSliverStickyHeader({ - @required Widget header, - @required List children, - }) : super( - header: header, - sliver: SliverPadding( - sliver: SliverList( - delegate: SliverChildListDelegate(children), - ), - padding: EdgeInsets.symmetric(vertical: 8.0), - ), - ); -} diff --git a/lib/core/ui/snackbar/snackbar.dart b/lib/core/ui/snackbar/snackbar.dart index 93ae000b..133e011e 100644 --- a/lib/core/ui/snackbar/snackbar.dart +++ b/lib/core/ui/snackbar/snackbar.dart @@ -51,25 +51,25 @@ Future showLunaSnackBar({ horizontalDismissDirection: HorizontalDismissDirection.horizontal, margin: EdgeInsets.all(_PADDING), position: position, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), borderColor: LunaUI.shouldUseBorder ? Colors.white12 : Colors.transparent, child: FlashBar( title: Text( title, style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_TITLE, + fontSize: LunaUI.FONT_SIZE_TITLE, ), ), message: Text( message, style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), icon: Padding( - child: LSIconButton( + child: LunaIconButton( icon: type.icon, color: type.color, ), diff --git a/lib/core/ui/snackbar/snackbar_error.dart b/lib/core/ui/snackbar/snackbar_error.dart index a76c07be..5693557f 100644 --- a/lib/core/ui/snackbar/snackbar_error.dart +++ b/lib/core/ui/snackbar/snackbar_error.dart @@ -3,8 +3,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; Future showLunaErrorSnackBar({ - // Not needed anymore, but keeping it to prevent breaking all instances for now - BuildContext context, @required String title, dynamic error, String message, diff --git a/lib/core/ui/snackbar/snackbar_info.dart b/lib/core/ui/snackbar/snackbar_info.dart index 0c1fa06f..9ec3a7c9 100644 --- a/lib/core/ui/snackbar/snackbar_info.dart +++ b/lib/core/ui/snackbar/snackbar_info.dart @@ -3,8 +3,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; Future showLunaInfoSnackBar({ - // Not needed anymore, but keeping it to prevent breaking all instances for now - BuildContext context, @required String title, @required String message, bool showButton = false, diff --git a/lib/core/ui/snackbar/snackbar_success.dart b/lib/core/ui/snackbar/snackbar_success.dart index 8a955bf5..b98eccdd 100644 --- a/lib/core/ui/snackbar/snackbar_success.dart +++ b/lib/core/ui/snackbar/snackbar_success.dart @@ -3,8 +3,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; Future showLunaSuccessSnackBar({ - // Not needed anymore, but keeping it to prevent breaking all instances for now - BuildContext context, @required String title, @required String message, bool showButton = false, diff --git a/lib/core/ui/will_pop_scope.dart b/lib/core/ui/will_pop_scope.dart new file mode 100644 index 00000000..4aa15d5a --- /dev/null +++ b/lib/core/ui/will_pop_scope.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class LunaWillPopScope extends WillPopScope { + LunaWillPopScope({ + @required GlobalKey scaffoldKey, + @required Widget child, + }) : super( + onWillPop: () async { + if(scaffoldKey?.currentState?.hasDrawer ?? false) { + if(scaffoldKey?.currentState?.isDrawerOpen ?? false) return true; + scaffoldKey?.currentState?.openDrawer(); + return false; + } + return true; + }, + child: child, + ); +} diff --git a/lib/core/utilities/changelog.dart b/lib/core/utilities/changelog.dart index 856895db..55172767 100644 --- a/lib/core/utilities/changelog.dart +++ b/lib/core/utilities/changelog.dart @@ -29,6 +29,7 @@ class LunaChangelog { context: LunaState.navigatorKey.currentContext, builder: (context) => Scaffold( body: LunaListViewModal( + shrinkWrap: false, children: [ LunaHeader(text: '$version ($buildNumber)', subtitle: changelog.motd), if((changelog.changesNew?.length ?? 0) != 0) LunaHeader(text: 'New'), diff --git a/lib/modules/dashboard.dart b/lib/modules/dashboard.dart index ecb3735b..183ccb7e 100644 --- a/lib/modules/dashboard.dart +++ b/lib/modules/dashboard.dart @@ -1,3 +1,2 @@ export 'dashboard/core.dart'; export 'dashboard/routes.dart'; -export 'dashboard/widgets.dart'; diff --git a/lib/modules/dashboard/core.dart b/lib/modules/dashboard/core.dart index fb66fe12..7bd2c9e1 100644 --- a/lib/modules/dashboard/core.dart +++ b/lib/modules/dashboard/core.dart @@ -1,5 +1,6 @@ export 'core/api.dart'; export 'core/adapters.dart'; export 'core/database.dart'; +export 'core/router.dart'; export 'core/state.dart'; export 'core/dialogs.dart'; diff --git a/lib/modules/dashboard/core/adapters/calendar_starting_type.dart b/lib/modules/dashboard/core/adapters/calendar_starting_type.dart index ca05c26e..61fe1374 100644 --- a/lib/modules/dashboard/core/adapters/calendar_starting_type.dart +++ b/lib/modules/dashboard/core/adapters/calendar_starting_type.dart @@ -16,24 +16,24 @@ extension CalendarStartingTypeExtension on CalendarStartingType { switch(this) { case CalendarStartingType.SCHEDULE: return 'Schedule'; case CalendarStartingType.CALENDAR: return 'Calendar'; - default: return null; } + throw Exception('Unknown CalendarStartingType'); } String get key { switch(this) { case CalendarStartingType.SCHEDULE: return 'schedule'; case CalendarStartingType.CALENDAR: return 'calendar'; - default: return null; } + throw Exception('Unknown CalendarStartingType'); } IconData get icon { switch(this) { - case CalendarStartingType.SCHEDULE: return Icons.calendar_view_day; - case CalendarStartingType.CALENDAR: return LunaIcons.calendar; - default: return null; + case CalendarStartingType.SCHEDULE: return LunaIcons.calendar; + case CalendarStartingType.CALENDAR: return Icons.calendar_view_day; } + throw Exception('Unknown CalendarStartingType'); } CalendarStartingType fromKey(String key) { diff --git a/lib/modules/dashboard/core/api/api.dart b/lib/modules/dashboard/core/api/api.dart index 75ccc632..2fe79cf1 100644 --- a/lib/modules/dashboard/core/api/api.dart +++ b/lib/modules/dashboard/core/api/api.dart @@ -23,10 +23,6 @@ class CalendarAPI { ); } - void logError(String text, Object error, StackTrace trace) => LunaLogger().error('Home: $text', error, trace); - - Future testConnection() async => true; - Future> getUpcoming(DateTime today) async { Map _upcoming = {}; if( @@ -41,72 +37,70 @@ class CalendarAPI { sonarr['enabled'] && DashboardDatabaseValue.CALENDAR_ENABLE_SONARR.data ) await _getSonarrUpcoming(_upcoming, today); - return _upcoming.isEmpty - ? await Future.delayed(Duration(milliseconds: 500), () => {}) - : _upcoming; + return _upcoming; } Future _getLidarrUpcoming(Map map, DateTime today) async { - try { - Map _headers = Map.from(lidarr['headers']); - Dio _client = Dio( - BaseOptions( - baseUrl: '${lidarr['host']}/api/v1/', - queryParameters: { - if(lidarr['key'] != '') 'apikey': lidarr['key'], - 'start': _startDate(today), - 'end': _startDate(today), - }, - headers: _headers, - followRedirects: true, - maxRedirects: 5, - ), - ); - Response response = await _client.get('calendar'); - if(response.data.length > 0) { - for(var entry in response.data) { - DateTime date = DateTime.tryParse(entry['releaseDate'] ?? '')?.toLocal()?.lunaFloor; - if(date != null) { - List day = map[date] ?? []; - day.add(CalendarLidarrData( - id: entry['id'] ?? 0, - title: entry['artist']['artistName'] ?? 'Unknown Artist', - albumTitle: entry['title'] ?? 'Unknown Album Title', - artistId: entry['artist']['id'] ?? 0, - hasAllFiles: (entry['statistics'] != null ? entry['statistics']['percentOfTracks'] ?? 0 : 0) == 100, - )); - map[date] = day; - } + Map _headers = Map.from(lidarr['headers']); + Dio _client = Dio( + BaseOptions( + baseUrl: '${lidarr['host']}/api/v1/', + queryParameters: { + if(lidarr['key'] != '') 'apikey': lidarr['key'], + 'start': _startDate(today), + 'end': _startDate(today), + }, + headers: _headers, + followRedirects: true, + maxRedirects: 5, + ), + ); + Response response = await _client.get('calendar'); + if(response.data.length > 0) { + for(var entry in response.data) { + DateTime date = DateTime.tryParse(entry['releaseDate'] ?? '')?.toLocal()?.lunaFloor; + if(date != null) { + List day = map[date] ?? []; + day.add(CalendarLidarrData( + id: entry['id'] ?? 0, + title: entry['artist']['artistName'] ?? 'Unknown Artist', + albumTitle: entry['title'] ?? 'Unknown Album Title', + artistId: entry['artist']['id'] ?? 0, + hasAllFiles: (entry['statistics'] != null ? entry['statistics']['percentOfTracks'] ?? 0 : 0) == 100, + )); + map[date] = day; } } - } catch (error, stack) { - logError('Failed to fetch Lidarr upcoming content', error, stack); } - return; } Future _getRadarrUpcoming(Map map, DateTime today) async { - try { - Map _headers = Map.from(radarr['headers']); - Dio _client = Dio( - BaseOptions( - baseUrl: '${radarr['host']}/api/', - queryParameters: { - if(radarr['key'] != '') 'apikey': radarr['key'], - 'start': _startDate(today), - 'end': _endDate(today), - }, - headers: _headers, - followRedirects: true, - maxRedirects: 5, - ), - ); - Response response = await _client.get('calendar'); - if(response.data.length > 0) { - for(var entry in response.data) { - DateTime date = DateTime.tryParse(entry['physicalRelease'] ?? '')?.toLocal()?.lunaFloor; - if(date != null) { - List day = map[date] ?? []; + Map _headers = Map.from(radarr['headers']); + Dio _client = Dio( + BaseOptions( + baseUrl: '${radarr['host']}/api/v3/', + queryParameters: { + if(radarr['key'] != '') 'apikey': radarr['key'], + 'start': _startDate(today), + 'end': _endDate(today), + }, + headers: _headers, + followRedirects: true, + maxRedirects: 5, + ), + ); + Response response = await _client.get('calendar'); + if(response.data.length > 0) { + for(var entry in response.data) { + DateTime physicalRelease = DateTime.tryParse(entry['physicalRelease'] ?? '')?.toLocal()?.lunaFloor; + DateTime digitalRelease = DateTime.tryParse(entry['digitalRelease'] ?? '')?.toLocal()?.lunaFloor; + DateTime release; + if(physicalRelease != null || digitalRelease != null) { + if(physicalRelease == null) release = digitalRelease; + if(digitalRelease == null) release = physicalRelease; + if(release == null) release = digitalRelease.isBefore(physicalRelease) ? digitalRelease : physicalRelease; + if(release != null) { + List day = map[release] ?? []; day.add(CalendarRadarrData( id: entry['id'] ?? 0, title: entry['title'] ?? 'Unknown Title', @@ -115,59 +109,51 @@ class CalendarAPI { year: entry['year'] ?? 0, runtime: entry['runtime'] ?? 0, )); - map[date] = day; + map[release] = day; } } } - } catch (error, stack) { - logError('Failed to fetch Radarr upcoming content', error, stack); } - return; } Future _getSonarrUpcoming(Map map, DateTime today) async { - try { - Map _headers = Map.from(sonarr['headers']); - Dio _client = Dio( - BaseOptions( - baseUrl: '${sonarr['host']}/api/', - queryParameters: { - if(sonarr['key'] != '') 'apikey': sonarr['key'], - 'start': _startDate(today), - 'end': _endDate(today), - }, - headers: _headers, - followRedirects: true, - maxRedirects: 5, - ), - ); - Response response = await _client.get('calendar'); - if(response.data.length > 0) { - for(var entry in response.data) { - DateTime date = DateTime.tryParse(entry['airDateUtc'] ?? '')?.toLocal()?.lunaFloor; - if(date != null) { - List day = map[date] ?? []; - day.add(CalendarSonarrData( - id: entry['id'] ?? 0, - seriesID: entry['seriesId'] ?? 0, - title: entry['series']['title'] ?? 'Unknown Series', - episodeTitle: entry['title'] ?? 'Unknown Episode Title', - seasonNumber: entry['seasonNumber'] ?? -1, - episodeNumber: entry['episodeNumber'] ?? -1, - airTime: entry['airDateUtc'] ?? '', - hasFile: entry['hasFile'] ?? false, - fileQualityProfile: entry['hasFile'] ? entry['episodeFile']['quality']['quality']['name'] : '', - )); - map[date] = day; - } + Map _headers = Map.from(sonarr['headers']); + Dio _client = Dio( + BaseOptions( + baseUrl: '${sonarr['host']}/api/', + queryParameters: { + if(sonarr['key'] != '') 'apikey': sonarr['key'], + 'start': _startDate(today), + 'end': _endDate(today), + }, + headers: _headers, + followRedirects: true, + maxRedirects: 5, + ), + ); + Response response = await _client.get('calendar'); + if(response.data.length > 0) { + for(var entry in response.data) { + DateTime date = DateTime.tryParse(entry['airDateUtc'] ?? '')?.toLocal()?.lunaFloor; + if(date != null) { + List day = map[date] ?? []; + day.add(CalendarSonarrData( + id: entry['id'] ?? 0, + seriesID: entry['seriesId'] ?? 0, + title: entry['series']['title'] ?? 'Unknown Series', + episodeTitle: entry['title'] ?? 'Unknown Episode Title', + seasonNumber: entry['seasonNumber'] ?? -1, + episodeNumber: entry['episodeNumber'] ?? -1, + airTime: entry['airDateUtc'] ?? '', + hasFile: entry['hasFile'] ?? false, + fileQualityProfile: entry['hasFile'] ? entry['episodeFile']['quality']['quality']['name'] : '', + )); + map[date] = day; } } - } catch (error, stack) { - logError('Failed to fetch Sonarr upcoming content', error, stack); } - return; } String _startDate(DateTime today) => DateFormat('y-MM-dd').format(today.subtract(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_PAST.data))); String _endDate(DateTime today) => DateFormat('y-MM-dd').format(today.add(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.data + 1))); -} \ No newline at end of file +} diff --git a/lib/modules/dashboard/core/api/data.dart b/lib/modules/dashboard/core/api/data.dart index fd11174f..5f625c46 100644 --- a/lib/modules/dashboard/core/api/data.dart +++ b/lib/modules/dashboard/core/api/data.dart @@ -1,3 +1,4 @@ export 'data/lidarr.dart'; export 'data/radarr.dart'; export 'data/sonarr.dart'; +export 'data/abstract.dart'; diff --git a/lib/modules/dashboard/core/api/data/lidarr.dart b/lib/modules/dashboard/core/api/data/lidarr.dart index ccf66260..cf244f96 100644 --- a/lib/modules/dashboard/core/api/data/lidarr.dart +++ b/lib/modules/dashboard/core/api/data/lidarr.dart @@ -28,7 +28,7 @@ class CalendarLidarrData extends CalendarData { TextSpan get subtitle => TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -41,7 +41,7 @@ class CalendarLidarrData extends CalendarData { text: '\nNot Downloaded', style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, - color: Colors.red, + color: LunaColours.red, ), ), if(hasAllFiles) TextSpan( @@ -62,7 +62,7 @@ class CalendarLidarrData extends CalendarData { ), ); - Widget trailing(BuildContext context) => LSIconButton( + Widget trailing(BuildContext context) => LunaIconButton( icon: Icons.search, onPressed: () async => trailingOnPress(context), onLongPress: () async => trailingOnLongPress(context), @@ -71,8 +71,8 @@ class CalendarLidarrData extends CalendarData { @override Future trailingOnPress(BuildContext context) async { await LidarrAPI.from(Database.currentProfileObject).searchAlbums([id]) - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: albumTitle)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Searching...', message: albumTitle)) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Search', error: error)); } @override diff --git a/lib/modules/dashboard/core/api/data/radarr.dart b/lib/modules/dashboard/core/api/data/radarr.dart index a20ea1ae..3d44e7fa 100644 --- a/lib/modules/dashboard/core/api/data/radarr.dart +++ b/lib/modules/dashboard/core/api/data/radarr.dart @@ -19,26 +19,29 @@ class CalendarRadarrData extends CalendarData { @required this.runtime, }): super(id, title); - String get runtimeString => '\t${Constants.TEXT_BULLET}\t${runtime.lunaRuntime()}'; - TextSpan get subtitle => TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), - children: [ + children: [ TextSpan( - text: '$year$runtimeString', + children: [ + TextSpan(text: year.toString()), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad(1, '\t')), + TextSpan(text: runtime.lunaRuntime()), + ], ), + TextSpan(text: '\n'), if(!hasFile) TextSpan( - text: '\nNot Downloaded', + text: 'Not Downloaded', style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, - color: Colors.red, + color: LunaColours.red, ), ), if(hasFile) TextSpan( - text: '\nDownloaded ($fileQualityProfile)', + text: 'Downloaded ($fileQualityProfile)', style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.accent, @@ -47,21 +50,28 @@ class CalendarRadarrData extends CalendarData { ], ); + @override String get bannerURI { - return api['enabled'] - ? (api['host'] as String).endsWith('/') - ? '${api['host']}api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}' - : '${api['host']}/api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}' - : ''; + if(api['enabled']) { + if((api['host'] as String).endsWith('/')) { + return '${api['host']}api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}'; + } + return '${api['host']}/api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}'; + } + return ''; } + @override Future enterContent(BuildContext context) async => RadarrMoviesDetailsRouter().navigateTo(context, movieId: id); - Widget trailing(BuildContext context) => LSIconButton( - icon: Icons.search, - onPressed: () async => trailingOnPress(context), - onLongPress: () async => trailingOnLongPress(context), - ); + @override + Widget trailing(BuildContext context) { + return LunaIconButton( + icon: Icons.search, + onPressed: () async => trailingOnPress(context), + onLongPress: () async => trailingOnLongPress(context), + ); + } @override Future trailingOnPress(BuildContext context) async => RadarrAPIHelper().automaticSearch(context: context, movieId: id, title: title); diff --git a/lib/modules/dashboard/core/api/data/sonarr.dart b/lib/modules/dashboard/core/api/data/sonarr.dart index e36093a8..a39c7694 100644 --- a/lib/modules/dashboard/core/api/data/sonarr.dart +++ b/lib/modules/dashboard/core/api/data/sonarr.dart @@ -29,7 +29,7 @@ class CalendarSonarrData extends CalendarData { TextSpan get subtitle => TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -45,7 +45,7 @@ class CalendarSonarrData extends CalendarData { text: hasAired ? '\nMissing' : '\nUnaired', style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, - color: hasAired ? Colors.red : LunaColours.blue, + color: hasAired ? LunaColours.red : LunaColours.blue, ), ), if(hasFile) TextSpan( @@ -77,7 +77,7 @@ class CalendarSonarrData extends CalendarData { ); Widget trailing(BuildContext context) => InkWell( - child: LSIconButton( + child: LunaIconButton( text: airTimeString, onPressed: () async => trailingOnPress(context), onLongPress: () => trailingOnLongPress(context), @@ -100,11 +100,9 @@ class CalendarSonarrData extends CalendarData { @override Future trailingOnPress(BuildContext context) async { if(context.read().api != null) context.read().api.command.episodeSearch(episodeIds: [id]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Searching for Episode...', message: episodeTitle, - type: SNACKBAR_TYPE.success, )) .catchError((error, stack) { LunaLogger().error( @@ -112,10 +110,9 @@ class CalendarSonarrData extends CalendarData { error, stack, ); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Search', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } diff --git a/lib/modules/dashboard/core/dialogs.dart b/lib/modules/dashboard/core/dialogs.dart index 762b83cc..3f86612c 100644 --- a/lib/modules/dashboard/core/dialogs.dart +++ b/lib/modules/dashboard/core/dialogs.dart @@ -13,25 +13,25 @@ class DashboardDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Page', content: List.generate( DashboardNavigationBar.titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: DashboardNavigationBar.titles[index], icon: DashboardNavigationBar.icons[index], iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _index); } - static Future> setPastDays(BuildContext context) async { + Future> setPastDays(BuildContext context) async { bool _flag = false; GlobalKey _formKey = GlobalKey(); TextEditingController _textController = TextEditingController(text: DashboardDatabaseValue.CALENDAR_DAYS_PAST.data.toString()); @@ -43,20 +43,20 @@ class DashboardDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Past Days', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Set the number of days in the past to fetch calendar entries for.'), + LunaDialog.textContent(text: 'Set the number of days in the past to fetch calendar entries for.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Past Days', onSubmitted: (_) => _setValues(true), @@ -69,13 +69,13 @@ class DashboardDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); - return [_flag, int.tryParse(_textController.text) ?? 14]; + return Tuple2(_flag, int.tryParse(_textController.text) ?? 14); } - static Future> setFutureDays(BuildContext context) async { + Future> setFutureDays(BuildContext context) async { bool _flag = false; GlobalKey _formKey = GlobalKey(); TextEditingController _textController = TextEditingController(text: DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.data.toString()); @@ -87,20 +87,20 @@ class DashboardDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Future Days', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Set the number of days in the future to fetch calendar entries for.'), + LunaDialog.textContent(text: 'Set the number of days in the future to fetch calendar entries for.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Future Days', onSubmitted: (_) => _setValues(true), @@ -113,9 +113,9 @@ class DashboardDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); - return [_flag, int.tryParse(_textController.text) ?? 14]; + return Tuple2(_flag, int.tryParse(_textController.text) ?? 14); } } \ No newline at end of file diff --git a/lib/modules/dashboard/core/router.dart b/lib/modules/dashboard/core/router.dart new file mode 100644 index 00000000..cc14781d --- /dev/null +++ b/lib/modules/dashboard/core/router.dart @@ -0,0 +1,33 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/dashboard.dart'; + +class DashboardRouter extends LunaModuleRouter { + @override + void defineAllRoutes(FluroRouter router) { + DashboardHomeRouter().defineRoute(router); + } +} + +abstract class DashboardPageRouter extends LunaPageRouter { + DashboardPageRouter(String route) : super(route); + + @override + void noParameterRouteDefinition(FluroRouter router, Widget widget, { bool homeRoute = false }) => router.define( + fullRoute, + handler: Handler(handlerFunc: (context, params) => widget), + transitionType: LunaRouter.transitionType, + ); + + @override + void withParameterRouteDefinition( + FluroRouter router, + Widget Function(BuildContext, Map>) handlerFunc, + { bool homeRoute = false } + ) => router.define( + fullRoute, + handler: Handler(handlerFunc: (context, params) => handlerFunc(context, params)), + transitionType: LunaRouter.transitionType, + ); +} diff --git a/lib/modules/dashboard/core/state.dart b/lib/modules/dashboard/core/state.dart index f0e52e00..7c453757 100644 --- a/lib/modules/dashboard/core/state.dart +++ b/lib/modules/dashboard/core/state.dart @@ -2,15 +2,15 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/dashboard.dart'; class DashboardState extends LunaModuleState { - @override - void reset() {} + DashboardState() { + reset(); + } - int _navigationIndex = DashboardDatabaseValue.NAVIGATION_INDEX.data; - int get navigationIndex => _navigationIndex; - set navigationIndex(int navigationIndex) { - assert(navigationIndex != null); - _navigationIndex = navigationIndex; - notifyListeners(); + @override + void reset() { + resetToday(); + resetAPI(); + resetUpcoming(); } CalendarStartingType _calendarStartingType = DashboardDatabaseValue.CALENDAR_STARTING_TYPE.data; @@ -20,4 +20,27 @@ class DashboardState extends LunaModuleState { _calendarStartingType = calendarStartingType; notifyListeners(); } + + CalendarAPI _api; + CalendarAPI get api => _api; + void resetAPI() { + ProfileHiveObject _profile = Database.currentProfileObject; + _api = CalendarAPI.from(_profile); + notifyListeners(); + } + + DateTime _today; + DateTime get today => _today; + void resetToday() { + _today = DateTime.now(); + notifyListeners(); + } + + Future> _upcoming; + Future> get upcoming => _upcoming; + void resetUpcoming() { + if(_api != null) _upcoming = _api.getUpcoming(DateTime.now()); + notifyListeners(); + } } + diff --git a/lib/modules/dashboard/routes.dart b/lib/modules/dashboard/routes.dart index 13ee4ef9..4a6191c3 100644 --- a/lib/modules/dashboard/routes.dart +++ b/lib/modules/dashboard/routes.dart @@ -1,3 +1,3 @@ export 'routes/calendar.dart'; -export 'routes/quick_access.dart'; -export 'routes/home.dart'; +export 'routes/dashboard.dart'; +export 'routes/modules.dart'; diff --git a/lib/modules/dashboard/routes/calendar.dart b/lib/modules/dashboard/routes/calendar.dart index 7d168975..749f4c19 100644 --- a/lib/modules/dashboard/routes/calendar.dart +++ b/lib/modules/dashboard/routes/calendar.dart @@ -1,69 +1,2 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/dashboard.dart'; - -class DashboardCalendar extends StatefulWidget { - static const ROUTE_NAME = '/dashboard/calendar'; - final CalendarAPI api = CalendarAPI.from(Database.currentProfileObject); - final GlobalKey refreshIndicatorKey; - - DashboardCalendar({ - Key key, - @required this.refreshIndicatorKey, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - DateTime _today; - Future> _future; - Map _events; - - @override - bool get wantKeepAlive => true; - - @override - initState() { - super.initState(); - _refresh(); - } - - Future _refresh() async { - _today = DateTime.now().lunaFloor; - setState(() { - _future = widget.api.getUpcoming(_today); - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _events = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); - } - - Widget get _list => DashboardCalendarWidget( - events: _events, - today: _today, - ); -} +export 'calendar/route.dart'; +export 'calendar/widgets.dart'; diff --git a/lib/modules/dashboard/routes/calendar/route.dart b/lib/modules/dashboard/routes/calendar/route.dart new file mode 100644 index 00000000..c81017c6 --- /dev/null +++ b/lib/modules/dashboard/routes/calendar/route.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/dashboard.dart'; + +class DashboardCalendarRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + Future loadCallback() async { + context.read().resetUpcoming(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: context.watch().upcoming, + builder: (context, snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Failed to fetch unified calendar data', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); + } + if(snapshot.connectionState == ConnectionState.done && snapshot.hasData) return DashboardCalendarWidget( + events: snapshot.data, + ); + return LunaLoader(); + }, + ), + ); + } +} diff --git a/lib/modules/dashboard/routes/calendar/widgets.dart b/lib/modules/dashboard/routes/calendar/widgets.dart new file mode 100644 index 00000000..5693ae1a --- /dev/null +++ b/lib/modules/dashboard/routes/calendar/widgets.dart @@ -0,0 +1 @@ +export 'widgets/calendar.dart'; diff --git a/lib/modules/dashboard/routes/calendar/widgets/calendar.dart b/lib/modules/dashboard/routes/calendar/widgets/calendar.dart new file mode 100644 index 00000000..6728ca0a --- /dev/null +++ b/lib/modules/dashboard/routes/calendar/widgets/calendar.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/dashboard.dart'; +import 'package:table_calendar/table_calendar.dart'; + +class DashboardCalendarWidget extends StatefulWidget { + final Map events; + + DashboardCalendarWidget({ + Key key, + @required this.events, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + DateTime _today; + + final TextStyle dayTileStyle = TextStyle( + color: Colors.white, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ); + final TextStyle outsideDayTileStyle = TextStyle( + color: Colors.white54, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ); + final TextStyle unavailableTitleStyle = TextStyle( + color: Colors.white12, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ); + final TextStyle weekdayTitleStyle = TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ); + + List _selectedEvents; + CalendarController _calendarController; + + @override + void initState() { + super.initState(); + _calendarController = CalendarController(); + _today = context.read().today.lunaFloor; + _selectedEvents = widget.events[_today] ?? []; + } + + @override + void dispose() { + _calendarController.dispose(); + super.dispose(); + } + + void _onDaySelected(DateTime day, List events, List _) { + HapticFeedback.selectionClick(); + setState(() { + _selectedEvents = events; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Selector( + selector: (_, state) => state.calendarStartingType, + builder: (context, startingType, _) { + if(startingType == CalendarStartingType.CALENDAR) { + return Padding( + child: Column( + children: [ + _calendar(), + _calendarList(), + ], + ), + padding: EdgeInsets.only(top: 8.0), + ); + } + return _schedule(); + }, + ) + ); + } + + Widget _calendar() { + return ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [ + DashboardDatabaseValue.CALENDAR_STARTING_DAY.key, + DashboardDatabaseValue.CALENDAR_STARTING_SIZE.key, + ]), + builder: (context, box, _) { + return LunaCard( + context: context, + child: Padding( + child: TableCalendar( + rowHeight: 48.0, + calendarController: _calendarController, + events: widget.events, + headerVisible: false, + startDay: DateTime.now().subtract(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_PAST.data)), + endDay: DateTime.now().add(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.data)), + startingDayOfWeek: (DashboardDatabaseValue.CALENDAR_STARTING_DAY.data as CalendarStartingDay).data, + calendarStyle: CalendarStyle( + selectedColor: LunaColours.accent.withOpacity(0.25), + markersMaxAmount: 1, + markersColor: LunaColours.accent, + weekendStyle: dayTileStyle, + weekdayStyle: dayTileStyle, + unavailableStyle: unavailableTitleStyle, + outsideStyle: outsideDayTileStyle, + selectedStyle: dayTileStyle, + outsideWeekendStyle: outsideDayTileStyle, + renderDaysOfWeek: true, + highlightToday: true, + todayColor: LunaColours.primary, + todayStyle: dayTileStyle, + outsideDaysVisible: false, + ), + daysOfWeekStyle: DaysOfWeekStyle( + weekendStyle: weekdayTitleStyle, + weekdayStyle: weekdayTitleStyle, + ), + headerStyle: HeaderStyle( + titleTextStyle: TextStyle( + fontSize: LunaUI.FONT_SIZE_HEADER, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + centerHeaderTitle: true, + formatButtonVisible: false, + leftChevronIcon: Icon(Icons.arrow_back_ios_rounded), + rightChevronIcon: Icon(Icons.arrow_forward_ios_rounded), + ), + initialCalendarFormat: (DashboardDatabaseValue.CALENDAR_STARTING_SIZE.data as CalendarStartingSize).data, + availableCalendarFormats: const { + CalendarFormat.month : 'Month', CalendarFormat.twoWeeks : '2 Weeks', CalendarFormat.week : 'Week'}, + onDaySelected: _onDaySelected, + ), + padding: EdgeInsets.only(top: 16.0, bottom: 8.0), + ), + ); + }, + ); + } + + Widget _calendarList() { + if((_selectedEvents?.length ?? 0) == 0) return Expanded( + child: LunaListView( + controller: DashboardNavigationBar.scrollControllers[1], + children: [ + LunaMessage.inList(text: 'No New Content'), + ], + ), + ); + return Expanded( + child: LunaListView( + controller: DashboardNavigationBar.scrollControllers[1], + children: _selectedEvents.map((event) => _entry(event)).toList(), + padding: MediaQuery.of(context).padding.copyWith(top: 0.0, bottom: 8.0), + ), + ); + } + + Widget _schedule() { + if((widget.events?.length ?? 0) == 0) return LunaListView( + controller: DashboardNavigationBar.scrollControllers[1], + children: [ + LunaMessage.inList(text: 'No New Content'), + ], + ); + return LunaListView( + controller: DashboardNavigationBar.scrollControllers[1], + children: _buildDays().expand((element) => element).toList(), + ); + } + + List> _buildDays() { + List> days = []; + List keys = widget.events.keys.toList(); + keys.sort(); + for(var key in keys) { + if(key.isAfter(_today.subtract(Duration(days: 1))) && widget.events[key].length > 0) days.add(_day(key)); + } + return days; + } + + List _day(DateTime day) { + List listCards = []; + for(int i=0; i event.enterContent(context), + contentPadding: true, + decoration: LunaCardDecoration( + uri: event.bannerURI, + headers: headers, + ), + ); + } +} diff --git a/lib/modules/dashboard/routes/dashboard.dart b/lib/modules/dashboard/routes/dashboard.dart new file mode 100644 index 00000000..72ec03c1 --- /dev/null +++ b/lib/modules/dashboard/routes/dashboard.dart @@ -0,0 +1,2 @@ +export 'dashboard/route.dart'; +export 'dashboard/widgets.dart'; diff --git a/lib/modules/dashboard/routes/dashboard/route.dart b/lib/modules/dashboard/routes/dashboard/route.dart new file mode 100644 index 00000000..5e521bf3 --- /dev/null +++ b/lib/modules/dashboard/routes/dashboard/route.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/dashboard.dart'; + +class DashboardHomeRouter extends DashboardPageRouter { + DashboardHomeRouter() : super('/'); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _DashboardHomeRoute()); +} + +class _DashboardHomeRoute extends StatefulWidget { + @override + State<_DashboardHomeRoute> createState() => _State(); +} + +class _State extends State<_DashboardHomeRoute> { + final _scaffoldKey = GlobalKey(); + LunaPageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = LunaPageController(initialPage: DashboardDatabaseValue.NAVIGATION_INDEX.data); + } + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + body: _body(), + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + ); + } + + Widget _drawer() => LunaDrawer(page: LunaModule.DASHBOARD.key); + + Widget _bottomNavigationBar() => DashboardNavigationBar(pageController: _pageController); + + Widget _appBar() { + return LunaAppBar( + title: 'LunaSea', + useDrawer: true, + scrollControllers: DashboardNavigationBar.scrollControllers, + pageController: _pageController, + actions: [DashboardAppBarSwitchViewAction()], + ); + } + + Widget _body() { + return LunaDatabaseValue.ENABLED_PROFILE.listen( + builder: (context, _, __) => PageView( + controller: _pageController, + children: _tabs, + ), + ); + } + + List get _tabs => [ + DashboardModulesRoute(), + DashboardCalendarRoute(), + ]; +} diff --git a/lib/modules/dashboard/routes/dashboard/widgets.dart b/lib/modules/dashboard/routes/dashboard/widgets.dart new file mode 100644 index 00000000..a84d76de --- /dev/null +++ b/lib/modules/dashboard/routes/dashboard/widgets.dart @@ -0,0 +1,2 @@ +export 'widgets/navigation_bar.dart'; +export 'widgets/appbar_switch_view_action.dart'; diff --git a/lib/modules/dashboard/routes/dashboard/widgets/appbar_switch_view_action.dart b/lib/modules/dashboard/routes/dashboard/widgets/appbar_switch_view_action.dart new file mode 100644 index 00000000..815ad58c --- /dev/null +++ b/lib/modules/dashboard/routes/dashboard/widgets/appbar_switch_view_action.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/dashboard.dart'; + +class DashboardAppBarSwitchViewAction extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, state) => state.calendarStartingType, + builder: (context, view, _) => LunaIconButton( + icon: view.icon, + onPressed: () { + view == CalendarStartingType.CALENDAR + ? context.read().calendarStartingType = CalendarStartingType.SCHEDULE + : context.read().calendarStartingType = CalendarStartingType.CALENDAR; + }, + ), + ); + } +} diff --git a/lib/modules/dashboard/routes/dashboard/widgets/navigation_bar.dart b/lib/modules/dashboard/routes/dashboard/widgets/navigation_bar.dart new file mode 100644 index 00000000..5583f8cf --- /dev/null +++ b/lib/modules/dashboard/routes/dashboard/widgets/navigation_bar.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class DashboardNavigationBar extends StatelessWidget { + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + final PageController pageController; + + static const List titles = [ + 'Modules', + 'Calendar', + ]; + + static const List icons = [ + LunaIcons.modules, + LunaIcons.calendar, + ]; + + DashboardNavigationBar({ + Key key, + @required this.pageController, + }): super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); + } +} \ No newline at end of file diff --git a/lib/modules/dashboard/routes/home.dart b/lib/modules/dashboard/routes/home.dart deleted file mode 100644 index 2ec38d59..00000000 --- a/lib/modules/dashboard/routes/home.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/dashboard.dart'; -import 'package:tuple/tuple.dart'; - -class Dashboard extends StatefulWidget { - static const ROUTE_NAME = '/'; - - @override - State createState() => _State(); -} - -class _State extends State { - final _scaffoldKey = GlobalKey(); - final _pageController = PageController(initialPage: DashboardDatabaseValue.NAVIGATION_INDEX.data); - final List _refreshKeys = [GlobalKey()]; - String _profileState = Database.currentProfileObject.toString(); - - @override - Widget build(BuildContext context) => WillPopScope( - onWillPop: _willPopScope, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]), - builder: (context, lunaBox, widget) { - return ValueListenableBuilder( - valueListenable: Database.profilesBox.listenable(keys: [Database.currentProfile]), - builder: (context, profileBox, widget) { - if(_profileState != Database.currentProfileObject.toString()) _refreshProfile(); - return Scaffold( - key: _scaffoldKey, - body: _body, - drawer: _drawer, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - ); - } - ); - }, - ), - ); - - Future _willPopScope() async { - if(_scaffoldKey.currentState.isDrawerOpen) { - return true; - } else { - _scaffoldKey.currentState.openDrawer(); - return false; - } - } - - Widget get _drawer => LunaDrawer(page: LunaModule.DASHBOARD.key); - - Widget get _appBar => LunaAppBar( - title: 'LunaSea', - useDrawer: true, - actions: Database.currentProfileObject.anyAutomationEnabled - ? [ - Selector>( - selector: (_, state) => Tuple2(state.navigationIndex, state.calendarStartingType), - builder: (context, state, _) => Visibility( - visible: state.item1 == 1, - child: LSIconButton( - icon: state.item2 == CalendarStartingType.CALENDAR - ? CalendarStartingType.SCHEDULE.icon - : CalendarStartingType.CALENDAR.icon, - onPressed: () async { - Provider.of(context, listen: false).calendarStartingType = state.item2 == CalendarStartingType.CALENDAR - ? CalendarStartingType.SCHEDULE - : CalendarStartingType.CALENDAR; - }, - ), - ), - ), - ] - : null, - ); - - Widget get _bottomNavigationBar => DashboardNavigationBar(pageController: _pageController); - - Widget get _body => PageView( - controller: _pageController, - children: _tabs, - onPageChanged: _onPageChanged, - ); - - List get _tabs => [ - DashboardQuickAccess(), - Database.currentProfileObject.anyAutomationEnabled - ? DashboardCalendar(refreshIndicatorKey: _refreshKeys[0]) - : LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false), - ]; - - void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; - - void _refreshProfile() { - _profileState = Database.currentProfileObject.toString(); - _refreshAllPages(); - } - - void _refreshAllPages() { - for(var key in _refreshKeys) { - key?.currentState?.show(); - } - } -} diff --git a/lib/modules/dashboard/routes/modules.dart b/lib/modules/dashboard/routes/modules.dart new file mode 100644 index 00000000..ed78b6ad --- /dev/null +++ b/lib/modules/dashboard/routes/modules.dart @@ -0,0 +1 @@ +export 'modules/route.dart'; diff --git a/lib/modules/dashboard/routes/modules/route.dart b/lib/modules/dashboard/routes/modules/route.dart new file mode 100644 index 00000000..da7de31a --- /dev/null +++ b/lib/modules/dashboard/routes/modules/route.dart @@ -0,0 +1,74 @@ + +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/dashboard.dart'; + +class DashboardModulesRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + body: _list(), + ); + } + + Widget _list() { + if(!(Database.indexersBox.length > 0) && !(Database.currentProfileObject.anythingEnabled)) { + return LunaMessage( + text: 'lunasea.NoModulesEnabled'.tr(), + buttonText: 'lunasea.GoToSettings'.tr(), + onTap: LunaModule.SETTINGS.launch, + ); + } + return _buildList(); + } + + Widget _buildList() { + List modules = []; + int index = 0; + // Build list of all modules + if(Database.indexersBox.length > 0) { + modules.add(_buildFromLunaModule(LunaModule.SEARCH, index)); + index++; + } + Database.currentProfileObject.enabledAutomationModules.forEach((module) { + modules.add(_buildFromLunaModule(module, index)); + index++; + }); + Database.currentProfileObject.enabledClientModules.forEach((module) { + modules.add(_buildFromLunaModule(module, index)); + index++; + }); + Database.currentProfileObject.enabledMonitoringModules.forEach((module) { + modules.add(_buildFromLunaModule(module, index)); + index++; + }); + modules.add(_buildFromLunaModule(LunaModule.SETTINGS, index)); + // Build that listview + return LunaListView( + controller: DashboardNavigationBar.scrollControllers[0], + children: modules, + ); + } + + Widget _buildFromLunaModule(LunaModule module, int listIndex) { + return LunaListTile( + context: context, + title: LunaText.title(text: module.name), + subtitle: LunaText.subtitle(text: module.description), + trailing: LunaIconButton( + icon: module.icon, + color: DashboardDatabaseValue.MODULES_BRAND_COLOURS.data ? module.color : LunaColours.list(listIndex), + ), + onTap: module.launch, + ); + } +} diff --git a/lib/modules/dashboard/routes/quick_access.dart b/lib/modules/dashboard/routes/quick_access.dart deleted file mode 100644 index c311d9a3..00000000 --- a/lib/modules/dashboard/routes/quick_access.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules.dart'; - -class DashboardQuickAccess extends StatefulWidget { - static const ROUTE_NAME = '/dashboard/quickaccess'; - final ProfileHiveObject profile = Database.currentProfileObject; - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return ValueListenableBuilder( - valueListenable: Database.indexersBox.listenable(), - builder: (context, box, _) => widget.profile.anythingEnabled || Database.indexersBox.length > 0 - ? _body - : LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false), - ); - } - - Widget get _body { - bool _hasIndexers = Database.indexersBox.length > 0; - int _serviceCount = widget.profile.enabledModules.length + (_hasIndexers ? 2 : 1); - return LSListViewBuilder( - itemCount: _serviceCount, - itemBuilder: (context, index) { - //Check if indexer search should be included at the top - if(index == 0 && _hasIndexers) return DashboardSummaryTile( - title: LunaModule.SEARCH.name, - subtitle: LunaModule.SEARCH.description, - icon: LunaModule.SEARCH.icon, - index: index, - route: LunaModule.SEARCH.route, - color: LunaModule.SEARCH.color, - ); - if(index == _serviceCount-1) return DashboardSummaryTile( - title: LunaModule.SETTINGS.name, - subtitle: LunaModule.SETTINGS.description, - icon: LunaModule.SETTINGS.icon, - index: index, - route: LunaModule.SETTINGS.route, - color: LunaModule.SETTINGS.color, - ); - LunaModule data = widget.profile.enabledModules[_hasIndexers ? index-1 : index]; - if(data != null) return DashboardSummaryTile( - title: data.name, - subtitle: data.description, - icon: data.icon, - index: index, - route: data.route, - color: data.color, - ); - return Container(); - }, - ); - } -} diff --git a/lib/modules/dashboard/widgets.dart b/lib/modules/dashboard/widgets.dart deleted file mode 100644 index 72b05bd2..00000000 --- a/lib/modules/dashboard/widgets.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'widgets/calendar.dart'; -export 'widgets/summary_tile.dart'; -export 'widgets/navigation_bar.dart'; diff --git a/lib/modules/dashboard/widgets/calendar.dart b/lib/modules/dashboard/widgets/calendar.dart deleted file mode 100644 index ea55e75d..00000000 --- a/lib/modules/dashboard/widgets/calendar.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:intl/intl.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/dashboard.dart'; -import 'package:table_calendar/table_calendar.dart'; - -class DashboardCalendarWidget extends StatefulWidget { - final Map events; - final DateTime today; - - DashboardCalendarWidget({ - Key key, - @required this.events, - @required this.today, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with TickerProviderStateMixin { - final TextStyle dayTileStyle = TextStyle( - color: Colors.white, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ); - final TextStyle outsideDayTileStyle = TextStyle( - color: Colors.white30, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ); - final TextStyle weekdayTitleStyle = TextStyle( - color: LunaColours.accent, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ); - - List _selectedEvents; - AnimationController _animationController; - CalendarController _calendarController; - - @override - void initState() { - super.initState(); - _selectedEvents = widget.events[widget.today] ?? []; - _calendarController = CalendarController(); - _animationController = AnimationController( vsync: this, duration: kThemeAnimationDuration); - _animationController?.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - _calendarController.dispose(); - super.dispose(); - } - - void _onDaySelected(DateTime day, List events, List _) { - HapticFeedback.selectionClick(); - setState(() { - _selectedEvents = events; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Provider.of(context).calendarStartingType == CalendarStartingType.CALENDAR - ? Padding( - child: Column( - children: [ - _calendar, - _list, - ], - ), - padding: EdgeInsets.only(top: 8.0), - ) - : _schedule - ); - } - - Widget get _calendar => ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ - DashboardDatabaseValue.CALENDAR_STARTING_DAY.key, - DashboardDatabaseValue.CALENDAR_STARTING_SIZE.key, - ]), - builder: (context, box, _) => LSCard( - child: Padding( - child: TableCalendar( - rowHeight: 48.0, - calendarController: _calendarController, - events: widget.events, - startingDayOfWeek: (DashboardDatabaseValue.CALENDAR_STARTING_DAY.data as CalendarStartingDay).data, - calendarStyle: CalendarStyle( - selectedColor: LunaColours.accent.withOpacity(0.25), - markersMaxAmount: 1, - markersColor: LunaColours.accent, - weekendStyle: dayTileStyle, - weekdayStyle: dayTileStyle, - outsideStyle: outsideDayTileStyle, - selectedStyle: dayTileStyle, - outsideWeekendStyle: outsideDayTileStyle, - renderDaysOfWeek: true, - highlightToday: true, - todayColor: LunaColours.primary, - todayStyle: dayTileStyle, - outsideDaysVisible: false, - ), - daysOfWeekStyle: DaysOfWeekStyle( - weekendStyle: weekdayTitleStyle, - weekdayStyle: weekdayTitleStyle, - ), - headerStyle: HeaderStyle( - titleTextStyle: TextStyle( - fontSize: Constants.UI_FONT_SIZE_HEADER, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - ), - centerHeaderTitle: true, - formatButtonVisible: false, - leftChevronIcon: Icon(Icons.arrow_back_ios_rounded), - rightChevronIcon: Icon(Icons.arrow_forward_ios_rounded), - ), - initialCalendarFormat: (DashboardDatabaseValue.CALENDAR_STARTING_SIZE.data as CalendarStartingSize).data, - availableCalendarFormats: const { - CalendarFormat.month : 'Month', CalendarFormat.twoWeeks : '2 Weeks', CalendarFormat.week : 'Week'}, - onDaySelected: _onDaySelected, - ), - padding: EdgeInsets.only(bottom: 12.0), - ), - ), - ); - - Widget get _list => Expanded( - child: LSListView( - children: _selectedEvents.length == 0 - ? [LSGenericMessage(text: 'No New Content')] - : _selectedEvents.map((event) => _entry(event)).toList(), - customPadding: EdgeInsets.only(bottom: 8.0), - ), - ); - - Widget get _schedule => LSListView( - children: widget.events.length == 0 - ? [LSGenericMessage(text: 'No New Content')] - : _days.expand((element) => element).toList(), - ); - - List> get _days { - List> days = []; - List keys = widget.events.keys.toList(); - keys.sort(); - for(var key in keys) { - if(key.isAfter(widget.today.subtract(Duration(days: 1))) && widget.events[key].length > 0) days.add(_day(key)); - } - return days; - } - - List _day(DateTime day) { - List listCards = []; - for(int i=0; i event.enterContent(context), - padContent: true, - decoration: LunaCardDecoration( - uri: event.bannerURI, - headers: headers, - ), - ); - } -} diff --git a/lib/modules/dashboard/widgets/navigation_bar.dart b/lib/modules/dashboard/widgets/navigation_bar.dart deleted file mode 100644 index 30cbd301..00000000 --- a/lib/modules/dashboard/widgets/navigation_bar.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/dashboard.dart'; - -class DashboardNavigationBar extends StatefulWidget { - static const List titles = [ - 'Modules', - 'Calendar', - ]; - - static const List icons = [ - LunaIcons.modules, - LunaIcons.calendar, - ]; - - final PageController pageController; - - DashboardNavigationBar({ - Key key, - @required this.pageController, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - Provider.of(context, listen: false).navigationIndex = DashboardDatabaseValue.NAVIGATION_INDEX.data; - }); - } - - @override - Widget build(BuildContext context) => Selector( - selector: (_, model) => model.navigationIndex, - builder: (context, index, _) => LSBottomNavigationBar( - index: index, - icons: DashboardNavigationBar.icons, - titles: DashboardNavigationBar.titles, - onTap: _navOnTap, - ), - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index) - .then((_) => Provider.of(context, listen: false).navigationIndex = index); - } -} \ No newline at end of file diff --git a/lib/modules/dashboard/widgets/summary_tile.dart b/lib/modules/dashboard/widgets/summary_tile.dart deleted file mode 100644 index 665de383..00000000 --- a/lib/modules/dashboard/widgets/summary_tile.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/dashboard.dart'; - -class DashboardSummaryTile extends StatelessWidget { - final String title; - final String subtitle; - final IconData icon; - final int index; - final String route; - final Color color; - - DashboardSummaryTile({ - @required this.title, - @required this.subtitle, - @required this.icon, - @required this.index, - @required this.route, - @required this.color, - }); - - @override - Widget build(BuildContext context) => ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [DashboardDatabaseValue.MODULES_BRAND_COLOURS.key]), - builder: (context, box, _) => LSCardTile( - title: LSTitle(text: title), - subtitle: LSSubtitle(text: subtitle), - trailing: LSIconButton( - icon: icon, - color: DashboardDatabaseValue.MODULES_BRAND_COLOURS.data - ? color - : LunaColours.list(index), - ), - onTap: () async => LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false), - ), - ); -} \ No newline at end of file diff --git a/lib/modules/lidarr/core/api/api.dart b/lib/modules/lidarr/core/api/api.dart index d9f493d3..e29c53d0 100644 --- a/lib/modules/lidarr/core/api/api.dart +++ b/lib/modules/lidarr/core/api/api.dart @@ -407,6 +407,15 @@ class LidarrAPI { )); break; } + case 'trackFileRetagged': { + _entries.add(LidarrHistoryDataTrackFileRetagged( + title: entry['sourceTitle'] ?? 'Unknown Title', + timestamp: entry['date'] ?? '', + artistID: entry['artistId'] ?? -1, + albumID: entry['albumId'] ?? -1, + )); + break; + } default: { _entries.add(LidarrHistoryDataGeneric( title: entry['sourceTitle'] ?? 'Unknown Title', diff --git a/lib/modules/lidarr/core/api/data/catalogue.dart b/lib/modules/lidarr/core/api/data/catalogue.dart index 5e421c10..6388e2f1 100644 --- a/lib/modules/lidarr/core/api/data/catalogue.dart +++ b/lib/modules/lidarr/core/api/data/catalogue.dart @@ -49,7 +49,7 @@ class LidarrCatalogueData { if(genres == null || genres.length == 0) { return 'Unknown'; } - return genres[0]; + return genres.join('\n'); } DateTime get dateAddedObject => DateTime.tryParse(added)?.toLocal(); diff --git a/lib/modules/lidarr/core/api/data/history.dart b/lib/modules/lidarr/core/api/data/history.dart index 8035cdac..a9cc301c 100644 --- a/lib/modules/lidarr/core/api/data/history.dart +++ b/lib/modules/lidarr/core/api/data/history.dart @@ -36,6 +36,12 @@ abstract class LidarrHistoryData { } List get subtitle; + + final Map historyReasonMessages = { + 'Upgrade': 'Upgraded File', + 'MissingFromDisk': 'Missing From Disk', + 'Manual': 'Manually Removed', + }; } class LidarrHistoryDataGeneric extends LidarrHistoryData { @@ -84,7 +90,7 @@ class LidarrHistoryDataGrabbed extends LidarrHistoryData { TextSpan( text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]} $indexer', style: TextStyle( - color: Colors.orange, + color: LunaColours.orange, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) @@ -162,7 +168,7 @@ class LidarrHistoryDataAlbumImportIncomplete extends LidarrHistoryData { TextSpan( text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]}', style: TextStyle( - color: Colors.orange, + color: LunaColours.orange, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) @@ -187,9 +193,9 @@ class LidarrHistoryDataTrackFileDeleted extends LidarrHistoryData { text: '$timestampString\n', ), TextSpan( - text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]} (${Constants.historyReasonMessages[reason] ?? reason})', + text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]} (${super.historyReasonMessages[reason] ?? reason})', style: TextStyle( - color: Colors.red, + color: LunaColours.red, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) @@ -213,7 +219,31 @@ class LidarrHistoryDataTrackFileRenamed extends LidarrHistoryData { TextSpan( text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]}', style: TextStyle( - color: LunaColours.accent, + color: LunaColours.blue, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ) + ]; + } +} + +class LidarrHistoryDataTrackFileRetagged extends LidarrHistoryData { + LidarrHistoryDataTrackFileRetagged({ + @required String title, + @required String timestamp, + @required int artistID, + @required int albumID, + }) : super(title, timestamp, 'trackFileRetagged', artistID, albumID); + + List get subtitle { + return [ + TextSpan( + text: '$timestampString\n', + ), + TextSpan( + text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]}', + style: TextStyle( + color: LunaColours.blue, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) diff --git a/lib/modules/lidarr/core/api/data/track.dart b/lib/modules/lidarr/core/api/data/track.dart index 88dc20fc..a05700df 100644 --- a/lib/modules/lidarr/core/api/data/track.dart +++ b/lib/modules/lidarr/core/api/data/track.dart @@ -31,7 +31,7 @@ class LidarrTrackData { return TextSpan( text: 'Not Downloaded', style: TextStyle( - color: monitored ? Colors.red : Colors.red.withOpacity(0.30), + color: monitored ? LunaColours.red : LunaColours.red.withOpacity(0.30), fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ); diff --git a/lib/modules/lidarr/core/constants.dart b/lib/modules/lidarr/core/constants.dart index 48093f94..0c0fe12a 100644 --- a/lib/modules/lidarr/core/constants.dart +++ b/lib/modules/lidarr/core/constants.dart @@ -5,6 +5,7 @@ class LidarrConstants { 'trackFileRenamed': 'Track File Renamed', 'trackFileDeleted': 'Track File Deleted', 'trackFileImported': 'Track File Imported', + 'trackFileRetagged': 'Track File Retagged', 'albumImportIncomplete': 'Album Import Incomplete', 'downloadImported': 'Download Imported', 'downloadFailed': 'Download Failed', diff --git a/lib/modules/lidarr/core/dialogs.dart b/lib/modules/lidarr/core/dialogs.dart index 7470c0c2..d11b9203 100644 --- a/lib/modules/lidarr/core/dialogs.dart +++ b/lib/modules/lidarr/core/dialogs.dart @@ -15,19 +15,19 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Quality Profile', content: List.generate( qualities.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: Icons.portrait, iconColor: LunaColours.list(index), text: qualities[index].name, onTap: () => _setValues(true, qualities[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _quality]; } @@ -42,19 +42,19 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Metadata Profile', content: List.generate( metadatas.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: Icons.portrait, iconColor: LunaColours.list(index), text: metadatas[index].name, onTap: () => _setValues(true, metadatas[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _metadata]; } @@ -69,25 +69,25 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Remove Artist', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Remove + Files', textColor: LunaColours.red, onPressed: () => _setValues(true, true), ), - LSDialog.button( + LunaDialog.button( text: 'Remove', textColor: LunaColours.red, onPressed: () => _setValues(true, false), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to remove the artist from Lidarr?'), + LunaDialog.textContent(text: 'Are you sure you want to remove the artist from Lidarr?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag, _files]; } @@ -100,19 +100,19 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Download Release', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Download', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to download this release? It has been marked as a rejected release by Lidarr.'), + LunaDialog.textContent(text: 'Are you sure you want to download this release? It has been marked as a rejected release by Lidarr.'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -125,19 +125,19 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Search All Missing', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Search', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to search for all missing albums?'), + LunaDialog.textContent(text: 'Are you sure you want to search for all missing albums?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -157,19 +157,19 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: entry.title, content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: _options[index][1], iconColor: LunaColours.list(index), text: _options[index][0], onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -184,16 +184,16 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Root Folder', content: List.generate( folders.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: folders[index].path, - subtitle: LSDialog.richText( + subtitle: LunaDialog.richText( children: [ - LSDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()), + LunaDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()), ], ), icon: Icons.folder, @@ -201,7 +201,7 @@ class LidarrDialogs { onTap: () => _setValues(true, folders[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _folder]; } @@ -223,19 +223,19 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Lidarr Settings', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -250,19 +250,19 @@ class LidarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Page', content: List.generate( LidarrNavigationBar.titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: LidarrNavigationBar.titles[index], icon: LidarrNavigationBar.icons[index], iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _index]; diff --git a/lib/modules/lidarr/core/state.dart b/lib/modules/lidarr/core/state.dart index ed5ebd8f..71931847 100644 --- a/lib/modules/lidarr/core/state.dart +++ b/lib/modules/lidarr/core/state.dart @@ -1,2 +1,96 @@ -export 'state/global.dart'; -export 'state/local.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/lidarr.dart'; + +class LidarrState extends LunaModuleState { + LidarrState() { + reset(); + } + + @override + void reset() {} + + ///Catalogue Sticky Header Content + String _searchCatalogueFilter = ''; + String get searchCatalogueFilter => _searchCatalogueFilter; + set searchCatalogueFilter(String searchCatalogueFilter) { + assert(searchCatalogueFilter != null); + _searchCatalogueFilter = searchCatalogueFilter; + notifyListeners(); + } + + LidarrCatalogueSorting _sortCatalogueType = LidarrCatalogueSorting.alphabetical; + LidarrCatalogueSorting get sortCatalogueType => _sortCatalogueType; + set sortCatalogueType(LidarrCatalogueSorting sortCatalogueType) { + assert(sortCatalogueType != null); + _sortCatalogueType = sortCatalogueType; + notifyListeners(); + } + + bool _sortCatalogueAscending = true; + bool get sortCatalogueAscending => _sortCatalogueAscending; + set sortCatalogueAscending(bool sortCatalogueAscending) { + assert(sortCatalogueAscending != null); + _sortCatalogueAscending = sortCatalogueAscending; + notifyListeners(); + } + + bool _hideUnmonitoredArtists = false; + bool get hideUnmonitoredArtists => _hideUnmonitoredArtists; + set hideUnmonitoredArtists(bool hideUnmonitoredArtists) { + assert(hideUnmonitoredArtists != null); + _hideUnmonitoredArtists = hideUnmonitoredArtists; + notifyListeners(); + } + + bool _hideUnmonitoredAlbums = false; + bool get hideUnmonitoredAlbums => _hideUnmonitoredAlbums; + set hideUnmonitoredAlbums(bool hideUnmonitoredAlbums) { + assert(hideUnmonitoredAlbums != null); + _hideUnmonitoredAlbums = hideUnmonitoredAlbums; + notifyListeners(); + } + + ///Releases Sticky Header Content + + String _searchReleasesFilter = ''; + String get searchReleasesFilter => _searchReleasesFilter; + set searchReleasesFilter(String searchReleasesFilter) { + assert(searchReleasesFilter != null); + _searchReleasesFilter = searchReleasesFilter; + notifyListeners(); + } + + LidarrReleasesSorting _sortReleasesType = LidarrReleasesSorting.weight; + LidarrReleasesSorting get sortReleasesType => _sortReleasesType; + set sortReleasesType(LidarrReleasesSorting sortReleasesType) { + assert(sortReleasesType != null); + _sortReleasesType = sortReleasesType; + notifyListeners(); + } + + bool _sortReleasesAscending = true; + bool get sortReleasesAscending => _sortReleasesAscending; + set sortReleasesAscending(bool sortReleasesAscending) { + assert(sortReleasesAscending != null); + _sortReleasesAscending = sortReleasesAscending; + notifyListeners(); + } + + bool _hideRejectedReleases = false; + bool get hideRejectedReleases => _hideRejectedReleases; + set hideRejectedReleases(bool hideRejectedReleases) { + assert(hideRejectedReleases != null); + _hideRejectedReleases = hideRejectedReleases; + notifyListeners(); + } + + /// Add New Series Content + + String _addSearchQuery = ''; + String get addSearchQuery => _addSearchQuery; + set addSearchQuery(String addSearchQuery) { + assert(addSearchQuery != null); + _addSearchQuery = addSearchQuery; + notifyListeners(); + } +} diff --git a/lib/modules/lidarr/core/state/global.dart b/lib/modules/lidarr/core/state/global.dart deleted file mode 100644 index afdad816..00000000 --- a/lib/modules/lidarr/core/state/global.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/lidarr.dart'; - -class LidarrState extends LunaModuleState { - LidarrState() { - reset(); - } - - @override - void reset() {} - - ///Catalogue Sticky Header Content - String _searchCatalogueFilter = ''; - String get searchCatalogueFilter => _searchCatalogueFilter; - set searchCatalogueFilter(String searchCatalogueFilter) { - assert(searchCatalogueFilter != null); - _searchCatalogueFilter = searchCatalogueFilter; - notifyListeners(); - } - - LidarrCatalogueSorting _sortCatalogueType = LidarrCatalogueSorting.alphabetical; - LidarrCatalogueSorting get sortCatalogueType => _sortCatalogueType; - set sortCatalogueType(LidarrCatalogueSorting sortCatalogueType) { - assert(sortCatalogueType != null); - _sortCatalogueType = sortCatalogueType; - notifyListeners(); - } - - bool _sortCatalogueAscending = true; - bool get sortCatalogueAscending => _sortCatalogueAscending; - set sortCatalogueAscending(bool sortCatalogueAscending) { - assert(sortCatalogueAscending != null); - _sortCatalogueAscending = sortCatalogueAscending; - notifyListeners(); - } - - bool _hideUnmonitoredArtists = false; - bool get hideUnmonitoredArtists => _hideUnmonitoredArtists; - set hideUnmonitoredArtists(bool hideUnmonitoredArtists) { - assert(hideUnmonitoredArtists != null); - _hideUnmonitoredArtists = hideUnmonitoredArtists; - notifyListeners(); - } - - bool _hideUnmonitoredAlbums = false; - bool get hideUnmonitoredAlbums => _hideUnmonitoredAlbums; - set hideUnmonitoredAlbums(bool hideUnmonitoredAlbums) { - assert(hideUnmonitoredAlbums != null); - _hideUnmonitoredAlbums = hideUnmonitoredAlbums; - notifyListeners(); - } - - ///Releases Sticky Header Content - - String _searchReleasesFilter = ''; - String get searchReleasesFilter => _searchReleasesFilter; - set searchReleasesFilter(String searchReleasesFilter) { - assert(searchReleasesFilter != null); - _searchReleasesFilter = searchReleasesFilter; - notifyListeners(); - } - - LidarrReleasesSorting _sortReleasesType = LidarrReleasesSorting.weight; - LidarrReleasesSorting get sortReleasesType => _sortReleasesType; - set sortReleasesType(LidarrReleasesSorting sortReleasesType) { - assert(sortReleasesType != null); - _sortReleasesType = sortReleasesType; - notifyListeners(); - } - - bool _sortReleasesAscending = true; - bool get sortReleasesAscending => _sortReleasesAscending; - set sortReleasesAscending(bool sortReleasesAscending) { - assert(sortReleasesAscending != null); - _sortReleasesAscending = sortReleasesAscending; - notifyListeners(); - } - - bool _hideRejectedReleases = false; - bool get hideRejectedReleases => _hideRejectedReleases; - set hideRejectedReleases(bool hideRejectedReleases) { - assert(hideRejectedReleases != null); - _hideRejectedReleases = hideRejectedReleases; - notifyListeners(); - } - - /// Add New Series Content - - String _addSearchQuery = ''; - String get addSearchQuery => _addSearchQuery; - set addSearchQuery(String addSearchQuery) { - assert(addSearchQuery != null); - _addSearchQuery = addSearchQuery; - notifyListeners(); - } - - int _navigationIndex = 0; - int get navigationIndex => _navigationIndex; - set navigationIndex(int navigationIndex) { - assert(navigationIndex != null); - _navigationIndex = navigationIndex; - notifyListeners(); - } - - int _artistNavigationIndex = 0; - int get artistNavigationIndex => _artistNavigationIndex; - set artistNavigationIndex(int artistNavigationIndex) { - assert(artistNavigationIndex != null); - _artistNavigationIndex = artistNavigationIndex; - notifyListeners(); - } -} diff --git a/lib/modules/lidarr/core/state/local.dart b/lib/modules/lidarr/core/state/local.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/modules/lidarr/routes/add_details.dart b/lib/modules/lidarr/routes/add_details.dart index 2d75a284..ef6bfa27 100644 --- a/lib/modules/lidarr/routes/add_details.dart +++ b/lib/modules/lidarr/routes/add_details.dart @@ -18,7 +18,7 @@ class LidarrAddDetails extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); LidarrAddDetailsArguments _arguments; Future _future; @@ -121,16 +121,17 @@ class _State extends State { ? null : LunaAppBar( title: _arguments.data.title, + scrollControllers: [scrollController], actions: [ - LSIconButton( + LunaIconButton( icon: Icons.link, - onPressed: () async => _arguments.data.discogsLink == null || _arguments.data.discogsLink == '' - ? LSSnackBar( - context: context, + onPressed: () async { + if(_arguments.data.discogsLink == null || _arguments.data.discogsLink == '') showLunaInfoSnackBar( title: 'No Discogs Page Available', message: 'No Discogs URL is available', - ) - : _arguments.data.discogsLink.lunaOpenGenericLink() + ); + _arguments.data.discogsLink.lunaOpenGenericLink(); + }, ) ], ); @@ -142,20 +143,21 @@ class _State extends State { builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError) return LunaMessage.error(onTap: () => _refresh()); return _list; } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ); - Widget get _list => LSListView( + Widget get _list => LunaListView( + controller: scrollController, children: [ - LSDescriptionBlock( + LidarrDescriptionBlock( title: _arguments.data.title ?? 'Unknown', description: _arguments.data.overview == '' ? 'No summary is available.' @@ -168,9 +170,10 @@ class _State extends State { ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [LidarrDatabaseValue.ADD_MONITORED.key]), builder: (context, box, widget) { - return LSCardTile( - title: LSTitle(text: 'Monitored'), - subtitle: LSSubtitle(text: 'Monitor artist for new releases'), + return LunaListTile( + context: context, + title: LunaText.title(text: 'Monitored'), + subtitle: LunaText.subtitle(text: 'Monitor artist for new releases'), trailing: LunaSwitch( value: LidarrDatabaseValue.ADD_MONITORED.data, onChanged: (value) => LidarrDatabaseValue.ADD_MONITORED.put(value), @@ -181,9 +184,10 @@ class _State extends State { ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [LidarrDatabaseValue.ADD_ALBUM_FOLDERS.key]), builder: (context, box, widget) { - return LSCardTile( - title: LSTitle(text: 'Use Album Folders'), - subtitle: LSSubtitle(text: 'Sort tracks into album folders'), + return LunaListTile( + context: context, + title: LunaText.title(text: 'Use Album Folders'), + subtitle: LunaText.subtitle(text: 'Sort tracks into album folders'), trailing: LunaSwitch( value: LidarrDatabaseValue.ADD_ALBUM_FOLDERS.data, onChanged: (value) => LidarrDatabaseValue.ADD_ALBUM_FOLDERS.put(value), @@ -195,10 +199,11 @@ class _State extends State { valueListenable: Database.lunaSeaBox.listenable(keys: [LidarrDatabaseValue.ADD_ROOT_FOLDER.key]), builder: (context, box, widget) { LidarrRootFolder _rootfolder = LidarrDatabaseValue.ADD_ROOT_FOLDER.data; - return LSCardTile( - title: LSTitle(text: 'Root Folder'), - subtitle: LSSubtitle(text: _rootfolder?.path ?? 'Unknown Root Folder'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + return LunaListTile( + context: context, + title: LunaText.title(text: 'Root Folder'), + subtitle: LunaText.subtitle(text: _rootfolder?.path ?? 'Unknown Root Folder'), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { List _values = await LidarrDialogs.editRootFolder(context, _rootFolders); if(_values[0]) LidarrDatabaseValue.ADD_ROOT_FOLDER.put(_values[1]); @@ -210,10 +215,11 @@ class _State extends State { valueListenable: Database.lunaSeaBox.listenable(keys: [LidarrDatabaseValue.ADD_QUALITY_PROFILE.key]), builder: (context, box, widget) { LidarrQualityProfile _profile = LidarrDatabaseValue.ADD_QUALITY_PROFILE.data; - return LSCardTile( - title: LSTitle(text: 'Quality Profile'), - subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + return LunaListTile( + context: context, + title: LunaText.title(text: 'Quality Profile'), + subtitle: LunaText.subtitle(text: _profile?.name ?? 'Unknown Profile'), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { List _values = await LidarrDialogs.editQualityProfile(context, _qualityProfiles); if(_values[0]) LidarrDatabaseValue.ADD_QUALITY_PROFILE.put(_values[1]); @@ -225,10 +231,11 @@ class _State extends State { valueListenable: Database.lunaSeaBox.listenable(keys: [LidarrDatabaseValue.ADD_METADATA_PROFILE.key]), builder: (context, box, widget) { LidarrMetadataProfile _profile = LidarrDatabaseValue.ADD_METADATA_PROFILE.data; - return LSCardTile( - title: LSTitle(text: 'Metadata Profile'), - subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + return LunaListTile( + context: context, + title: LunaText.title(text: 'Metadata Profile'), + subtitle: LunaText.subtitle(text: _profile?.name ?? 'Unknown Profile'), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { List _values = await LidarrDialogs.editMetadataProfile(context, _metadataProfiles); if(_values[0]) LidarrDatabaseValue.ADD_METADATA_PROFILE.put(_values[1]); @@ -251,6 +258,12 @@ class _State extends State { search: search, ) .then((id) => Navigator.of(context).pop(['artist_added', _arguments.data.title, id])) - .catchError((_) => LSSnackBar(context: context, title: search ? 'Failed to Add Artist (With Search)' : 'Failed to Add Artist', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .catchError((error, stack) { + LunaLogger().error('Failed to add artist', error, stack); + showLunaErrorSnackBar( + title: search ? 'Failed to Add Artist (With Search)' : 'Failed to Add Artist', + error: error, + ); + }); } } diff --git a/lib/modules/lidarr/routes/add_search.dart b/lib/modules/lidarr/routes/add_search.dart index 6a7254bc..3bfc592d 100644 --- a/lib/modules/lidarr/routes/add_search.dart +++ b/lib/modules/lidarr/routes/add_search.dart @@ -39,14 +39,17 @@ class _State extends State with LunaScrollControllerMixin { Future _fetchAvailableArtists() async { await LidarrAPI.from(Database.currentProfileObject).getAllArtistIDs() .then((data) => _availableIDs = data) - .catchError((_) => _availableIDs = []); + .catchError((error) => _availableIDs = []); } Widget _appBar() { return LunaAppBar( scrollControllers: [scrollController], title: 'Add Artist', - bottom: LidarrAddSearchBar(callback: _refresh), + bottom: LidarrAddSearchBar( + callback: _refresh, + scrollController: scrollController, + ), ); } diff --git a/lib/modules/lidarr/routes/catalogue.dart b/lib/modules/lidarr/routes/catalogue.dart index d476256f..51dae24a 100644 --- a/lib/modules/lidarr/routes/catalogue.dart +++ b/lib/modules/lidarr/routes/catalogue.dart @@ -17,9 +17,8 @@ class LidarrCatalogue extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { - final _scaffoldKey = GlobalKey(); - final _scrollController = ScrollController(); +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); Future> _future; List _results = []; @@ -27,17 +26,10 @@ class _State extends State with AutomaticKeepAliveClientMixin { bool get wantKeepAlive => true; @override - void initState() { - super.initState(); - _refresh(); - } - - Future _refresh() async { + Future loadCallback() async { if(mounted) setState(() => _results = []); final _api = LidarrAPI.from(Database.currentProfileObject); if(mounted) setState(() => { _future = _api.getAllArtists() }); - //Clear the search filter using a microtask - Future.microtask(() => Provider.of(context, listen: false)?.searchCatalogueFilter = ''); } void _refreshState() => setState(() {}); @@ -49,92 +41,86 @@ class _State extends State with AutomaticKeepAliveClientMixin { super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), + appBar: _appBar(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; + Widget _appBar() { + return LunaAppBar.empty( + child: LidarrCatalogueSearchBar(scrollController: LidarrNavigationBar.scrollControllers[0]), + height: 62.0, + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: widget.refreshIndicatorKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: _future, + builder: (context, snapshot) { + switch(snapshot.connectionState) { + case ConnectionState.done: { + if(snapshot.hasError || snapshot.data == null) { + return LunaMessage.error(onTap: () => widget.refreshIndicatorKey.currentState?.show); + } + _results = snapshot.data; + return _list(); + } + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + default: return LunaLoader(); } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); + }, + ), + ); + } - Widget get _searchSortBar => LSContainerRow( - padding: EdgeInsets.zero, - backgroundColor: Theme.of(context).primaryColor, - children: [ - LidarrCatalogueSearchBar(), - LidarrCatalogueHideButton(controller: _scrollController), - LidarrCatalogueSortButton(controller: _scrollController), - ], - ); - - Widget get _list => _results.length == 0 - ? LSGenericMessage( + Widget _list() { + if((_results?.length ?? 0) == 0) return LunaMessage( text: 'No Artists Found', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => _refresh(), - ) - : Consumer( - builder: (context, model, widget) { - //Filter and sort the results - List _filtered = _sort(model, _filter(model.searchCatalogueFilter)); - _filtered = model.hideUnmonitoredArtists ? _hide(_filtered) : _filtered; - return _listBody(_filtered); - } + onTap: widget.refreshIndicatorKey.currentState?.show, ); - - Widget _listBody(List filtered) { - List _children = filtered.length == 0 - ? [LSGenericMessage(text: 'No Results Found')] - : List.generate( - filtered.length, - (index) => LidarrCatalogueTile( - data: filtered[index], - scaffoldKey: _scaffoldKey, - refresh: () => _refreshAllPages(), - refreshState: () => _refreshState(), - ), - ); - return LSListViewStickyHeader( - controller: _scrollController, - slivers: [ - LunaSliverStickyHeader( - header: _searchSortBar, - children: _children, - ), - ], + return Consumer( + builder: (context, state, _) { + List filtered = _filterAndSort(_results, state.searchCatalogueFilter); + if((filtered?.length ?? 0) == 0) return LunaListView( + controller: LidarrNavigationBar.scrollControllers[0], + children: [ + LunaMessage.inList(text: 'No Artists Found'), + ], + ); + return LunaListViewBuilder( + controller: LidarrNavigationBar.scrollControllers[0], + itemCount: filtered.length, + itemBuilder: (context, index) => LidarrCatalogueTile( + data: filtered[index], + scaffoldKey: _scaffoldKey, + refresh: () => _refreshAllPages(), + refreshState: () => _refreshState(), + ), + ); + }, ); } - List _filter(String filter) => _results.where( - (entry) => filter == null || filter == '' - ? entry != null - : entry.title.toLowerCase().contains(filter.toLowerCase()) - ).toList(); - - List _sort(LidarrState state, List data) { - if(data != null && data.length != 0) return state.sortCatalogueType.sort(data, state.sortCatalogueAscending); - return data; + List _filterAndSort(List artists, String query) { + if((artists?.length ?? 0) == 0) return artists; + LidarrCatalogueSorting sorting = context.read().sortCatalogueType; + bool shouldHide = context.read().hideUnmonitoredArtists; + bool ascending = context.read().sortCatalogueAscending; + // Filter + List filtered = artists.where((artist) { + if(shouldHide && !artist.monitored) return false; + if(query != null && query.isNotEmpty && artist.artistID != null) + return artist.title.toLowerCase().contains(query.toLowerCase()); + return (artist != null && artist.artistID != null); + }).toList(); + filtered = sorting.sort(filtered, ascending); + return filtered; } - - List _hide(List data) => data == null || data.length == 0 - ? data - : data.where((entry) => entry.monitored).toList(); } diff --git a/lib/modules/lidarr/routes/details_album.dart b/lib/modules/lidarr/routes/details_album.dart index 99f58df2..c4425fed 100644 --- a/lib/modules/lidarr/routes/details_album.dart +++ b/lib/modules/lidarr/routes/details_album.dart @@ -22,7 +22,7 @@ class LidarrDetailsAlbum extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @@ -55,8 +55,9 @@ class _State extends State { Widget get _appBar => LunaAppBar( title: _arguments == null ? 'Details Album' : _arguments.title, + scrollControllers: [scrollController], actions: [ - LSIconButton( + LunaIconButton( icon: Icons.search, onPressed: () async => _automaticSearch(), onLongPress: () async => _manualSearch(), @@ -66,35 +67,36 @@ class _State extends State { Widget get _body => _arguments == null ? null - : LSRefreshIndicator( - refreshKey: _refreshKey, + : LunaRefreshIndicator( + context: context, + key: _refreshKey, onRefresh: _refresh, child: FutureBuilder( future: _future, builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError || snapshot.data == null) return LunaMessage.error(onTap: () => _refresh()); _results = snapshot.data; return _list; } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ), ); Widget get _list => _results.length == 0 - ? LSGenericMessage( + ? LunaMessage( text: 'No Tracks Found', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => _refresh(), + onTap: () => _refresh(), ) - : LSListViewBuilder( + : LunaListViewBuilder( + controller: scrollController, itemCount: _results.length, itemBuilder: (context, index) { return LidarrDetailsTrackTile( @@ -108,8 +110,19 @@ class _State extends State { Future _automaticSearch() async { LidarrAPI _api = LidarrAPI.from(Database.currentProfileObject); _api.searchAlbums([_arguments.albumID]) - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: _arguments.title)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) { + showLunaSuccessSnackBar( + title: 'Searching...', + message: _arguments.title, + ); + }) + .catchError((error, stack) { + LunaLogger().error('Failed to search for album', error, stack); + showLunaErrorSnackBar( + title: 'Failed to Search', + error: error, + ); + }); } Future _manualSearch() async => Navigator.of(context).pushNamed( diff --git a/lib/modules/lidarr/routes/details_artist.dart b/lib/modules/lidarr/routes/details_artist.dart index f782a5de..4aa876ec 100644 --- a/lib/modules/lidarr/routes/details_artist.dart +++ b/lib/modules/lidarr/routes/details_artist.dart @@ -22,7 +22,7 @@ class LidarrDetailsArtist extends StatefulWidget { class _State extends State { final GlobalKey _scaffoldKey = GlobalKey(); - final _pageController = PageController(initialPage: 1); + LunaPageController _pageController = LunaPageController(initialPage: 1); LidarrDetailsArtistArguments _arguments; bool _error = false; @@ -31,7 +31,6 @@ class _State extends State { super.initState(); SchedulerBinding.instance.addPostFrameCallback((_) { _arguments = ModalRoute.of(context).settings.arguments; - Provider.of(context, listen: false).artistNavigationIndex = 1; _fetch(); }); } @@ -44,7 +43,7 @@ class _State extends State { _arguments.data = data; _error = false; }); - }).catchError((_) { + }).catchError((error) { if(mounted) setState(() => _error = true); }); } @@ -60,8 +59,8 @@ class _State extends State { ? _arguments.data != null ? _body : _error - ? LSErrorMessage(onTapHandler: () => _fetch()) - : LSLoader() + ? LunaMessage.error(onTap: () => _fetch()) + : LunaLoader() : null, ); @@ -69,6 +68,8 @@ class _State extends State { title: _arguments == null || _arguments.data == null ? 'Artist Details' : _arguments.data.title, + pageController: _pageController, + scrollControllers: LidarrArtistNavigationBar.scrollControllers, actions: _arguments == null || _arguments.data == null ? null : [ @@ -91,10 +92,7 @@ class _State extends State { Widget get _body => PageView( controller: _pageController, children: _tabs, - onPageChanged: _onPageChanged, ); - void _onPageChanged(int index) => Provider.of(context, listen: false).artistNavigationIndex = index; - Future _removeCallback(bool withData) async => Navigator.of(context).pop(['remove_artist', withData]); } diff --git a/lib/modules/lidarr/routes/edit_artist.dart b/lib/modules/lidarr/routes/edit_artist.dart index d07d4b7c..933dc0a0 100644 --- a/lib/modules/lidarr/routes/edit_artist.dart +++ b/lib/modules/lidarr/routes/edit_artist.dart @@ -18,7 +18,7 @@ class LidarrEditArtist extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaScrollControllerMixin { final _scaffoldKey = GlobalKey(); LidarrEditArtistArguments _arguments; @@ -91,7 +91,10 @@ class _State extends State { }); } - Widget get _appBar => LunaAppBar(title: _arguments?.entry?.title ?? 'Edit Artist'); + Widget get _appBar => LunaAppBar( + title: _arguments?.entry?.title ?? 'Edit Artist', + scrollControllers: [scrollController], + ); Widget _bottomActionBar() { return LunaBottomActionBar( @@ -110,51 +113,57 @@ class _State extends State { builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError || snapshot.data == null) return LunaMessage.error(onTap: () => _refresh()); return _list; } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ); - Widget get _list => LSListView( + Widget get _list => LunaListView( + controller: scrollController, children: [ - LSCardTile( - title: LSTitle(text: 'Monitored'), - subtitle: LSSubtitle(text: 'Monitor artist for new releases'), + LunaListTile( + context: context, + title: LunaText.title(text: 'Monitored'), + subtitle: LunaText.subtitle(text: 'Monitor artist for new releases'), trailing: LunaSwitch( value: _monitored, onChanged: (value) => setState(() => _monitored = value), ), ), - LSCardTile( - title: LSTitle(text: 'Use Album Folders'), - subtitle: LSSubtitle(text: 'Sort tracks into album folders'), + LunaListTile( + context: context, + title: LunaText.title(text: 'Use Album Folders'), + subtitle: LunaText.subtitle(text: 'Sort tracks into album folders'), trailing: LunaSwitch( value: _albumFolders, onChanged: (value) => setState(() => _albumFolders = value), ), ), - LSCardTile( - title: LSTitle(text: 'Artist Path'), - subtitle: LSSubtitle(text: _path), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + LunaListTile( + context: context, + title: LunaText.title(text: 'Artist Path'), + subtitle: LunaText.subtitle(text: _path), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () => _changePath(), ), - LSCardTile( - title: LSTitle(text: 'Quality Profile'), - subtitle: LSSubtitle(text: _qualityProfile.name), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + LunaListTile( + context: context, + title: LunaText.title(text: 'Quality Profile'), + subtitle: LunaText.subtitle(text: _qualityProfile.name), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () => _changeProfile(), ), - LSCardTile( - title: LSTitle(text: 'Metadata Profile'), - subtitle: LSSubtitle(text: _metadataProfile.name), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + LunaListTile( + context: context, + title: LunaText.title(text: 'Metadata Profile'), + subtitle: LunaText.subtitle(text: _metadataProfile.name), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () => _changeMetadata(), ), ], @@ -195,6 +204,12 @@ class _State extends State { _arguments.entry.albumFolders = _albumFolders; Navigator.of(context).pop([true]); }) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Update', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .catchError((error, stack) { + LunaLogger().error('Failed to update artist', error, stack); + showLunaErrorSnackBar( + title: 'Failed to Update', + error: error, + ); + }); } } diff --git a/lib/modules/lidarr/routes/history.dart b/lib/modules/lidarr/routes/history.dart index 8e784cfa..03d7f950 100644 --- a/lib/modules/lidarr/routes/history.dart +++ b/lib/modules/lidarr/routes/history.dart @@ -46,39 +46,44 @@ class _State extends State with AutomaticKeepAliveClientMixin { super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: widget.refreshIndicatorKey, + onRefresh: _refresh, + child: FutureBuilder( + future: _future, + builder: (context, snapshot) { + switch(snapshot.connectionState) { + case ConnectionState.done: { + if(snapshot.hasError || snapshot.data == null) { + return LunaMessage.error(onTap: () => _refresh()); + } + _results = snapshot.data; + return _list; + } + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + default: return LunaLoader(); } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); + }, + ), + ); + } Widget get _list => _results.length == 0 - ? LSGenericMessage( + ? LunaMessage( text: 'No History Found', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => _refresh(), + onTap: () => _refresh(), ) - : LSListViewBuilder( + : LunaListViewBuilder( + controller: LidarrNavigationBar.scrollControllers[2], itemCount: _results.length, itemBuilder: (context, index) => LidarrHistoryTile( entry: _results[index], diff --git a/lib/modules/lidarr/routes/lidarr.dart b/lib/modules/lidarr/routes/lidarr.dart index 8a7b2980..3d06dc32 100644 --- a/lib/modules/lidarr/routes/lidarr.dart +++ b/lib/modules/lidarr/routes/lidarr.dart @@ -11,7 +11,7 @@ class Lidarr extends StatefulWidget { class _State extends State { final _scaffoldKey = GlobalKey(); - final _pageController = PageController(initialPage: LidarrDatabaseValue.NAVIGATION_INDEX.data); + LunaPageController _pageController; String _profileState = Database.currentProfileObject.toString(); LidarrAPI _api = LidarrAPI.from(Database.currentProfileObject); @@ -24,80 +24,79 @@ class _State extends State { @override void initState() { super.initState(); - Future.microtask(() => Provider.of(context, listen: false).navigationIndex = 0); + _pageController = LunaPageController(initialPage: LidarrDatabaseValue.NAVIGATION_INDEX.data); } @override - Widget build(BuildContext context) => WillPopScope( - onWillPop: () async { - if(_scaffoldKey.currentState.isDrawerOpen) { - //If the drawer is open, return true to close it - return true; - } else { - //If the drawer isn't open, open the drawer - _scaffoldKey.currentState.openDrawer(); - return false; - } - }, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]), - builder: (context, box, widget) { + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + body: _body(), + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + onProfileChange: (_) { if(_profileState != Database.currentProfileObject.toString()) _refreshProfile(); - return Scaffold( - key: _scaffoldKey, - body: _body, - drawer: _drawer, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - ); }, - ), - ); + ); + } - Widget get _drawer => LunaDrawer(page: LunaModule.LIDARR.key); + Widget _drawer() => LunaDrawer(page: LunaModule.LIDARR.key); - Widget get _bottomNavigationBar => LidarrNavigationBar(pageController: _pageController); + Widget _bottomNavigationBar() { + if(_api.enabled) return LidarrNavigationBar(pageController: _pageController); + return null; + } - List get _tabs => [ - LidarrCatalogue( - refreshIndicatorKey: _refreshKeys[0], - refreshAllPages: _refreshAllPages, - ), - LidarrMissing( - refreshIndicatorKey: _refreshKeys[1], - refreshAllPages: _refreshAllPages, - ), - LidarrHistory( - refreshIndicatorKey: _refreshKeys[2], - refreshAllPages: _refreshAllPages, - ), - ]; + Widget _body() { + if(!_api.enabled) return LunaMessage.moduleNotEnabled( + context: context, + module: LunaModule.LIDARR.name, + ); + return PageView( + controller: _pageController, + children: [ + LidarrCatalogue( + refreshIndicatorKey: _refreshKeys[0], + refreshAllPages: _refreshAllPages, + ), + LidarrMissing( + refreshIndicatorKey: _refreshKeys[1], + refreshAllPages: _refreshAllPages, + ), + LidarrHistory( + refreshIndicatorKey: _refreshKeys[2], + refreshAllPages: _refreshAllPages, + ), + ], + ); + } - Widget get _body => PageView( - controller: _pageController, - children: _api.enabled ? _tabs : List.generate(_tabs.length, (_) => LSNotEnabled(LunaModule.LIDARR.name)), - onPageChanged: _onPageChanged, - ); - - Widget get _appBar => LunaAppBar.dropdown( - title: LunaModule.LIDARR.name, - profiles: Database.profilesBox.keys.fold([], (value, element) { + Widget _appBar() { + List profiles = Database.profilesBox.keys.fold([], (value, element) { if(Database.profilesBox.get(element)?.lidarrEnabled ?? false) value.add(element); return value; - }), - actions: _api.enabled - ? [ - LSIconButton( - icon: Icons.add, - onPressed: () async => _enterAddArtist(), - ), - LSIconButton( - icon: Icons.more_vert, - onPressed: () async => _handlePopup(), - ) - ] - : null, - ); + }); + List actions; + if(_api.enabled) actions = [ + LunaIconButton( + icon: Icons.add, + onPressed: () async => _enterAddArtist(), + ), + LunaIconButton( + icon: Icons.more_vert, + onPressed: () async => _handlePopup(), + ), + ]; + return LunaAppBar.dropdown( + title: LunaModule.LIDARR.name, + useDrawer: true, + profiles: profiles, + actions: actions, + pageController: _pageController, + scrollControllers: LidarrNavigationBar.scrollControllers, + ); + } Future _enterAddArtist() async { final _model = Provider.of(context, listen: false); @@ -105,11 +104,9 @@ class _State extends State { final dynamic result = await Navigator.of(context).pushNamed(LidarrAddSearch.ROUTE_NAME); if(result != null) switch(result[0]) { case 'artist_added': { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: 'Artist Added', message: result[1], - type: SNACKBAR_TYPE.success, showButton: true, buttonOnPressed: () => Navigator.of(context).pushNamed( LidarrDetailsArtist.ROUTE_NAME, @@ -131,29 +128,27 @@ class _State extends State { if(values[0]) switch(values[1]) { case 'web_gui': await _api.host?.toString()?.lunaOpenGenericLink(); break; case 'update_library': await _api.updateLibrary() - .then((_) => LSSnackBar(context: context, title: 'Updating Library...', message: 'Updating your library in the background')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Update Library', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Updating Library...', message: 'Updating your library in the background')) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Update Library', error: error)); break; case 'rss_sync': await _api.triggerRssSync() - .then((_) => LSSnackBar(context: context, title: 'Running RSS Sync...', message: 'Running RSS sync in the background')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Run RSS Sync', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Running RSS Sync...', message: 'Running RSS sync in the background')) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Run RSS Sync', error: error)); break; case 'backup': await _api.triggerBackup() - .then((_) => LSSnackBar(context: context, title: 'Backing Up Database...', message: 'Backing up database in the background')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Backup Database', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Backing Up Database...', message: 'Backing up database in the background')) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Backup Database', error: error)); break; case 'missing_search': { List values = await LidarrDialogs.searchAllMissing(context); if(values[0]) await _api.searchAllMissing() - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: 'Search for all missing albums')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Searching...', message: 'Search for all missing albums')) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Search', error: error)); break; } default: LunaLogger().warning('Lidarr', '_handlePopup', 'Unknown Case: ${values[1]}'); } } - - void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; void _refreshProfile() { _api = LidarrAPI.from(Database.currentProfileObject); diff --git a/lib/modules/lidarr/routes/missing.dart b/lib/modules/lidarr/routes/missing.dart index 97d759dd..2e237f9c 100644 --- a/lib/modules/lidarr/routes/missing.dart +++ b/lib/modules/lidarr/routes/missing.dart @@ -50,35 +50,36 @@ class _State extends State with AutomaticKeepAliveClientMixin { ); } - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, + Widget get _body => LunaRefreshIndicator( + context: context, + key: widget.refreshIndicatorKey, onRefresh: _refresh, child: FutureBuilder( future: _future, builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError || snapshot.data == null) return LunaMessage.error(onTap: () => _refresh()); _results = snapshot.data; return _list; } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ), ); Widget get _list => _results.length == 0 - ? LSGenericMessage( + ? LunaMessage( text: 'No Missing Albums', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => widget.refreshIndicatorKey?.currentState?.show(), + onTap: () => widget.refreshIndicatorKey?.currentState?.show(), ) - : LSListViewBuilder( + : LunaListViewBuilder( + controller: LidarrNavigationBar.scrollControllers[1], itemCount: _results.length, itemBuilder: (context, index) => LidarrMissingTile( scaffoldKey: _scaffoldKey, diff --git a/lib/modules/lidarr/routes/search_results.dart b/lib/modules/lidarr/routes/search_results.dart index e112ef69..df44b160 100644 --- a/lib/modules/lidarr/routes/search_results.dart +++ b/lib/modules/lidarr/routes/search_results.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; +import 'package:lunasea/modules.dart'; import 'package:lunasea/modules/lidarr.dart'; class LidarrSearchResultsArguments { @@ -20,25 +20,15 @@ class LidarrSearchResults extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaScrollControllerMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - final ScrollController _scrollController = ScrollController(); - LidarrSearchResultsArguments _arguments; Future> _future; - List _results; + List _results = []; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - _arguments = ModalRoute.of(context).settings.arguments; - _refresh(); - }); - } - - Future _refresh() async { + Future loadCallback() async { + _arguments = ModalRoute.of(context).settings.arguments; if(mounted) setState(() => _results = []); final _api = LidarrAPI.from(Database.currentProfileObject); setState(() => { _future = _api.getReleases(_arguments.albumID) }); @@ -47,94 +37,86 @@ class _State extends State { } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - body: _body, - appBar: _appBar, - ); + Widget build(BuildContext context) { + _arguments = ModalRoute.of(context).settings.arguments; + return Scaffold( + key: _scaffoldKey, + body: _body(), + appBar: _appBar(), + ); + } - Widget get _appBar => _arguments == null - ? null - : LunaAppBar(title: _arguments.title); + Widget _appBar() { + return LunaAppBar( + title: _arguments.title, + scrollControllers: [scrollController], + bottom: LidarrReleasesSearchBar(scrollController: scrollController), + ); + } - Widget get _body => _arguments == null - ? null - : LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: () => _refresh(), + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, child: FutureBuilder( future: _future, builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError || snapshot.data == null) { + return LunaMessage.error(onTap: () => _refreshKey.currentState?.show); + } _results = snapshot.data; - return _list; + return _list(); } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ), ); + } - Widget get _searchSortBar => LSContainerRow( - padding: EdgeInsets.zero, - backgroundColor: Theme.of(context).primaryColor, - children: [ - LidarrReleasesSearchBar(), - LidarrReleasesHideButton(controller: _scrollController), - LidarrReleasesSortButton(controller: _scrollController), - ], - ); - - Widget get _list => _results.length == 0 - ? LSGenericMessage( - text: 'No Results Found', - showButton: true, + Widget _list() { + if((_results?.length ?? 0) == 0) return LunaMessage( + text: 'No Releases Found', buttonText: 'Refresh', - onTapHandler: () => _refresh(), - ) - : Consumer( - builder: (context, model, widget) { - List _filtered = _sort(model, _filter(model.searchReleasesFilter)); - _filtered = model.hideRejectedReleases ? _hide(_filtered) : _filtered; - return _listBody(_filtered); + onTap: _refreshKey.currentState?.show, + ); + return Consumer( + builder: (context, state, _) { + List filtered = _filterAndSort(_results, state.searchReleasesFilter); + if((filtered?.length ?? 0) == 0) return LunaListView( + controller: scrollController, + children: [ + LunaMessage.inList(text: 'No Releases Found'), + ], + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: filtered.length, + itemBuilder: (context, index) => LidarrReleasesTile(release: filtered[index]), + ); }, ); - - Widget _listBody(List filtered) { - List _children = filtered.length == 0 - ? [LSGenericMessage(text: 'No Results Found')] - : List.generate( - filtered.length, - (index) => LidarrReleasesTile(release: filtered[index]), - ); - return LSListViewStickyHeader( - controller: _scrollController, - slivers: [ - LunaSliverStickyHeader( - header: _searchSortBar, - children: _children, - ) - ], - ); } - List _filter(String filter) => _results.where( - (entry) => filter == null || filter == '' - ? entry != null - : entry.title.toLowerCase().contains(filter.toLowerCase()) - ).toList(); - - List _sort(LidarrState state, List data) { - if(data != null && data.length != 0) return state.sortReleasesType.sort(data, state.sortReleasesAscending); - return data; + List _filterAndSort(List releases, String query) { + if((releases?.length ?? 0) == 0) return releases; + LidarrReleasesSorting sorting = context.read().sortReleasesType; + bool shouldHide = context.read().hideRejectedReleases; + bool ascending = context.read().sortReleasesAscending; + // Filter + List filtered = releases.where((release) { + if(shouldHide && !release.approved) return false; + if(query != null && query.isNotEmpty) + return release.title.toLowerCase().contains(query.toLowerCase()); + return release != null; + }).toList(); + filtered = sorting.sort(filtered, ascending); + return filtered; } - - List _hide(List data) => data == null || data.length == 0 - ? data - : data.where((entry) => entry.approved).toList(); } diff --git a/lib/modules/lidarr/widgets.dart b/lib/modules/lidarr/widgets.dart index 37008422..5d57f82a 100644 --- a/lib/modules/lidarr/widgets.dart +++ b/lib/modules/lidarr/widgets.dart @@ -4,6 +4,7 @@ export 'widgets/missing_tile.dart'; export 'widgets/catalogue_search_bar.dart'; export 'widgets/catalogue_sorting_button.dart'; export 'widgets/catalogue_hide_button.dart'; +export 'widgets/description_block.dart'; export 'widgets/details_edit_button.dart'; export 'widgets/details_hide_button.dart'; export 'widgets/details_settings_button.dart'; diff --git a/lib/modules/lidarr/widgets/add_search_bar.dart b/lib/modules/lidarr/widgets/add_search_bar.dart index d9f054ff..9d50b4cc 100644 --- a/lib/modules/lidarr/widgets/add_search_bar.dart +++ b/lib/modules/lidarr/widgets/add_search_bar.dart @@ -3,15 +3,17 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/lidarr.dart'; class LidarrAddSearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; final Function callback; LidarrAddSearchBar({ Key key, + @required this.scrollController, @required this.callback, }) : super(key: key); @override - Size get preferredSize => Size.fromHeight(62.0); + Size get preferredSize => Size.fromHeight(LunaTextInputBar.appBarHeight); @override State createState() => _State(); @@ -29,25 +31,28 @@ class _State extends State { @override Widget build(BuildContext context) { - return Padding( + return Container( child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: Consumer( builder: (context, state, _) => LunaTextInputBar( controller: _controller, - autofocus: true, + scrollController: widget.scrollController, + autofocus: false, onChanged: (value) => context.read().addSearchQuery = value, onSubmitted: (value) { if(value.isNotEmpty) widget.callback(); }, - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + margin: LunaTextInputBar.appBarMargin, ), ), ), ], ), - padding: EdgeInsets.symmetric(vertical: 1.0), + height: LunaTextInputBar.appBarHeight, ); } } diff --git a/lib/modules/lidarr/widgets/add_search_result_tile.dart b/lib/modules/lidarr/widgets/add_search_result_tile.dart index dd85d7e0..2e7f58a7 100644 --- a/lib/modules/lidarr/widgets/add_search_result_tile.dart +++ b/lib/modules/lidarr/widgets/add_search_result_tile.dart @@ -12,25 +12,25 @@ class LidarrAddSearchResultTile extends StatelessWidget { }); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: data.title, darken: alreadyAdded), - subtitle: LSSubtitle( + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title(text: data.title, darken: alreadyAdded), + subtitle: LunaText.subtitle( text: data.overview.trim()+"\n\n", darken: alreadyAdded, maxLines: 2, ), - padContent: true, + contentPadding: true, trailing: alreadyAdded ? null - : LSIconButton(icon: Icons.arrow_forward_ios_rounded), + : LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: alreadyAdded ? () => _showAlreadyAddedMessage(context) : () async => _enterDetails(context), ); - Future _showAlreadyAddedMessage(BuildContext context) => LSSnackBar( - context: context, + Future _showAlreadyAddedMessage(BuildContext context) => showLunaInfoSnackBar( title: 'Artist Already in Lidarr', message: data.title, ); diff --git a/lib/modules/lidarr/widgets/artist_navigation_bar.dart b/lib/modules/lidarr/widgets/artist_navigation_bar.dart index ed90832e..13821718 100644 --- a/lib/modules/lidarr/widgets/artist_navigation_bar.dart +++ b/lib/modules/lidarr/widgets/artist_navigation_bar.dart @@ -1,43 +1,32 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/lidarr.dart'; -class LidarrArtistNavigationBar extends StatefulWidget { +class LidarrArtistNavigationBar extends StatelessWidget { + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); final PageController pageController; + static const List titles = [ + 'Overview', + 'Albums', + ]; + + static const List icons = [ + Icons.subject_rounded, + LunaIcons.music, + ]; + LidarrArtistNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - static const List _navbarTitles = [ - 'Overview', - 'Albums', - ]; - - static const List _navbarIcons = [ - Icons.subject_rounded, - LunaIcons.music, - ]; - - @override - Widget build(BuildContext context) => Selector( - selector: (_, state) => state.artistNavigationIndex, - builder: (context, index, _) => LSBottomNavigationBar( - index: index, - icons: _navbarIcons, - titles: _navbarTitles, - onTap: (index) async => await _navOnTap(index), - ), - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index) - .then((_) => Provider.of(context, listen: false).artistNavigationIndex = index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/lidarr/widgets/catalogue_hide_button.dart b/lib/modules/lidarr/widgets/catalogue_hide_button.dart index 18650edf..5461285d 100644 --- a/lib/modules/lidarr/widgets/catalogue_hide_button.dart +++ b/lib/modules/lidarr/widgets/catalogue_hide_button.dart @@ -20,16 +20,16 @@ class _State extends State { context: context, child: Consumer( builder: (context, model, widget) => InkWell( - child: LSIconButton( + child: LunaIconButton( icon: model.hideUnmonitoredArtists ? Icons.visibility_off : Icons.visibility, ), onTap: () => model.hideUnmonitoredArtists = !model.hideUnmonitoredArtists, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: EdgeInsets.symmetric(horizontal: 12.0), color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/lidarr/widgets/catalogue_search_bar.dart b/lib/modules/lidarr/widgets/catalogue_search_bar.dart index dbb463c8..a9ff395a 100644 --- a/lib/modules/lidarr/widgets/catalogue_search_bar.dart +++ b/lib/modules/lidarr/widgets/catalogue_search_bar.dart @@ -3,26 +3,46 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/lidarr.dart'; class LidarrCatalogueSearchBar extends StatefulWidget { + final ScrollController scrollController; + + LidarrCatalogueSearchBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + @override State createState() => _State(); } class _State extends State { - final _textController = TextEditingController(); + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = context.read().searchCatalogueFilter; + } @override - Widget build(BuildContext context) => Expanded( - child: Consumer( - builder: (context, state, widget) => LSTextInputBar( - controller: _textController, - onChanged: (text, update) => _onChanged(state, text, update), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), - ), - ), - ); - - void _onChanged(LidarrState state, String text, bool update) { - state.searchCatalogueFilter = text; - if(update) _textController.text = ''; - } + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + scrollController: widget.scrollController, + autofocus: false, + onChanged: (value) => context.read().searchCatalogueFilter = value, + margin: EdgeInsets.zero, + ), + ), + ), + LidarrCatalogueHideButton(controller: widget.scrollController), + LidarrCatalogueSortButton(controller: widget.scrollController), + ], + ); + } } diff --git a/lib/modules/lidarr/widgets/catalogue_sorting_button.dart b/lib/modules/lidarr/widgets/catalogue_sorting_button.dart index 7a4f0017..c8e0c21f 100644 --- a/lib/modules/lidarr/widgets/catalogue_sorting_button.dart +++ b/lib/modules/lidarr/widgets/catalogue_sorting_button.dart @@ -41,14 +41,14 @@ class _State extends State { Text( LidarrCatalogueSorting.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), if(model.sortCatalogueType == LidarrCatalogueSorting.values[index]) Icon( model.sortCatalogueAscending ? Icons.arrow_upward : Icons.arrow_downward, - size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, + size: LunaUI.FONT_SIZE_SUBTITLE+2.0, color: LunaColours.accent, ), ], @@ -57,7 +57,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: EdgeInsets.zero, color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/lidarr/widgets/catalogue_tile.dart b/lib/modules/lidarr/widgets/catalogue_tile.dart index 00bb6a35..fa6229ea 100644 --- a/lib/modules/lidarr/widgets/catalogue_tile.dart +++ b/lib/modules/lidarr/widgets/catalogue_tile.dart @@ -21,20 +21,21 @@ class LidarrCatalogueTile extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle( + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title( text: widget.data.title, darken: !widget.data.monitored, ), subtitle: Selector( selector: (_, state) => state.sortCatalogueType, - builder: (context, type, _) => LSSubtitle( + builder: (context, type, _) => LunaText.subtitle( text: widget.data.subtitle(type), darken: !widget.data.monitored, maxLines: 2, ), ), - trailing: LSIconButton( + trailing: LunaIconButton( icon: widget.data.monitored ? Icons.turned_in : Icons.turned_in_not, @@ -43,7 +44,7 @@ class _State extends State { : Colors.white30, onPressed: () => _toggleMonitoredStatus(), ), - padContent: true, + contentPadding: true, decoration: LunaCardDecoration( uri: widget.data.bannerURI(), headers: Database.currentProfileObject.getLidarr()['headers'], @@ -58,19 +59,15 @@ class _State extends State { .then((_) { if(mounted) setState(() => widget.data.monitored = !widget.data.monitored); widget.refreshState(); - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: widget.data.monitored ? 'Monitoring' : 'No Longer Monitoring', message: widget.data.title, - type: SNACKBAR_TYPE.success, ); }) - .catchError((_) { - LSSnackBar( - context: context, + .catchError((error) { + showLunaErrorSnackBar( title: widget.data.monitored ? 'Failed to Stop Monitoring' : 'Failed to Monitor', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, ); }); } @@ -85,11 +82,9 @@ class _State extends State { ); if(result != null) switch(result[0]) { case 'remove_artist': { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: result[1] ? 'Removed (With Data)' : 'Removed', message: widget.data.title, - type: SNACKBAR_TYPE.success, ); widget.refresh(); break; @@ -113,19 +108,17 @@ class _State extends State { LidarrEditArtist.ROUTE_NAME, arguments: LidarrEditArtistArguments(entry: widget.data), ); - if(result != null && result[0]) LSSnackBar( - context: context, + if(result != null && result[0]) showLunaSuccessSnackBar( title: 'Updated', message: widget.data.title, - type: SNACKBAR_TYPE.success, ); } Future _refreshArtist() async { final _api = LidarrAPI.from(Database.currentProfileObject); await _api.refreshArtist(widget.data.artistID) - .then((_) => LSSnackBar(context: context, title: 'Refreshing...', message: widget.data.title)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Refresh', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Refreshing...', message: widget.data.title)) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Refresh', error: error)); } Future _removeArtist() async { @@ -137,18 +130,18 @@ class _State extends State { if(values[0]) { await _api.removeArtist(widget.data.artistID, deleteFiles: true) .then((_) { - LSSnackBar(context: context, title: 'Removed (With Data)', message: widget.data.title, type: SNACKBAR_TYPE.success); + showLunaSuccessSnackBar(title: 'Removed (With Data)', message: widget.data.title); widget.refresh(); }) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove (With Data)', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Remove (With Data)', error: error)); } } else { await _api.removeArtist(widget.data.artistID, deleteFiles: false) .then((_) { - LSSnackBar(context: context, title: 'Removed', message: widget.data.title, type: SNACKBAR_TYPE.success); + showLunaSuccessSnackBar(title: 'Removed', message: widget.data.title); widget.refresh(); }) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Remove', error: error)); } } } diff --git a/lib/core/ui/deprecated/description_block.dart b/lib/modules/lidarr/widgets/description_block.dart similarity index 81% rename from lib/core/ui/deprecated/description_block.dart rename to lib/modules/lidarr/widgets/description_block.dart index 9c504074..726ac3f5 100644 --- a/lib/core/ui/deprecated/description_block.dart +++ b/lib/modules/lidarr/widgets/description_block.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -class LSDescriptionBlock extends StatefulWidget { +class LidarrDescriptionBlock extends StatefulWidget { final String description; final String title; final String uri; @@ -9,7 +9,7 @@ class LSDescriptionBlock extends StatefulWidget { final bool squareImage; final Map headers; - LSDescriptionBlock({ + LidarrDescriptionBlock({ @required this.description, @required this.title, @required this.uri, @@ -22,21 +22,22 @@ class LSDescriptionBlock extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State { final double _imageDimension = 105.0; @override Widget build(BuildContext context) { - return LSCard( + return LunaCard( + context: context, child: InkWell( child: Row( children: [ widget.uri != null - ? LSNetworkImage( + ? LunaNetworkImage( height: _imageDimension, width: widget.squareImage ? _imageDimension : _imageDimension/1.5, url: widget.uri, - placeholder: widget.fallbackImage, + placeholderAsset: widget.fallbackImage, headers: ((Database.currentProfileObject.getLidarr()['headers'] ?? {}) as Map).cast(), ) : Container(), @@ -50,18 +51,18 @@ class _State extends State { maxLines: 5, style: TextStyle( color: Colors.white, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), textAlign: TextAlign.start, ), padding: EdgeInsets.all(12.0), ), - alignment: Alignment.center, + alignment: Alignment.topLeft, ), ), ], ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), onTap: () => LunaDialogs().textPreview(context, widget.title, widget.description.trim() ?? 'No summary is available.'), ), ); diff --git a/lib/modules/lidarr/widgets/details_album_list.dart b/lib/modules/lidarr/widgets/details_album_list.dart index 98514046..a4197b9e 100644 --- a/lib/modules/lidarr/widgets/details_album_list.dart +++ b/lib/modules/lidarr/widgets/details_album_list.dart @@ -44,22 +44,23 @@ class _State extends State with AutomaticKeepAliveClient return _body; } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, + Widget get _body => LunaRefreshIndicator( + context: context, + key: _refreshKey, onRefresh: _refresh, child: FutureBuilder( future: _future, builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError || snapshot.data == null) return LunaMessage.error(onTap: () => _refresh()); _results = snapshot.data; return _list; } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ), @@ -68,10 +69,11 @@ class _State extends State with AutomaticKeepAliveClient Widget get _list => Consumer( builder: (context, model, widget) { List _filtered = model.hideUnmonitoredAlbums ? _hide(_results) : _results; - return LSListViewBuilder( + return LunaListViewBuilder( + controller: LidarrArtistNavigationBar.scrollControllers[1], itemCount: _filtered.length == 0 ? 1 : _filtered.length, itemBuilder: _filtered.length == 0 - ? (context, _) => LSGenericMessage(text: 'No Albums Found') + ? (context, _) => LunaMessage(text: 'No Albums Found') : (context, index) => LidarrDetailsAlbumTile( data: _filtered[index], refreshState: _refreshState, diff --git a/lib/modules/lidarr/widgets/details_album_tile.dart b/lib/modules/lidarr/widgets/details_album_tile.dart index 5f2e4c9d..f3a68a3a 100644 --- a/lib/modules/lidarr/widgets/details_album_tile.dart +++ b/lib/modules/lidarr/widgets/details_album_tile.dart @@ -20,15 +20,16 @@ class _State extends State { final double _imageDimension = 70.0; @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), child: Row( children: [ widget.data.albumCoverURI() != null && widget.data.albumCoverURI() != '' - ? LSNetworkImage( + ? LunaNetworkImage( url: widget.data.albumCoverURI(), - placeholder: 'assets/images/blanks/audio.png', + placeholderAsset: 'assets/images/blanks/audio.png', headers: ((Database.currentProfileObject.getLidarr()['headers'] ?? {}) as Map).cast(), height: _imageDimension, width: _imageDimension, @@ -38,13 +39,13 @@ class _State extends State { child: Padding( child: Column( children: [ - LSTitle(text: widget.data.title, darken: !widget.data.monitored), + LunaText.title(text: widget.data.title, darken: !widget.data.monitored), RichText( text: TextSpan( text: "${widget.data.tracks}", style: TextStyle( color: widget.data.monitored ? Colors.white70 : Colors.white30, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -64,7 +65,7 @@ class _State extends State { ), ), Padding( - child: LSIconButton( + child: LunaIconButton( icon: widget.data.monitored ? Icons.turned_in : Icons.turned_in_not, @@ -88,9 +89,9 @@ class _State extends State { .then((_) { if(mounted) setState(() => widget.data.monitored = !widget.data.monitored); widget.refreshState(); - LSSnackBar(context: context, title: widget.data.monitored ? 'Monitoring' : 'No Longer Monitoring', message: widget.data.title, type: SNACKBAR_TYPE.success); + showLunaSuccessSnackBar(title: widget.data.monitored ? 'Monitoring' : 'No Longer Monitoring', message: widget.data.title); }) - .catchError((_) => LSSnackBar(context: context, title: widget.data.monitored ? 'Failed to Stop Monitoring' : 'Failed to Monitor', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .catchError((error) => showLunaErrorSnackBar(title: widget.data.monitored ? 'Failed to Stop Monitoring' : 'Failed to Monitor', error: error)); } Future _enterAlbum() async => await Navigator.of(context).pushNamed( diff --git a/lib/modules/lidarr/widgets/details_album_track_tile.dart b/lib/modules/lidarr/widgets/details_album_track_tile.dart index 3125ff9b..1e99e4a7 100644 --- a/lib/modules/lidarr/widgets/details_album_track_tile.dart +++ b/lib/modules/lidarr/widgets/details_album_track_tile.dart @@ -18,15 +18,16 @@ class LidarrDetailsTrackTile extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.data.title, darken: !widget.monitored), + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title(text: widget.data.title, darken: !widget.monitored), subtitle: RichText( text: TextSpan( style: TextStyle( color: widget.monitored ? Colors.white70 : Colors.white30, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -42,11 +43,11 @@ class _State extends State { style: TextStyle( color: widget.monitored ? Colors.white : Colors.white30, fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_TITLE, + fontSize: LunaUI.FONT_SIZE_TITLE, ), ), onPressed: null, ), - padContent: true, + contentPadding: true, ); } \ No newline at end of file diff --git a/lib/modules/lidarr/widgets/details_edit_button.dart b/lib/modules/lidarr/widgets/details_edit_button.dart index 8c35c7d0..ef2bf51a 100644 --- a/lib/modules/lidarr/widgets/details_edit_button.dart +++ b/lib/modules/lidarr/widgets/details_edit_button.dart @@ -16,7 +16,7 @@ class LidarrDetailsEditButton extends StatefulWidget { class _State extends State { @override Widget build(BuildContext context) => Consumer( - builder: (context, model, widget) => LSIconButton( + builder: (context, model, widget) => LunaIconButton( icon: Icons.edit, onPressed: () async => _enterEditArtist(context), ), @@ -27,11 +27,9 @@ class _State extends State { LidarrEditArtist.ROUTE_NAME, arguments: LidarrEditArtistArguments(entry: widget.data), ); - if(result != null && result[0]) LSSnackBar( - context: context, + if(result != null && result[0]) showLunaSuccessSnackBar( title: 'Updated', message: widget.data.title, - type: SNACKBAR_TYPE.success, ); } } diff --git a/lib/modules/lidarr/widgets/details_hide_button.dart b/lib/modules/lidarr/widgets/details_hide_button.dart index 95af16e8..1706c868 100644 --- a/lib/modules/lidarr/widgets/details_hide_button.dart +++ b/lib/modules/lidarr/widgets/details_hide_button.dart @@ -5,7 +5,7 @@ import 'package:lunasea/modules/lidarr.dart'; class LidarrDetailsHideButton extends StatelessWidget { @override Widget build(BuildContext context) => Consumer( - builder: (context, model, widget) => LSIconButton( + builder: (context, model, widget) => LunaIconButton( icon: model.hideUnmonitoredAlbums ? Icons.visibility_off : Icons.visibility, onPressed: () => model.hideUnmonitoredAlbums = !model.hideUnmonitoredAlbums, ), diff --git a/lib/modules/lidarr/widgets/details_overview.dart b/lib/modules/lidarr/widgets/details_overview.dart index 342fe563..e2c6e3d3 100644 --- a/lib/modules/lidarr/widgets/details_overview.dart +++ b/lib/modules/lidarr/widgets/details_overview.dart @@ -21,9 +21,10 @@ class _State extends State with AutomaticKeepAliveClientM @override Widget build(BuildContext context) { super.build(context); - return LSListView( + return LunaListView( + controller: LidarrArtistNavigationBar.scrollControllers[0], children: [ - LSDescriptionBlock( + LidarrDescriptionBlock( title: widget?.data?.title ?? 'Unknown', description: widget?.data?.overview == '' ? 'No summary is available.' @@ -33,97 +34,57 @@ class _State extends State with AutomaticKeepAliveClientM squareImage: true, headers: Database.currentProfileObject.getLidarr()['headers'], ), - LSCardTile( - title: LSTitle(text: 'Artist Path', centerText: true), - subtitle: LSSubtitle(text: widget?.data?.path ?? 'Unknown', centerText: true), - onTap: () => LunaDialogs().textPreview(context, 'Artist Path', widget?.data?.path ?? 'Unknown'), - ), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Quality Profile', centerText: true), - subtitle: LSSubtitle(text: widget?.data?.quality ?? 'Unknown', centerText: true), - reducedMargin: true, + LunaButtonContainer( + children: [ + if(widget.data.bandsintownURI != null && widget.data.bandsintownURI.isNotEmpty) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset('assets/images/services/bandsintown.png'), + padding: EdgeInsets.all(12.0), + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => await widget.data?.bandsintownURI?.lunaOpenGenericLink(), ), + height: 50.0, + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Metadata Profile', centerText: true), - subtitle: LSSubtitle(text: widget?.data?.metadata ?? 'Unknown', centerText: true), - reducedMargin: true, + if(widget.data.discogsURI != null && widget.data.discogsURI.isNotEmpty) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset('assets/images/services/discogs.png'), + padding: EdgeInsets.all(12.0), + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => await widget.data?.discogsURI?.lunaOpenGenericLink(), ), + height: 50.0, + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + if(widget.data.lastfmURI != null && widget.data.lastfmURI.isNotEmpty) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset('assets/images/services/lastfm.png'), + padding: EdgeInsets.all(12.0), + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => await widget.data?.lastfmURI?.lunaOpenGenericLink(), + ), + height: 50.0, + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), ), ], ), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Genre', centerText: true), - subtitle: LSSubtitle(text: widget?.data?.genre ?? 'None', centerText: true), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Statistics', centerText: true), - subtitle: LSSubtitle(text: '${widget.data.albums ?? 'Unknown'}, ${widget.data.tracks ?? 'Unknown'}', centerText: true), - reducedMargin: true, - ), - ), - ], - ), - LSContainerRow( - children: [ - if(widget.data.bandsintownURI != '') Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset( - 'assets/images/services/bandsintown.png', - height: 25.0, - ), - padding: EdgeInsets.all(16.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await widget.data?.bandsintownURI?.lunaOpenGenericLink(), - ), - reducedMargin: true, - ), - ), - if(widget.data.discogsURI != '') Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset( - 'assets/images/services/discogs.png', - height: 19.0, - ), - padding: EdgeInsets.all(19.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await widget.data?.discogsURI?.lunaOpenGenericLink(), - ), - reducedMargin: true, - ), - ), - if(widget.data.lastfmURI != '') Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset( - 'assets/images/services/lastfm.png', - height: 17.0, - ), - padding: EdgeInsets.all(20.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await widget.data?.lastfmURI?.lunaOpenGenericLink(), - ), - reducedMargin: true, - ), - ), + LunaTableCard( + content: [ + LunaTableContent(title: 'Path', body: widget?.data?.path ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'Quality', body: widget?.data?.quality ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'Metadata', body: widget?.data?.metadata ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'Albums', body: widget?.data?.albums ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'Tracks', body: widget?.data?.tracks ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'Genres', body: widget?.data?.genre ?? LunaUI.TEXT_EMDASH), ], ), ], diff --git a/lib/modules/lidarr/widgets/details_settings_button.dart b/lib/modules/lidarr/widgets/details_settings_button.dart index d483e2dd..9fb207fb 100644 --- a/lib/modules/lidarr/widgets/details_settings_button.dart +++ b/lib/modules/lidarr/widgets/details_settings_button.dart @@ -18,7 +18,7 @@ class LidarrDetailsSettingsButton extends StatefulWidget { class _State extends State { @override Widget build(BuildContext context) => Consumer( - builder: (context, model, widget) => LSIconButton( + builder: (context, model, widget) => LunaIconButton( icon: Icons.more_vert, onPressed: () async => _handlePopup(context), ), @@ -39,19 +39,17 @@ class _State extends State { LidarrEditArtist.ROUTE_NAME, arguments: LidarrEditArtistArguments(entry: widget.data), ); - if(result != null && result[0]) LSSnackBar( - context: context, + if(result != null && result[0]) showLunaSuccessSnackBar( title: 'Updated', message: widget.data.title, - type: SNACKBAR_TYPE.success, ); } Future _refreshArtist(BuildContext context) async { final _api = LidarrAPI.from(Database.currentProfileObject); await _api.refreshArtist(widget.data.artistID) - .then((_) => LSSnackBar(context: context, title: 'Refreshing...', message: widget.data.title)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Refresh', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Refreshing...', message: widget.data.title)) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Refresh', error: error)); } Future _removeArtist(BuildContext context) async { @@ -63,12 +61,12 @@ class _State extends State { if(values[0]) { await _api.removeArtist(widget.data.artistID, deleteFiles: true) .then((_) => widget.remove(true)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove (With Data)', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Remove (With Data)', error: error)); } } else { await _api.removeArtist(widget.data.artistID) .then((_) => widget.remove(false)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Remove', error: error)); } } } diff --git a/lib/modules/lidarr/widgets/history_tile.dart b/lib/modules/lidarr/widgets/history_tile.dart index e30881b9..49a27f68 100644 --- a/lib/modules/lidarr/widgets/history_tile.dart +++ b/lib/modules/lidarr/widgets/history_tile.dart @@ -19,28 +19,28 @@ class LidarrHistoryTile extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.entry.title), + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title(text: widget.entry.title), subtitle: RichText( text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: widget.entry.subtitle, ), ), - trailing: LSIconButton( + trailing: LunaIconButton( icon: Icons.arrow_forward_ios_rounded, ), - padContent: true, + contentPadding: true, onTap: () async => _enterArtist(), ); Future _enterArtist() async { if(widget.entry.artistID == null || widget.entry.artistID == -1) { - LSSnackBar( - context: context, + showLunaInfoSnackBar( title: 'No Artist Available', message: 'There is no artist associated with this history entry', ); @@ -54,11 +54,9 @@ class _State extends State { ); if(result != null) switch(result[0]) { case 'remove_artist': { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: result[1] ? 'Removed (With Data)' : 'Removed', message: 'Removed artist with ID ${widget.entry.artistID}', - type: SNACKBAR_TYPE.success, ); widget.refresh(); break; diff --git a/lib/modules/lidarr/widgets/missing_tile.dart b/lib/modules/lidarr/widgets/missing_tile.dart index 0f1c1ac8..9a7f3398 100644 --- a/lib/modules/lidarr/widgets/missing_tile.dart +++ b/lib/modules/lidarr/widgets/missing_tile.dart @@ -19,12 +19,13 @@ class LidarrMissingTile extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.entry.artistTitle), + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title(text: widget.entry.artistTitle), subtitle: RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -37,7 +38,7 @@ class _State extends State { TextSpan( text: '\nReleased ${widget.entry.releaseDateString}', style: TextStyle( - color: Colors.red, + color: LunaColours.red, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), @@ -47,7 +48,7 @@ class _State extends State { softWrap: false, maxLines: 2, ), - trailing: LSIconButton( + trailing: LunaIconButton( icon: Icons.search, onPressed: () async => _search(), onLongPress: () async => _interactiveSearch(), @@ -58,14 +59,14 @@ class _State extends State { uri: widget.entry.bannerURI(), headers: Database.currentProfileObject.getLidarr()['headers'], ), - padContent: true, + contentPadding: true, ); Future _search() async { final _api = LidarrAPI.from(Database.currentProfileObject); await _api.searchAlbums([widget.entry.albumID]) - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: widget.entry.title)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure)); + .then((_) => showLunaSuccessSnackBar(title: 'Searching...', message: widget.entry.title)) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Search', error: error)); } Future _interactiveSearch() async => Navigator.of(context).pushNamed( @@ -86,11 +87,9 @@ class _State extends State { ); if(result != null) switch(result[0]) { case 'remove_artist': { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: result[1] ? 'Removed (With Data)' : 'Removed', message: widget.entry.artistTitle, - type: SNACKBAR_TYPE.success, ); widget.refresh(); break; diff --git a/lib/modules/lidarr/widgets/navigation_bar.dart b/lib/modules/lidarr/widgets/navigation_bar.dart index e0b02bab..56c0cb40 100644 --- a/lib/modules/lidarr/widgets/navigation_bar.dart +++ b/lib/modules/lidarr/widgets/navigation_bar.dart @@ -1,53 +1,34 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/lidarr.dart'; -class LidarrNavigationBar extends StatefulWidget { +class LidarrNavigationBar extends StatelessWidget { + final PageController pageController; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + static const List icons = [ LunaIcons.music, LunaIcons.calendar_missing, LunaIcons.history, ]; - static const List titles = [ - 'Catalogue', + static List get titles => [ + 'Artists', 'Missing', 'History', ]; - final PageController pageController; - LidarrNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - Provider.of(context, listen: false).navigationIndex = LidarrDatabaseValue.NAVIGATION_INDEX.data; - }); - } - - @override - Widget build(BuildContext context) => Selector( - selector: (_, state) => state.navigationIndex, - builder: (context, index, _) => LSBottomNavigationBar( - index: index, - icons: LidarrNavigationBar.icons, - titles: LidarrNavigationBar.titles, - onTap: _navOnTap, - ), - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index) - .then((_) => Provider.of(context, listen: false).navigationIndex = index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/lidarr/widgets/releases_hide_button.dart b/lib/modules/lidarr/widgets/releases_hide_button.dart index 5e2f4931..4109ac85 100644 --- a/lib/modules/lidarr/widgets/releases_hide_button.dart +++ b/lib/modules/lidarr/widgets/releases_hide_button.dart @@ -19,14 +19,14 @@ class _State extends State { Widget build(BuildContext context) => LunaCard( context: context, child: Consumer( - builder: (context, model, widget) => LSIconButton( + builder: (context, model, widget) => LunaIconButton( icon: model.hideRejectedReleases ? Icons.visibility_off : Icons.visibility, onPressed: () => model.hideRejectedReleases = !model.hideRejectedReleases, ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: LunaTextInputBar.appBarMargin.subtract(EdgeInsets.only(left: 12.0)), color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/lidarr/widgets/releases_search_bar.dart b/lib/modules/lidarr/widgets/releases_search_bar.dart index c7796950..fd4670df 100644 --- a/lib/modules/lidarr/widgets/releases_search_bar.dart +++ b/lib/modules/lidarr/widgets/releases_search_bar.dart @@ -2,40 +2,47 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/lidarr.dart'; -class LidarrReleasesSearchBar extends StatefulWidget { - final String prefill; +class LidarrReleasesSearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; LidarrReleasesSearchBar({ Key key, - this.prefill = '', + @required this.scrollController, }): super(key: key); + @override + Size get preferredSize => Size.fromHeight(LunaTextInputBar.appBarHeight); + @override State createState() => _State(); } class _State extends State { - final _textController = TextEditingController(); - - void initState() { - super.initState(); - _textController.text = widget.prefill ?? ''; - } + final _controller = TextEditingController(); @override - Widget build(BuildContext context) => Expanded( - child: Consumer( - builder: (context, state, widget) => LSTextInputBar( - controller: _textController, - labelText: 'Search Releases...', - onChanged: (text, update) => _onChanged(state, text, update), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), + Widget build(BuildContext context) { + return Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + scrollController: widget.scrollController, + autofocus: false, + onChanged: (value) => context.read().searchReleasesFilter = value, + margin: LunaTextInputBar.appBarMargin, + ), + ), + ), + LidarrReleasesHideButton(controller: widget.scrollController), + LidarrReleasesSortButton(controller: widget.scrollController), + ], ), - ), - ); - - void _onChanged(LidarrState state, String text, bool update) { - state.searchReleasesFilter = text; - if(update) _textController.text = ''; + height: LunaTextInputBar.appBarHeight, + ); } } diff --git a/lib/modules/lidarr/widgets/releases_sorting_button.dart b/lib/modules/lidarr/widgets/releases_sorting_button.dart index e8027344..f854e4f9 100644 --- a/lib/modules/lidarr/widgets/releases_sorting_button.dart +++ b/lib/modules/lidarr/widgets/releases_sorting_button.dart @@ -41,14 +41,14 @@ class _State extends State { Text( LidarrReleasesSorting.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), if(model.sortReleasesType == LidarrReleasesSorting.values[index]) Icon( model.sortReleasesAscending ? Icons.arrow_upward : Icons.arrow_downward, - size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, + size: LunaUI.FONT_SIZE_SUBTITLE+2.0, color: LunaColours.accent, ), ], @@ -57,7 +57,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: LunaTextInputBar.appBarMargin.subtract(EdgeInsets.only(left: 12.0)), color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/lidarr/widgets/search_result_tile.dart b/lib/modules/lidarr/widgets/search_result_tile.dart index dcb38bef..ed13b2b5 100644 --- a/lib/modules/lidarr/widgets/search_result_tile.dart +++ b/lib/modules/lidarr/widgets/search_result_tile.dart @@ -140,13 +140,12 @@ class _State extends State { buttonOnPressed: () => Navigator.of(context).popUntil((Route route) { return !route.willHandlePopInternally && route is ModalRoute - && (route.settings.name == Lidarr.ROUTE_NAME || route.settings.name == Dashboard.ROUTE_NAME); + && (route.settings.name == Lidarr.ROUTE_NAME || route.settings.name == DashboardHomeRouter().fullRoute); }), ); }) .catchError((error, stack) { showLunaErrorSnackBar( - context: context, title: 'Failed to Start Downloading', error: error, ); diff --git a/lib/modules/nzbget/core/dialogs.dart b/lib/modules/nzbget/core/dialogs.dart index 26f49fb9..909872a5 100644 --- a/lib/modules/nzbget/core/dialogs.dart +++ b/lib/modules/nzbget/core/dialogs.dart @@ -21,19 +21,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'NZBGet Settings', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -58,19 +58,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: title, content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -83,20 +83,20 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Job', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete this job?'), + LunaDialog.textContent(text: 'Are you sure you want to delete this job?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -113,11 +113,11 @@ class NZBGetDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Rename Job', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Rename', onPressed: () => _setValues(true), ), @@ -125,7 +125,7 @@ class NZBGetDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Job Name', onSubmitted: (_) => _setValues(true), @@ -135,7 +135,7 @@ class NZBGetDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -150,19 +150,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Change Priority', content: List.generate( NZBGetPriority.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: NZBGetPriority.values[index].name, icon: Icons.low_priority, iconColor: LunaColours.list(index), onTap: () => _setValues(true, NZBGetPriority.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _priority]; } @@ -177,19 +177,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Change Category', content: List.generate( categories.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: categories[index].name, icon: Icons.category, iconColor: LunaColours.list(index), onTap: () => _setValues(true, categories[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _category]; } @@ -206,11 +206,11 @@ class NZBGetDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Set Password', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), @@ -218,7 +218,7 @@ class NZBGetDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Job Password', onSubmitted: (_) => _setValues(true), @@ -229,7 +229,7 @@ class NZBGetDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -244,24 +244,24 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Remove History', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Hide', onPressed: () => _setValues(true, true), ), - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true, false), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to remove the history for this job?'), + LunaDialog.textContent(text: 'Are you sure you want to remove the history for this job?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag, _hide]; } @@ -276,19 +276,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Sort Queue', content: List.generate( NZBGetSort.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: NZBGetSort.values[index].name, icon: NZBGetSort.values[index].icon, iconColor: LunaColours.list(index), onTap: () => _setValues(true, NZBGetSort.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _sort]; } @@ -307,19 +307,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add NZB', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _type]; } @@ -336,11 +336,11 @@ class NZBGetDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add NZB by URL', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Add', onPressed: () => _setValues(true), ), @@ -348,7 +348,7 @@ class NZBGetDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'NZB URL', keyboardType: TextInputType.url, @@ -359,7 +359,7 @@ class NZBGetDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -384,19 +384,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: currentSpeed == 'Unlimited' ? 'Speed Limit (Unlimited)' : 'Speed Limit ($currentSpeed/s)', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _limit]; } @@ -413,20 +413,20 @@ class NZBGetDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Custom Speed Limit', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Please enter a speed limit in KB/s.'), + LunaDialog.textContent(text: 'Please enter a speed limit in KB/s.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Speed Limit', onSubmitted: (_) => _setValues(true), @@ -441,7 +441,7 @@ class NZBGetDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, int.tryParse(_textController.text)]; } @@ -461,19 +461,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: title, content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -497,19 +497,19 @@ class NZBGetDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Pause Queue For...', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _duration]; } @@ -526,27 +526,27 @@ class NZBGetDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Custom Pause Duration', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Pause', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.textSpanContent(text: 'Please enter how long in '), - LSDialog.bolded(text: 'minutes'), - LSDialog.textSpanContent(text: ' you want to pause the queue for.'), + LunaDialog.textSpanContent(text: 'Please enter how long in '), + LunaDialog.bolded(text: 'minutes'), + LunaDialog.textSpanContent(text: ' you want to pause the queue for.'), ], alignment: TextAlign.center, ), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Pause Duration in Minutes', keyboardType: TextInputType.number, @@ -561,7 +561,7 @@ class NZBGetDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, int.tryParse(_textController.text)]; } @@ -576,19 +576,19 @@ class NZBGetDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Page', content: List.generate( NZBGetNavigationBar.titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: NZBGetNavigationBar.titles[index], icon: NZBGetNavigationBar.icons[index], iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _index]; diff --git a/lib/modules/nzbget/core/state.dart b/lib/modules/nzbget/core/state.dart index ed5ebd8f..c69f8617 100644 --- a/lib/modules/nzbget/core/state.dart +++ b/lib/modules/nzbget/core/state.dart @@ -1,2 +1,82 @@ -export 'state/global.dart'; -export 'state/local.dart'; +import 'package:lunasea/core.dart'; + +class NZBGetState extends LunaModuleState { + NZBGetState() { + reset(); + } + + @override + void reset() {} + + bool _error = false; + bool get error => _error; + set error(bool error) { + assert(error != null); + _error = error; + notifyListeners(); + } + + bool _paused = true; + bool get paused => _paused; + set paused(bool paused) { + assert(paused != null); + _paused = paused; + notifyListeners(); + } + + int _speed = 0; + int get speed => _speed; + set speed(int speed) { + assert(speed != null); + _speed = speed; + notifyListeners(); + } + + String _currentSpeed = '0.0 B/s'; + String get currentSpeed => _currentSpeed; + set currentSpeed(String currentSpeed) { + assert(currentSpeed != null); + _currentSpeed = currentSpeed; + notifyListeners(); + } + + String _queueTimeLeft = '0:00:00'; + String get queueTimeLeft => _queueTimeLeft; + set queueTimeLeft(String queueTimeLeft) { + assert(queueTimeLeft != null); + _queueTimeLeft = queueTimeLeft; + notifyListeners(); + } + + String _queueSizeLeft = '0.0 B'; + String get queueSizeLeft => _queueSizeLeft; + set queueSizeLeft(String queueSizeLeft) { + assert(queueSizeLeft != null); + _queueSizeLeft = queueSizeLeft; + notifyListeners(); + } + + String _speedLimit = '0.0 B'; + String get speedLimit => _speedLimit; + set speedLimit(String speedLimit) { + assert(speedLimit != null); + _speedLimit = speedLimit; + notifyListeners(); + } + + String _historySearchFilter = ''; + String get historySearchFilter => _historySearchFilter; + set historySearchFilter(String historySearchFilter) { + assert(historySearchFilter != null); + _historySearchFilter = historySearchFilter; + notifyListeners(); + } + + bool _historyHideFailed = false; + bool get historyHideFailed => _historyHideFailed; + set historyHideFailed(bool historyHideFailed) { + assert(historyHideFailed != null); + _historyHideFailed = historyHideFailed; + notifyListeners(); + } +} diff --git a/lib/modules/nzbget/core/state/global.dart b/lib/modules/nzbget/core/state/global.dart deleted file mode 100644 index 266b76f5..00000000 --- a/lib/modules/nzbget/core/state/global.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:lunasea/core.dart'; - -class NZBGetState extends LunaModuleState { - NZBGetState() { - reset(); - } - - @override - void reset() {} - - bool _error = false; - bool get error => _error; - set error(bool error) { - assert(error != null); - _error = error; - notifyListeners(); - } - - bool _paused = true; - bool get paused => _paused; - set paused(bool paused) { - assert(paused != null); - _paused = paused; - notifyListeners(); - } - - int _speed = 0; - int get speed => _speed; - set speed(int speed) { - assert(speed != null); - _speed = speed; - notifyListeners(); - } - - String _currentSpeed = '0.0 B/s'; - String get currentSpeed => _currentSpeed; - set currentSpeed(String currentSpeed) { - assert(currentSpeed != null); - _currentSpeed = currentSpeed; - notifyListeners(); - } - - String _queueTimeLeft = '0:00:00'; - String get queueTimeLeft => _queueTimeLeft; - set queueTimeLeft(String queueTimeLeft) { - assert(queueTimeLeft != null); - _queueTimeLeft = queueTimeLeft; - notifyListeners(); - } - - String _queueSizeLeft = '0.0 B'; - String get queueSizeLeft => _queueSizeLeft; - set queueSizeLeft(String queueSizeLeft) { - assert(queueSizeLeft != null); - _queueSizeLeft = queueSizeLeft; - notifyListeners(); - } - - String _speedLimit = '0.0 B'; - String get speedLimit => _speedLimit; - set speedLimit(String speedLimit) { - assert(speedLimit != null); - _speedLimit = speedLimit; - notifyListeners(); - } - - String _historySearchFilter = ''; - String get historySearchFilter => _historySearchFilter; - set historySearchFilter(String historySearchFilter) { - assert(historySearchFilter != null); - _historySearchFilter = historySearchFilter; - notifyListeners(); - } - - bool _historyHideFailed = false; - bool get historyHideFailed => _historyHideFailed; - set historyHideFailed(bool historyHideFailed) { - assert(historyHideFailed != null); - _historyHideFailed = historyHideFailed; - notifyListeners(); - } - - int _navigationIndex = 0; - int get navigationIndex => _navigationIndex; - set navigationIndex(int navigationIndex) { - assert(navigationIndex != null); - _navigationIndex = navigationIndex; - notifyListeners(); - } -} diff --git a/lib/modules/nzbget/core/state/local.dart b/lib/modules/nzbget/core/state/local.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/modules/nzbget/routes/history.dart b/lib/modules/nzbget/routes/history.dart index 9ea07382..a944c4de 100644 --- a/lib/modules/nzbget/routes/history.dart +++ b/lib/modules/nzbget/routes/history.dart @@ -16,26 +16,18 @@ class NZBGetHistory extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { - final _scaffoldKey = GlobalKey(); - final _scrollController = ScrollController(); +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); Future> _future; List _results = []; @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - _refresh(); - } - - Future _refresh() async { - _results = []; + Future loadCallback() async { + if(mounted) setState(() => _results = []); final _api = NZBGetAPI.from(Database.currentProfileObject); if(mounted) setState(() { _future = _api.getHistory(); }); - Future.microtask(() => Provider.of(context, listen: false)?.historySearchFilter = ''); } @override @@ -43,46 +35,49 @@ class _State extends State with AutomaticKeepAliveClientMixin { super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), + appBar: _appBar(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); + Widget _appBar() { + return LunaAppBar.empty( + child: NZBGetHistorySearchBar(scrollController: NZBGetNavigationBar.scrollControllers[1]), + height: 62.0, + ); + } - Widget get _searchBar => LSContainerRow( - padding: EdgeInsets.zero, - backgroundColor: Theme.of(context).primaryColor, - children: [ - NZBGetHistorySearchBar(), - NZBGetHistoryHideButton(), - ], - ); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: widget.refreshIndicatorKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: _future, + builder: (context, snapshot) { + switch(snapshot.connectionState) { + case ConnectionState.done: { + if(snapshot.hasError || snapshot.data == null) { + return LunaMessage.error(onTap: () => widget.refreshIndicatorKey.currentState.show()); + } + _results = snapshot.data; + return _list; + } + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + default: return LunaLoader(); + } + }, + ), + ); + } Widget get _list => _results.length == 0 - ? LSGenericMessage( + ? LunaMessage( text: 'No History Found', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => _refresh(), + onTap: () => loadCallback(), ) : Selector>( selector: (_, model) => Tuple2(model.historySearchFilter, model.historyHideFailed), @@ -95,22 +90,17 @@ class _State extends State with AutomaticKeepAliveClientMixin { Widget _listBody(List filtered) { List _children = filtered.length == 0 - ? [LSGenericMessage(text: 'No Results Found')] + ? [LunaMessage.inList(text: 'No History Found')] : List.generate( filtered.length, (index) => NZBGetHistoryTile( data: filtered[index], - refresh: () => _refresh(), + refresh: () => loadCallback(), ), ); - return LSListViewStickyHeader( - controller: _scrollController, - slivers: [ - LunaSliverStickyHeader( - header: _searchBar, - children: _children, - ), - ], + return LunaListView( + controller: NZBGetNavigationBar.scrollControllers[1], + children: _children, ); } diff --git a/lib/modules/nzbget/routes/nzbget.dart b/lib/modules/nzbget/routes/nzbget.dart index aa33df94..5689e231 100644 --- a/lib/modules/nzbget/routes/nzbget.dart +++ b/lib/modules/nzbget/routes/nzbget.dart @@ -12,7 +12,7 @@ class NZBGet extends StatefulWidget { class _State extends State { final _scaffoldKey = GlobalKey(); - final _pageController = PageController(initialPage: NZBGetDatabaseValue.NAVIGATION_INDEX.data); + LunaPageController _pageController; String _profileState = Database.currentProfileObject.toString(); NZBGetAPI _api = NZBGetAPI.from(Database.currentProfileObject); @@ -24,72 +24,77 @@ class _State extends State { @override void initState() { super.initState(); - Future.microtask(() => Provider.of(context, listen: false).navigationIndex = 0); + _pageController = LunaPageController(initialPage: NZBGetDatabaseValue.NAVIGATION_INDEX.data); } @override - Widget build(BuildContext context) => WillPopScope( - onWillPop: () async { - if(_scaffoldKey.currentState.isDrawerOpen) { - //If the drawer is open, return true to close it - return true; - } else { - //If the drawer isn't open, open the drawer - _scaffoldKey.currentState.openDrawer(); - return false; - } - }, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]), - builder: (context, box, widget) { + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + body: _body(), + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + extendBodyBehindAppBar: false, + extendBody: false, + onProfileChange: (_) { if(_profileState != Database.currentProfileObject.toString()) _refreshProfile(); - return Scaffold( - key: _scaffoldKey, - body: _body, - drawer: _drawer, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - ); }, - ), - ); + ); + } - Widget get _drawer => LunaDrawer(page: LunaModule.NZBGET.key); + Widget _drawer() => LunaDrawer(page: LunaModule.NZBGET.key); - Widget get _bottomNavigationBar => NZBGetNavigationBar(pageController: _pageController); + Widget _bottomNavigationBar() { + if(_api.enabled) return NZBGetNavigationBar(pageController: _pageController); + return null; + } - List get _tabs => [ - NZBGetQueue(refreshIndicatorKey: _refreshKeys[0]), - NZBGetHistory(refreshIndicatorKey: _refreshKeys[1]), - ]; - - Widget get _body => PageView( - controller: _pageController, - children: _api.enabled ? _tabs : List.generate(_tabs.length, (_) => LSNotEnabled('NZBGet')), - onPageChanged: _onPageChanged, - ); - - Widget get _appBar => LunaAppBar.dropdown( - title: 'NZBGet', - profiles: Database.profilesBox.keys.fold([], (value, element) { + Widget _appBar() { + List profiles = Database.profilesBox.keys.fold([], (value, element) { if(Database.profilesBox.get(element)?.nzbgetEnabled ?? false) value.add(element); return value; - }), - actions: _api.enabled - ? [ - Selector( - selector: (_, model) => model.error, - builder: (context, error, widget) => error - ? Container() - : NZBGetAppBarStats(), + }); + List actions; + if(_api.enabled) actions = [ + Selector( + selector: (_, model) => model.error, + builder: (context, error, widget) => error + ? Container() + : NZBGetAppBarStats(), + ), + LunaIconButton( + icon: Icons.more_vert, + onPressed: () async => _handlePopup(), + ), + ]; + return LunaAppBar.dropdown( + title: LunaModule.NZBGET.name, + useDrawer: true, + profiles: profiles, + actions: actions, + pageController: _pageController, + scrollControllers: NZBGetNavigationBar.scrollControllers, + ); + } + + Widget _body() { + if(!_api.enabled) return LunaMessage.moduleNotEnabled( + context: context, + module: LunaModule.NZBGET.name, + ); + return PageView( + controller: _pageController, + children: [ + NZBGetQueue( + refreshIndicatorKey: _refreshKeys[0], ), - LSIconButton( - icon: Icons.more_vert, - onPressed: () async => _handlePopup(), + NZBGetHistory( + refreshIndicatorKey: _refreshKeys[1], ), - ] - : null, - ); + ], + ); + } Future _handlePopup() async { List values = await NZBGetDialogs.globalSettings(context); @@ -114,18 +119,8 @@ class _State extends State { Future _addByURL() async { List values = await NZBGetDialogs.addNZBUrl(context); if(values[0]) await _api.uploadURL(values[1]) - .then((_) => LSSnackBar( - context: context, - title: 'Uploaded NZB (URL)', - message: values[1], - type: SNACKBAR_TYPE.success, - )) - .catchError((_) => LSSnackBar( - context: context, - title: 'Failed to Upload NZB', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, - )); + .then((_) => showLunaSuccessSnackBar(title: 'Uploaded NZB (URL)', message: values[1])) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Upload NZB', error: error)); } Future _addByFile() async { @@ -147,20 +142,17 @@ class _State extends State { .then((value) { _refreshKeys[0]?.currentState?.show(); showLunaSuccessSnackBar( - context: context, title: 'Uploaded NZB (File)', message: _name, ); }) .catchError((error, stack) => showLunaErrorSnackBar( - context: context, title: 'Failed to Upload NZB', message: LunaLogger.checkLogsMessage, error: error, )); } else { showLunaErrorSnackBar( - context: context, title: 'Failed to Upload NZB', message: 'The selected file is not valid', ); @@ -168,7 +160,6 @@ class _State extends State { } catch (error, stack) { LunaLogger().error('Failed to add NZB by file', error, stack); showLunaErrorSnackBar( - context: context, title: 'Failed to Upload NZB', error: error, ); @@ -180,25 +171,13 @@ class _State extends State { if(values[0]) await _api.sortQueue(values[1]) .then((_) { _refreshKeys[0]?.currentState?.show(); - LSSnackBar( - context: context, - title: 'Sorted Queue', - message: (values[1] as NZBGetSort).name, - type: SNACKBAR_TYPE.success, - ); + showLunaSuccessSnackBar(title: 'Sorted Queue', message: (values[1] as NZBGetSort).name); }) - .catchError((_) => LSSnackBar( - context: context, - title: 'Failed to Sort Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Sort Queue', error: error)); } Future _serverDetails() async => Navigator.of(context).pushNamed(NZBGetStatistics.ROUTE_NAME); - void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; - void _refreshProfile() { _api = NZBGetAPI.from(Database.currentProfileObject); _profileState = Database.currentProfileObject.toString(); diff --git a/lib/modules/nzbget/routes/queue.dart b/lib/modules/nzbget/routes/queue.dart index c91d3eea..932ab64e 100644 --- a/lib/modules/nzbget/routes/queue.dart +++ b/lib/modules/nzbget/routes/queue.dart @@ -19,7 +19,6 @@ class NZBGetQueue extends StatefulWidget { class _State extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { final _scaffoldKey = GlobalKey(); - final _scrollController = ScrollController(); Timer _timer; Future _future; List _queue = []; @@ -39,7 +38,9 @@ class _State extends State with TickerProviderStateMixin, Automatic return Scaffold( key: _scaffoldKey, body: _body, - floatingActionButton: NZBGetQueueFAB(scrollController: _scrollController), + floatingActionButton: context.watch().error + ? null + : NZBGetQueueFAB(scrollController: NZBGetNavigationBar.scrollControllers[0]), ); } @@ -57,7 +58,7 @@ class _State extends State with TickerProviderStateMixin, Automatic Future _fetchWithoutMessage() async { _fetch().then((_) => { if(mounted) setState(() {}) }) - .catchError((_) => _queue = null); + .catchError((error) => _queue = null); } Future _fetch() async { @@ -106,35 +107,36 @@ class _State extends State with TickerProviderStateMixin, Automatic _model.error = error; } - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, + Widget get _body => LunaRefreshIndicator( + context: context, + key: widget.refreshIndicatorKey, onRefresh: () => _fetchWithoutMessage(), child: FutureBuilder( future: _future, builder: (context, snapshot) { if( - snapshot.connectionState == ConnectionState.done - && (snapshot.hasError || snapshot.data == null) - ) return LSErrorMessage(onTapHandler: () => _refresh()); - return _list; + snapshot.connectionState == ConnectionState.done && + context.read().error + ) return LunaMessage.error(onTap: () => _refresh()); + if(snapshot.hasData) return _list; + return LunaLoader(); }, ), ); Widget get _list => _queue == null - ? LSErrorMessage(onTapHandler: () => _refresh()) + ? LunaMessage.error(onTap: () => _refresh()) : _queue.length == 0 - ? LSGenericMessage( + ? LunaMessage( text: 'Empty Queue', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => _fetchWithoutMessage(), + onTap: () => _fetchWithoutMessage(), ) - : _reorderableList; + : _reorderableList(); - Widget get _reorderableList => Scrollbar( - child: LSReorderableListView( - scrollController: _scrollController, + Widget _reorderableList() { + return LunaReorderableListViewBuilder( + controller: NZBGetNavigationBar.scrollControllers[0], onReorder: (oIndex, nIndex) async { if (oIndex > _queue.length) oIndex = _queue.length; if (oIndex < nIndex) nIndex--; @@ -144,37 +146,16 @@ class _State extends State with TickerProviderStateMixin, Automatic _queue.insert(nIndex, data); }); await NZBGetAPI.from(Database.currentProfileObject).moveQueue(data.id, (nIndex - oIndex)) - .then((_) => LSSnackBar( - context: context, - title: 'Moved Job in Queue', - message: data.name, - type: SNACKBAR_TYPE.success, - )) - .catchError((_) => LSSnackBar( - context: context, - title: 'Failed to Move Job', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, - )); + .then((_) => showLunaSuccessSnackBar(title: 'Moved Job in Queue', message: data.name)) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Move Job', error: error)); }, - children: List.generate( - _queue.length, - (index) => NZBGetQueueTile( - key: Key(_queue[index].id.toString()), - data: _queue[index], - snackbar: _snackBar, - queueContext: context, - refresh: () => _fetchWithoutMessage(), - ), + itemCount: _queue.length, + itemBuilder: (context, index) => NZBGetQueueTile( + key: Key(_queue[index].id.toString()), + data: _queue[index], + queueContext: context, + refresh: () => _fetchWithoutMessage(), ), - padding: EdgeInsets.only(top: 8.0), - ), - ); - - void _snackBar(String title, String message, SNACKBAR_TYPE type) => LSSnackBar( - context: context, - title: title, - message: message, - type: type, - ); + ); + } } diff --git a/lib/modules/nzbget/routes/statistics.dart b/lib/modules/nzbget/routes/statistics.dart index f0868158..d6f5a229 100644 --- a/lib/modules/nzbget/routes/statistics.dart +++ b/lib/modules/nzbget/routes/statistics.dart @@ -9,7 +9,7 @@ class NZBGetStatistics extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaScrollControllerMixin { final _scaffoldKey = GlobalKey(); final _refreshKey = GlobalKey(); @@ -51,151 +51,56 @@ class _State extends State { body: _body, ); - Widget get _appBar => LunaAppBar(title: 'Server Statistics'); + Widget get _appBar => LunaAppBar( + title: 'Server Statistics', + scrollControllers: [scrollController], + ); - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, + Widget get _body => LunaRefreshIndicator( + context: context, + key: _refreshKey, onRefresh: _refresh, child: FutureBuilder( future: _future, builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError || snapshot.data == null) return LunaMessage.error(onTap: () => _refresh()); return _list; } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ), ); - Widget get _list => LSListView( + Widget get _list => LunaListView( + controller: scrollController, children: [ - ..._statusBlock, - ..._statisticsBlock, - ..._logsBlock, + LunaHeader(text: 'Status'), + _statusBlock(), + LunaHeader(text: 'Logs'), + for(var entry in _logs) NZBGetLogTile( + data: entry, + ), ], ); - List get _statusBlock => [ - LSHeader(text: 'Status'), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle( - text: 'Server', - centerText: true, - ), - subtitle: LSSubtitle( - text: _statistics.serverPaused ? 'Paused' : 'Active', - centerText: true, - ), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle( - text: 'Post', - centerText: true, - ), - subtitle: LSSubtitle( - text: _statistics.postPaused ? 'Paused' : 'Active', - centerText: true, - ), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle( - text: 'Scan', - centerText: true, - ), - subtitle: LSSubtitle( - text: _statistics.scanPaused ? 'Paused' : 'Active', - centerText: true, - ), - reducedMargin: true, - ), - ), + Widget _statusBlock() { + return LunaTableCard( + content: [ + LunaTableContent(title: 'Server', body: _statistics.serverPaused ? 'Paused' : 'Active'), + LunaTableContent(title: 'Post', body: _statistics.postPaused ? 'Paused' : 'Active'), + LunaTableContent(title: 'Scan', body: _statistics.scanPaused ? 'Paused' : 'Active'), + LunaTableContent(title: '', body: ''), + LunaTableContent(title: 'Uptime', body: _statistics.uptimeString), + LunaTableContent(title: 'Speed Limit', body: _statistics.speedLimitString), + LunaTableContent(title: 'Free Space', body: _statistics.freeSpaceString), + LunaTableContent(title: 'Download', body: _statistics.downloadedString), ], - ) - ]; - - List get _statisticsBlock => [ - LSHeader(text: 'Statistics'), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle( - text: 'Uptime', - centerText: true, - ), - subtitle: LSSubtitle( - text: _statistics.uptimeString, - centerText: true, - ), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle( - text: 'Speed Limit', - centerText: true, - ), - subtitle: LSSubtitle( - text: _statistics.speedLimitString, - centerText: true, - ), - reducedMargin: true, - ), - ), - ], - ), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle( - text: 'Free Space', - centerText: true, - ), - subtitle: LSSubtitle( - text: _statistics.freeSpaceString, - centerText: true, - ), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle( - text: 'Downloaded', - centerText: true, - ), - subtitle: LSSubtitle( - text: _statistics.downloadedString, - centerText: true, - ), - reducedMargin: true, - ), - ), - ], - ), - ]; - - List get _logsBlock => [ - LSHeader(text: 'Logs'), - for(var entry in _logs) NZBGetLogTile( - data: entry, - ), - ]; + ); + } } diff --git a/lib/modules/nzbget/widgets/app_bar_stats.dart b/lib/modules/nzbget/widgets/app_bar_stats.dart index 876faf56..b3310c58 100644 --- a/lib/modules/nzbget/widgets/app_bar_stats.dart +++ b/lib/modules/nzbget/widgets/app_bar_stats.dart @@ -21,14 +21,14 @@ class NZBGetAppBarStats extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white54, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( text: _status(data.item1, data.item2), style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_HEADER, + fontSize: LunaUI.FONT_SIZE_HEADER, color: LunaColours.accent, ), ), @@ -60,32 +60,24 @@ class NZBGetAppBarStats extends StatelessWidget { case -1: { values = await NZBGetDialogs.customSpeedLimit(context); if(values[0]) NZBGetAPI.from(Database.currentProfileObject).setSpeedLimit(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Speed Limit Set', message: 'Set to ${(values[1] as int).lunaKilobytesToString(decimals: 0)}/s', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Set Speed Limit', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); break; } default: NZBGetAPI.from(Database.currentProfileObject).setSpeedLimit(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Speed Limit Set', message: 'Set to ${(values[1] as int).lunaKilobytesToString(decimals: 0)}/s', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Set Speed Limit', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } } diff --git a/lib/modules/nzbget/widgets/history_hide_button.dart b/lib/modules/nzbget/widgets/history_hide_button.dart index df7a2b1b..d7053076 100644 --- a/lib/modules/nzbget/widgets/history_hide_button.dart +++ b/lib/modules/nzbget/widgets/history_hide_button.dart @@ -3,6 +3,13 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/nzbget.dart'; class NZBGetHistoryHideButton extends StatefulWidget { + final ScrollController controller; + + NZBGetHistoryHideButton({ + Key key, + @required this.controller, + }): super(key: key); + @override State createState() => _State(); } @@ -12,14 +19,14 @@ class _State extends State { Widget build(BuildContext context) => LunaCard( context: context, child: Consumer( - builder: (context, model, widget) => LSIconButton( + builder: (context, model, widget) => LunaIconButton( icon: model.historyHideFailed ? Icons.visibility_off : Icons.visibility, onPressed: () => model.historyHideFailed = !model.historyHideFailed, ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: EdgeInsets.only(left: 12.0), color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/nzbget/widgets/history_search_bar.dart b/lib/modules/nzbget/widgets/history_search_bar.dart index 16034e11..283b04b6 100644 --- a/lib/modules/nzbget/widgets/history_search_bar.dart +++ b/lib/modules/nzbget/widgets/history_search_bar.dart @@ -3,27 +3,45 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/nzbget.dart'; class NZBGetHistorySearchBar extends StatefulWidget { + final ScrollController scrollController; + + NZBGetHistorySearchBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + @override State createState() => _State(); } class _State extends State { - final _textController = TextEditingController(); + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = context.read().historySearchFilter; + } @override - Widget build(BuildContext context) => Expanded( - child: Consumer( - builder: (context, model, widget) => LSTextInputBar( - controller: _textController, - labelText: 'Search History...', - onChanged: (text, update) => _onChanged(model, text, update), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), - ), - ), - ); - - void _onChanged(NZBGetState model, String text, bool update) { - model.historySearchFilter = text; - if(update) _textController.text = ''; + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + scrollController: widget.scrollController, + autofocus: false, + onChanged: (value) => context.read().historySearchFilter = value, + margin: EdgeInsets.zero, + ), + ), + ), + NZBGetHistoryHideButton(controller: widget.scrollController), + ], + ); } } diff --git a/lib/modules/nzbget/widgets/history_tile.dart b/lib/modules/nzbget/widgets/history_tile.dart index f8d03b49..b938ff74 100644 --- a/lib/modules/nzbget/widgets/history_tile.dart +++ b/lib/modules/nzbget/widgets/history_tile.dart @@ -92,36 +92,29 @@ class _State extends State { await NZBGetAPI.from(Database.currentProfileObject).retryHistoryEntry(widget.data.id) .then((_) { widget.refresh(); - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: 'Retrying Job...', message: widget.data.name, ); }) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Retry Job', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); break; } case 'hide': await NZBGetAPI.from(Database.currentProfileObject).deleteHistoryEntry(widget.data.id, hide: true) .then((_) => _handleDelete('History Hidden')) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Hide History', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); break; case 'delete': await NZBGetAPI.from(Database.currentProfileObject).deleteHistoryEntry(widget.data.id, hide: true) .then((_) => _handleDelete('History Deleted')) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Delete History', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } } @@ -133,20 +126,16 @@ class _State extends State { hide: values[1], ) .then((_) => _handleDelete(values[1] ? 'History Hidden' : 'History Deleted')) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Delete History', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } void _handleDelete(String title) { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: title, message: widget.data.name, - type: SNACKBAR_TYPE.success, ); widget.refresh(); } diff --git a/lib/modules/nzbget/widgets/log_tile.dart b/lib/modules/nzbget/widgets/log_tile.dart index 3a6bc82c..63329348 100644 --- a/lib/modules/nzbget/widgets/log_tile.dart +++ b/lib/modules/nzbget/widgets/log_tile.dart @@ -10,10 +10,11 @@ class NZBGetLogTile extends StatelessWidget { }); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: data.text), - subtitle: LSSubtitle(text: data.timestamp), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title(text: data.text), + subtitle: LunaText.subtitle(text: data.timestamp), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async => LunaDialogs().textPreview(context, 'Log Entry', data.text), ); } diff --git a/lib/modules/nzbget/widgets/navigation_bar.dart b/lib/modules/nzbget/widgets/navigation_bar.dart index 63d7a417..9290c4e9 100644 --- a/lib/modules/nzbget/widgets/navigation_bar.dart +++ b/lib/modules/nzbget/widgets/navigation_bar.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/nzbget.dart'; -class NZBGetNavigationBar extends StatefulWidget { - static const List icons = [ - LunaIcons.queue, - LunaIcons.history, - ]; +class NZBGetNavigationBar extends StatelessWidget { + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + final PageController pageController; static const List titles = [ 'Queue', 'History', ]; - - final PageController pageController; + + static const List icons = [ + LunaIcons.queue, + LunaIcons.history, + ]; NZBGetNavigationBar({ Key key, @@ -22,30 +21,12 @@ class NZBGetNavigationBar extends StatefulWidget { }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - Provider.of(context, listen: false).navigationIndex = NZBGetDatabaseValue.NAVIGATION_INDEX.data; - }); - } - - @override - Widget build(BuildContext context) => Selector( - selector: (_, model) => model.navigationIndex, - builder: (context, index, _) => LSBottomNavigationBar( - index: index, - icons: NZBGetNavigationBar.icons, - titles: NZBGetNavigationBar.titles, - onTap: _navOnTap, - ), - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index) - .then((_) => Provider.of(context, listen: false).navigationIndex = index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/nzbget/widgets/queue_fab.dart b/lib/modules/nzbget/widgets/queue_fab.dart index 4c86137e..42f5b107 100644 --- a/lib/modules/nzbget/widgets/queue_fab.dart +++ b/lib/modules/nzbget/widgets/queue_fab.dart @@ -33,6 +33,7 @@ class _State extends State with TickerProviderStateMixin { void dispose() { _iconController?.dispose(); _hideController?.dispose(); + widget.scrollController?.removeListener(scrollControllerListener); super.dispose(); } @@ -48,20 +49,23 @@ class _State extends State with TickerProviderStateMixin { vsync: this, duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), ); - _hideController.forward(); - widget.scrollController.addListener(() { - switch(widget.scrollController.position.userScrollDirection) { - case ScrollDirection.forward: if(!_visible) { - _hideController.forward(); - _visible = true; - } break; - case ScrollDirection.reverse: if(_visible) { - _hideController.reverse(); - _visible = false; - } break; - case ScrollDirection.idle: break; - } - }); + _hideController?.forward(); + widget.scrollController?.addListener(scrollControllerListener); + } + + void scrollControllerListener() { + if(!widget.scrollController.hasClients) return; + switch(widget.scrollController.position.userScrollDirection) { + case ScrollDirection.forward: if(!_visible) { + _hideController?.forward(); + _visible = true; + } break; + case ScrollDirection.reverse: if(_visible) { + _hideController?.reverse(); + _visible = false; + } break; + case ScrollDirection.idle: break; + } } @override @@ -69,14 +73,14 @@ class _State extends State with TickerProviderStateMixin { selector: (_, model) => Tuple2(model.error, model.paused), builder: (context, data, _) { data.item2 - ? _iconController.forward() - : _iconController.reverse(); + ? _iconController?.forward() + : _iconController?.reverse(); return data.item1 ? Container() : ScaleTransition( scale: _hideController, child: InkWell( - child: LSFloatingActionButtonAnimated( + child: LunaFloatingActionButtonAnimated( controller: _iconController, icon: AnimatedIcons.pause_play, onPressed: () => _toggle(context, data.item2), @@ -103,31 +107,23 @@ class _State extends State with TickerProviderStateMixin { if(values[1] == -1) { List values = await NZBGetDialogs.customPauseFor(context); if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).pauseQueueFor(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Pausing Queue', message: 'For ${(values[1] as int).lunaDuration(multiplier: 60)}', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Pause Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } else { await NZBGetAPI.from(Database.currentProfileObject).pauseQueueFor(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Pausing Queue', message: 'For ${(values[1] as int).lunaDuration(multiplier: 60)}', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Pause Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } } @@ -139,13 +135,11 @@ class _State extends State with TickerProviderStateMixin { .then((_) { Provider.of(context, listen: false).paused = true; }) - .catchError((_) { + .catchError((error) { _iconController.reverse(); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Pause Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, ); }); } @@ -156,13 +150,11 @@ class _State extends State with TickerProviderStateMixin { .then((_) { Provider.of(context, listen: false).paused = false; }) - .catchError((_) { + .catchError((error) { _iconController.forward(); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Resume Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, ); }); } diff --git a/lib/modules/nzbget/widgets/queue_tile.dart b/lib/modules/nzbget/widgets/queue_tile.dart index 9eb9e51b..780ae8ac 100644 --- a/lib/modules/nzbget/widgets/queue_tile.dart +++ b/lib/modules/nzbget/widgets/queue_tile.dart @@ -6,13 +6,11 @@ import 'package:lunasea/modules/nzbget.dart'; class NZBGetQueueTile extends StatefulWidget { final NZBGetQueueData data; - final Function(String, String, SNACKBAR_TYPE) snackbar; final Function refresh; final BuildContext queueContext; NZBGetQueueTile({ @required this.data, - @required this.snackbar, @required this.queueContext, @required this.refresh, Key key, @@ -24,40 +22,43 @@ class NZBGetQueueTile extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle( - text: widget.data.name, - darken: widget.data.paused, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - child: LinearPercentIndicator( - percent: min(1.0, max(0, widget.data.percentageDone/100)), - padding: EdgeInsets.symmetric(horizontal: 2.0), - progressColor: widget.data.paused ? LunaColours.accent.withOpacity(0.30) : LunaColours.accent, - backgroundColor: widget.data.paused ? LunaColours.accent.withOpacity(0.05) : LunaColours.accent.withOpacity(0.15), - lineHeight: 4.0, + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title( + text: widget.data.name, + darken: widget.data.paused, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + child: LinearPercentIndicator( + percent: min(1.0, max(0, widget.data.percentageDone/100)), + padding: EdgeInsets.symmetric(horizontal: 2.0), + progressColor: widget.data.paused ? LunaColours.accent.withOpacity(0.30) : LunaColours.accent, + backgroundColor: widget.data.paused ? LunaColours.accent.withOpacity(0.05) : LunaColours.accent.withOpacity(0.15), + lineHeight: 4.0, + ), + padding: EdgeInsets.symmetric(vertical: 6.0), ), - padding: EdgeInsets.symmetric(vertical: 6.0), - ), - LSSubtitle( - text: widget.data.subtitle, - darken: widget.data.paused, - ), - ], - ), - trailing: LSIconButton( - icon: Icons.more_vert, - onPressed: () async => _handlePopup(), - color: widget.data.paused ? Colors.white30 : Colors.white, - ), - ); + LunaText.subtitle( + text: widget.data.subtitle, + darken: widget.data.paused, + ), + ], + ), + trailing: LunaIconButton( + icon: Icons.more_vert, + onPressed: () async => _handlePopup(), + color: widget.data.paused ? Colors.white30 : Colors.white, + ), + ); + } Future _handlePopup() async { - _Helper _helper = _Helper(widget.queueContext, widget.data, widget.snackbar, widget.refresh); + _Helper _helper = _Helper(widget.queueContext, widget.data, widget.refresh); List values = await NZBGetDialogs.queueSettings(widget.queueContext, widget.data.name, widget.data.paused); if(values[0]) switch(values[1]) { case 'status': widget.data.paused @@ -77,48 +78,30 @@ class _State extends State { class _Helper { final BuildContext context; final NZBGetQueueData data; - final Function snackbar; final Function refresh; _Helper( this.context, this.data, - this.snackbar, this.refresh, ); Future _pauseJob() async { await NZBGetAPI.from(Database.currentProfileObject).pauseSingleJob(data.id) .then((_) { - snackbar( - 'Job Paused', - data.name, - SNACKBAR_TYPE.success, - ); + showLunaSuccessSnackBar(title: 'Job Paused', message: data.name); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Pause Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Pause Job', error: error)); } Future _resumeJob() async { await NZBGetAPI.from(Database.currentProfileObject).resumeSingleJob(data.id) .then((_) { - snackbar( - 'Job Resumed', - data.name, - SNACKBAR_TYPE.success, - ); + showLunaSuccessSnackBar(title: 'Job Resumed', message: data.name); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Resume Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Resume Job', error: error)); } Future _category() async { @@ -126,89 +109,52 @@ class _Helper { List values = await NZBGetDialogs.changeCategory(context, categories); if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).setJobCategory(data.id, values[1]) .then((_) { - snackbar( - values[1].name == '' ? 'Category Set (No Category)' : 'Category Set (${values[1].name})', - data.name, - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: values[1].name == '' ? 'Category Set (No Category)' : 'Category Set (${values[1].name})', + message: data.name, ); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Set Category', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Set Category', error: error)); } Future _priority() async { List values = await NZBGetDialogs.changePriority(context); if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).setJobPriority(data.id, values[1]) .then((_) { - snackbar( - 'Priority Set (${(values[1] as NZBGetPriority).name})', - data.name, - SNACKBAR_TYPE.success, - ); + showLunaSuccessSnackBar(title: 'Priority Set (${(values[1] as NZBGetPriority).name})', message: data.name); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Set Priority', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Set Priority', error: error)); } Future _rename() async { List values = await NZBGetDialogs.renameJob(context, data.name); if(values[0]) NZBGetAPI.from(Database.currentProfileObject).renameJob(data.id, values[1]) .then((_) { - snackbar( - 'Job Renamed', - values[1], - SNACKBAR_TYPE.success, - ); + showLunaSuccessSnackBar(title: 'Job Renamed', message: values[1]); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Rename Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Rename Job', error: error)); } Future _delete() async { List values = await NZBGetDialogs.deleteJob(context); if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).deleteJob(data.id) .then((_) { - snackbar( - 'Job Deleted', - data.name, - SNACKBAR_TYPE.success, - ); + showLunaSuccessSnackBar(title: 'Job Deleted', message: data.name); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Delete Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Delete Job', error: error)); } Future _password() async { List values = await NZBGetDialogs.setPassword(context); if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).setJobPassword(data.id, values[1]) .then((_) { - snackbar( - 'Job Password Set', - data.name, - SNACKBAR_TYPE.success, - ); + showLunaSuccessSnackBar(title: 'Job Password Set', message: data.name); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Set Job Password', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, - )); + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Set Job Password', error: error)); } } \ No newline at end of file diff --git a/lib/modules/radarr/core.dart b/lib/modules/radarr/core.dart index 25381027..cc3b9b2d 100644 --- a/lib/modules/radarr/core.dart +++ b/lib/modules/radarr/core.dart @@ -1,4 +1,5 @@ export 'core/api_helper.dart'; +export 'core/bottom_modal_sheets.dart'; export 'core/database.dart'; export 'core/dialogs.dart'; export 'core/extensions.dart'; diff --git a/lib/modules/radarr/core/api_helper.dart b/lib/modules/radarr/core/api_helper.dart index 7357bc36..a5d410aa 100644 --- a/lib/modules/radarr/core/api_helper.dart +++ b/lib/modules/radarr/core/api_helper.dart @@ -86,7 +86,6 @@ class RadarrAPIHelper { ) .then((movie) { if(showSnackbar) showLunaSuccessSnackBar( - context: context, title: [ 'Movie Added', if(searchForMovie) '(Searching...)', @@ -112,7 +111,7 @@ class RadarrAPIHelper { if(context.read().enabled) { return await context.read().api.command.backup() .then((_) { - if(showSnackbar) showLunaSuccessSnackBar(title: 'Backing Up Database${Constants.TEXT_ELLIPSIS}', message: 'Backing up the database in the background'); + if(showSnackbar) showLunaSuccessSnackBar(title: 'Backing Up Database${LunaUI.TEXT_ELLIPSIS}', message: 'Backing up the database in the background'); return true; }) .catchError((error, stack) { @@ -132,7 +131,7 @@ class RadarrAPIHelper { if(context.read().enabled) { return await context.read().api.command.missingMovieSearch() .then((_) { - if(showSnackbar) showLunaSuccessSnackBar(title: 'Searching${Constants.TEXT_ELLIPSIS}', message: 'Searching for all missing movies'); + if(showSnackbar) showLunaSuccessSnackBar(title: 'Searching${LunaUI.TEXT_ELLIPSIS}', message: 'Searching for all missing movies'); return true; }) .catchError((error, stack) { @@ -152,7 +151,7 @@ class RadarrAPIHelper { if(context.read().enabled) { return await context.read().api.command.rssSync() .then((_) { - if(showSnackbar) showLunaSuccessSnackBar(title: 'Running RSS Sync${Constants.TEXT_ELLIPSIS}', message: 'Running RSS sync in the background'); + if(showSnackbar) showLunaSuccessSnackBar(title: 'Running RSS Sync${LunaUI.TEXT_ELLIPSIS}', message: 'Running RSS sync in the background'); return true; }) .catchError((error, stack) { @@ -172,7 +171,7 @@ class RadarrAPIHelper { if(context.read().enabled) { return await context.read().api.command.refreshMovie() .then((_) { - if(showSnackbar) showLunaSuccessSnackBar(title: 'Updating Library${Constants.TEXT_ELLIPSIS}', message: 'Updating library in the background'); + if(showSnackbar) showLunaSuccessSnackBar(title: 'Updating Library${LunaUI.TEXT_ELLIPSIS}', message: 'Updating library in the background'); return true; }) .catchError((error, stack) { @@ -246,7 +245,6 @@ class RadarrAPIHelper { movie.id = null; return await context.read().setSingleMovie(movie).then((_) { if(showSnackbar) showLunaSuccessSnackBar( - context: context, title: [ 'Removed Movie', if(RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.data) '(With Files)', @@ -276,7 +274,6 @@ class RadarrAPIHelper { return await context.read().api.command.downloadedMoviesScan(path: path) .then((_) async { if(showSnackbar) showLunaSuccessSnackBar( - context: context, title: 'Running Quick Import${LunaUI.TEXT_ELLIPSIS}', message: path, ); @@ -304,7 +301,6 @@ class RadarrAPIHelper { return await context.read().api.command.moviesSearch(movieIds: [movieId]) .then((_) async { if(showSnackbar) showLunaSuccessSnackBar( - context: context, title: 'Searching for Movie...', message: title, ); @@ -330,7 +326,6 @@ class RadarrAPIHelper { return await context.read().api.release.push(indexerId: release.indexerId, guid: release.guid) .then((value) { if(showSnackbar) showLunaSuccessSnackBar( - context: context, title: 'Downloading Release...', message: release.title, ); diff --git a/lib/modules/radarr/core/bottom_modal_sheets.dart b/lib/modules/radarr/core/bottom_modal_sheets.dart new file mode 100644 index 00000000..48c4c8c4 --- /dev/null +++ b/lib/modules/radarr/core/bottom_modal_sheets.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrBottomModalSheets { + Future configureManualImport(BuildContext context) async { + await LunaBottomModalSheet().showModal( + context: context, + expand: false, + builder: (_) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => LunaListViewModal( + children: [ + LunaHeader( + text: 'radarr.Configure'.tr(), + subtitle: context.read().manualImport.relativePath, + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'radarr.SelectMovie'.tr()), + subtitle: LunaText.subtitle(text: context.watch().manualImport.lunaMovie), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async { + Tuple2 result = await selectMovie(context); + if(result.item1) context.read().fetchUpdates(context, result.item2.id); + }, + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'radarr.SelectQuality'.tr()), + subtitle: LunaText.subtitle(text: context.watch().manualImport.lunaQualityProfile), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => selectQuality(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'radarr.SelectLanguage'.tr()), + subtitle: LunaText.subtitle(text: context.watch().manualImport.lunaLanguage), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async { + List languages = await context.read().languages; + await RadarrDialogs().setManualImportLanguages(context, languages); + }, + ), + ], + ), + ), + ); + } + + Future selectQuality(BuildContext context) async { + await LunaBottomModalSheet().showModal( + context: context, + builder: (_) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => LunaListViewModal( + children: [ + LunaHeader(text: 'radarr.SelectQuality'.tr()), + LunaListTile( + context: context, + title: LunaText.title(text: 'radarr.Quality'.tr()), + subtitle: LunaText.subtitle(text: context.watch().manualImport.lunaQualityProfile), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async { + List profiles = await context.read().qualityDefinitions; + Tuple2 result = await RadarrDialogs().selectQualityDefinition(context, profiles); + if(result.item1) context.read().updateQuality(result.item2.quality); + }, + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Proper'), + trailing: Switch( + value: context.watch().manualImport.quality?.revision?.version == 2, + onChanged: (value) async { + RadarrManualImport _import = context.read().manualImport; + _import.quality?.revision?.version = value ? 2 : 1; + context.read().manualImport = _import; + }, + ), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Real'), + trailing: Switch( + value: context.watch().manualImport.quality?.revision?.real == 1, + onChanged: (value) async { + RadarrManualImport _import = context.read().manualImport; + _import.quality?.revision?.real = value ? 1 : 0; + context.read().manualImport = _import; + }, + ), + ), + ], + ), + ), + ); + } + + Future> selectMovie(BuildContext context) async { + bool result = false; + RadarrMovie movie; + context.read().configureMoviesSearchQuery = ''; + + List _sortAndFilter(List movies, String query) { + List _filtered = movies..sort((a,b) => a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase())); + _filtered = _filtered.where((movie) => movie.title.toLowerCase().contains(query)).toList(); + return _filtered; + } + + await LunaBottomModalSheet().showModal( + context: context, + builder: (_) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => Scaffold( + appBar: LunaAppBar( + title: 'radarr.SelectMovie'.tr(), + bottom: RadarrManualImportDetailsConfigureMoviesSearchBar(), + hideLeading: true, + ), + body: FutureBuilder( + future: context.watch().movies, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Radarr movies', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage(text: 'lunasea.AnErrorHasOccurred'.tr()); + } + if(snapshot.hasData) { + if((snapshot.data?.length ?? 0) == 0) return LunaMessage(text: 'radarr.NoMoviesFound'.tr()); + String _query = context.watch().configureMoviesSearchQuery; + List movies = _sortAndFilter(snapshot.data, _query); + if((movies?.length ?? 0) == 0) return LunaListViewModal( + children: [ + LunaMessage.inList(text: 'radarr.NoMoviesFound'.tr()), + ], + ); + // Return the final movie list + return LunaListViewModalBuilder( + itemCount: movies.length, + itemBuilder: (context, index) { + String title = movies[index].title; + if(movies[index].year != null && movies[index].year != 0) title += ' (${movies[index].year})'; + return LunaListTile( + context: context, + title: LunaText.title(text: title), + subtitle: LunaText.subtitle( + text: movies[index].overview, + softWrap: true, + maxLines: 2, + ), + contentPadding: true, + onTap: () { + result = true; + movie = movies[index]; + Navigator.of(context).pop(); + }, + ); + }, + shrinkWrap: false, + ); + } + return LunaLoader(); + }, + ), + ), + ), + ); + return Tuple2(result, movie); + } +} diff --git a/lib/modules/radarr/core/dialogs.dart b/lib/modules/radarr/core/dialogs.dart index ce42e64f..64b7f153 100644 --- a/lib/modules/radarr/core/dialogs.dart +++ b/lib/modules/radarr/core/dialogs.dart @@ -13,19 +13,19 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'lunasea.Settings'.tr(), content: List.generate( RadarrGlobalSettingsType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: RadarrGlobalSettingsType.values[index].name, icon: RadarrGlobalSettingsType.values[index].icon, iconColor: LunaColours.list(index), onTap: () => _setValues(true, RadarrGlobalSettingsType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _value); } @@ -40,19 +40,19 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: movie.title, content: List.generate( RadarrMovieSettingsType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: RadarrMovieSettingsType.values[index].name(movie), icon: RadarrMovieSettingsType.values[index].icon(movie), iconColor: LunaColours.list(index), onTap: () => _setValues(true, RadarrMovieSettingsType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _value); } @@ -70,19 +70,19 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Page', content: List.generate( titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: titles[index], icon: icons[index], iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _index); @@ -100,19 +100,19 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Sorting & Filtering', content: List.generate( titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: titles[index], icon: Icons.sort, iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _index); @@ -130,11 +130,11 @@ class RadarrDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add Tag', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Add', onPressed: () => _setValues(true), ), @@ -142,7 +142,7 @@ class RadarrDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Tag Label', onSubmitted: (_) => _setValues(true), @@ -153,7 +153,7 @@ class RadarrDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return Tuple2(_flag, _textController.text); } @@ -166,20 +166,20 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Tag', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete this tag?'), + LunaDialog.textContent(text: 'Are you sure you want to delete this tag?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return _flag; } @@ -192,19 +192,19 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Missing Movies', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Search', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to search for all missing movies?'), + LunaDialog.textContent(text: 'Are you sure you want to search for all missing movies?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return _flag; } @@ -217,20 +217,20 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete File', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete this movie file?'), + LunaDialog.textContent(text: 'Are you sure you want to delete this movie file?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return _flag; } @@ -245,19 +245,19 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'radarr.MinimumAvailability'.tr(), content: List.generate( RadarrAvailability.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: RadarrAvailability.values[index].readable, icon: Icons.folder, iconColor: LunaColours.list(index), onTap: () => _setValues(true, RadarrAvailability.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, availability); } @@ -272,23 +272,85 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'radarr.QualityProfile'.tr(), content: List.generate( profiles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: profiles[index].name, - icon: Icons.portrait, + icon: Icons.portrait_rounded, iconColor: LunaColours.list(index), onTap: () => _setValues(true, profiles[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, profile); } + Future> selectQualityDefinition(BuildContext context, List definitions) async { + bool _flag = false; + RadarrQualityDefinition profile; + + void _setValues(bool flag, RadarrQualityDefinition value) { + _flag = flag; + profile = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'radarr.Quality'.tr(), + content: List.generate( + definitions.length, + (index) => LunaDialog.tile( + text: definitions[index].title, + icon: Icons.portrait_rounded, + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, definitions[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, profile); + } + + Future setManualImportLanguages(BuildContext context, List languages) async { + List filteredLanguages = languages.where((lang) => lang.id >= 0).toList(); + await showDialog( + context: context, + builder: (_) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => AlertDialog( + actions: [ + LunaDialog.button( + text: 'Close', + onPressed: () => Navigator.of(context, rootNavigator: true).pop(), + ), + ], + title: LunaDialog.title(text: 'radarr.Languages'.tr()), + content: Consumer( + builder: (context, manualImport, _) => LunaDialog.content( + children: List.generate( + filteredLanguages.length, + (index) => LunaDialog.checkbox( + title: filteredLanguages[index].name, + value: context.read().manualImport.languages.indexWhere((lang) => lang.id == filteredLanguages[index].id) != -1, + onChanged: (value) => value + ? context.read().addLanguage(filteredLanguages[index]) + : context.read().removeLanguage(filteredLanguages[index]) + ), + ), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + shape: LunaUI.shapeBorder, + ), + ), + ); + } + Future setEditTags(BuildContext context) async { await showDialog( context: context, @@ -297,26 +359,26 @@ class RadarrDialogs { builder: (context, _) => AlertDialog( actions: [ RadarrTagsAppBarActionAddTag(asDialogButton: true), - LSDialog.button( + LunaDialog.button( text: 'Close', onPressed: () => Navigator.of(context, rootNavigator: true).pop(), ), ], - title: LSDialog.title(text: 'Tags'), + title: LunaDialog.title(text: 'Tags'), content: Selector>>( selector: (_, state) => state.tags, builder: (context, future, _) => FutureBuilder( future: future, builder: (context, AsyncSnapshot> snapshot) { - if((snapshot.data?.length ?? 0) == 0) return LSDialog.content( + if((snapshot.data?.length ?? 0) == 0) return LunaDialog.content( children: [ - LSDialog.textContent(text: 'No Tags Found'), + LunaDialog.textContent(text: 'No Tags Found'), ], ); - return LSDialog.content( + return LunaDialog.content( children: List.generate( snapshot.data.length, - (index) => LSDialog.checkbox( + (index) => LunaDialog.checkbox( title: snapshot.data[index].label, value: context.watch().tags.where((tag) => tag.id == snapshot.data[index].id).length != 0, onChanged: (selected) { @@ -330,7 +392,7 @@ class RadarrDialogs { }, ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), shape: LunaUI.shapeBorder, ), ), @@ -340,32 +402,32 @@ class RadarrDialogs { Future setAddTags(BuildContext context) async { await showDialog( context: context, - builder: (dContext) => ChangeNotifierProvider.value( + builder: (_) => ChangeNotifierProvider.value( value: context.read(), builder: (context, _) => AlertDialog( actions: [ RadarrTagsAppBarActionAddTag(asDialogButton: true), - LSDialog.button( + LunaDialog.button( text: 'Close', onPressed: () => Navigator.of(context, rootNavigator: true).pop(), ), ], - title: LSDialog.title(text: 'Tags'), + title: LunaDialog.title(text: 'Tags'), content: Selector>>( selector: (_, state) => state.tags, builder: (context, future, _) => FutureBuilder( future: future, builder: (context, AsyncSnapshot> snapshot) { - if((snapshot.data?.length ?? 0) == 0) return LSDialog.content( + if((snapshot.data?.length ?? 0) == 0) return LunaDialog.content( children: [ - LSDialog.textContent(text: 'No Tags Found'), + LunaDialog.textContent(text: 'No Tags Found'), ], ); - return LSDialog.content( + return LunaDialog.content( children: List.generate( snapshot.data.length, - (index) => LSDialog.checkbox( + (index) => LunaDialog.checkbox( title: snapshot.data[index].label, value: context.watch().tags.where((tag) => tag.id == snapshot.data[index].id).length != 0, onChanged: (selected) { @@ -379,7 +441,7 @@ class RadarrDialogs { }, ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), shape: LunaUI.shapeBorder, ), ), @@ -396,16 +458,16 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'radarr.RootFolder'.tr(), content: List.generate( folders.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: folders[index].path, - subtitle: LSDialog.richText( + subtitle: LunaDialog.richText( children: [ - LSDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()) + LunaDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()) ], ), icon: Icons.folder, @@ -413,7 +475,7 @@ class RadarrDialogs { onTap: () => _setValues(true, folders[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _folder); } @@ -426,11 +488,11 @@ class RadarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Remove Movie', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Remove', textColor: LunaColours.red, onPressed: () => _setValues(true), @@ -438,21 +500,21 @@ class RadarrDialogs { ], content: [ RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST.listen( - builder: (context, value, _) => LSDialog.checkbox( + builder: (context, value, _) => LunaDialog.checkbox( title: 'Add to Exclusion List', value: RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST.data, onChanged: (value) => RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST.put(value), ), ), RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.listen( - builder: (context, value, _) => LSDialog.checkbox( + builder: (context, value, _) => LunaDialog.checkbox( title: 'Delete Files', value: RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.data, onChanged: (value) => RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.put(value), ), ), ], - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return _flag; } diff --git a/lib/modules/radarr/core/extensions/radarr_event_type.dart b/lib/modules/radarr/core/extensions/radarr_event_type.dart index 8e19e1b4..7d352214 100644 --- a/lib/modules/radarr/core/extensions/radarr_event_type.dart +++ b/lib/modules/radarr/core/extensions/radarr_event_type.dart @@ -33,7 +33,7 @@ extension lunaRadarrEventType on RadarrEventType { Color get lunaIconColour { switch(this) { case RadarrEventType.GRABBED: return Colors.white; - case RadarrEventType.DOWNLOAD_FAILED: return Colors.red; + case RadarrEventType.DOWNLOAD_FAILED: return LunaColours.red; case RadarrEventType.DOWNLOAD_FOLDER_IMPORTED: return Colors.white; case RadarrEventType.DOWNLOAD_IGNORED: return Colors.white; case RadarrEventType.MOVIE_FILE_DELETED: return Colors.white; diff --git a/lib/modules/radarr/core/state.dart b/lib/modules/radarr/core/state.dart index 1e312d0a..cfb618c6 100644 --- a/lib/modules/radarr/core/state.dart +++ b/lib/modules/radarr/core/state.dart @@ -14,6 +14,7 @@ class RadarrState extends LunaModuleState { _missing = null; _rootFolders = null; _qualityProfiles = null; + _qualityDefinitions = null; _languages = null; _tags = null; // Reinitialize @@ -21,6 +22,7 @@ class RadarrState extends LunaModuleState { if(_enabled) { fetchRootFolders(); fetchQualityProfiles(); + fetchQualityDefinitions(); fetchLanguages(); fetchTags(); fetchMovies(); @@ -215,6 +217,19 @@ class RadarrState extends LunaModuleState { notifyListeners(); } + Future> _qualityDefinitions; + Future> get qualityDefinitions => _qualityDefinitions; + set qualityDefinitions(Future> qualityDefinitions) { + assert(qualityDefinitions != null); + _qualityDefinitions = qualityDefinitions; + notifyListeners(); + } + + void fetchQualityDefinitions() { + if(_api != null) _qualityDefinitions = _api.qualityProfile.getDefinitions(); + notifyListeners(); + } + Future> _languages; Future> get languages => _languages; set languages(Future> languages) { diff --git a/lib/modules/radarr/routes/add_movie/state.dart b/lib/modules/radarr/routes/add_movie/state.dart index 7927622e..fecc8292 100644 --- a/lib/modules/radarr/routes/add_movie/state.dart +++ b/lib/modules/radarr/routes/add_movie/state.dart @@ -20,7 +20,7 @@ class RadarrAddMovieState extends ChangeNotifier { Future> _lookup; Future> get lookup => _lookup; void fetchLookup(BuildContext context) { - if((context?.read()?.enabled ?? false)) { + if((context.read().enabled ?? false)) { _lookup = context.read().api.movieLookup.get(term: _searchQuery); } notifyListeners(); @@ -29,7 +29,7 @@ class RadarrAddMovieState extends ChangeNotifier { Future> _exclusions; Future> get exclusions => _exclusions; void fetchExclusions(BuildContext context) { - if((context?.read()?.enabled ?? false)) { + if((context.read().enabled ?? false)) { _exclusions = context.read().api.exclusions.getAll(); } notifyListeners(); @@ -38,7 +38,7 @@ class RadarrAddMovieState extends ChangeNotifier { Future> _discovery; Future> get discovery => _discovery; void fetchDiscovery(BuildContext context) { - if((context?.read()?.enabled ?? false)) { + if((context.read().enabled ?? false)) { _discovery = context.read().api.importList.getMovies(includeRecommendations: RadarrDatabaseValue.ADD_DISCOVER_USE_SUGGESTIONS.data); } notifyListeners(); diff --git a/lib/modules/radarr/routes/add_movie/widgets/page_search.dart b/lib/modules/radarr/routes/add_movie/widgets/page_search.dart index 0b61bb0c..fa4ad0f6 100644 --- a/lib/modules/radarr/routes/add_movie/widgets/page_search.dart +++ b/lib/modules/radarr/routes/add_movie/widgets/page_search.dart @@ -45,8 +45,9 @@ class _State extends State with AutomaticKeepAliveClie child: RadarrAddMovieSearchSearchBar( query: context.read().searchQuery, autofocus: widget.autofocusSearchBar, + scrollController: RadarrAddMovieNavigationBar.scrollControllers[0], ), - height: 62.0, + height: LunaTextInputBar.appBarHeight, ); } @@ -87,7 +88,12 @@ class _State extends State with AutomaticKeepAliveClie } Widget _list(List movies, List results, List exclusions) { - if((results?.length ?? 0) == 0) return LunaListView(children: [LunaMessage.inList(text: 'radarr.NoResultsFound'.tr())], controller: null); + if((results?.length ?? 0) == 0) return LunaListView( + controller: RadarrAddMovieNavigationBar.scrollControllers[0], + children: [ + LunaMessage.inList(text: 'radarr.NoResultsFound'.tr()), + ], + ); return LunaListViewBuilder( controller: RadarrAddMovieNavigationBar.scrollControllers[0], itemCount: results.length, diff --git a/lib/modules/radarr/routes/add_movie/widgets/search_search_bar.dart b/lib/modules/radarr/routes/add_movie/widgets/search_search_bar.dart index dbb34fda..4bf431e9 100644 --- a/lib/modules/radarr/routes/add_movie/widgets/search_search_bar.dart +++ b/lib/modules/radarr/routes/add_movie/widgets/search_search_bar.dart @@ -5,11 +5,13 @@ import 'package:lunasea/modules/radarr.dart'; class RadarrAddMovieSearchSearchBar extends StatefulWidget implements PreferredSizeWidget { final String query; final bool autofocus; + final ScrollController scrollController; RadarrAddMovieSearchSearchBar({ Key key, @required this.query, @required this.autofocus, + @required this.scrollController, }) : super(key: key); @override @@ -37,12 +39,13 @@ class _State extends State { child: Consumer( builder: (context, state, _) => LunaTextInputBar( controller: _controller, + scrollController: widget.scrollController, autofocus: widget.autofocus, onChanged: (value) => context.read().searchQuery = value, onSubmitted: (value) { if(value.isNotEmpty) context.read().fetchLookup(context); }, - margin: LunaTextInputBar.preferredSizeWidgetMargin, + margin: EdgeInsets.zero, ), ), ), diff --git a/lib/modules/radarr/routes/add_movie_details/route.dart b/lib/modules/radarr/routes/add_movie_details/route.dart index 2b44d504..e9bc2387 100644 --- a/lib/modules/radarr/routes/add_movie_details/route.dart +++ b/lib/modules/radarr/routes/add_movie_details/route.dart @@ -87,7 +87,7 @@ class _State extends State<_RadarrMoviesAddDetailsRoute> with LunaLoadCallbackMi builder: (context, AsyncSnapshot> snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Radarr add movie data', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Radarr add movie data', snapshot.error, snapshot.stackTrace); } return LunaMessage.error(onTap: _refreshKey.currentState?.show ?? () {}); } diff --git a/lib/modules/radarr/routes/add_movie_details/state.dart b/lib/modules/radarr/routes/add_movie_details/state.dart index 9f93e402..84d5f130 100644 --- a/lib/modules/radarr/routes/add_movie_details/state.dart +++ b/lib/modules/radarr/routes/add_movie_details/state.dart @@ -47,7 +47,7 @@ class RadarrAddMovieDetailsState extends ChangeNotifier { void initializeRootFolder(List rootFolders) { _rootFolder = (rootFolders ?? []).firstWhere( (element) => element.id == RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID.data, - orElse: () => (rootFolders?.length ?? 0) != 0 ? rootFolders[0] : RadarrRootFolder(id: -1, freeSpace: 0, path: Constants.TEXT_EMDASH), + orElse: () => (rootFolders?.length ?? 0) != 0 ? rootFolders[0] : RadarrRootFolder(id: -1, freeSpace: 0, path: LunaUI.TEXT_EMDASH), ); } @@ -62,7 +62,7 @@ class RadarrAddMovieDetailsState extends ChangeNotifier { void initializeQualityProfile(List qualityProfiles) { _qualityProfile = (qualityProfiles ?? []).firstWhere( (element) => element.id == RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID.data, - orElse: () => (qualityProfiles?.length ?? 0) != 0 ? qualityProfiles[0] : RadarrQualityProfile(id: -1, name: Constants.TEXT_EMDASH), + orElse: () => (qualityProfiles?.length ?? 0) != 0 ? qualityProfiles[0] : RadarrQualityProfile(id: -1, name: LunaUI.TEXT_EMDASH), ); } diff --git a/lib/modules/radarr/routes/add_movie_details/widgets/tile_minimum_availability.dart b/lib/modules/radarr/routes/add_movie_details/widgets/tile_minimum_availability.dart index 6fb7e1d4..99520ec0 100644 --- a/lib/modules/radarr/routes/add_movie_details/widgets/tile_minimum_availability.dart +++ b/lib/modules/radarr/routes/add_movie_details/widgets/tile_minimum_availability.dart @@ -10,7 +10,7 @@ class RadarrAddMovieDetailsMinimumAvailabilityTile extends StatelessWidget { title: LunaText.title(text: 'radarr.MinimumAvailability'.tr()), subtitle: Selector( selector: (_, state) => state.availability, - builder: (context, availability, _) => LunaText.subtitle(text: availability?.readable ?? Constants.TEXT_EMDASH), + builder: (context, availability, _) => LunaText.subtitle(text: availability?.readable ?? LunaUI.TEXT_EMDASH), ), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { diff --git a/lib/modules/radarr/routes/add_movie_details/widgets/tile_quality_profile.dart b/lib/modules/radarr/routes/add_movie_details/widgets/tile_quality_profile.dart index b69e125b..35da6f16 100644 --- a/lib/modules/radarr/routes/add_movie_details/widgets/tile_quality_profile.dart +++ b/lib/modules/radarr/routes/add_movie_details/widgets/tile_quality_profile.dart @@ -10,7 +10,7 @@ class RadarrAddMovieDetailsQualityProfileTile extends StatelessWidget { title: LunaText.title(text: 'radarr.QualityProfile'.tr()), subtitle: Selector( selector: (_, state) => state.qualityProfile, - builder: (context, profile, _) => LunaText.subtitle(text: profile?.name ?? Constants.TEXT_EMDASH), + builder: (context, profile, _) => LunaText.subtitle(text: profile?.name ?? LunaUI.TEXT_EMDASH), ), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { diff --git a/lib/modules/radarr/routes/add_movie_details/widgets/tile_root_folder.dart b/lib/modules/radarr/routes/add_movie_details/widgets/tile_root_folder.dart index 0a4fe022..df2a3505 100644 --- a/lib/modules/radarr/routes/add_movie_details/widgets/tile_root_folder.dart +++ b/lib/modules/radarr/routes/add_movie_details/widgets/tile_root_folder.dart @@ -10,7 +10,7 @@ class RadarrAddMovieDetailsRootFolderTile extends StatelessWidget { title: LunaText.title(text: 'radarr.RootFolder'.tr()), subtitle: Selector( selector: (_, state) => state.rootFolder, - builder: (context, folder, _) => LunaText.subtitle(text: folder?.path ?? Constants.TEXT_EMDASH), + builder: (context, folder, _) => LunaText.subtitle(text: folder?.path ?? LunaUI.TEXT_EMDASH), ), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { diff --git a/lib/modules/radarr/routes/catalogue/route.dart b/lib/modules/radarr/routes/catalogue/route.dart index 15dcf46d..f67754b2 100644 --- a/lib/modules/radarr/routes/catalogue/route.dart +++ b/lib/modules/radarr/routes/catalogue/route.dart @@ -32,6 +32,7 @@ class _State extends State with AutomaticKeepAliveClientMi key: _scaffoldKey, body: _body(), appBar: _appBar(), + extendBody: true, ); } @@ -84,7 +85,7 @@ class _State extends State with AutomaticKeepAliveClientMi controller: RadarrNavigationBar.scrollControllers[0], children: [ LunaMessage.inList(text: 'radarr.NoMoviesFound'.tr()), - LunaButtonContainer( + if((query ?? '').isNotEmpty) LunaButtonContainer( children: [ LunaButton.text( text: query.length > 20 diff --git a/lib/modules/radarr/routes/catalogue/widgets/search_bar.dart b/lib/modules/radarr/routes/catalogue/widgets/search_bar.dart index e401f1dc..96e1d9b8 100644 --- a/lib/modules/radarr/routes/catalogue/widgets/search_bar.dart +++ b/lib/modules/radarr/routes/catalogue/widgets/search_bar.dart @@ -29,14 +29,17 @@ class _State extends State { @override Widget build(BuildContext context) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: Consumer( builder: (context, state, _) => LunaTextInputBar( controller: _controller, + scrollController: widget.scrollController, autofocus: false, onChanged: (value) => context.read().moviesSearchQuery = value, - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + margin: EdgeInsets.zero, ), ), ), diff --git a/lib/modules/radarr/routes/catalogue/widgets/search_bar_filter_button.dart b/lib/modules/radarr/routes/catalogue/widgets/search_bar_filter_button.dart index de48c13b..0a310cfe 100644 --- a/lib/modules/radarr/routes/catalogue/widgets/search_bar_filter_button.dart +++ b/lib/modules/radarr/routes/catalogue/widgets/search_bar_filter_button.dart @@ -42,7 +42,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + margin: EdgeInsets.symmetric(horizontal: 12.0), color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/radarr/routes/catalogue/widgets/search_bar_sort_button.dart b/lib/modules/radarr/routes/catalogue/widgets/search_bar_sort_button.dart index db86a021..ed816a32 100644 --- a/lib/modules/radarr/routes/catalogue/widgets/search_bar_sort_button.dart +++ b/lib/modules/radarr/routes/catalogue/widgets/search_bar_sort_button.dart @@ -57,7 +57,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + margin: EdgeInsets.zero, color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/radarr/routes/edit_movie/widgets/tile_path.dart b/lib/modules/radarr/routes/edit_movie/widgets/tile_path.dart index 2690714a..50bf721b 100644 --- a/lib/modules/radarr/routes/edit_movie/widgets/tile_path.dart +++ b/lib/modules/radarr/routes/edit_movie/widgets/tile_path.dart @@ -8,7 +8,7 @@ class RadarrMoviesEditPathTile extends StatelessWidget { return LunaListTile( context: context, title: LunaText.title(text: 'radarr.MoviePath'.tr()), - subtitle: LunaText.subtitle(text: context.watch().path ?? Constants.TEXT_EMDASH), + subtitle: LunaText.subtitle(text: context.watch().path ?? LunaUI.TEXT_EMDASH), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { Tuple2 _values = await LunaDialogs().editText(context, 'radarr.MoviePath'.tr(), prefill: context.read().path); diff --git a/lib/modules/radarr/routes/edit_movie/widgets/tile_quality_profile.dart b/lib/modules/radarr/routes/edit_movie/widgets/tile_quality_profile.dart index f41defc2..4fafd541 100644 --- a/lib/modules/radarr/routes/edit_movie/widgets/tile_quality_profile.dart +++ b/lib/modules/radarr/routes/edit_movie/widgets/tile_quality_profile.dart @@ -15,7 +15,7 @@ class RadarrMoviesEditQualityProfileTile extends StatelessWidget { return LunaListTile( context: context, title: LunaText.title(text: 'radarr.QualityProfile'.tr()), - subtitle: LunaText.subtitle(text: context.watch().qualityProfile?.name ?? Constants.TEXT_EMDASH), + subtitle: LunaText.subtitle(text: context.watch().qualityProfile?.name ?? LunaUI.TEXT_EMDASH), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { Tuple2 values = await RadarrDialogs().editQualityProfile(context, profiles); diff --git a/lib/modules/radarr/routes/manual_import/route.dart b/lib/modules/radarr/routes/manual_import/route.dart index d422f4bd..7f05a995 100644 --- a/lib/modules/radarr/routes/manual_import/route.dart +++ b/lib/modules/radarr/routes/manual_import/route.dart @@ -35,7 +35,7 @@ class _State extends State<_RadarrManualImportRoute> with LunaScrollControllerMi return LunaAppBar( title: 'radarr.ManualImport'.tr(), scrollControllers: [scrollController], - bottom: RadarrManualImportPathBar(), + bottom: RadarrManualImportPathBar(scrollController: scrollController), ); } @@ -86,6 +86,6 @@ class _State extends State<_RadarrManualImportRoute> with LunaScrollControllerMi List _filterDirectories(String path, RadarrFileSystem fileSystem) { if(path == null || path.isEmpty) return fileSystem.directories ?? []; if(fileSystem?.directories == null || fileSystem.directories.length == 0) return []; - return fileSystem.directories.where((element) => element.path.contains(path)).toList(); + return fileSystem.directories.where((element) => (element.path?.toLowerCase() ?? '').contains(path?.toLowerCase() ?? '')).toList(); } } diff --git a/lib/modules/radarr/routes/manual_import/widgets/appbar_path_bar.dart b/lib/modules/radarr/routes/manual_import/widgets/appbar_path_bar.dart index df098b74..16d36f4b 100644 --- a/lib/modules/radarr/routes/manual_import/widgets/appbar_path_bar.dart +++ b/lib/modules/radarr/routes/manual_import/widgets/appbar_path_bar.dart @@ -3,6 +3,13 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/radarr.dart'; class RadarrManualImportPathBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; + + RadarrManualImportPathBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + @override Size get preferredSize => Size.fromHeight(62.0); @@ -22,6 +29,7 @@ class _State extends State { labelIcon: Icons.sd_storage_rounded, labelText: 'radarr.FileBrowser'.tr(), controller: context.watch().currentPathTextController, + scrollController: widget.scrollController, autofocus: false, onChanged: (value) { context.read().currentPath = value; diff --git a/lib/modules/radarr/routes/manual_import_details/route.dart b/lib/modules/radarr/routes/manual_import_details/route.dart index 77c588d2..17c4bb9b 100644 --- a/lib/modules/radarr/routes/manual_import_details/route.dart +++ b/lib/modules/radarr/routes/manual_import_details/route.dart @@ -25,15 +25,22 @@ class RadarrManualImportDetailsRouter extends RadarrPageRouter { ); @override - void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrMoviesAddDetailsRoute()); + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrManualImportDetailsRoute()); } -class _RadarrMoviesAddDetailsRoute extends StatefulWidget { +class _RadarrManualImportDetailsRoute extends StatefulWidget { @override State createState() => _State(); } -class _State extends State<_RadarrMoviesAddDetailsRoute> with LunaScrollControllerMixin { +class _State extends State<_RadarrManualImportDetailsRoute> with LunaScrollControllerMixin, LunaLoadCallbackMixin { + @override + Future loadCallback() async { + context.read().fetchMovies(); + context.read().fetchQualityDefinitions(); + context.read().fetchLanguages(); + } + @override Widget build(BuildContext context) { _RadarrManualImportDetailsArguments arguments = ModalRoute.of(context).settings.arguments; @@ -62,8 +69,12 @@ class _State extends State<_RadarrMoviesAddDetailsRoute> with LunaScrollControll Widget _body(BuildContext context) { return FutureBuilder( - future: context.select((RadarrManualImportDetailsState state) => state.manualImport), - builder: (context, AsyncSnapshot> snapshot) { + future: Future.wait([ + context.select((RadarrManualImportDetailsState state) => state.manualImport), + context.select((RadarrState state) => state.qualityProfiles), + context.select((RadarrState state) => state.languages), + ]), + builder: (context, AsyncSnapshot> snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( 'Unable to fetch Radarr manual import: ${context.read().path}', @@ -76,7 +87,7 @@ class _State extends State<_RadarrMoviesAddDetailsRoute> with LunaScrollControll } if(snapshot.connectionState == ConnectionState.done && snapshot.hasData) return _list( context, - manualImport: snapshot.data, + manualImport: snapshot.data[0], ); return LunaLoader(); }, @@ -95,7 +106,10 @@ class _State extends State<_RadarrMoviesAddDetailsRoute> with LunaScrollControll return LunaListViewBuilder( controller: scrollController, itemCount: manualImport.length, - itemBuilder: (context, index) => RadarrManualImportDetailsImportTile(manualImport: manualImport[index]), + itemBuilder: (context, index) => RadarrManualImportDetailsTile( + key: ObjectKey(manualImport[index].id), + manualImport: manualImport[index], + ), ); } } diff --git a/lib/modules/radarr/routes/manual_import_details/widgets.dart b/lib/modules/radarr/routes/manual_import_details/widgets.dart index 303774ae..bbd0f1db 100644 --- a/lib/modules/radarr/routes/manual_import_details/widgets.dart +++ b/lib/modules/radarr/routes/manual_import_details/widgets.dart @@ -1,2 +1,3 @@ +export 'widgets/appbar_configure_movies.dart'; export 'widgets/bottom_action_bar.dart'; export 'widgets/import_tile.dart'; diff --git a/lib/modules/radarr/routes/manual_import_details/widgets/appbar_configure_movies.dart b/lib/modules/radarr/routes/manual_import_details/widgets/appbar_configure_movies.dart new file mode 100644 index 00000000..601df828 --- /dev/null +++ b/lib/modules/radarr/routes/manual_import_details/widgets/appbar_configure_movies.dart @@ -0,0 +1,37 @@ + +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrManualImportDetailsConfigureMoviesSearchBar extends StatefulWidget implements PreferredSizeWidget { + @override + Size get preferredSize => Size.fromHeight(LunaTextInputBar.appBarHeight); + + @override + State createState() => _State(); +} + +class _State extends State { + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, state, _) => Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: LunaTextInputBar( + controller: _controller, + autofocus: false, + onChanged: (value) => context.read().configureMoviesSearchQuery = value, + margin: LunaTextInputBar.appBarMargin, + ), + ), + ], + ), + height: LunaTextInputBar.appBarHeight, + ), + ); +} diff --git a/lib/modules/radarr/routes/manual_import_details/widgets/import_tile.dart b/lib/modules/radarr/routes/manual_import_details/widgets/import_tile.dart index 5042ceac..c47276a5 100644 --- a/lib/modules/radarr/routes/manual_import_details/widgets/import_tile.dart +++ b/lib/modules/radarr/routes/manual_import_details/widgets/import_tile.dart @@ -2,30 +2,28 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/radarr.dart'; -class RadarrManualImportDetailsImportTile extends StatelessWidget { +class RadarrManualImportDetailsTile extends StatelessWidget { final RadarrManualImport manualImport; - RadarrManualImportDetailsImportTile({ + RadarrManualImportDetailsTile({ Key key, @required this.manualImport, }) : super(key: key); Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (_) => _State(manualImport), + create: (_) => RadarrManualImportDetailsTileState(context, manualImport), builder: (context, _) => LunaExpandableListTile( key: ObjectKey(manualImport), - title: context.watch<_State>().manualImport.relativePath, - collapsedTrailing: Consumer( - builder: (context, state, _) => Checkbox( - value: state.selectedFiles.contains(manualImport.id), - onChanged: (value) => state.setSelectedFile(manualImport.id, value), - ), - ), + title: context.watch().manualImport.relativePath, + collapsedTrailing: _trailing(context), collapsedSubtitle1: _subtitle1(context), collapsedSubtitle2: _subtitle2(context), expandedTableButtons: _buttons(context), expandedTableContent: _table(context), + backgroundColor: context.watch().selectedFiles.contains(manualImport.id) + ? LunaColours.accent.withOpacity(0.15) + : null, ), ); } @@ -33,18 +31,18 @@ class RadarrManualImportDetailsImportTile extends StatelessWidget { TextSpan _subtitle1(BuildContext context) { return TextSpan( children: [ - TextSpan(text: context.watch<_State>().manualImport.lunaQualityProfile), + TextSpan(text: context.watch().manualImport.lunaQualityProfile), TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), - TextSpan(text: context.watch<_State>().manualImport.lunaLanguage), + TextSpan(text: context.watch().manualImport.lunaLanguage), TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), - TextSpan(text: context.watch<_State>().manualImport.lunaSize), + TextSpan(text: context.watch().manualImport.lunaSize), ], ); } TextSpan _subtitle2(BuildContext context) { return TextSpan( - text: context.watch<_State>().manualImport.lunaMovie, + text: context.watch().manualImport.lunaMovie, style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.accent, @@ -52,19 +50,40 @@ class RadarrManualImportDetailsImportTile extends StatelessWidget { ); } + Widget _trailing(BuildContext context) { + return Consumer( + builder: (context, state, _) => Checkbox( + value: state.selectedFiles.contains(manualImport.id), + onChanged: (value) => state.setSelectedFile(manualImport.id, value), + ), + ); + } + List _table(BuildContext context) { return [ - LunaTableContent(title: 'radarr.Movie'.tr(), body: context.watch<_State>().manualImport.lunaMovie), - LunaTableContent(title: 'radarr.Quality'.tr(), body: context.watch<_State>().manualImport.lunaQualityProfile), - LunaTableContent(title: 'radarr.Language'.tr(), body: context.watch<_State>().manualImport.lunaLanguage), - LunaTableContent(title: 'radarr.Size'.tr(), body: context.watch<_State>().manualImport.lunaSize), + LunaTableContent( + title: 'radarr.Movie'.tr(), + body: context.watch().manualImport.lunaMovie, + ), + LunaTableContent( + title: 'radarr.Quality'.tr(), + body: context.watch().manualImport.lunaQualityProfile, + ), + LunaTableContent( + title: 'radarr.Languages'.tr(), + body: context.watch().manualImport.lunaLanguage, + ), + LunaTableContent( + title: 'radarr.Size'.tr(), + body: context.watch().manualImport.lunaSize, + ), ]; } List _buttons(BuildContext context) { return [ _configureButton(context), - if((context.read<_State>().manualImport.rejections?.length ?? 0) > 0) _rejectionsButton(context), + if((context.read().manualImport.rejections?.length ?? 0) > 0) _rejectionsButton(context), ]; } @@ -72,7 +91,10 @@ class RadarrManualImportDetailsImportTile extends StatelessWidget { return LunaButton.text( text: 'radarr.Configure'.tr(), icon: Icons.edit_rounded, - onTap: () async {}, + onTap: () async { + await RadarrBottomModalSheets().configureManualImport(context); + Future.microtask(() => context.read().checkIfShouldSelect(context)); + } ); } @@ -83,14 +105,23 @@ class RadarrManualImportDetailsImportTile extends StatelessWidget { color: LunaColours.red, onTap: () async => LunaDialogs().showRejections( context, - context.read<_State>().manualImport.rejections?.map((rejection) => rejection.reason)?.toList(), + context.read().manualImport.rejections?.map((rejection) => rejection.reason)?.toList(), ), ); } } -class _State extends ChangeNotifier { - _State(this._manualImport); +class RadarrManualImportDetailsTileState extends ChangeNotifier { + RadarrManualImportDetailsTileState(BuildContext context, this._manualImport) { + checkIfShouldSelect(context); + } + + String _configureMoviesSearchQuery = ''; + String get configureMoviesSearchQuery => _configureMoviesSearchQuery; + set configureMoviesSearchQuery(String configureMoviesSearchQuery) { + _configureMoviesSearchQuery = configureMoviesSearchQuery ?? ''; + notifyListeners(); + } RadarrManualImport _manualImport; RadarrManualImport get manualImport => _manualImport; @@ -99,18 +130,23 @@ class _State extends ChangeNotifier { _manualImport = manualImport; notifyListeners(); } - - void _update(BuildContext context, RadarrManualImportUpdate updates) { - RadarrManualImport _import = _manualImport; - _import.movie = updates.movie; - _import.id = updates.id; - _import.path = updates.path; - _import.rejections = updates.rejections; - manualImport = _import; - _checkIfShouldSelect(context); + + void addLanguage(RadarrLanguage language) { + assert(language != null); + if((_manualImport.languages ?? []).indexWhere((lang) => lang.id == language.id) >= 0) return; + _manualImport.languages.add(language); + notifyListeners(); } - void _checkIfShouldSelect(BuildContext context) { + void removeLanguage(RadarrLanguage language) { + assert(language != null); + int index = (_manualImport.languages ?? []).indexWhere((lang) => lang.id == language.id); + if(index == -1) return; + _manualImport.languages.removeAt(index); + notifyListeners(); + } + + void checkIfShouldSelect(BuildContext context) { if( _manualImport.movie != null && _manualImport.quality != null && @@ -128,8 +164,21 @@ class _State extends ChangeNotifier { ); context.read().api.manualImport.update(data: [data]) .then((value) { - if((value?.length ?? 0) > 0) _update(context, value[0]); + if((value?.length ?? 0) > 0) { + RadarrManualImport _import = _manualImport; + _import.movie = value[0].movie; + _import.id = value[0].id; + _import.path = value[0].path; + _import.rejections = value[0].rejections; + manualImport = _import; + } }); } } + + void updateQuality(RadarrQuality quality) { + assert(quality != null); + _manualImport.quality.quality = quality; + notifyListeners(); + } } diff --git a/lib/modules/radarr/routes/movie_details/widgets/overview_movie_links_section.dart b/lib/modules/radarr/routes/movie_details/widgets/overview_movie_links_section.dart index 31cad81a..2453355a 100644 --- a/lib/modules/radarr/routes/movie_details/widgets/overview_movie_links_section.dart +++ b/lib/modules/radarr/routes/movie_details/widgets/overview_movie_links_section.dart @@ -21,7 +21,7 @@ class RadarrMovieDetailsOverviewLinksSection extends StatelessWidget { child: Image.asset('assets/images/services/imdb.png'), padding: EdgeInsets.all(12.0), ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), onTap: () async => await movie?.imdbId?.lunaOpenIMDB(), ), height: 50.0, @@ -34,7 +34,7 @@ class RadarrMovieDetailsOverviewLinksSection extends StatelessWidget { child: Image.asset('assets/images/services/themoviedb.png'), padding: EdgeInsets.all(12.0), ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), onTap: () async => await movie?.tmdbId?.toString()?.lunaOpenTheMovieDBMovie(), ), height: 50.0, @@ -47,7 +47,7 @@ class RadarrMovieDetailsOverviewLinksSection extends StatelessWidget { child: Image.asset('assets/images/services/trakt.png'), padding: EdgeInsets.all(12.0), ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), onTap: () async => await movie?.tmdbId?.toString()?.lunaOpenTraktMovie(), ), height: 50.0, @@ -60,7 +60,7 @@ class RadarrMovieDetailsOverviewLinksSection extends StatelessWidget { child: Image.asset('assets/images/services/youtube.png'), padding: EdgeInsets.all(12.0), ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), onTap: () async => await movie?.youTubeTrailerId?.lunaOpenYouTube(), ), height: 50.0, diff --git a/lib/modules/radarr/routes/radarr/route.dart b/lib/modules/radarr/routes/radarr/route.dart index 72efd687..cd3f4d09 100644 --- a/lib/modules/radarr/routes/radarr/route.dart +++ b/lib/modules/radarr/routes/radarr/route.dart @@ -27,27 +27,15 @@ class _State extends State<_RadarrHomeRoute> { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ LunaDatabaseValue.ENABLED_PROFILE.key ]), - builder: (context, box, _) => Scaffold( - key: _scaffoldKey, - drawer: _drawer(), - appBar: _appBar(), - bottomNavigationBar: _bottomNavigationBar(), - body: _body(), - ), - ), + return LunaScaffold( + scaffoldKey: _scaffoldKey, + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), ); } - Future _onWillPop() async { - if(_scaffoldKey.currentState.isDrawerOpen) return true; - _scaffoldKey.currentState.openDrawer(); - return false; - } - Widget _drawer() => LunaDrawer(page: LunaModule.RADARR.key); Widget _bottomNavigationBar() { @@ -67,6 +55,7 @@ class _State extends State<_RadarrHomeRoute> { ]; return LunaAppBar.dropdown( title: LunaModule.RADARR.name, + useDrawer: true, profiles: profiles, actions: actions, pageController: _pageController, diff --git a/lib/modules/radarr/routes/releases/widgets/search_bar.dart b/lib/modules/radarr/routes/releases/widgets/search_bar.dart index be24d22f..f698af34 100644 --- a/lib/modules/radarr/routes/releases/widgets/search_bar.dart +++ b/lib/modules/radarr/routes/releases/widgets/search_bar.dart @@ -28,6 +28,7 @@ class _State extends State { child: Consumer( builder: (context, state, _) => LunaTextInputBar( controller: _controller, + scrollController: widget.scrollController, autofocus: false, onChanged: (value) => context.read().searchQuery = value, margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), diff --git a/lib/modules/radarr/routes/releases/widgets/search_bar_filter_button.dart b/lib/modules/radarr/routes/releases/widgets/search_bar_filter_button.dart index f235e281..d489835e 100644 --- a/lib/modules/radarr/routes/releases/widgets/search_bar_filter_button.dart +++ b/lib/modules/radarr/routes/releases/widgets/search_bar_filter_button.dart @@ -34,7 +34,7 @@ class _State extends State { child: Text( RadarrReleasesFilter.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: state.filterType == RadarrReleasesFilter.values[index] ? LunaColours.accent : Colors.white, diff --git a/lib/modules/radarr/routes/releases/widgets/search_bar_sort_button.dart b/lib/modules/radarr/routes/releases/widgets/search_bar_sort_button.dart index 2d807323..60555681 100644 --- a/lib/modules/radarr/routes/releases/widgets/search_bar_sort_button.dart +++ b/lib/modules/radarr/routes/releases/widgets/search_bar_sort_button.dart @@ -42,7 +42,7 @@ class _State extends State { Text( RadarrReleasesSorting.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: state.sortType == RadarrReleasesSorting.values[index] ? LunaColours.accent : Colors.white, @@ -52,7 +52,7 @@ class _State extends State { state.sortAscending ? Icons.arrow_upward : Icons.arrow_downward, - size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, + size: LunaUI.FONT_SIZE_SUBTITLE+2.0, color: LunaColours.accent, ), ], diff --git a/lib/modules/radarr/routes/tags/widgets/appbar_action_add_tag.dart b/lib/modules/radarr/routes/tags/widgets/appbar_action_add_tag.dart index 4832e510..cb3984f3 100644 --- a/lib/modules/radarr/routes/tags/widgets/appbar_action_add_tag.dart +++ b/lib/modules/radarr/routes/tags/widgets/appbar_action_add_tag.dart @@ -12,7 +12,7 @@ class RadarrTagsAppBarActionAddTag extends StatelessWidget { @override Widget build(BuildContext context) { - if(asDialogButton) return LSDialog.button( + if(asDialogButton) return LunaDialog.button( text: 'Add', textColor: Colors.white, onPressed: () async => _onPressed(context), diff --git a/lib/modules/radarr/routes/tags/widgets/tag_tile.dart b/lib/modules/radarr/routes/tags/widgets/tag_tile.dart index 44e9322b..46cbaf45 100644 --- a/lib/modules/radarr/routes/tags/widgets/tag_tile.dart +++ b/lib/modules/radarr/routes/tags/widgets/tag_tile.dart @@ -26,7 +26,7 @@ class _State extends State with LunaLoadCallbackMixin { _movies.sort(); if(mounted) setState(() => movieList = _movies); }) - .catchError((_) { + .catchError((error) { if(mounted) setState(() => movieList = null); }); } @@ -66,7 +66,6 @@ class _State extends State with LunaLoadCallbackMixin { Future _delete() async { if(movieList == null || movieList.length != 0) { showLunaErrorSnackBar( - context: context, title: 'Cannot Delete Tag', message: 'The tag must not be attached to any movies', ); diff --git a/lib/modules/sabnzbd/core/dialogs.dart b/lib/modules/sabnzbd/core/dialogs.dart index 1b780d02..4a15497d 100644 --- a/lib/modules/sabnzbd/core/dialogs.dart +++ b/lib/modules/sabnzbd/core/dialogs.dart @@ -23,19 +23,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'SABnzbd Settings', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -60,19 +60,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: title, content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -92,19 +92,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: title, content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: _options.length == 1 ? LunaColours.red : LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -119,19 +119,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Change Category', content: List.generate( categories.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: categories[index].category, icon: Icons.category, iconColor: LunaColours.list(index), onTap: () => _setValues(true, categories[index].category), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -158,19 +158,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Sort Queue', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2], _options[index][3], _options[index][0]) ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _sort, _dir, _name]; } @@ -188,19 +188,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add NZB', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -217,11 +217,11 @@ class SABnzbdDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add NZB by URL', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Add', onPressed: () => _setValues(true), ), @@ -229,7 +229,7 @@ class SABnzbdDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'NZB URL', keyboardType: TextInputType.url, @@ -240,7 +240,7 @@ class SABnzbdDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -257,11 +257,11 @@ class SABnzbdDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Rename Job', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Rename', onPressed: () => _setValues(true), ), @@ -269,7 +269,7 @@ class SABnzbdDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Job Name', onSubmitted: (_) => _setValues(true), @@ -279,7 +279,7 @@ class SABnzbdDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -296,11 +296,11 @@ class SABnzbdDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Set Password', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), @@ -308,7 +308,7 @@ class SABnzbdDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Job Password', onSubmitted: (_) => _setValues(true), @@ -319,7 +319,7 @@ class SABnzbdDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -342,19 +342,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Speed Limit ($currentSpeed%)', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]) ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _limit]; } @@ -378,19 +378,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Pause Queue For...', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _duration]; } @@ -407,27 +407,27 @@ class SABnzbdDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Custom Pause Duration', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Pause', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.textSpanContent(text: 'Please enter how long in '), - LSDialog.bolded(text: 'minutes'), - LSDialog.textSpanContent(text: ' you want to pause the queue for.'), + LunaDialog.textSpanContent(text: 'Please enter how long in '), + LunaDialog.bolded(text: 'minutes'), + LunaDialog.textSpanContent(text: ' you want to pause the queue for.'), ], alignment: TextAlign.center, ), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Pause Duration in Minutes', keyboardType: TextInputType.number, @@ -442,7 +442,7 @@ class SABnzbdDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, int.tryParse(_textController.text)]; } @@ -467,19 +467,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Change Priority', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2], _options[index][0]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _priority, _name]; } @@ -503,19 +503,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'On Complete Action', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2], _options[index][0]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _action, _name]; } @@ -540,19 +540,19 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Clear History', content: List.generate( _options.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: _options[index][0], icon: _options[index][1], iconColor: LunaColours.list(index), onTap: () => _setValues(true, _options[index][2], _options[index][3], _options[index][0]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _action, _delete, _name]; } @@ -569,20 +569,20 @@ class SABnzbdDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Custom Speed Limit', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Please enter a percentage between 1 and 100.'), + LunaDialog.textContent(text: 'Please enter a percentage between 1 and 100.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Speed Limit', keyboardType: TextInputType.number, @@ -597,7 +597,7 @@ class SABnzbdDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, int.tryParse(_textController.text)]; } @@ -610,20 +610,20 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Job', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete this job?'), + LunaDialog.textContent(text: 'Are you sure you want to delete this job?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -636,20 +636,20 @@ class SABnzbdDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete History', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete the history for this job?'), + LunaDialog.textContent(text: 'Are you sure you want to delete the history for this job?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -664,19 +664,19 @@ class SABnzbdDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Page', content: List.generate( SABnzbdNavigationBar.titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: SABnzbdNavigationBar.titles[index], icon: SABnzbdNavigationBar.icons[index], iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _index]; diff --git a/lib/modules/sabnzbd/routes/history.dart b/lib/modules/sabnzbd/routes/history.dart index 5d24dd1b..026fdb79 100644 --- a/lib/modules/sabnzbd/routes/history.dart +++ b/lib/modules/sabnzbd/routes/history.dart @@ -16,26 +16,18 @@ class SABnzbdHistory extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final _scaffoldKey = GlobalKey(); - final _scrollController = ScrollController(); Future> _future; List _results = []; @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - _refresh(); - } - - Future _refresh() async { - _results = []; + Future loadCallback() async { + if(mounted) setState(() => _results = []); final _api = SABnzbdAPI.from(Database.currentProfileObject); if(mounted) setState(() { _future = _api.getHistory(); }); - Future.microtask(() => Provider.of(context, listen: false)?.historySearchFilter = ''); } @override @@ -43,46 +35,49 @@ class _State extends State with AutomaticKeepAliveClientMixin { super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), + appBar: _appBar(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); + Widget _appBar() { + return LunaAppBar.empty( + child: SABnzbdHistorySearchBar(scrollController: SABnzbdNavigationBar.scrollControllers[1]), + height: 62.0, + ); + } - Widget get _searchBar => LSContainerRow( - padding: EdgeInsets.zero, - backgroundColor: Theme.of(context).primaryColor, - children: [ - SABnzbdHistorySearchBar(), - SABnzbdHistoryHideButton(), - ], - ); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: widget.refreshIndicatorKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: _future, + builder: (context, snapshot) { + switch(snapshot.connectionState) { + case ConnectionState.done: { + if(snapshot.hasError || snapshot.data == null) { + return LunaMessage.error(onTap: () => widget.refreshIndicatorKey.currentState.show()); + } + _results = snapshot.data; + return _list; + } + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + default: return LunaLoader(); + } + }, + ), + ); + } Widget get _list => _results.length == 0 - ? LSGenericMessage( + ? LunaMessage( text: 'No History Found', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => _refresh(), + onTap: () => loadCallback(), ) : Selector>( selector: (_, model) => Tuple2(model.historySearchFilter, model.historyHideFailed), @@ -95,22 +90,17 @@ class _State extends State with AutomaticKeepAliveClientMixin { Widget _listBody(List filtered) { List _children = filtered.length == 0 - ? [LSGenericMessage(text: 'No Results Found')] + ? [LunaMessage.inList(text: 'No History Found')] : List.generate( filtered.length, (index) => SABnzbdHistoryTile( data: filtered[index], - refresh: () => _refresh(), + refresh: () => loadCallback(), ), ); - return LSListViewStickyHeader( - controller: _scrollController, - slivers: [ - LunaSliverStickyHeader( - header: _searchBar, - children: _children, - ), - ], + return LunaListView( + controller: SABnzbdNavigationBar.scrollControllers[1], + children: _children, ); } diff --git a/lib/modules/sabnzbd/routes/history_stages.dart b/lib/modules/sabnzbd/routes/history_stages.dart index c1003683..91034d2f 100644 --- a/lib/modules/sabnzbd/routes/history_stages.dart +++ b/lib/modules/sabnzbd/routes/history_stages.dart @@ -18,7 +18,7 @@ class SABnzbdHistoryStages extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); SABnzbdHistoryStagesArguments _arguments; @@ -37,17 +37,22 @@ class _State extends State { body: _body, ); - Widget get _appBar => LunaAppBar(title: 'Stages'); + Widget get _appBar => LunaAppBar( + title: 'Stages', + scrollControllers: [scrollController], + ); Widget get _body => _arguments == null ? null - : LSListView( + : LunaListView( + controller: scrollController, children: List.generate( _arguments.data.stageLog.length, - (index) => LSCardTile( - title: LSTitle(text: _arguments.data.stageLog[index]['name']), - subtitle: LSSubtitle(text: _arguments.data.stageLog[index]['actions'][0].replaceAll('
', '.\n')), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), + (index) => LunaListTile( + context: context, + title: LunaText.title(text: _arguments.data.stageLog[index]['name']), + subtitle: LunaText.subtitle(text: _arguments.data.stageLog[index]['actions'][0].replaceAll('
', '.\n')), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { String _data = _arguments.data.stageLog[index]['actions'].join(',\n').replaceAll('
', '.\n'); LunaDialogs().textPreview(context, _arguments.data.stageLog[index]['name'], _data); diff --git a/lib/modules/sabnzbd/routes/queue.dart b/lib/modules/sabnzbd/routes/queue.dart index b6c3de27..c6fa7286 100644 --- a/lib/modules/sabnzbd/routes/queue.dart +++ b/lib/modules/sabnzbd/routes/queue.dart @@ -18,7 +18,6 @@ class SABnzbdQueue extends StatefulWidget { class _State extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { final _scaffoldKey = GlobalKey(); - final _scrollController = ScrollController(); Timer _timer; Future _future; List _queue = []; @@ -38,7 +37,9 @@ class _State extends State with TickerProviderStateMixin, Automati return Scaffold( key: _scaffoldKey, body: _body, - floatingActionButton: SABnzbdQueueFAB(scrollController: _scrollController), + floatingActionButton: context.watch().error + ? null + : SABnzbdQueueFAB(scrollController: SABnzbdNavigationBar.scrollControllers[0]), ); } @@ -56,7 +57,7 @@ class _State extends State with TickerProviderStateMixin, Automati Future _fetchWithoutMessage() async { _fetch().then((_) => { if(mounted) setState(() {}) }) - .catchError((_) => _queue = null); + .catchError((error) => _queue = null); } Future _fetch() async { @@ -96,35 +97,36 @@ class _State extends State with TickerProviderStateMixin, Automati _model.error = error; } - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, + Widget get _body => LunaRefreshIndicator( + context: context, + key: widget.refreshIndicatorKey, onRefresh: () => _fetchWithoutMessage(), child: FutureBuilder( future: _future, builder: (context, snapshot) { if( - snapshot.connectionState == ConnectionState.done - && (snapshot.hasError || snapshot.data == null) - ) return LSErrorMessage(onTapHandler: () => _refresh()); - return _list; + snapshot.connectionState == ConnectionState.done && + context.read().error + ) return LunaMessage.error(onTap: () => _refresh()); + if(snapshot.hasData) return _list; + return LunaLoader(); }, ), ); Widget get _list => _queue == null - ? LSErrorMessage(onTapHandler: () => _refresh()) + ? LunaMessage.error(onTap: () => _refresh()) : _queue.length == 0 - ? LSGenericMessage( + ? LunaMessage( text: 'Empty Queue', - showButton: true, buttonText: 'Refresh', - onTapHandler: () => _fetchWithoutMessage(), + onTap: () => _fetchWithoutMessage(), ) - : _reorderableList; + : _reorderableList(); - Widget get _reorderableList => Scrollbar( - child: LSReorderableListView( - scrollController: _scrollController, + Widget _reorderableList() { + return LunaReorderableListViewBuilder( + controller: SABnzbdNavigationBar.scrollControllers[0], onReorder: (oIndex, nIndex) async { if (oIndex > _queue.length) oIndex = _queue.length; if (oIndex < nIndex) nIndex--; @@ -134,37 +136,16 @@ class _State extends State with TickerProviderStateMixin, Automati _queue.insert(nIndex, data); }); await SABnzbdAPI.from(Database.currentProfileObject).moveQueue(data.nzoId, nIndex) - .then((_) => LSSnackBar( - context: context, - title: 'Moved Job in Queue', - message: data.name, - type: SNACKBAR_TYPE.success, - )) - .catchError((_) => LSSnackBar( - context: context, - title: 'Failed to Move Job', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, - )); + .then((_) => showLunaSuccessSnackBar(title: 'Moved Job in Queue', message: data.name)) + .catchError((error) => showLunaErrorSnackBar(title: 'Failed to Move Job', error: error)); }, - children: List.generate( - _queue.length, - (index) => SABnzbdQueueTile( - key: Key(_queue[index].nzoId), - data: _queue[index], - snackbar: _snackBar, - queueContext: context, - refresh: () => _fetchWithoutMessage(), - ), + itemCount: _queue.length, + itemBuilder: (context, index) => SABnzbdQueueTile( + key: Key(_queue[index].nzoId), + data: _queue[index], + queueContext: context, + refresh: () => _fetchWithoutMessage(), ), - padding: EdgeInsets.only(top: 8.0), - ), - ); - - void _snackBar(String title, String message, SNACKBAR_TYPE type) => LSSnackBar( - context: context, - title: title, - message: message, - type: type, - ); + ); + } } diff --git a/lib/modules/sabnzbd/routes/sabnzbd.dart b/lib/modules/sabnzbd/routes/sabnzbd.dart index e917683c..d476181d 100644 --- a/lib/modules/sabnzbd/routes/sabnzbd.dart +++ b/lib/modules/sabnzbd/routes/sabnzbd.dart @@ -12,7 +12,7 @@ class SABnzbd extends StatefulWidget { class _State extends State { final _scaffoldKey = GlobalKey(); - final _pageController = PageController(initialPage: SABnzbdDatabaseValue.NAVIGATION_INDEX.data); + LunaPageController _pageController; String _profileState = Database.currentProfileObject.toString(); SABnzbdAPI _api = SABnzbdAPI.from(Database.currentProfileObject); @@ -24,72 +24,77 @@ class _State extends State { @override void initState() { super.initState(); - Future.microtask(() => Provider.of(context, listen: false).navigationIndex = 0); + _pageController = LunaPageController(initialPage: SABnzbdDatabaseValue.NAVIGATION_INDEX.data); } @override - Widget build(BuildContext context) => WillPopScope( - onWillPop: () async { - if(_scaffoldKey.currentState.isDrawerOpen) { - //If the drawer is open, return true to close it - return true; - } else { - //If the drawer isn't open, open the drawer - _scaffoldKey.currentState.openDrawer(); - return false; - } - }, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]), - builder: (context, box, widget) { + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + body: _body(), + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + extendBodyBehindAppBar: false, + extendBody: false, + onProfileChange: (_) { if(_profileState != Database.currentProfileObject.toString()) _refreshProfile(); - return Scaffold( - key: _scaffoldKey, - body: _body, - drawer: _drawer, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - ); }, - ), - ); + ); + } - Widget get _drawer => LunaDrawer(page: LunaModule.SABNZBD.key); + Widget _drawer() => LunaDrawer(page: LunaModule.SABNZBD.key); - Widget get _bottomNavigationBar => SABnzbdNavigationBar(pageController: _pageController); + Widget _bottomNavigationBar() { + if(_api.enabled) return SABnzbdNavigationBar(pageController: _pageController); + return null; + } - List get _tabs => [ - SABnzbdQueue(refreshIndicatorKey: _refreshKeys[0]), - SABnzbdHistory(refreshIndicatorKey: _refreshKeys[1]), - ]; - - Widget get _body => PageView( - controller: _pageController, - children: _api.enabled ? _tabs : List.generate(_tabs.length, (_) => LSNotEnabled('SABnzbd')), - onPageChanged: _onPageChanged, - ); - - Widget get _appBar => LunaAppBar.dropdown( - title: 'SABnzbd', - profiles: Database.profilesBox.keys.fold([], (value, element) { + Widget _appBar() { + List profiles = Database.profilesBox.keys.fold([], (value, element) { if(Database.profilesBox.get(element)?.sabnzbdEnabled ?? false) value.add(element); return value; - }), - actions: _api.enabled - ? [ - Selector( - selector: (_, model) => model.error, - builder: (context, error, widget) => error - ? Container() - : SABnzbdAppBarStats(), + }); + List actions; + if(_api.enabled) actions = [ + Selector( + selector: (_, model) => model.error, + builder: (context, error, widget) => error + ? Container() + : SABnzbdAppBarStats(), + ), + LunaIconButton( + icon: Icons.more_vert, + onPressed: () async => _handlePopup(), + ), + ]; + return LunaAppBar.dropdown( + title: LunaModule.SABNZBD.name, + useDrawer: true, + profiles: profiles, + actions: actions, + pageController: _pageController, + scrollControllers: SABnzbdNavigationBar.scrollControllers, + ); + } + + Widget _body() { + if(!_api.enabled) return LunaMessage.moduleNotEnabled( + context: context, + module: LunaModule.SABNZBD.name, + ); + return PageView( + controller: _pageController, + children: [ + SABnzbdQueue( + refreshIndicatorKey: _refreshKeys[0], ), - LSIconButton( - icon: Icons.more_vert, - onPressed: () async => _handlePopup(), + SABnzbdHistory( + refreshIndicatorKey: _refreshKeys[1], ), - ] - : null, - ); + ], + ); + } Future _handlePopup() async { List values = await SABnzbdDialogs.globalSettings(context); @@ -109,17 +114,13 @@ class _State extends State { Future _completeAction() async { List values = await SABnzbdDialogs.changeOnCompleteAction(context); if(values[0]) SABnzbdAPI.from(Database.currentProfileObject).setOnCompleteAction(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'On Complete Action Set', message: values[2], - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Set Complete Action', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } @@ -127,19 +128,15 @@ class _State extends State { List values = await SABnzbdDialogs.clearAllHistory(context); if(values[0]) SABnzbdAPI.from(Database.currentProfileObject).clearHistory(values[1], values[2]) .then((_) { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: 'History Cleared', message: values[3], - type: SNACKBAR_TYPE.success, ); _refreshAllPages(); }) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Upload NZB', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } @@ -147,19 +144,15 @@ class _State extends State { List values = await SABnzbdDialogs.sortQueue(context); if(values[0]) await SABnzbdAPI.from(Database.currentProfileObject).sortQueue(values[1], values[2]) .then((_) { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: 'Sorted Queue', message: values[3], - type: SNACKBAR_TYPE.success, ); (_refreshKeys[0] as GlobalKey)?.currentState?.show(); }) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Sort Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } @@ -193,20 +186,17 @@ class _State extends State { .then((value) { _refreshKeys[0]?.currentState?.show(); showLunaSuccessSnackBar( - context: context, title: 'Uploaded NZB (File)', message: _name, ); }) .catchError((error, stack) => showLunaErrorSnackBar( - context: context, title: 'Failed to Upload NZB', message: LunaLogger.checkLogsMessage, error: error, )); } else { showLunaErrorSnackBar( - context: context, title: 'Failed to Upload NZB', message: 'The selected file is not valid', ); @@ -214,7 +204,6 @@ class _State extends State { } catch (error, stack) { LunaLogger().error('Failed to add NZB by file', error, stack); showLunaErrorSnackBar( - context: context, title: 'Failed to Upload NZB', error: error, ); @@ -224,22 +213,16 @@ class _State extends State { Future _addByURL() async { List values = await SABnzbdDialogs.addNZBUrl(context); if(values[0]) await _api.uploadURL(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Uploaded NZB (URL)', message: values[1], - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Upload NZB', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } - void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; - void _refreshProfile() { _api = SABnzbdAPI.from(Database.currentProfileObject); _profileState = Database.currentProfileObject.toString(); diff --git a/lib/modules/sabnzbd/routes/statistics.dart b/lib/modules/sabnzbd/routes/statistics.dart index db2ccb4e..ec3f9029 100644 --- a/lib/modules/sabnzbd/routes/statistics.dart +++ b/lib/modules/sabnzbd/routes/statistics.dart @@ -9,7 +9,7 @@ class SABnzbdStatistics extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaScrollControllerMixin { final _scaffoldKey = GlobalKey(); final _refreshKey = GlobalKey(); Future _future; @@ -36,136 +36,77 @@ class _State extends State { }); } - Widget get _appBar => LunaAppBar(title: 'Server Statistics'); + Widget get _appBar => LunaAppBar( + title: 'Server Statistics', + scrollControllers: [scrollController], + ); - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, + Widget get _body => LunaRefreshIndicator( + context: context, + key: _refreshKey, onRefresh: _refresh, child: FutureBuilder( future: _future, builder: (context, snapshot) { switch(snapshot.connectionState) { case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.hasError || snapshot.data == null) return LunaMessage.error(onTap: () => _refresh()); _data = snapshot.data; return _list; } case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoader(); + default: return LunaLoader(); } }, ), ); - Widget get _list => LSListView( + Widget get _list => LunaListView( + controller: scrollController, children: [ - ..._status, - ..._freeSpace, - ..._statistics, + LunaHeader(text: 'Status'), + _status(), + LunaHeader(text: 'Statistics'), + _statistics(), + ..._serverStatistics(), ], ); - List get _status => [ - LSHeader(text: 'Status'), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Uptime', centerText: true), - subtitle: LSSubtitle(text: _data.uptime, centerText: true), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Version', centerText: true), - subtitle: LSSubtitle(text: _data.version, centerText: true), - reducedMargin: true, - ), - ), + Widget _status() { + return LunaTableCard( + content: [ + LunaTableContent(title: 'Uptime', body: _data.uptime), + LunaTableContent(title: 'Version', body: _data.version), + LunaTableContent(title: 'Temp. Space', body: '${_data.tempFreespace.toString()} GB'), + LunaTableContent(title: 'Final Space', body: '${_data.finalFreespace.toString()} GB'), ], - ), - ]; - - List get _freeSpace => [ - LSHeader(text: 'Free Space'), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Temporary', centerText: true), - subtitle: LSSubtitle(text: '${_data.tempFreespace.toString()} GB', centerText: true), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Final', centerText: true), - subtitle: LSSubtitle(text: '${_data.finalFreespace.toString()} GB', centerText: true), - reducedMargin: true, - ), - ), - ], - ), - ]; - - List get _statistics { - List _blocks = [..._statisticsBlock( - 'Total Statistics', - _data.dailyUsage, - _data.weeklyUsage, - _data.monthlyUsage, - _data.totalUsage, - )]; - for(var server in _data.servers) _blocks.addAll(_statisticsBlock( - server.name, - server.dailyUsage, - server.weeklyUsage, - server.monthlyUsage, - server.totalUsage, - )); - return _blocks; + ); } - List _statisticsBlock(String title, int daily, int weekly, int monthly, int total) => [ - LSHeader(text: title), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Daily', centerText: true), - subtitle: LSSubtitle(text: daily.lunaBytesToString(), centerText: true), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Weekly', centerText: true), - subtitle: LSSubtitle(text: weekly.lunaBytesToString(), centerText: true), - reducedMargin: true, - ), - ), + Widget _statistics() { + return LunaTableCard( + content: [ + LunaTableContent(title: 'Daily', body: _data.dailyUsage.lunaBytesToString()), + LunaTableContent(title: 'Weekly', body: _data.weeklyUsage.lunaBytesToString()), + LunaTableContent(title: 'Monthly', body: _data.monthlyUsage.lunaBytesToString()), + LunaTableContent(title: 'Total', body: _data.totalUsage.lunaBytesToString()), ], - ), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Monthly', centerText: true), - subtitle: LSSubtitle(text: monthly.lunaBytesToString(), centerText: true), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Total', centerText: true), - subtitle: LSSubtitle(text: total.lunaBytesToString(), centerText: true), - reducedMargin: true, - ), - ), - ], - ), - ]; + ); + } + + List _serverStatistics() { + return _data.servers.map((server) => [ + LunaHeader(text: server.name), + LunaTableCard( + content: [ + LunaTableContent(title: 'Daily', body: server.dailyUsage.lunaBytesToString()), + LunaTableContent(title: 'Weekly', body: server.weeklyUsage.lunaBytesToString()), + LunaTableContent(title: 'Monthly', body: server.monthlyUsage.lunaBytesToString()), + LunaTableContent(title: 'Total', body: server.totalUsage.lunaBytesToString()), + ], + ), + ]).expand((element) => element).toList(); + } } diff --git a/lib/modules/sabnzbd/widgets/app_bar_stats.dart b/lib/modules/sabnzbd/widgets/app_bar_stats.dart index c0fb93a2..98f24c49 100644 --- a/lib/modules/sabnzbd/widgets/app_bar_stats.dart +++ b/lib/modules/sabnzbd/widgets/app_bar_stats.dart @@ -21,14 +21,14 @@ class SABnzbdAppBarStats extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white54, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( text: _status(data.item1, data.item2), style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_HEADER, + fontSize: LunaUI.FONT_SIZE_HEADER, color: LunaColours.accent, ), ), @@ -60,32 +60,24 @@ class SABnzbdAppBarStats extends StatelessWidget { case -1: { values = await SABnzbdDialogs.customSpeedLimit(context); if(values[0]) SABnzbdAPI.from(Database.currentProfileObject).setSpeedLimit(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Speed Limit Set', message: 'Set to ${values[1]}%', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Set Speed Limit', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); break; } default: SABnzbdAPI.from(Database.currentProfileObject).setSpeedLimit(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Speed Limit Set', message: 'Set to ${values[1]}%', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Set Speed Limit', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } } diff --git a/lib/modules/sabnzbd/widgets/history_hide_button.dart b/lib/modules/sabnzbd/widgets/history_hide_button.dart index 330067bd..6d8729ef 100644 --- a/lib/modules/sabnzbd/widgets/history_hide_button.dart +++ b/lib/modules/sabnzbd/widgets/history_hide_button.dart @@ -3,6 +3,13 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sabnzbd.dart'; class SABnzbdHistoryHideButton extends StatefulWidget { + final ScrollController controller; + + SABnzbdHistoryHideButton({ + Key key, + @required this.controller, + }): super(key: key); + @override State createState() => _State(); } @@ -12,14 +19,14 @@ class _State extends State { Widget build(BuildContext context) => LunaCard( context: context, child: Consumer( - builder: (context, model, widget) => LSIconButton( + builder: (context, model, widget) => LunaIconButton( icon: model.historyHideFailed ? Icons.visibility_off : Icons.visibility, onPressed: () => model.historyHideFailed = !model.historyHideFailed, ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: EdgeInsets.only(left: 12.0), color: Theme.of(context).canvasColor, ); } diff --git a/lib/modules/sabnzbd/widgets/history_search_bar.dart b/lib/modules/sabnzbd/widgets/history_search_bar.dart index dade90c9..3d8579f9 100644 --- a/lib/modules/sabnzbd/widgets/history_search_bar.dart +++ b/lib/modules/sabnzbd/widgets/history_search_bar.dart @@ -3,27 +3,45 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sabnzbd.dart'; class SABnzbdHistorySearchBar extends StatefulWidget { + final ScrollController scrollController; + + SABnzbdHistorySearchBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + @override State createState() => _State(); } class _State extends State { - final _textController = TextEditingController(); + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = context.read().historySearchFilter; + } @override - Widget build(BuildContext context) => Expanded( - child: Consumer( - builder: (context, model, widget) => LSTextInputBar( - controller: _textController, - labelText: 'Search History...', - onChanged: (text, update) => _onChanged(model, text, update), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), - ), - ), - ); - - void _onChanged(SABnzbdState model, String text, bool update) { - model.historySearchFilter = text; - if(update) _textController.text = ''; + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + scrollController: widget.scrollController, + autofocus: false, + onChanged: (value) => context.read().historySearchFilter = value, + margin: EdgeInsets.zero, + ), + ), + ), + SABnzbdHistoryHideButton(controller: widget.scrollController), + ], + ); } } diff --git a/lib/modules/sabnzbd/widgets/history_tile.dart b/lib/modules/sabnzbd/widgets/history_tile.dart index e14566e0..c60e524a 100644 --- a/lib/modules/sabnzbd/widgets/history_tile.dart +++ b/lib/modules/sabnzbd/widgets/history_tile.dart @@ -112,11 +112,9 @@ class _State extends State { if(values[0]) { SABnzbdAPI.from(Database.currentProfileObject).deleteHistory(widget.data.nzoId) .then((_) => _handleRefresh('History Deleted')) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Delete History', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } } @@ -125,31 +123,25 @@ class _State extends State { List values = await SABnzbdDialogs.setPassword(context); if(values[0]) SABnzbdAPI.from(Database.currentProfileObject).retryFailedJobPassword(widget.data.nzoId, values[1]) .then((_) => _handleRefresh('Password Set / Retrying...')) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Set Password / Retry Job', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } Future _retry() async { SABnzbdAPI.from(Database.currentProfileObject).retryFailedJob(widget.data.nzoId) .then((_) => _handleRefresh('Retrying Job')) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Retry Job', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } void _handleRefresh(String title) { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: title, message: widget.data.name, - type: SNACKBAR_TYPE.success, ); widget.refresh(); } diff --git a/lib/modules/sabnzbd/widgets/navigation_bar.dart b/lib/modules/sabnzbd/widgets/navigation_bar.dart index 04f35c1e..0f0984b9 100644 --- a/lib/modules/sabnzbd/widgets/navigation_bar.dart +++ b/lib/modules/sabnzbd/widgets/navigation_bar.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/sabnzbd.dart'; -class SABnzbdNavigationBar extends StatefulWidget { +class SABnzbdNavigationBar extends StatelessWidget { + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + final PageController pageController; + static const List icons = [ LunaIcons.queue, LunaIcons.history, @@ -14,38 +15,18 @@ class SABnzbdNavigationBar extends StatefulWidget { 'History', ]; - final PageController pageController; - SABnzbdNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - Provider.of(context, listen: false).navigationIndex = SABnzbdDatabaseValue.NAVIGATION_INDEX.data; - }); - } - - @override - Widget build(BuildContext context) => Selector( - selector: (_, model) => model.navigationIndex, - builder: (context, index, _) => LSBottomNavigationBar( - index: index, - icons: SABnzbdNavigationBar.icons, - titles: SABnzbdNavigationBar.titles, - onTap: _navOnTap, - ), - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index) - .then((_) => Provider.of(context, listen: false).navigationIndex = index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/sabnzbd/widgets/queue_fab.dart b/lib/modules/sabnzbd/widgets/queue_fab.dart index 713f7f7b..8f939dcd 100644 --- a/lib/modules/sabnzbd/widgets/queue_fab.dart +++ b/lib/modules/sabnzbd/widgets/queue_fab.dart @@ -33,6 +33,7 @@ class _State extends State with TickerProviderStateMixin { void dispose() { _iconController?.dispose(); _hideController?.dispose(); + widget.scrollController?.removeListener(scrollControllerListener); super.dispose(); } @@ -49,19 +50,22 @@ class _State extends State with TickerProviderStateMixin { duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), ); _hideController.forward(); - widget.scrollController.addListener(() { - switch(widget.scrollController.position.userScrollDirection) { - case ScrollDirection.forward: if(!_visible) { - _hideController.forward(); - _visible = true; - } break; - case ScrollDirection.reverse: if(_visible) { - _hideController.reverse(); - _visible = false; - } break; - case ScrollDirection.idle: break; - } - }); + widget.scrollController.addListener(scrollControllerListener); + } + + void scrollControllerListener() { + if(!widget.scrollController.hasClients) return; + switch(widget.scrollController.position.userScrollDirection) { + case ScrollDirection.forward: if(!_visible) { + _hideController.forward(); + _visible = true; + } break; + case ScrollDirection.reverse: if(_visible) { + _hideController.reverse(); + _visible = false; + } break; + case ScrollDirection.idle: break; + } } @override @@ -76,7 +80,7 @@ class _State extends State with TickerProviderStateMixin { : ScaleTransition( scale: _hideController, child: InkWell( - child: LSFloatingActionButtonAnimated( + child: LunaFloatingActionButtonAnimated( onPressed: () => _toggle(context, data.item2), icon: AnimatedIcons.pause_play, controller: _iconController, @@ -103,31 +107,23 @@ class _State extends State with TickerProviderStateMixin { if(values[1] == -1) { List values = await SABnzbdDialogs.customPauseFor(context); if(values[0]) await SABnzbdAPI.from(Database.currentProfileObject).pauseQueueFor(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Pausing Queue', message: 'For ${(values[1] as int).lunaDuration(multiplier: 60)}', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Pause Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } else { await SABnzbdAPI.from(Database.currentProfileObject).pauseQueueFor(values[1]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Pausing Queue', message: 'For ${(values[1] as int).lunaDuration(multiplier: 60)}', - type: SNACKBAR_TYPE.success, )) - .catchError((_) => LSSnackBar( - context: context, + .catchError((error) => showLunaErrorSnackBar( title: 'Failed to Pause Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, )); } } @@ -139,12 +135,10 @@ class _State extends State with TickerProviderStateMixin { .then((_) { Provider.of(context, listen: false).paused = true; }) - .catchError((_) { - LSSnackBar( - context: context, + .catchError((error) { + showLunaErrorSnackBar( title: 'Failed to Pause Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, ); _iconController.reverse(); }); @@ -156,12 +150,10 @@ class _State extends State with TickerProviderStateMixin { .then((_) { Provider.of(context, listen: false).paused = false; }) - .catchError((_) { - LSSnackBar( - context: context, + .catchError((error) { + showLunaErrorSnackBar( title: 'Failed to Resume Queue', - message: LunaLogger.checkLogsMessage, - type: SNACKBAR_TYPE.failure, + error: error, ); _iconController.forward(); }); diff --git a/lib/modules/sabnzbd/widgets/queue_tile.dart b/lib/modules/sabnzbd/widgets/queue_tile.dart index abb293af..05cc3396 100644 --- a/lib/modules/sabnzbd/widgets/queue_tile.dart +++ b/lib/modules/sabnzbd/widgets/queue_tile.dart @@ -6,13 +6,11 @@ import 'package:lunasea/modules/sabnzbd.dart'; class SABnzbdQueueTile extends StatefulWidget { final SABnzbdQueueData data; - final Function(String, String, SNACKBAR_TYPE) snackbar; final Function refresh; final BuildContext queueContext; SABnzbdQueueTile({ @required this.data, - @required this.snackbar, @required this.queueContext, @required this.refresh, Key key, @@ -24,8 +22,9 @@ class SABnzbdQueueTile extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle( + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title( text: widget.data.name, darken: widget.data.isPaused, ), @@ -43,13 +42,13 @@ class _State extends State { ), padding: EdgeInsets.symmetric(vertical: 6.0), ), - LSSubtitle( + LunaText.subtitle( text: widget.data.subtitle, darken: widget.data.isPaused, ), ], ), - trailing: LSIconButton( + trailing: LunaIconButton( icon: Icons.more_vert, onPressed: () async => _handlePopup(), color: widget.data.isPaused ? Colors.white30 : Colors.white, @@ -57,7 +56,7 @@ class _State extends State { ); Future _handlePopup() async { - _Helper _helper = _Helper(widget.queueContext, widget.data, widget.snackbar, widget.refresh); + _Helper _helper = _Helper(widget.queueContext, widget.data, widget.refresh); List values = await SABnzbdDialogs.queueSettings(widget.queueContext, widget.data.name, widget.data.isPaused); if(values[0]) switch(values[1]) { case 'status': widget.data.isPaused @@ -77,47 +76,41 @@ class _State extends State { class _Helper { final BuildContext context; final SABnzbdQueueData data; - final Function snackbar; final Function refresh; _Helper( this.context, this.data, - this.snackbar, this.refresh, ); Future _pauseJob() async { await SABnzbdAPI.from(Database.currentProfileObject).pauseSingleJob(data.nzoId) .then((_) { - snackbar( - 'Job Paused', - data.name, - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: 'Job Paused', + message: data.name, ); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Pause Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, + .catchError((error) => showLunaErrorSnackBar( + title: 'Failed to Pause Job', + error: error, )); } Future _resumeJob() async { await SABnzbdAPI.from(Database.currentProfileObject).resumeSingleJob(data.nzoId) .then((_) { - snackbar( - 'Job Resumed', - data.name, - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: 'Job Resumed', + message: data.name, ); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Resume Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, + .catchError((error) => showLunaErrorSnackBar( + title: 'Failed to Resume Job', + error: error, )); } @@ -126,17 +119,15 @@ class _Helper { List values = await SABnzbdDialogs.changeCategory(context, categories); if(values[0]) await SABnzbdAPI.from(Database.currentProfileObject).setCategory(data.nzoId, values[1]) .then((_) { - snackbar( - values[1] == '' ? 'Category Set (No Category)' : 'Category Set (${values[1]})', - data.name, - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: values[1] == '' ? 'Category Set (No Category)' : 'Category Set (${values[1]})', + message: data.name, ); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Set Category', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, + .catchError((error) => showLunaErrorSnackBar( + title: 'Failed to Set Category', + error: error, )); } @@ -144,17 +135,15 @@ class _Helper { List values = await SABnzbdDialogs.changePriority(context); if(values[0]) await SABnzbdAPI.from(Database.currentProfileObject).setJobPriority(data.nzoId, values[1]) .then((_) { - snackbar( - 'Priority Set (${(values[2])})', - data.name, - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: 'Priority Set (${(values[2])})', + message: data.name, ); refresh(); }) - .catchError((error) => snackbar( - 'Failed to Set Priority', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, + .catchError((error) => showLunaErrorSnackBar( + title: 'Failed to Set Priority', + error: error, )); } @@ -162,17 +151,15 @@ class _Helper { List values = await SABnzbdDialogs.renameJob(context, data.name); if(values[0]) SABnzbdAPI.from(Database.currentProfileObject).renameJob(data.nzoId, values[1]) .then((_) { - snackbar( - 'Job Renamed', - values[1], - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: 'Job Renamed', + message: values[1], ); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Rename Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, + .catchError((error) => showLunaErrorSnackBar( + title: 'Failed to Rename Job', + error: error, )); } @@ -180,17 +167,15 @@ class _Helper { List values = await SABnzbdDialogs.deleteJob(context); if(values[0]) await SABnzbdAPI.from(Database.currentProfileObject).deleteJob(data.nzoId) .then((_) { - snackbar( - 'Job Deleted', - data.name, - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: 'Job Deleted', + message: data.name, ); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Delete Job', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, + .catchError((error) => showLunaErrorSnackBar( + title: 'Failed to Delete Job', + error: error, )); } @@ -198,17 +183,15 @@ class _Helper { List values = await SABnzbdDialogs.setPassword(context); if(values[0]) await SABnzbdAPI.from(Database.currentProfileObject).setJobPassword(data.nzoId, data.name, values[1]) .then((_) { - snackbar( - 'Job Password Set', - data.name, - SNACKBAR_TYPE.success, + showLunaSuccessSnackBar( + title: 'Job Password Set', + message: data.name, ); refresh(); }) - .catchError((_) => snackbar( - 'Failed to Set Job Password', - LunaLogger.checkLogsMessage, - SNACKBAR_TYPE.failure, + .catchError((error) => showLunaErrorSnackBar( + title: 'Failed to Set Job Password', + error: error, )); } } \ No newline at end of file diff --git a/lib/modules/search/core/dialogs.dart b/lib/modules/search/core/dialogs.dart index 9a1f5ae3..e12285f8 100644 --- a/lib/modules/search/core/dialogs.dart +++ b/lib/modules/search/core/dialogs.dart @@ -15,11 +15,11 @@ class SearchDialogs { Navigator.of(context).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'search.Download'.tr(), customContent: LunaDatabaseValue.ENABLED_PROFILE.listen( - builder: (context, lunaBox, widget) => LSDialog.content( + builder: (context, lunaBox, widget) => LunaDialog.content( children: [ Padding( child: LunaPopupMenuButton( @@ -32,7 +32,7 @@ class SearchDialogs { child: Text( LunaDatabaseValue.ENABLED_PROFILE.data, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), ), @@ -62,27 +62,27 @@ class SearchDialogs { child: Text( profile, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ), )]; }, ), - padding: LSDialog.tileContentPadding().add(EdgeInsets.only(bottom: 16.0)), + padding: LunaDialog.tileContentPadding().add(EdgeInsets.only(bottom: 16.0)), ), - if(Database.currentProfileObject.sabnzbdEnabled) LSDialog.tile( + if(Database.currentProfileObject.sabnzbdEnabled) LunaDialog.tile( icon: SearchDownloadType.SABNZBD.icon, iconColor: LunaColours.list(0), text: SearchDownloadType.SABNZBD.name, onTap: () => _setValues(true, SearchDownloadType.SABNZBD), ), - if(Database.currentProfileObject.nzbgetEnabled) LSDialog.tile( + if(Database.currentProfileObject.nzbgetEnabled) LunaDialog.tile( icon: SearchDownloadType.NZBGET.icon, iconColor: LunaColours.list(1), text: SearchDownloadType.NZBGET.name, onTap: () => _setValues(true, SearchDownloadType.NZBGET), ), - LSDialog.tile( + LunaDialog.tile( icon: SearchDownloadType.FILESYSTEM.icon, iconColor: LunaColours.list(2), text: SearchDownloadType.FILESYSTEM.name, @@ -91,7 +91,7 @@ class SearchDialogs { ], ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _type); } diff --git a/lib/modules/search/routes/indexers/route.dart b/lib/modules/search/routes/indexers/route.dart index 5d732ad2..75c7907f 100644 --- a/lib/modules/search/routes/indexers/route.dart +++ b/lib/modules/search/routes/indexers/route.dart @@ -17,25 +17,13 @@ class _SearchHomeRoute extends StatefulWidget { class _State extends State<_SearchHomeRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); - Future _onWillPop() async { - if(_scaffoldKey.currentState.isDrawerOpen) return true; - _scaffoldKey.currentState.openDrawer(); - return false; - } - @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - child: ValueListenableBuilder( - valueListenable: Database.indexersBox.listenable(), - builder: (context, indexerBox, widget) => Scaffold( - key: _scaffoldKey, - appBar: _appBar(), - drawer: _drawer(), - body: _body(), - ), - ), + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar(), + drawer: _drawer(), + body: _body(), ); } @@ -65,7 +53,7 @@ class _State extends State<_SearchHomeRoute> with LunaScrollControllerMixin { Database.indexersBox.length, (index) => SearchIndexerTile(indexer: Database.indexersBox.getAt(index)), ); - list.sort((a,b) => a.indexer.displayName.compareTo(b.indexer.displayName)); + list.sort((a,b) => a.indexer.displayName.toLowerCase().compareTo(b.indexer.displayName.toLowerCase())); return list; } } diff --git a/lib/modules/search/routes/search/route.dart b/lib/modules/search/routes/search/route.dart index 9094a1d3..3ebdc94a 100644 --- a/lib/modules/search/routes/search/route.dart +++ b/lib/modules/search/routes/search/route.dart @@ -71,6 +71,7 @@ class _State extends State<_SearchSearchRoute> with LunaScrollControllerMixin { scrollControllers: [scrollController], bottom: SearchSearchBar( submitCallback: _searchCallback, + scrollController: scrollController, ), ); } diff --git a/lib/modules/search/routes/search/widgets/search_bar.dart b/lib/modules/search/routes/search/widgets/search_bar.dart index f6295677..2739cb1f 100644 --- a/lib/modules/search/routes/search/widgets/search_bar.dart +++ b/lib/modules/search/routes/search/widgets/search_bar.dart @@ -3,11 +3,13 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/search.dart'; class SearchSearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; final Function(String) submitCallback; SearchSearchBar({ Key key, @required this.submitCallback, + @required this.scrollController, }) : super(key: key); @override @@ -28,6 +30,7 @@ class _State extends State { child: Consumer( builder: (context, state, _) => LunaTextInputBar( controller: _controller, + scrollController: widget.scrollController, autofocus: true, onChanged: (value) => context.read().searchQuery = value, onSubmitted: widget.submitCallback, diff --git a/lib/modules/settings/core/dialogs.dart b/lib/modules/settings/core/dialogs.dart index bced342d..4ca04859 100644 --- a/lib/modules/settings/core/dialogs.dart +++ b/lib/modules/settings/core/dialogs.dart @@ -15,22 +15,22 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Sign Out', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Sign Out', - textColor: Colors.red, + textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent( + LunaDialog.textContent( text: 'Are you sure you want to sign out of your LunaSea account?' ), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return _flag; } @@ -45,21 +45,21 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Backup List', content: backups.length > 0 ? List.generate( backups.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: Icons.file_copy, iconColor: LunaColours.list(index), text: backups[index].title.toString(), onTap: () => _setValues(true, backups[index]), ), ) - : [LSDialog.textContent(text: 'No Backups Found')], - contentPadding: backups.length > 0 ? LSDialog.listDialogContentPadding() : LSDialog.textDialogContentPadding(), + : [LunaDialog.textContent(text: 'No Backups Found')], + contentPadding: backups.length > 0 ? LunaDialog.listDialogContentPadding() : LunaDialog.textDialogContentPadding(), ); return [_flag, _document]; } @@ -76,35 +76,35 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: title, buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Save', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.textSpanContent(text: '${Constants.TEXT_BULLET}\tThis is the URL in which you access the web GUI for the service\n'), - LSDialog.textSpanContent(text: '${Constants.TEXT_BULLET}\tYou must include either '), - LSDialog.bolded(text: 'http://'), - LSDialog.textSpanContent(text: ' or '), - LSDialog.bolded(text: 'https://\n'), - LSDialog.textSpanContent(text: '${Constants.TEXT_BULLET}\tDo not use '), - LSDialog.bolded(text: 'localhost'), - LSDialog.textSpanContent(text: ' or '), - LSDialog.bolded(text: '127.0.0.1\n'), - LSDialog.textSpanContent(text: '${Constants.TEXT_BULLET}\tWhen not using a reverse proxy, please include the port: '), - LSDialog.bolded(text: 'url:port\n'), - LSDialog.textSpanContent(text: '${Constants.TEXT_BULLET}\tTo add basic authentication, please set a custom header in the advanced section'), + LunaDialog.textSpanContent(text: '${LunaUI.TEXT_BULLET}\tThis is the URL in which you access the web GUI for the service\n'), + LunaDialog.textSpanContent(text: '${LunaUI.TEXT_BULLET}\tYou must include either '), + LunaDialog.bolded(text: 'http://'), + LunaDialog.textSpanContent(text: ' or '), + LunaDialog.bolded(text: 'https://\n'), + LunaDialog.textSpanContent(text: '${LunaUI.TEXT_BULLET}\tDo not use '), + LunaDialog.bolded(text: 'localhost'), + LunaDialog.textSpanContent(text: ' or '), + LunaDialog.bolded(text: '127.0.0.1\n'), + LunaDialog.textSpanContent(text: '${LunaUI.TEXT_BULLET}\tWhen not using a reverse proxy, please include the port: '), + LunaDialog.bolded(text: 'url:port\n'), + LunaDialog.textSpanContent(text: '${LunaUI.TEXT_BULLET}\tTo add basic authentication, please set a custom header in the advanced section'), ], ), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: title, keyboardType: TextInputType.url, @@ -120,7 +120,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -133,22 +133,22 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Indexer', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', - textColor: Colors.red, + textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent( + LunaDialog.textContent( text: 'Are you sure you want to delete this indexer?' ), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -161,22 +161,22 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Header', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', - textColor: Colors.red, + textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent( + LunaDialog.textContent( text: 'Are you sure you want to delete this header?' ), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -191,19 +191,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add Header', content: List.generate( HeaderType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: HeaderType.values[index].name, icon: HeaderType.values[index].icon, iconColor: LunaColours.list(index), onTap: () => _setValues(true, HeaderType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _type); } @@ -221,11 +221,11 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Custom Header', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Add', onPressed: () => _setValues(true), ), @@ -235,13 +235,13 @@ class SettingsDialogs { key: formKey, child: Column( children: [ - LSDialog.textFormInput( + LunaDialog.textFormInput( controller: _key, validator: (key) => key.length > 0 ? null : 'Key Required', onSubmitted: (_) => _setValues(true), title: 'Header Key', ), - LSDialog.textFormInput( + LunaDialog.textFormInput( controller: _value, validator: (value) => value.length > 0 ? null : 'Value Required', onSubmitted: (_) => _setValues(true), @@ -251,7 +251,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return [_flag, _key.text, _value.text]; } @@ -269,38 +269,38 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Basic Authentication', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Add', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.textSpanContent(text: '•\tThe username '), - LSDialog.bolded(text: 'cannot'), - LSDialog.textSpanContent(text: ' contain a colon\n'), - LSDialog.textSpanContent(text: '•\tThe password '), - LSDialog.bolded(text: 'can'), - LSDialog.textSpanContent(text: ' contain a colon\n'), - LSDialog.textSpanContent(text: '•\tThe username and password are automatically converted to base64 encoding\n'), + LunaDialog.textSpanContent(text: '•\tThe username '), + LunaDialog.bolded(text: 'cannot'), + LunaDialog.textSpanContent(text: ' contain a colon\n'), + LunaDialog.textSpanContent(text: '•\tThe password '), + LunaDialog.bolded(text: 'can'), + LunaDialog.textSpanContent(text: ' contain a colon\n'), + LunaDialog.textSpanContent(text: '•\tThe username and password are automatically converted to base64 encoding\n'), ] ), Form( key: _formKey, child: Column( children: [ - LSDialog.textFormInput( + LunaDialog.textFormInput( controller: _username, validator: (username) => username.length > 0 ? null : 'Username Required', onSubmitted: (_) => _setValues(true), title: 'Username', ), - LSDialog.textFormInput( + LunaDialog.textFormInput( controller: _password, validator: (password) => password.length > 0 ? null : 'Password Required', onSubmitted: (_) => _setValues(true), @@ -311,7 +311,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, _username.text, _password.text]; } @@ -324,21 +324,21 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Clear Logs', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Clear', onPressed: () => _setValues(true), textColor: LunaColours.red, ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to clear all recorded logs?\n'), - LSDialog.textContent(text: 'Logs can be useful for bug reports and debugging.'), + LunaDialog.textContent(text: 'Are you sure you want to clear all recorded logs?\n'), + LunaDialog.textContent(text: 'Logs can be useful for bug reports and debugging.'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return _flag; } @@ -355,11 +355,11 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add Profile', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Add', onPressed: () => _setValues(true), ), @@ -367,7 +367,7 @@ class SettingsDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _controller, validator: (value) { if(profiles.contains(value)) return 'Profile Already Exists'; @@ -379,7 +379,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return Tuple2(_flag, _controller.text); } @@ -394,19 +394,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Rename Profile', content: List.generate( profiles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: Icons.settings, iconColor: LunaColours.list(index), text: profiles[index], onTap: () => _setValues(true, profiles[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _profile); } @@ -423,16 +423,16 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Rename Profile', buttons: [ - LSDialog.button(text: 'Rename', onPressed: () => _setValues(true), textColor: LunaColours.accent), + LunaDialog.button(text: 'Rename', onPressed: () => _setValues(true), textColor: LunaColours.accent), ], content: [ Form( key: _formKey, - child:LSDialog.textFormInput( + child:LunaDialog.textFormInput( controller: _controller, validator: (value) { if(profiles.contains(value)) return 'Profile Already Exists'; @@ -444,7 +444,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); return Tuple2(_flag, _controller.text); } @@ -459,19 +459,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Profile', content: List.generate( profiles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: Icons.settings, iconColor: LunaColours.list(index), text: profiles[index], onTap: () => _setValues(true, profiles[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _profile); } @@ -486,19 +486,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Enabled Profile', content: List.generate( profiles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: Icons.settings, iconColor: LunaColours.list(index), text: profiles[index], onTap: () => _setValues(true, profiles[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _profile]; } @@ -513,19 +513,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Open Links In...', content: List.generate( LunaBrowser.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: LunaBrowser.values[index].icon, iconColor: LunaColours.list(index), text: LunaBrowser.values[index].name, onTap: () => _setValues(true, LunaBrowser.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _browser); } @@ -541,19 +541,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Language', content: List.generate( languages.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: Icons.language_rounded, iconColor: LunaColours.list(index), text: languages[index].name, onTap: () => _setValues(true, languages[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _language); } @@ -568,19 +568,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Starting Day', content: List.generate( CalendarStartingDay.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: LunaIcons.calendar, iconColor: LunaColours.list(index), text: CalendarStartingDay.values[index].name, onTap: () => _setValues(true, CalendarStartingDay.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _startingDate]; } @@ -595,19 +595,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Starting Size', content: List.generate( CalendarStartingSize.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: CalendarStartingSize.values[index].icon, iconColor: LunaColours.list(index), text: CalendarStartingSize.values[index].name, onTap: () => _setValues(true, CalendarStartingSize.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _startingSize]; } @@ -622,19 +622,19 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Starting Type', content: List.generate( CalendarStartingType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( icon: CalendarStartingType.values[index].icon, iconColor: LunaColours.list(index), text: CalendarStartingType.values[index].name, onTap: () => _setValues(true, CalendarStartingType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _startingType]; } @@ -651,27 +651,27 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Broadcast Address', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Save', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.textSpanContent(text: '•\tThis is the broadcast address of your local network\n'), - LSDialog.textSpanContent(text: '•\tTypically this is the IP address of your machine with the last octet set to 255\n'), - LSDialog.textSpanContent(text: '•\tGiven an example machine IP address of 192.168.1.111, the resulting broadcast IP address is '), - LSDialog.bolded(text: '192.168.1.255'), + LunaDialog.textSpanContent(text: '•\tThis is the broadcast address of your local network\n'), + LunaDialog.textSpanContent(text: '•\tTypically this is the IP address of your machine with the last octet set to 255\n'), + LunaDialog.textSpanContent(text: '•\tGiven an example machine IP address of 192.168.1.111, the resulting broadcast IP address is '), + LunaDialog.bolded(text: '192.168.1.255'), ], ), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _controller, validator: (address) { if(address.isEmpty) return null; @@ -682,7 +682,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, _controller.text]; } @@ -699,29 +699,29 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'MAC Address', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Save', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.textSpanContent(text: '•\tThis is the MAC address of the machine that you want to wake up\n'), - LSDialog.textSpanContent(text: '•\tMAC addresses contain six two-digit hexidecimal nibbles (an octet)\n'), - LSDialog.textSpanContent(text: '•\tHexidecimal digits range from 0-9 and A-F\n'), - LSDialog.textSpanContent(text: '•\tEach hexidecimal octet should be separated by a colon\n'), - LSDialog.textSpanContent(text: '•\tExample: '), - LSDialog.bolded(text: 'A4:83:E7:0D:7F:4F'), + LunaDialog.textSpanContent(text: '•\tThis is the MAC address of the machine that you want to wake up\n'), + LunaDialog.textSpanContent(text: '•\tMAC addresses contain six two-digit hexidecimal nibbles (an octet)\n'), + LunaDialog.textSpanContent(text: '•\tHexidecimal digits range from 0-9 and A-F\n'), + LunaDialog.textSpanContent(text: '•\tEach hexidecimal octet should be separated by a colon\n'), + LunaDialog.textSpanContent(text: '•\tExample: '), + LunaDialog.bolded(text: 'A4:83:E7:0D:7F:4F'), ], ), Form( key: formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _controller, validator: (address) { if(address.isEmpty) return null; @@ -732,7 +732,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, _controller.text]; } @@ -745,22 +745,22 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Reset Configuration', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Reset', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to clear your configuration?\n'), - LSDialog.textContent(text: 'You will be starting from a clean slate, please ensure you backup your current configuration first!\n'), - LSDialog.textContent(text: 'If you are signed into a LunaSea account, you will be signed out.'), + LunaDialog.textContent(text: 'Are you sure you want to clear your configuration?\n'), + LunaDialog.textContent(text: 'You will be starting from a clean slate, please ensure you backup your current configuration first!\n'), + LunaDialog.textContent(text: 'If you are signed into a LunaSea account, you will be signed out.'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return _flag; } @@ -777,20 +777,20 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Decrypt Backup', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Restore', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Please enter the encryption key for this backup.'), + LunaDialog.textContent(text: 'Please enter the encryption key for this backup.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Encryption Key', obscureText: true, @@ -801,7 +801,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -818,26 +818,26 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Backup Configuration', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Backup', textColor: LunaColours.accent, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.textSpanContent(text: '•\tAll backups are encrypted before being exported\n'), - LSDialog.textSpanContent(text: '•\tThe encryption key must be at least 8 characters'), + LunaDialog.textSpanContent(text: '•\tAll backups are encrypted before being exported\n'), + LunaDialog.textSpanContent(text: '•\tThe encryption key must be at least 8 characters'), ], ), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( obscureText: true, controller: _textController, title: 'Encryption Key', @@ -851,7 +851,7 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, _textController.text]; } @@ -864,42 +864,42 @@ class SettingsDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Firebase Crashlytics', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Website', onPressed: () => Constants.URL_CRASHLYTICS.lunaOpenGenericLink(), textColor: LunaColours.accent, ), - LSDialog.button( + LunaDialog.button( text: 'Disable', onPressed: () => _setValues(true), textColor: LunaColours.red, ), ], content: [ - LSDialog.richText( + LunaDialog.richText( children: [ - LSDialog.bolded( + LunaDialog.bolded( text: 'Errors and stacktraces contain absolutely no identifying information on any users.\n\n', color: LunaColours.red, - fontSize: LSDialog.SUBBODY_SIZE, + fontSize: LunaDialog.SUBBODY_SIZE, ), - LSDialog.textSpanContent(text: 'Firebase Crashlytics is a tool used for capturing crashes and errors.\n\n'), - LSDialog.textSpanContent(text: 'To reserve your right to privacy, you have the option to disable error and crash tracking, but please know that these errors and stacktraces are incredibly useful for catching and pinpointing bugs!\n\n'), - LSDialog.textSpanContent(text: 'A link to the Firebase website is available below for more information to help make an informed decision.\n\n'), - LSDialog.bolded( + LunaDialog.textSpanContent(text: 'Firebase Crashlytics is a tool used for capturing crashes and errors.\n\n'), + LunaDialog.textSpanContent(text: 'To reserve your right to privacy, you have the option to disable error and crash tracking, but please know that these errors and stacktraces are incredibly useful for catching and pinpointing bugs!\n\n'), + LunaDialog.textSpanContent(text: 'A link to the Firebase website is available below for more information to help make an informed decision.\n\n'), + LunaDialog.bolded( text: 'A restart of LunaSea is required for the changes to take effect.', color: LunaColours.accent, - fontSize: LSDialog.SUBBODY_SIZE, + fontSize: LunaDialog.SUBBODY_SIZE, ), ], alignment: TextAlign.center, ), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return _flag; } @@ -926,20 +926,20 @@ class SettingsDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Image Background Opacity', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Save', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Set the opacity of background images.\n\nTo completely disable fetching background images, set the value to 0.'), + LunaDialog.textContent(text: 'Set the opacity of background images.\n\nTo completely disable fetching background images, set the value to 0.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Background Image Opacity', keyboardType: TextInputType.number, @@ -953,83 +953,79 @@ class SettingsDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return Tuple2(_flag, _opacity); } Future moduleInformation(BuildContext context, LunaModule module) async { List _buttons = [ - if(module.github?.isNotEmpty ?? false) LSDialog.button( + if(module.github?.isNotEmpty ?? false) LunaDialog.button( text: 'GitHub', onPressed: () async => module.github.lunaOpenGenericLink(), ), - if(module.website?.isNotEmpty ?? false) LSDialog.button( + if(module.website?.isNotEmpty ?? false) LunaDialog.button( text: 'Website', textColor: LunaColours.orange, onPressed: () async => module.website.lunaOpenGenericLink(), ), ]; - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: module.name ?? LunaUI.TEXT_EMDASH, buttons: _buttons.length == 0 ? null : _buttons, - content: [LSDialog.textContent(text: module.information ?? LunaUI.TEXT_EMDASH)], - contentPadding: LSDialog.textDialogContentPadding(), + content: [LunaDialog.textContent(text: module.information ?? LunaUI.TEXT_EMDASH)], + contentPadding: LunaDialog.textDialogContentPadding(), ); } static Future helpMessage(BuildContext context, { @required String title, @required String message, String website, String github }) async { - await LSDialog.dialog( + await LunaDialog.dialog( context: context, - title: title ?? Constants.TEXT_EMDASH, + title: title ?? LunaUI.TEXT_EMDASH, buttons: ((github?.isNotEmpty ?? false) || (website?.isNotEmpty ?? false)) ? [ - if(github?.isNotEmpty ?? false) LSDialog.button(text: 'GitHub', onPressed: () async => github.lunaOpenGenericLink()), - if(website?.isNotEmpty ?? false) LSDialog.button(text: 'Website', textColor: LunaColours.orange, onPressed: () async => website.lunaOpenGenericLink()), + if(github?.isNotEmpty ?? false) LunaDialog.button(text: 'GitHub', onPressed: () async => github.lunaOpenGenericLink()), + if(website?.isNotEmpty ?? false) LunaDialog.button(text: 'Website', textColor: LunaColours.orange, onPressed: () async => website.lunaOpenGenericLink()), ] : null, - content: [LSDialog.textContent(text: message)], - contentPadding: LSDialog.textDialogContentPadding(), + content: [LunaDialog.textContent(text: message)], + contentPadding: LunaDialog.textDialogContentPadding(), ); } Future accountHelpMessage(BuildContext context) async { - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'LunaSea Account', buttons: [ - if(LunaFirebaseAuth().isSignedIn) LSDialog.button( + if(LunaFirebaseAuth().isSignedIn) LunaDialog.button( text: 'User ID', onPressed: () async { if(!LunaFirebaseAuth().isSignedIn) return; String userId = LunaFirebaseAuth().uid; await Clipboard.setData(ClipboardData(text: userId)); - LSSnackBar( - context: context, + showLunaInfoSnackBar( title: 'Copied User ID', message: 'Copied your user ID to the clipboard', - type: SNACKBAR_TYPE.info, ); Navigator.of(context, rootNavigator: true).pop(); }, ), - LSDialog.button( + LunaDialog.button( text: 'Device ID', onPressed: () async { String deviceId = await LunaFirebaseMessaging().token; await Clipboard.setData(ClipboardData(text: deviceId)); - LSSnackBar( - context: context, + showLunaInfoSnackBar( title: 'Copied Device ID', message: 'Copied your device ID to the clipboard', - type: SNACKBAR_TYPE.info, ); Navigator.of(context, rootNavigator: true).pop(); }, ), ], - content: [LSDialog.textContent(text: 'LunaSea offers a free account to backup your configuration to the cloud, with additional features coming in the future!')], - contentPadding: LSDialog.textDialogContentPadding(), + content: [LunaDialog.textContent(text: 'LunaSea offers a free account to backup your configuration to the cloud, with additional features coming in the future!')], + contentPadding: LunaDialog.textDialogContentPadding(), ); } } diff --git a/lib/modules/settings/routes/account/pages/password_reset.dart b/lib/modules/settings/routes/account/pages/password_reset.dart index 48d18aa1..de9d4b33 100644 --- a/lib/modules/settings/routes/account/pages/password_reset.dart +++ b/lib/modules/settings/routes/account/pages/password_reset.dart @@ -51,10 +51,11 @@ class _State extends State<_SettingsAccountPasswordResetRoute> with LunaScrollCo controller: scrollController, children: [ AutofillGroup( - child: LSCard( + child: LunaCard( + context: context, child: Column( children: [ - LSTextInputBar( + LunaTextInputBar( controller: _emailController, isFormField: true, margin: EdgeInsets.all(12.0), @@ -63,9 +64,6 @@ class _State extends State<_SettingsAccountPasswordResetRoute> with LunaScrollCo action: TextInputAction.next, keyboardType: TextInputType.emailAddress, autofillHints: [AutofillHints.username, AutofillHints.email], - onChanged: (value, updateController) => setState(() { - if(updateController) _emailController.text = value; - }), ), ], ), @@ -79,13 +77,12 @@ class _State extends State<_SettingsAccountPasswordResetRoute> with LunaScrollCo if(_validateEmailAddress()) { LunaFirebaseAuth().resetPassword(_emailController.text) .then((_) => showLunaSuccessSnackBar( - context: context, title: 'Email Sent', message: 'An email to reset your password has been sent!', )) .catchError((error, stack) { LunaLogger().error('Failed to reset password: ${_emailController.text}', error, stack); - showLunaErrorSnackBar(context: context, title: 'Failed to Reset Password', error: error); + showLunaErrorSnackBar(title: 'Failed to Reset Password', error: error); }); } } @@ -94,7 +91,6 @@ class _State extends State<_SettingsAccountPasswordResetRoute> with LunaScrollCo const _regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"; if(!RegExp(_regex).hasMatch(_emailController.text)) { if(showSnackBarOnFailure) showLunaErrorSnackBar( - context: context, title: 'Invalid Email', message: 'The email address is invalid', ); diff --git a/lib/modules/settings/routes/account/pages/signed_in.dart b/lib/modules/settings/routes/account/pages/signed_in.dart index 574128d7..12f58e83 100644 --- a/lib/modules/settings/routes/account/pages/signed_in.dart +++ b/lib/modules/settings/routes/account/pages/signed_in.dart @@ -33,10 +33,10 @@ class _State extends State { onTap: () async { bool result = await SettingsDialogs().confirmAccountSignOut(context); if(result) LunaFirebaseAuth().signOut() - .then((_) => showLunaSuccessSnackBar(context: context, title: 'Signed Out', message: 'Signed out of your LunaSea account')) + .then((_) => showLunaSuccessSnackBar(title: 'Signed Out', message: 'Signed out of your LunaSea account')) .catchError((error, stack) { LunaLogger().error('Failed to sign out', error, stack); - showLunaErrorSnackBar(context: context, title: 'Failed to Sign Out', error: error); + showLunaErrorSnackBar(title: 'Failed to Sign Out', error: error); }); }, ), diff --git a/lib/modules/settings/routes/account/pages/signed_out.dart b/lib/modules/settings/routes/account/pages/signed_out.dart index 0db944a3..2ee6c789 100644 --- a/lib/modules/settings/routes/account/pages/signed_out.dart +++ b/lib/modules/settings/routes/account/pages/signed_out.dart @@ -99,7 +99,7 @@ class _State extends State { style: TextStyle( color: LunaColours.accent, fontWeight: LunaUI.FONT_WEIGHT_BOLD, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), textAlign: TextAlign.center, ), @@ -117,7 +117,6 @@ class _State extends State { const _regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"; if(!RegExp(_regex).hasMatch(_emailController.text)) { if(showSnackBarOnFailure) showLunaErrorSnackBar( - context: context, title: 'Invalid Email', message: 'The email address is invalid', ); @@ -129,7 +128,6 @@ class _State extends State { bool _validatePassword({ bool showSnackBarOnFailure = true }) { if(_passwordController.text.isEmpty) { if(showSnackBarOnFailure) showLunaErrorSnackBar( - context: context, title: 'Invalid Password', message: 'The password is invalid', ); @@ -145,12 +143,12 @@ class _State extends State { .then((response) { if(mounted) setState(() => _state = LunaLoadingState.INACTIVE); response.state - ? showLunaSuccessSnackBar(context: context, title: 'Successfully Registered', message: response.user.email) - : showLunaErrorSnackBar(context: context, title: 'Failed to Register', message: response.error?.message ?? 'Unknown Error'); + ? showLunaSuccessSnackBar(title: 'Successfully Registered', message: response.user.email) + : showLunaErrorSnackBar(title: 'Failed to Register', message: response.error?.message ?? 'Unknown Error'); }) .catchError((error, stack) { if(mounted) setState(() => _state = LunaLoadingState.INACTIVE); - showLunaErrorSnackBar(context: context, title: 'Failed to Register', error: error); + showLunaErrorSnackBar(title: 'Failed to Register', error: error); }); } @@ -161,12 +159,12 @@ class _State extends State { .then((response) { if(mounted) setState(() => _state = LunaLoadingState.INACTIVE); response.state - ? showLunaSuccessSnackBar(context: context, title: 'Successfully Signed In', message: response.user.email) - : showLunaErrorSnackBar(context: context, title: 'Failed to Sign In', message: response.error?.message ?? 'Unknown Error'); + ? showLunaSuccessSnackBar(title: 'Successfully Signed In', message: response.user.email) + : showLunaErrorSnackBar(title: 'Failed to Sign In', message: response.error?.message ?? 'Unknown Error'); }) .catchError((error, stack) { if(mounted) setState(() => _state = LunaLoadingState.INACTIVE); - showLunaErrorSnackBar(context: context, title: 'Failed to Sign In', error: error); + showLunaErrorSnackBar(title: 'Failed to Sign In', error: error); }); } } diff --git a/lib/modules/settings/routes/account/widgets/backup_tile.dart b/lib/modules/settings/routes/account/widgets/backup_tile.dart index 3bc01760..2169b723 100644 --- a/lib/modules/settings/routes/account/widgets/backup_tile.dart +++ b/lib/modules/settings/routes/account/widgets/backup_tile.dart @@ -26,16 +26,15 @@ class SettingsAccountBackupConfigurationTile extends StatelessWidget { String id = LunaUUID().uuid; if(encrypted != LunaEncryption.ENCRYPTION_FAILURE) LunaFirebaseFirestore().addBackupEntry(id, timestamp, title: title) .then((_) => LunaFirebaseStorage().uploadBackup(encrypted, id)) - .then((_) => showLunaSuccessSnackBar(context: context, title: 'Successfully Backed Up', message: title.replaceAll('\n', ' ${Constants.TEXT_EMDASH} '))) + .then((_) => showLunaSuccessSnackBar(title: 'Successfully Backed Up', message: title.replaceAll('\n', ' ${LunaUI.TEXT_EMDASH} '))) .catchError((error, stack) { LunaLogger().error('Failed to backup configuration to the cloud', error, stack); - showLunaErrorSnackBar(context: context, title: 'Failed to Backup', error: error); + showLunaErrorSnackBar(title: 'Failed to Backup', error: error); }); } } catch (error, stack) { LunaLogger().error('Backup Failed', error, stack); showLunaErrorSnackBar( - context: context, title: 'Failed to Backup', error: error, ); diff --git a/lib/modules/settings/routes/account/widgets/delete_tile.dart b/lib/modules/settings/routes/account/widgets/delete_tile.dart index 3e2ab50a..87b817fd 100644 --- a/lib/modules/settings/routes/account/widgets/delete_tile.dart +++ b/lib/modules/settings/routes/account/widgets/delete_tile.dart @@ -23,22 +23,17 @@ class SettingsAccountDeleteConfigurationTile extends StatelessWidget { LunaFirebaseFirestore().deleteBackupEntry(selected.id) .then((_) => LunaFirebaseStorage().deleteBackup(selected.id)) .then((_) => showLunaSuccessSnackBar( - context: context, title: 'Deleted Backup', - message: selected.title.replaceAll('\n', ' ${Constants.TEXT_EMDASH} '), + message: selected.title.replaceAll('\n', ' ${LunaUI.TEXT_EMDASH} '), )) .catchError((error, stack) { LunaLogger().error('Failed to delete backup entry', error, stack); - showLunaErrorSnackBar(context: context, title: 'Failed to Delete Backup', error: error); + showLunaErrorSnackBar(title: 'Failed to Delete Backup', error: error); }); } } catch (error, stack) { LunaLogger().error('Deletion Failed', error, stack); - showLunaErrorSnackBar( - context: context, - title: 'Failed to Delete', - error: error, - ); + showLunaErrorSnackBar(title: 'Failed to Delete', error: error); } } } diff --git a/lib/modules/settings/routes/account/widgets/restore_tile.dart b/lib/modules/settings/routes/account/widgets/restore_tile.dart index 47722e6d..a0c2424a 100644 --- a/lib/modules/settings/routes/account/widgets/restore_tile.dart +++ b/lib/modules/settings/routes/account/widgets/restore_tile.dart @@ -26,13 +26,11 @@ class SettingsAccountRestoreConfigurationTile extends StatelessWidget { String decrypted = LunaEncryption().decrypt(key[1], encrypted); if(decrypted != LunaEncryption.ENCRYPTION_FAILURE) { LunaConfiguration().import(context, decrypted).then((_) => showLunaSuccessSnackBar( - context: context, title: 'Restored', message: 'Your configuration has been restored', )); } else { showLunaErrorSnackBar( - context: context, title: 'Failed to Restore', message: 'An incorrect encryption key was supplied', ); @@ -42,7 +40,6 @@ class SettingsAccountRestoreConfigurationTile extends StatelessWidget { } catch (error, stack) { LunaLogger().error('Restore Failed', error, stack); showLunaErrorSnackBar( - context: context, title: 'Failed to Restore', error: error, ); diff --git a/lib/modules/settings/routes/configuration_dashboard/pages/calendar_settings.dart b/lib/modules/settings/routes/configuration_dashboard/pages/calendar_settings.dart index 3b8de5ca..ab38725b 100644 --- a/lib/modules/settings/routes/configuration_dashboard/pages/calendar_settings.dart +++ b/lib/modules/settings/routes/configuration_dashboard/pages/calendar_settings.dart @@ -64,8 +64,8 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon ), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { - List _values = await DashboardDialogs.setPastDays(context); - if(_values[0]) DashboardDatabaseValue.CALENDAR_DAYS_PAST.put(_values[1]); + Tuple2 result = await DashboardDialogs().setPastDays(context); + if(result.item1) DashboardDatabaseValue.CALENDAR_DAYS_PAST.put(result.item2); }, ), ); @@ -82,8 +82,8 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon ), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), onTap: () async { - List _values = await DashboardDialogs.setFutureDays(context); - if(_values[0]) DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.put(_values[1]); + Tuple2 result = await DashboardDialogs().setFutureDays(context); + if(result.item1) DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.put(result.item2); }, ), ); diff --git a/lib/modules/settings/routes/configuration_lidarr/pages/connection_details.dart b/lib/modules/settings/routes/configuration_lidarr/pages/connection_details.dart index 7a093c82..7c63397c 100644 --- a/lib/modules/settings/routes/configuration_lidarr/pages/connection_details.dart +++ b/lib/modules/settings/routes/configuration_lidarr/pages/connection_details.dart @@ -102,7 +102,6 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon ProfileHiveObject _profile = Database.currentProfileObject; if(_profile.lidarrHost == null || _profile.lidarrHost.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'Host Required', message: 'Host is required to connect to Lidarr', ); @@ -110,7 +109,6 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon } if(_profile.lidarrKey == null || _profile.lidarrKey.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'API Key Required', message: 'API key is required to connect to Lidarr', ); @@ -118,13 +116,11 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon } LidarrAPI.from(Database.currentProfileObject).testConnection() .then((_) => showLunaSuccessSnackBar( - context: context, title: 'Connected Successfully', message: 'Lidarr is ready to use with LunaSea', )).catchError((error, trace) { LunaLogger().error('Connection Test Failed', error, trace); showLunaErrorSnackBar( - context: context, title: 'Connection Test Failed', error: error, ); diff --git a/lib/modules/settings/routes/configuration_nzbget/pages/connection_details.dart b/lib/modules/settings/routes/configuration_nzbget/pages/connection_details.dart index 492dfa55..0546ed51 100644 --- a/lib/modules/settings/routes/configuration_nzbget/pages/connection_details.dart +++ b/lib/modules/settings/routes/configuration_nzbget/pages/connection_details.dart @@ -108,9 +108,9 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon 'NZBGet Password', prefill: Database.currentProfileObject.nzbgetPass ?? '', extraText: [ - LSDialog.textSpanContent(text: '${LunaUI.TEXT_BULLET}\tIf your password includes special characters, considering adding a '), - LSDialog.bolded(text: 'basic authentication'), - LSDialog.textSpanContent(text: ' header with your username and password instead for better support'), + LunaDialog.textSpanContent(text: '${LunaUI.TEXT_BULLET}\tIf your password includes special characters, considering adding a '), + LunaDialog.bolded(text: 'basic authentication'), + LunaDialog.textSpanContent(text: ' header with your username and password instead for better support'), ], ); if(_values.item1) { @@ -130,7 +130,6 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon ProfileHiveObject _profile = Database.currentProfileObject; if(_profile.nzbgetHost == null || _profile.nzbgetHost.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'Host Required', message: 'Host is required to connect to NZBGet', ); @@ -138,13 +137,11 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon } NZBGetAPI.from(Database.currentProfileObject).testConnection() .then((_) => showLunaSuccessSnackBar( - context: context, title: 'Connected Successfully', message: 'NZBGet is ready to use with LunaSea', )).catchError((error, trace) { LunaLogger().error('Connection Test Failed', error, trace); showLunaErrorSnackBar( - context: context, title: 'Connection Test Failed', error: error, ); diff --git a/lib/modules/settings/routes/configuration_radarr/pages/connection_details.dart b/lib/modules/settings/routes/configuration_radarr/pages/connection_details.dart index 7c7e66ee..46fe6018 100644 --- a/lib/modules/settings/routes/configuration_radarr/pages/connection_details.dart +++ b/lib/modules/settings/routes/configuration_radarr/pages/connection_details.dart @@ -102,7 +102,6 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon ProfileHiveObject _profile = Database.currentProfileObject; if(_profile.radarrHost == null || _profile.radarrHost.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'Host Required', message: 'Host is required to connect to Radarr', ); @@ -110,7 +109,6 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon } if(_profile.radarrKey == null || _profile.radarrKey.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'API Key Required', message: 'API key is required to connect to Radarr', ); @@ -118,13 +116,11 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon } Radarr(host: _profile.radarrHost, apiKey: _profile.radarrKey, headers: Map.from(_profile.radarrHeaders)) .system.status().then((_) => showLunaSuccessSnackBar( - context: context, title: 'Connected Successfully', message: 'Radarr is ready to use with LunaSea', )).catchError((error, trace) { LunaLogger().error('Connection Test Failed', error, trace); showLunaErrorSnackBar( - context: context, title: 'Connection Test Failed', error: error, ); diff --git a/lib/modules/settings/routes/configuration_sabnzbd/pages/connection_details.dart b/lib/modules/settings/routes/configuration_sabnzbd/pages/connection_details.dart index 2ca2c44f..44d853ea 100644 --- a/lib/modules/settings/routes/configuration_sabnzbd/pages/connection_details.dart +++ b/lib/modules/settings/routes/configuration_sabnzbd/pages/connection_details.dart @@ -102,7 +102,6 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> with LunaScrollCo ProfileHiveObject _profile = Database.currentProfileObject; if(_profile.sabnzbdHost == null || _profile.sabnzbdHost.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'Host Required', message: 'Host is required to connect to SABnzbd', ); @@ -110,7 +109,6 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> with LunaScrollCo } if(_profile.sabnzbdKey == null || _profile.sabnzbdKey.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'API Key Required', message: 'API key is required to connect to SABnzbd', ); @@ -118,13 +116,11 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> with LunaScrollCo } SABnzbdAPI.from(Database.currentProfileObject).testConnection() .then((_) => showLunaSuccessSnackBar( - context: context, title: 'Connected Successfully', message: 'SABnzbd is ready to use with LunaSea', )).catchError((error, trace) { LunaLogger().error('Connection Test Failed', error, trace); showLunaErrorSnackBar( - context: context, title: 'Connection Test Failed', error: error, ); diff --git a/lib/modules/settings/routes/configuration_search/pages/add_indexer.dart b/lib/modules/settings/routes/configuration_search/pages/add_indexer.dart index 4f28a057..de32c953 100644 --- a/lib/modules/settings/routes/configuration_search/pages/add_indexer.dart +++ b/lib/modules/settings/routes/configuration_search/pages/add_indexer.dart @@ -50,7 +50,6 @@ class _State extends State<_SettingsConfigurationSearchAddRoute> with LunaScroll } else { Database.indexersBox.add(_indexer); showLunaSuccessSnackBar( - context: context, title: 'Indexer Added', message: _indexer.displayName, ); diff --git a/lib/modules/settings/routes/configuration_search/route.dart b/lib/modules/settings/routes/configuration_search/route.dart index 04add183..78553df6 100644 --- a/lib/modules/settings/routes/configuration_search/route.dart +++ b/lib/modules/settings/routes/configuration_search/route.dart @@ -68,31 +68,34 @@ class _State extends State<_SettingsConfigurationSearchRoute> with LunaScrollCon } List get _indexerSection => [ - if(Database.indexersBox.isEmpty) LSGenericMessage(text: 'No Indexers Found'), + if(Database.indexersBox.isEmpty) LunaMessage(text: 'No Indexers Found'), ..._indexers, ]; List get _indexers { - List list = List.generate( - Database.indexersBox.length, + List indexers = Database.indexersBox.values.toList(); + indexers.sort((a,b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); + List list = List.generate( + indexers.length, (index) { - IndexerHiveObject indexer = Database.indexersBox.getAt(index); - return _indexerTile(indexer, indexer.key); + return _indexerTile(indexers[index], indexers[index].key); } ); - list.sort((a,b) => (a.subtitle as LSSubtitle).text.toLowerCase().trim().compareTo((b.subtitle as LSSubtitle).text.toLowerCase().trim())); return list; } - Widget _indexerTile(IndexerHiveObject indexer, int index) => LSCardTile( - title: LSTitle(text: indexer.displayName), - subtitle: LSSubtitle(text: indexer.host), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => SettingsConfigurationSearchEditRouter().navigateTo( - context, - indexerId: index, - ), - ); + Widget _indexerTile(IndexerHiveObject indexer, int index) { + return LunaListTile( + context: context, + title: LunaText.title(text: indexer.displayName), + subtitle: LunaText.subtitle(text: indexer.host), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => SettingsConfigurationSearchEditRouter().navigateTo( + context, + indexerId: index, + ), + ); + } List _customization() { return [ diff --git a/lib/modules/settings/routes/configuration_sonarr/pages/connection_details.dart b/lib/modules/settings/routes/configuration_sonarr/pages/connection_details.dart index 288b7816..3fbb8b73 100644 --- a/lib/modules/settings/routes/configuration_sonarr/pages/connection_details.dart +++ b/lib/modules/settings/routes/configuration_sonarr/pages/connection_details.dart @@ -119,7 +119,6 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon ProfileHiveObject _profile = Database.currentProfileObject; if(_profile.sonarrHost == null || _profile.sonarrHost.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'Host Required', message: 'Host is required to connect to Sonarr', ); @@ -127,7 +126,6 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon } if(_profile.sonarrKey == null || _profile.sonarrKey.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'API Key Required', message: 'API key is required to connect to Sonarr', ); @@ -135,13 +133,11 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon } Sonarr(host: _profile.sonarrHost, apiKey: _profile.sonarrKey, headers: Map.from(_profile.sonarrHeaders)) .system.getStatus().then((_) => showLunaSuccessSnackBar( - context: context, title: 'Connected Successfully', message: 'Sonarr is ready to use with LunaSea', )).catchError((error, trace) { LunaLogger().error('Connection Test Failed', error, trace); showLunaErrorSnackBar( - context: context, title: 'Connection Test Failed', error: error, ); diff --git a/lib/modules/settings/routes/configuration_tautulli/pages/connection_details.dart b/lib/modules/settings/routes/configuration_tautulli/pages/connection_details.dart index f03acf93..a59126f0 100644 --- a/lib/modules/settings/routes/configuration_tautulli/pages/connection_details.dart +++ b/lib/modules/settings/routes/configuration_tautulli/pages/connection_details.dart @@ -102,7 +102,6 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC ProfileHiveObject _profile = Database.currentProfileObject; if(_profile.tautulliHost == null || _profile.tautulliHost.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'Host Required', message: 'Host is required to connect to Tautulli', ); @@ -110,7 +109,6 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC } if(_profile.tautulliKey == null || _profile.tautulliKey.isEmpty) { showLunaErrorSnackBar( - context: context, title: 'API Key Required', message: 'API key is required to connect to Tautulli', ); @@ -118,13 +116,11 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC } Tautulli(host: _profile.tautulliHost, apiKey: _profile.tautulliKey, headers: Map.from(_profile.tautulliHeaders)) .miscellaneous.arnold().then((_) => showLunaSuccessSnackBar( - context: context, title: 'Connected Successfully', message: 'Tautulli is ready to use with LunaSea', )).catchError((error, trace) { LunaLogger().error('Connection Test Failed', error, trace); showLunaErrorSnackBar( - context: context, title: 'Connection Test Failed', error: error, ); diff --git a/lib/modules/settings/routes/donations/route.dart b/lib/modules/settings/routes/donations/route.dart index 67d6a80c..7410b372 100644 --- a/lib/modules/settings/routes/donations/route.dart +++ b/lib/modules/settings/routes/donations/route.dart @@ -47,7 +47,6 @@ class _State extends State<_SettingsDonationsRoute> with LunaScrollControllerMix void _purchasedSuccess() => SettingsDonationsThankYouRouter().navigateTo(context); void _purchaseFailed() => showLunaErrorSnackBar( - context: context, title: 'Transaction Failure', message: 'The transaction has failed, please try again', ); diff --git a/lib/modules/settings/routes/settings.dart b/lib/modules/settings/routes/settings.dart index 1907b85d..9464a303 100644 --- a/lib/modules/settings/routes/settings.dart +++ b/lib/modules/settings/routes/settings.dart @@ -20,23 +20,14 @@ class _State extends State<_SettingsHomeRoute> with LunaScrollControllerMixin { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - child: Scaffold( - key: _scaffoldKey, - appBar: _appBar(), - drawer: _drawer(), - body: _body(), - ), + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar(), + drawer: _drawer(), + body: _body(), ); } - Future _onWillPop() async { - if(_scaffoldKey.currentState.isDrawerOpen) return true; - _scaffoldKey.currentState.openDrawer(); - return false; - } - Widget _drawer() => LunaDrawer(page: LunaModule.SETTINGS.key); Widget _appBar() { diff --git a/lib/modules/settings/routes/system/route.dart b/lib/modules/settings/routes/system/route.dart index 3947a277..fae77224 100644 --- a/lib/modules/settings/routes/system/route.dart +++ b/lib/modules/settings/routes/system/route.dart @@ -147,7 +147,6 @@ class _State extends State<_SettingsSystemRoute> with LunaScrollControllerMixin LunaFirebaseAuth().signOut(); LunaState.reset(context); showLunaSuccessSnackBar( - context: context, title: 'Configuration Cleared', message: 'Your configuration has been cleared', ); diff --git a/lib/modules/settings/routes/system/widgets/backup_tile.dart b/lib/modules/settings/routes/system/widgets/backup_tile.dart index bffaa7e7..085d1f99 100644 --- a/lib/modules/settings/routes/system/widgets/backup_tile.dart +++ b/lib/modules/settings/routes/system/widgets/backup_tile.dart @@ -27,7 +27,6 @@ class SettingsSystemBackupRestoreBackupTile extends StatelessWidget { } catch (error, stack) { LunaLogger().error('Backup Failed', error, stack); showLunaErrorSnackBar( - context: context, title: 'Failed to Backup', error: error, ); diff --git a/lib/modules/settings/routes/system/widgets/restore_tile.dart b/lib/modules/settings/routes/system/widgets/restore_tile.dart index 561a5d56..4c96079d 100644 --- a/lib/modules/settings/routes/system/widgets/restore_tile.dart +++ b/lib/modules/settings/routes/system/widgets/restore_tile.dart @@ -34,13 +34,11 @@ class SettingsSystemBackupRestoreRestoreTile extends StatelessWidget { String _decrypted = LunaEncryption().decrypt(_key[1], _data); if(_decrypted != LunaEncryption.ENCRYPTION_FAILURE) { LunaConfiguration().import(context, _decrypted).then((_) => showLunaSuccessSnackBar( - context: context, title: 'Restored', message: 'Your configuration has been restored', )); } else { showLunaErrorSnackBar( - context: context, title: 'Failed to Restore', message: 'An incorrect encryption key was supplied', ); @@ -48,7 +46,6 @@ class SettingsSystemBackupRestoreRestoreTile extends StatelessWidget { } } else { showLunaErrorSnackBar( - context: context, title: 'Failed to Restore', message: 'Please select a valid backup file', ); @@ -56,7 +53,6 @@ class SettingsSystemBackupRestoreRestoreTile extends StatelessWidget { } catch (error, stack) { LunaLogger().error('Restore Failed', error, stack); showLunaErrorSnackBar( - context: context, title: 'Failed to Restore', error: error, ); diff --git a/lib/modules/settings/routes/system_logs/route.dart b/lib/modules/settings/routes/system_logs/route.dart index cda0cabb..ec58f0cd 100644 --- a/lib/modules/settings/routes/system_logs/route.dart +++ b/lib/modules/settings/routes/system_logs/route.dart @@ -85,7 +85,6 @@ class _State extends State<_SettingsSystemLogsRoute> with LunaScrollControllerMi if(result) { LunaLogger().clearLogs(); showLunaSuccessSnackBar( - context: context, title: 'Logs Cleared', message: 'All recorded logs have been cleared', ); @@ -100,7 +99,6 @@ class _State extends State<_SettingsSystemLogsRoute> with LunaScrollControllerMi icon: Icons.file_download, onTap: () async { showLunaInfoSnackBar( - context: context, title: 'Exporting Logs', message: 'Please wait...', ); diff --git a/lib/modules/sonarr/core/dialogs.dart b/lib/modules/sonarr/core/dialogs.dart index e8550150..5fb0ad60 100644 --- a/lib/modules/sonarr/core/dialogs.dart +++ b/lib/modules/sonarr/core/dialogs.dart @@ -13,24 +13,24 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Settings', content: List.generate( SonarrGlobalSettingsType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: SonarrGlobalSettingsType.values[index].name, icon: SonarrGlobalSettingsType.values[index].icon, iconColor: LunaColours.list(index), onTap: () => _setValues(true, SonarrGlobalSettingsType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } - static Future> seriesSettings(BuildContext context, SonarrSeries series) async { + Future> seriesSettings(BuildContext context, SonarrSeries series) async { bool _flag = false; SonarrSeriesSettingsType _value; @@ -40,21 +40,21 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: series.title, content: List.generate( SonarrSeriesSettingsType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: SonarrSeriesSettingsType.values[index].name(series), icon: SonarrSeriesSettingsType.values[index].icon(series), iconColor: LunaColours.list(index), onTap: () => _setValues(true, SonarrSeriesSettingsType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); - return [_flag, _value]; + return Tuple2(_flag, _value); } static Future> episodeSettings(BuildContext context, SonarrEpisode episode) async { @@ -67,21 +67,21 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: episode.title, content: List.generate( episode.hasFile ? SonarrEpisodeSettingsType.values.length : SonarrEpisodeSettingsType.values.length-1, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: SonarrEpisodeSettingsType.values[index].name(episode), icon: SonarrEpisodeSettingsType.values[index].icon(episode), iconColor: LunaColours.list(index), onTap: () => _setValues(true, SonarrEpisodeSettingsType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -96,21 +96,21 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: seasonNumber == 0 ? 'Specials' : 'Season $seasonNumber', content: List.generate( context.read().enableVersion3 ? SonarrSeasonSettingsType.values.length : SonarrSeasonSettingsType.values.length-1, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: SonarrSeasonSettingsType.values[index].name, icon: SonarrSeasonSettingsType.values[index].icon, iconColor: LunaColours.list(index), onTap: () => _setValues(true, SonarrSeasonSettingsType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _value]; } @@ -128,25 +128,25 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Page', content: List.generate( titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: titles[index], icon: icons[index], iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _index]; } - static Future setAddTags(BuildContext context) async { + Future setAddTags(BuildContext context) async { await showDialog( context: context, builder: (dContext) => ChangeNotifierProvider.value( @@ -154,27 +154,27 @@ class SonarrDialogs { builder: (context, _) => AlertDialog( actions: [ SonarrTagsAppBarActionAddTag(asDialogButton: true), - LSDialog.button( + LunaDialog.button( text: 'Close', onPressed: () => Navigator.of(context, rootNavigator: true).pop(), ), ], - title: LSDialog.title(text: 'Tags'), + title: LunaDialog.title(text: 'Tags'), content: Selector>>( selector: (_, state) => state.tags, builder: (context, future, _) => FutureBuilder( future: future, builder: (context, AsyncSnapshot> snapshot) { - if((snapshot.data?.length ?? 0) == 0) return LSDialog.content( + if((snapshot.data?.length ?? 0) == 0) return LunaDialog.content( children: [ - LSDialog.textContent(text: 'No Tags Found'), + LunaDialog.textContent(text: 'No Tags Found'), ], ); - return LSDialog.content( + return LunaDialog.content( children: List.generate( snapshot.data.length, - (index) => LSDialog.checkbox( + (index) => LunaDialog.checkbox( title: snapshot.data[index].label, value: context.watch().tags.where((tag) => tag.id == snapshot.data[index].id).length != 0, onChanged: (selected) { @@ -188,14 +188,14 @@ class SonarrDialogs { }, ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), shape: LunaUI.shapeBorder, ), ), ); } - static Future setEditTags(BuildContext context) async { + Future setEditTags(BuildContext context) async { await showDialog( context: context, builder: (dContext) => ChangeNotifierProvider.value( @@ -203,27 +203,27 @@ class SonarrDialogs { builder: (context, _) => AlertDialog( actions: [ SonarrTagsAppBarActionAddTag(asDialogButton: true), - LSDialog.button( + LunaDialog.button( text: 'Close', onPressed: () => Navigator.of(context, rootNavigator: true).pop(), ), ], - title: LSDialog.title(text: 'Tags'), + title: LunaDialog.title(text: 'Tags'), content: Selector>>( selector: (_, state) => state.tags, builder: (context, future, _) => FutureBuilder( future: future, builder: (context, AsyncSnapshot> snapshot) { - if((snapshot.data?.length ?? 0) == 0) return LSDialog.content( + if((snapshot.data?.length ?? 0) == 0) return LunaDialog.content( children: [ - LSDialog.textContent(text: 'No Tags Found'), + LunaDialog.textContent(text: 'No Tags Found'), ], ); - return LSDialog.content( + return LunaDialog.content( children: List.generate( snapshot.data.length, - (index) => LSDialog.checkbox( + (index) => LunaDialog.checkbox( title: snapshot.data[index].label, value: context.watch().tags.where((tag) => tag.id == snapshot.data[index].id).length != 0, onChanged: (selected) { @@ -237,14 +237,14 @@ class SonarrDialogs { }, ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), shape: LunaUI.shapeBorder, ), ), ); } - static Future> addNewTag(BuildContext context) async { + Future> addNewTag(BuildContext context) async { bool _flag = false; final _formKey = GlobalKey(); final _textController = TextEditingController(); @@ -256,11 +256,11 @@ class SonarrDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Add Tag', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Add', onPressed: () => _setValues(true), ), @@ -268,7 +268,7 @@ class SonarrDialogs { content: [ Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Tag Label', onSubmitted: (_) => _setValues(true), @@ -279,9 +279,9 @@ class SonarrDialogs { ), ), ], - contentPadding: LSDialog.inputDialogContentPadding(), + contentPadding: LunaDialog.inputDialogContentPadding(), ); - return [_flag, _textController.text]; + return Tuple2(_flag, _textController.text); } static Future> setDefaultSortingOrFiltering(BuildContext context, { @@ -296,19 +296,19 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Sorting & Filtering', content: List.generate( titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: titles[index], icon: Icons.sort, iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _index]; @@ -322,24 +322,24 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Missing Episodes', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Search', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to search for all missing episodes?'), + LunaDialog.textContent(text: 'Are you sure you want to search for all missing episodes?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } - static Future> deleteTag(BuildContext context) async { + Future deleteTag(BuildContext context) async { bool _flag = false; void _setValues(bool flag) { @@ -347,25 +347,25 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Tag', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete this tag?'), + LunaDialog.textContent(text: 'Are you sure you want to delete this tag?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); - return [_flag]; + return _flag; } - static Future> editLanguageProfiles(BuildContext context, List profiles) async { + Future> editLanguageProfiles(BuildContext context, List profiles) async { bool _flag = false; SonarrLanguageProfile profile; @@ -375,24 +375,24 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Language Profile', content: List.generate( profiles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: profiles[index].name, icon: Icons.portrait, iconColor: LunaColours.list(index), onTap: () => _setValues(true, profiles[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); - return [_flag, profile]; + return Tuple2(_flag, profile); } - static Future> editQualityProfile(BuildContext context, List profiles) async { + Future> editQualityProfile(BuildContext context, List profiles) async { bool _flag = false; SonarrQualityProfile profile; @@ -402,24 +402,24 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Quality Profile', content: List.generate( profiles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: profiles[index].name, icon: Icons.portrait, iconColor: LunaColours.list(index), onTap: () => _setValues(true, profiles[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); - return [_flag, profile]; + return Tuple2(_flag, profile); } - static Future> editRootFolder(BuildContext context, List folders) async { + Future> editRootFolder(BuildContext context, List folders) async { bool _flag = false; SonarrRootFolder _folder; @@ -429,16 +429,16 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Root Folder', content: List.generate( folders.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: folders[index].path, - subtitle: LSDialog.richText( + subtitle: LunaDialog.richText( children: [ - LSDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()) + LunaDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()) ], ), icon: Icons.folder, @@ -446,12 +446,12 @@ class SonarrDialogs { onTap: () => _setValues(true, folders[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); - return [_flag, _folder]; + return Tuple2(_flag, _folder); } - static Future> editMonitorStatus(BuildContext context) async { + Future> editMonitorStatus(BuildContext context) async { bool _flag = false; SonarrMonitorStatus _status; @@ -461,24 +461,24 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Monitor Status', content: List.generate( SonarrMonitorStatus.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: SonarrMonitorStatus.values[index].name, icon: Icons.view_list, iconColor: LunaColours.list(index), onTap: () => _setValues(true, SonarrMonitorStatus.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); - return [_flag, _status]; + return Tuple2(_flag, _status); } - static Future> editSeriesType(BuildContext context) async { + Future> editSeriesType(BuildContext context) async { bool _flag = false; SonarrSeriesType _type; @@ -488,21 +488,21 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Series Type', content: List.generate( SonarrSeriesType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: SonarrSeriesType.values[index].value.lunaCapitalizeFirstLetters(), icon: Icons.folder_open, iconColor: LunaColours.list(index), onTap: () => _setValues(true, SonarrSeriesType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); - return [_flag, _type]; + return Tuple2(_flag, _type); } static Future> confirmDeleteSeries(BuildContext context) async { @@ -513,11 +513,11 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Remove Series', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Remove', textColor: LunaColours.red, onPressed: () => _setValues(true), @@ -526,14 +526,14 @@ class SonarrDialogs { content: [ Selector( selector: (_, state) => state.removeSeriesDeleteFiles, - builder: (context, value, _) => LSDialog.checkbox( + builder: (context, value, _) => LunaDialog.checkbox( title: 'Delete Files', value: value, onChanged: (selected) => Provider.of(context, listen: false).removeSeriesDeleteFiles = selected, ), ), ], - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag]; } @@ -546,11 +546,11 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Remove From Queue', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Remove', textColor: LunaColours.red, onPressed: () => _setValues(true), @@ -559,14 +559,14 @@ class SonarrDialogs { content: [ Selector( selector: (_, state) => state.removeQueueBlacklist, - builder: (context, value, _) => LSDialog.checkbox( + builder: (context, value, _) => LunaDialog.checkbox( title: 'Blacklist Release', value: value, onChanged: (selected) => Provider.of(context, listen: false).removeQueueBlacklist = selected, ), ), ], - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return _flag; } @@ -579,20 +579,20 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Delete Episode File', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Delete', textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Are you sure you want to delete this episode file?'), + LunaDialog.textContent(text: 'Are you sure you want to delete this episode file?'), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } @@ -605,23 +605,23 @@ class SonarrDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Season Search', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Search', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent( + LunaDialog.textContent( text: seasonNumber == 0 ? 'Search for all episodes in specials?' : 'Search for all episodes in season $seasonNumber?', ), ], - contentPadding: LSDialog.textDialogContentPadding(), + contentPadding: LunaDialog.textDialogContentPadding(), ); return [_flag]; } diff --git a/lib/modules/sonarr/core/extensions/sonarr_calendar.dart b/lib/modules/sonarr/core/extensions/sonarr_calendar.dart index 494a0e71..53303009 100644 --- a/lib/modules/sonarr/core/extensions/sonarr_calendar.dart +++ b/lib/modules/sonarr/core/extensions/sonarr_calendar.dart @@ -7,7 +7,7 @@ extension SonarrCalendarExtension on SonarrCalendar { if(this.airDateUtc != null) return LunaDatabaseValue.USE_24_HOUR_TIME.data ? DateFormat.Hm().format(this.airDateUtc.toLocal()) : DateFormat('hh:mm a').format(this.airDateUtc.toLocal()); - return Constants.TEXT_EMDASH; + return LunaUI.TEXT_EMDASH; } bool get lunaHasAired { diff --git a/lib/modules/sonarr/core/extensions/sonarr_series.dart b/lib/modules/sonarr/core/extensions/sonarr_series.dart index 171c4973..4555fb61 100644 --- a/lib/modules/sonarr/core/extensions/sonarr_series.dart +++ b/lib/modules/sonarr/core/extensions/sonarr_series.dart @@ -15,7 +15,7 @@ extension SonarrSeriesExtension on SonarrSeries { } String get lunaNextAiring { - if(this.nextAiring == null) return Constants.TEXT_EMDASH; + if(this.nextAiring == null) return LunaUI.TEXT_EMDASH; return DateFormat('MMMM dd, y').format(this.nextAiring.toLocal()); } @@ -50,8 +50,8 @@ extension SonarrSeriesExtension on SonarrSeries { } String get lunaAirsOn { - if(this.status == 'ended') return 'Aired on ${this.network ?? Constants.TEXT_EMDASH}'; - return '${this.lunaAirTime ?? 'Unknown Time'} on ${this.network ?? Constants.TEXT_EMDASH}'; + if(this.status == 'ended') return 'Aired on ${this.network ?? LunaUI.TEXT_EMDASH}'; + return '${this.lunaAirTime ?? 'Unknown Time'} on ${this.network ?? LunaUI.TEXT_EMDASH}'; } String get lunaEpisodeCount { diff --git a/lib/modules/sonarr/core/state.dart b/lib/modules/sonarr/core/state.dart index e4864b7f..e86a2c80 100644 --- a/lib/modules/sonarr/core/state.dart +++ b/lib/modules/sonarr/core/state.dart @@ -28,7 +28,6 @@ class SonarrState extends LunaModuleState { _episodes = {}; _selectedEpisodes = []; // Reset search query fields - _addSearchQuery = ''; _releasesSearchQuery = ''; // Reinitialize resetProfile(); @@ -169,26 +168,6 @@ class SonarrState extends LunaModuleState { notifyListeners(); } - ////////////////// - /// ADD SERIES /// - ////////////////// - - String _addSearchQuery = ''; - String get addSearchQuery => _addSearchQuery; - set addSearchQuery(String addSearchQuery) { - assert(addSearchQuery != null); - _addSearchQuery = addSearchQuery; - notifyListeners(); - } - - Future> _seriesLookup; - Future> get seriesLookup => _seriesLookup; - void fetchSeriesLookup() { - if(_api != null) - _seriesLookup = _api.seriesLookup.getSeriesLookup(term: _addSearchQuery); - notifyListeners(); - } - Future> _rootFolders; Future> get rootFolders => _rootFolders; void resetRootFolders() { diff --git a/lib/modules/sonarr/core/types/sorting_series.dart b/lib/modules/sonarr/core/types/sorting_series.dart index 26a766a7..2606547c 100644 --- a/lib/modules/sonarr/core/types/sorting_series.dart +++ b/lib/modules/sonarr/core/types/sorting_series.dart @@ -127,8 +127,8 @@ class _Sorter { List _network(List series, bool ascending) { series.sort((a,b) { int _comparison = ascending - ? (a.network ?? Constants.TEXT_EMDASH).compareTo((b.network ?? Constants.TEXT_EMDASH)) - : (b.network ?? Constants.TEXT_EMDASH).compareTo((a.network ?? Constants.TEXT_EMDASH)); + ? (a.network ?? LunaUI.TEXT_EMDASH).compareTo((b.network ?? LunaUI.TEXT_EMDASH)) + : (b.network ?? LunaUI.TEXT_EMDASH).compareTo((a.network ?? LunaUI.TEXT_EMDASH)); return _comparison == 0 ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) : _comparison; diff --git a/lib/modules/sonarr/routes/add_series.dart b/lib/modules/sonarr/routes/add_series.dart index bef1be1e..6b9a8e26 100644 --- a/lib/modules/sonarr/routes/add_series.dart +++ b/lib/modules/sonarr/routes/add_series.dart @@ -1,2 +1,3 @@ export 'add_series/route.dart'; +export 'add_series/state.dart'; export 'add_series/widgets.dart'; diff --git a/lib/modules/sonarr/routes/add_series/route.dart b/lib/modules/sonarr/routes/add_series/route.dart index 422f7344..ee136bb3 100644 --- a/lib/modules/sonarr/routes/add_series/route.dart +++ b/lib/modules/sonarr/routes/add_series/route.dart @@ -28,13 +28,28 @@ class _SonarrAddSeriesRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_SonarrAddSeriesRoute> { +class _State extends State<_SonarrAddSeriesRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); + _SonarrAddSeriesArguments _arguments; @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: SonarrSeriesAddAppBar(), - body: SonarrSeriesAddSearchResults(), - ); + Widget build(BuildContext context) { + _arguments = ModalRoute.of(context).settings.arguments; + return ChangeNotifierProvider( + create: (context) => SonarrAddSeriesState(context, _arguments?.query ?? ''), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: SonarrSeriesAddSearchResults(scrollController: scrollController), + ), + ); + } + + Widget _appBar() { + return SonarrSeriesAddAppBar( + scrollController: scrollController, + query: _arguments?.query, + autofocus: (_arguments?.query ?? '').isEmpty, + ); + } } diff --git a/lib/modules/sonarr/routes/add_series/state.dart b/lib/modules/sonarr/routes/add_series/state.dart new file mode 100644 index 00000000..08cbaa0c --- /dev/null +++ b/lib/modules/sonarr/routes/add_series/state.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/sonarr.dart'; + +class SonarrAddSeriesState extends ChangeNotifier { + SonarrAddSeriesState(BuildContext context, String query) { + _searchQuery = query ?? ''; + } + + String _searchQuery; + String get searchQuery => _searchQuery; + set searchQuery(String searchQuery) { + assert(searchQuery != null); + _searchQuery = searchQuery; + notifyListeners(); + } + + Future> _lookup; + Future> get lookup => _lookup; + void fetchLookup(BuildContext context) { + if(context.read().enabled ?? false) { + _lookup = context.read().api.seriesLookup.getSeriesLookup(term: _searchQuery); + } + notifyListeners(); + } +} diff --git a/lib/modules/sonarr/routes/add_series/widgets/appbar.dart b/lib/modules/sonarr/routes/add_series/widgets/appbar.dart index 54735fa1..bf3d110a 100644 --- a/lib/modules/sonarr/routes/add_series/widgets/appbar.dart +++ b/lib/modules/sonarr/routes/add_series/widgets/appbar.dart @@ -3,14 +3,34 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; // ignore: non_constant_identifier_names -Widget SonarrSeriesAddAppBar() => LunaAppBar( +Widget SonarrSeriesAddAppBar({ + @required ScrollController scrollController, + @required String query, + @required bool autofocus, +}) => LunaAppBar( title: 'Add Series', - bottom: _SearchBar(), + scrollControllers: [scrollController], + bottom: _SearchBar( + scrollController: scrollController, + query: query ?? '', + autofocus: autofocus, + ), ); class _SearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; + final String query; + final bool autofocus; + + _SearchBar({ + Key key, + @required this.scrollController, + @required this.query, + @required this.autofocus, + }) : super(key: key); + @override - Size get preferredSize => Size.fromHeight(62.0); + Size get preferredSize => Size.fromHeight(LunaTextInputBar.appBarHeight); @override State<_SearchBar> createState() => _State(); @@ -22,32 +42,31 @@ class _State extends State<_SearchBar> { @override void initState() { super.initState(); - _controller.text = context.read().addSearchQuery; + _controller.text = widget.query; } @override Widget build(BuildContext context) => Consumer( - builder: (context, state, widget) => Row( - children: [ - Expanded( - child: LSTextInputBar( - controller: _controller, - autofocus: _controller.text.isEmpty, - onChanged: (text, updateController) => _onChange(text, updateController), - onSubmitted: _onSubmit, - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + builder: (context, state, _) => Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: LunaTextInputBar( + controller: _controller, + scrollController: widget.scrollController, + autofocus: widget.autofocus, + onChanged: (value) => context.read().searchQuery = value, + onSubmitted: (value) { + if(value.isNotEmpty) context.read().fetchLookup(context); + }, + margin: LunaTextInputBar.appBarMargin, + ), ), - ), - ], + ], + ), + height: LunaTextInputBar.appBarHeight, ), ); - - void _onChange(String text, bool updateController) { - context.read().addSearchQuery = text; - if(updateController) _controller.text = text; - } - - Future _onSubmit(String value) async { - if(value.isNotEmpty) context.read().fetchSeriesLookup(); - } } diff --git a/lib/modules/sonarr/routes/add_series/widgets/search_results.dart b/lib/modules/sonarr/routes/add_series/widgets/search_results.dart index e75793c0..5163355b 100644 --- a/lib/modules/sonarr/routes/add_series/widgets/search_results.dart +++ b/lib/modules/sonarr/routes/add_series/widgets/search_results.dart @@ -2,62 +2,88 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; -import 'package:tuple/tuple.dart'; class SonarrSeriesAddSearchResults extends StatefulWidget { + final ScrollController scrollController; + + SonarrSeriesAddSearchResults({ + Key key, + @required this.scrollController, + }) : super(key: key); + @override State createState() => _State(); } -class _State extends State { +class _State extends State with LunaLoadCallbackMixin { final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { - if(context.read().addSearchQuery.isNotEmpty) context.read().fetchSeriesLookup(); + Future loadCallback() async { + if(context.read().searchQuery.isNotEmpty) { + context.read().fetchLookup(context); + } } @override - Widget build(BuildContext context) => Selector>, Future>>>( - selector: (_, state) => Tuple2(state.seriesLookup, state.series), - builder: (context, futures, _) { - if(futures.item1 == null) return Container(); - return _futureBuilder(context, futures.item1, futures.item2); - }, - ); - - Widget _futureBuilder(BuildContext context, Future> lookup, Future> series) => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: Future.wait([ - lookup, - series, - ]), - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr series lookup', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.connectionState == ConnectionState.none) - return Container(); - if(snapshot.connectionState == ConnectionState.done && snapshot.hasData) - return _results(snapshot.data[0], snapshot.data[1]); - return LSLoader(); - }, - ), - ); - - Widget _results(List results, List series) => LSListView( - children: results.length == 0 - ? [ LSGenericMessage(text: 'No Results Found') ] - : List.generate( - results.length, - (index) => SonarrSeriesAddSearchResultTile( - series: results[index], - exists: series.indexWhere((series) => series.tvdbId == results[index].tvdbId) >= 0, - ), + Widget build(BuildContext context) { + return Selector>>( + selector: (_, state) => state.series, + builder: (context, series, _) => Selector>>( + selector: (_, state) => state.lookup, + builder: (context, lookup, _) { + if(lookup == null) return Container(); + return _futureBuilder( + lookup: lookup, + series: series, + ); + }, ), - ); + ); + } + + Widget _futureBuilder({ + @required Future> lookup, + @required Future> series, + }) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: Future.wait([ + lookup, + series, + ]), + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Sonarr series lookup', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _results(snapshot.data[0], snapshot.data[1]); + return LunaLoader(); + }, + ), + ); + } + + Widget _results(List results, List series) { + if((results?.length ?? 0) == 0) return LunaListView( + controller: widget.scrollController, + children: [ + LunaMessage.inList(text: 'No Results Found'), + ], + ); + return LunaListViewBuilder( + controller: widget.scrollController, + itemCount: results.length, + itemBuilder: (context, index) => SonarrSeriesAddSearchResultTile( + series: results[index], + exists: series.indexWhere((series) => series.tvdbId == results[index].tvdbId) >= 0, + ), + ); + } } diff --git a/lib/modules/sonarr/routes/add_series/widgets/search_results_tile.dart b/lib/modules/sonarr/routes/add_series/widgets/search_results_tile.dart index 28b74087..eb3c7788 100644 --- a/lib/modules/sonarr/routes/add_series/widgets/search_results_tile.dart +++ b/lib/modules/sonarr/routes/add_series/widgets/search_results_tile.dart @@ -24,31 +24,35 @@ class _State extends State { final double _padding = 8.0; @override - Widget build(BuildContext context) => LSCard( - child: InkWell( - child: Row( - children: [ - _poster(), - Expanded(child: _information), - ], + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + child: Row( + children: [ + _poster(), + Expanded(child: _information), + ], + ), + onTap: _onTap, + onLongPress: _onLongPress, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), ), - onTap: _onTap, - onLongPress: _onLongPress, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - ), - decoration: widget.series.lunaBannerURL == null ? null : LunaCardDecoration( - uri: widget.series.lunaBannerURL, - headers: Provider.of(context, listen: false).headers, - ), - ); + decoration: widget.series.lunaBannerURL == null ? null : LunaCardDecoration( + uri: widget.series.lunaBannerURL, + headers: context.read().headers, + ), + height: _height, + ); + } Widget _poster() { - if(widget.series.remotePoster != null) return LSNetworkImage( + if(widget.series.remotePoster != null) return LunaNetworkImage( url: widget.series.remotePoster, - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', height: _height, width: _width, - headers: Provider.of(context, listen: false).headers.cast(), + headers: context.read().headers.cast(), ); return ClipRRect( child: Image.asset( @@ -56,7 +60,7 @@ class _State extends State { width: _width, height: _height, ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), ); } @@ -64,7 +68,7 @@ class _State extends State { child: Container( child: Column( children: [ - LSTitle(text: widget.series.title, darken: widget.exists, maxLines: 1), + LunaText.title(text: widget.series.title, darken: widget.exists, maxLines: 1), _subtitleOne, _subtitleTwo, ], @@ -80,14 +84,14 @@ class _State extends State { Widget get _subtitleOne => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: widget.exists ? Colors.white30 : Colors.white70, ), children: [ TextSpan(text: widget.series.seasonCount == 1 ? '1 Season' : '${widget.series.seasonCount} Seasons'), - TextSpan(text: ' ${Constants.TEXT_BULLET} '), + TextSpan(text: ' ${LunaUI.TEXT_BULLET} '), TextSpan(text: (widget.series.year ?? 0) == 0 ? 'Unknown Year' : widget.series.year.toString()), - TextSpan(text: ' ${Constants.TEXT_BULLET} '), + TextSpan(text: ' ${LunaUI.TEXT_BULLET} '), TextSpan(text: widget.series.network ?? 'Unknown Network'), ], ), @@ -99,7 +103,7 @@ class _State extends State { Widget get _subtitleTwo => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, fontStyle: FontStyle.italic, color: widget.exists ? Colors.white30 : Colors.white70, ), @@ -114,13 +118,11 @@ class _State extends State { if(widget.onTapShowOverview) { LunaDialogs().textPreview(context, widget.series.title, widget.series.overview ?? 'No summary is available.'); } else if(widget.exists) { - Provider.of(context, listen: false).enableVersion3 + context.read().enableVersion3 ? SonarrSeriesDetailsRouter().navigateTo(context, seriesId: widget.series.id ?? -1) - : LSSnackBar( - context: context, + : showLunaInfoSnackBar( title: 'Series Already Exists', message: 'This series already exists in Sonarr', - type: SNACKBAR_TYPE.info, ); } else { SonarrAddSeriesDetailsRouter().navigateTo( diff --git a/lib/modules/sonarr/routes/add_series_details/route.dart b/lib/modules/sonarr/routes/add_series_details/route.dart index 28c7a027..c2429203 100644 --- a/lib/modules/sonarr/routes/add_series_details/route.dart +++ b/lib/modules/sonarr/routes/add_series_details/route.dart @@ -71,34 +71,39 @@ class _State extends State<_SonarrAddSeriesDetailsRoute> with LunaLoadCallbackMi } Widget _body(BuildContext context) { - return FutureBuilder( - future: Future.wait([ - context.watch().rootFolders, - context.watch().tags, - context.watch().qualityProfiles, - if(context.watch().enableVersion3) - context.watch().languageProfiles, - ]), - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr add series data', snapshot.error, StackTrace.current); + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: Future.wait([ + context.watch().rootFolders, + context.watch().tags, + context.watch().qualityProfiles, + if(context.watch().enableVersion3) + context.watch().languageProfiles, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr add series data', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LunaMessage.error(onTap: _refreshKey.currentState?.show ?? () {}); - } - if(snapshot.hasData) { - return _list( - context, - rootFolders: snapshot.data[0], - tags: snapshot.data[1], - qualityProfiles: snapshot.data[2], - languageProfiles: context.read().enableVersion3 - ? snapshot.data[3] - : null, - ); - } - return LunaLoader(); - }, + if(snapshot.hasData) { + return _list( + context, + rootFolders: snapshot.data[0], + tags: snapshot.data[1], + qualityProfiles: snapshot.data[2], + languageProfiles: context.read().enableVersion3 + ? snapshot.data[3] + : null, + ); + } + return LunaLoader(); + }, + ), ); } @@ -116,29 +121,24 @@ class _State extends State<_SonarrAddSeriesDetailsRoute> with LunaLoadCallbackMi context.read().initializeLanguageProfile(languageProfiles); context.read().initializeTags(tags); context.read().canExecuteAction = true; - return LunaRefreshIndicator( - context: context, - key: _refreshKey, - onRefresh: loadCallback, - child: LunaListView( - controller: scrollController, - children: [ - SonarrSeriesAddSearchResultTile( - series: context.read().series, - onTapShowOverview: true, - exists: false, - ), - SonarrSeriesAddDetailsMonitoredTile(), - SonarrSeriesAddDetailsUseSeasonFoldersTile(), - SonarrSeriesAddDetailsSeriesTypeTile(), - SonarrSeriesAddDetailsMonitorStatusTile(), - SonarrSeriesAddDetailsRootFolderTile(), - SonarrSeriesAddDetailsQualityProfileTile(), - if(context.watch().enableVersion3) - SonarrSeriesAddDetailsLanguageProfileTile(), - SonarrSeriesAddDetailsTagsTile(), - ], - ), + return LunaListView( + controller: scrollController, + children: [ + SonarrSeriesAddSearchResultTile( + series: context.read().series, + onTapShowOverview: true, + exists: false, + ), + SonarrSeriesAddDetailsMonitoredTile(), + SonarrSeriesAddDetailsUseSeasonFoldersTile(), + SonarrSeriesAddDetailsSeriesTypeTile(), + SonarrSeriesAddDetailsMonitorStatusTile(), + SonarrSeriesAddDetailsRootFolderTile(), + SonarrSeriesAddDetailsQualityProfileTile(), + if(context.watch().enableVersion3) + SonarrSeriesAddDetailsLanguageProfileTile(), + SonarrSeriesAddDetailsTagsTile(), + ], ); } } diff --git a/lib/modules/sonarr/routes/add_series_details/state.dart b/lib/modules/sonarr/routes/add_series_details/state.dart index b6face10..70d8a75d 100644 --- a/lib/modules/sonarr/routes/add_series_details/state.dart +++ b/lib/modules/sonarr/routes/add_series_details/state.dart @@ -64,7 +64,7 @@ class SonarrSeriesAddDetailsState extends ChangeNotifier { void initializeRootFolder(List rootFolders) { _rootFolder = (rootFolders ?? []).firstWhere( (element) => element.id == SonarrDatabaseValue.ADD_SERIES_DEFAULT_ROOT_FOLDER.data, - orElse: () => (rootFolders?.length ?? 0) != 0 ? rootFolders[0] : SonarrRootFolder(id: -1, freeSpace: 0, path: Constants.TEXT_EMDASH), + orElse: () => (rootFolders?.length ?? 0) != 0 ? rootFolders[0] : SonarrRootFolder(id: -1, freeSpace: 0, path: LunaUI.TEXT_EMDASH), ); } @@ -78,7 +78,7 @@ class SonarrSeriesAddDetailsState extends ChangeNotifier { void initializeQualityProfile(List qualityProfiles) { _qualityProfile = (qualityProfiles ?? []).firstWhere( (element) => element.id == SonarrDatabaseValue.ADD_SERIES_DEFAULT_QUALITY_PROFILE.data, - orElse: () => (qualityProfiles?.length ?? 0) != 0 ? qualityProfiles[0] : SonarrQualityProfile(id: -1, name: Constants.TEXT_EMDASH), + orElse: () => (qualityProfiles?.length ?? 0) != 0 ? qualityProfiles[0] : SonarrQualityProfile(id: -1, name: LunaUI.TEXT_EMDASH), ); } @@ -92,7 +92,7 @@ class SonarrSeriesAddDetailsState extends ChangeNotifier { void initializeLanguageProfile(List languageProfiles) { _languageProfile = (languageProfiles ?? []).firstWhere( (element) => element.id == SonarrDatabaseValue.ADD_SERIES_DEFAULT_LANGUAGE_PROFILE.data, - orElse: () => (languageProfiles?.length ?? 0) != 0 ? languageProfiles[0] : SonarrLanguageProfile(id: -1, name: Constants.TEXT_EMDASH), + orElse: () => (languageProfiles?.length ?? 0) != 0 ? languageProfiles[0] : SonarrLanguageProfile(id: -1, name: LunaUI.TEXT_EMDASH), ); } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/bottom_action_bar.dart b/lib/modules/sonarr/routes/add_series_details/widgets/bottom_action_bar.dart index e0f2c3cb..a16a2a34 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/bottom_action_bar.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/bottom_action_bar.dart @@ -54,7 +54,6 @@ class SonarrAddSeriesDetailsActionBar extends StatelessWidget { await context.read().series; context.read().series.id = addedSeries.id; showLunaSuccessSnackBar( - context: context, title: searchOnAdd ? 'Series Added (Searching...)' : 'Series Added', message: context.read().series.title, ); @@ -63,7 +62,7 @@ class SonarrAddSeriesDetailsActionBar extends StatelessWidget { .catchError((error, stack) { context.read().state = LunaLoadingState.ERROR; LunaLogger().error('Failed to add series: ${context.read().series.tvdbId}', error, stack); - showLunaErrorSnackBar(context: context, title: 'Failed to Add Series', error: error); + showLunaErrorSnackBar(title: 'Failed to Add Series', error: error); }); context.read().state = LunaLoadingState.INACTIVE; } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_language_profile.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_language_profile.dart index 8f425902..c49dc0c9 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_language_profile.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_language_profile.dart @@ -4,19 +4,22 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesAddDetailsLanguageProfileTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Language Profile'), - subtitle: LSSubtitle(text: context.watch().languageProfile?.name ?? Constants.TEXT_EMDASH), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Language Profile'), + subtitle: LunaText.subtitle(text: context.watch().languageProfile?.name ?? LunaUI.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { List _profiles = await context.read().languageProfiles; - List _values = await SonarrDialogs.editLanguageProfiles(context, _profiles); - if(_values[0]) { - context.read().languageProfile = _values[1]; - SonarrDatabaseValue.ADD_SERIES_DEFAULT_LANGUAGE_PROFILE.put((_values[1] as SonarrLanguageProfile).id); + Tuple2 result = await SonarrDialogs().editLanguageProfiles(context, _profiles); + if(result.item1) { + context.read().languageProfile = result.item2; + SonarrDatabaseValue.ADD_SERIES_DEFAULT_LANGUAGE_PROFILE.put(result.item2.id); } } } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitor_status.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitor_status.dart index 54100824..8068c52d 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitor_status.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitor_status.dart @@ -4,24 +4,25 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesAddDetailsMonitorStatusTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Monitor Status'), - subtitle: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_STATUS.key]), - builder: (context, box, _) => LSSubtitle( - text: (SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_STATUS.data as SonarrMonitorStatus).name, + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Monitor Status'), + subtitle: SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_STATUS.listen( + builder: (context, box, _) => LunaText.subtitle( + text: (SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_STATUS.data as SonarrMonitorStatus).name, + ), ), - ), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { - List _values = await SonarrDialogs.editMonitorStatus(context); - if(_values[0]) { - SonarrMonitorStatus _status = _values[1]; - _status.process(context.read().series.seasons); - SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_STATUS.put(_values[1]); + Tuple2 result = await SonarrDialogs().editMonitorStatus(context); + if(result.item1) { + result.item2.process(context.read().series.seasons); + SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_STATUS.put(result.item2); } } } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitored.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitored.dart index 97e970c1..043fc9cd 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitored.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_monitored.dart @@ -4,15 +4,18 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesAddDetailsMonitoredTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Monitored'), - subtitle: LSSubtitle(text: 'Monitor series for new releases'), - trailing: LunaSwitch( - value: context.watch().monitored, - onChanged: (value) { - context.read().monitored = value; - SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITORED.put(value); - }, - ), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Monitored'), + subtitle: LunaText.subtitle(text: 'Monitor series for new releases'), + trailing: LunaSwitch( + value: context.watch().monitored, + onChanged: (value) { + context.read().monitored = value; + SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITORED.put(value); + }, + ), + ); + } } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_quality_profile.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_quality_profile.dart index 3eb74e70..0775654f 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_quality_profile.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_quality_profile.dart @@ -4,19 +4,22 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesAddDetailsQualityProfileTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Quality Profile'), - subtitle: LSSubtitle(text: context.watch().qualityProfile?.name ?? Constants.TEXT_EMDASH), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Quality Profile'), + subtitle: LunaText.subtitle(text: context.watch().qualityProfile?.name ?? LunaUI.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { List _profiles = await context.read().qualityProfiles; - List _values = await SonarrDialogs.editQualityProfile(context, _profiles); - if(_values[0]) { - context.read().qualityProfile = _values[1]; - SonarrDatabaseValue.ADD_SERIES_DEFAULT_QUALITY_PROFILE.put((_values[1] as SonarrQualityProfile).id); + Tuple2 result = await SonarrDialogs().editQualityProfile(context, _profiles); + if(result.item1) { + context.read().qualityProfile = result.item2; + SonarrDatabaseValue.ADD_SERIES_DEFAULT_QUALITY_PROFILE.put(result.item2.id); } } } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_root_folder.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_root_folder.dart index a48caf1a..c03e443e 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_root_folder.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_root_folder.dart @@ -4,19 +4,22 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesAddDetailsRootFolderTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Root Folder'), - subtitle: LSSubtitle(text: context.watch().rootFolder?.path ?? Constants.TEXT_EMDASH), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Root Folder'), + subtitle: LunaText.subtitle(text: context.watch().rootFolder?.path ?? LunaUI.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { List _folders = await context.read().rootFolders; - List _values = await SonarrDialogs.editRootFolder(context, _folders); - if(_values[0]) { - context.read().rootFolder = _values[1]; - SonarrDatabaseValue.ADD_SERIES_DEFAULT_ROOT_FOLDER.put((_values[1] as SonarrRootFolder).id); + Tuple2 result = await SonarrDialogs().editRootFolder(context, _folders); + if(result.item1) { + context.read().rootFolder = result.item2; + SonarrDatabaseValue.ADD_SERIES_DEFAULT_ROOT_FOLDER.put(result.item2.id); } } } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_series_type.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_series_type.dart index 44e0846d..107a4751 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_series_type.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_series_type.dart @@ -4,18 +4,21 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesAddDetailsSeriesTypeTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Series Type'), - subtitle: LSSubtitle(text: context.watch().seriesType?.value?.lunaCapitalizeFirstLetters() ?? Constants.TEXT_EMDASH), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Series Type'), + subtitle: LunaText.subtitle(text: context.watch().seriesType?.value?.lunaCapitalizeFirstLetters() ?? LunaUI.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { - List _values = await SonarrDialogs.editSeriesType(context); - if(_values[0]) { - context.read().seriesType = _values[1]; - SonarrDatabaseValue.ADD_SERIES_DEFAULT_SERIES_TYPE.put((_values[1] as SonarrSeriesType).value); + Tuple2 result = await SonarrDialogs().editSeriesType(context); + if(result.item1) { + context.read().seriesType = result.item2; + SonarrDatabaseValue.ADD_SERIES_DEFAULT_SERIES_TYPE.put(result.item2.value); } } } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_tags.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_tags.dart index d338aa09..127bd5fa 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_tags.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_tags.dart @@ -12,7 +12,7 @@ class SonarrSeriesAddDetailsTagsTile extends StatelessWidget { ? LunaUI.TEXT_EMDASH : context.watch().tags.map((e) => e.label).join(', ')), trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => await SonarrDialogs.setAddTags(context), + onTap: () async => await SonarrDialogs().setAddTags(context), ); } } diff --git a/lib/modules/sonarr/routes/add_series_details/widgets/tile_use_season_folders.dart b/lib/modules/sonarr/routes/add_series_details/widgets/tile_use_season_folders.dart index 63af8119..345d5698 100644 --- a/lib/modules/sonarr/routes/add_series_details/widgets/tile_use_season_folders.dart +++ b/lib/modules/sonarr/routes/add_series_details/widgets/tile_use_season_folders.dart @@ -4,9 +4,10 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesAddDetailsUseSeasonFoldersTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Use Season Folders'), - subtitle: LSSubtitle(text: 'Sort episodes into season folders'), + Widget build(BuildContext context) => LunaListTile( + context: context, + title: LunaText.title(text: 'Use Season Folders'), + subtitle: LunaText.subtitle(text: 'Sort episodes into season folders'), trailing: LunaSwitch( value: context.watch().useSeasonFolders, onChanged: (value) { diff --git a/lib/modules/sonarr/routes/edit_series/widgets/button_update_series.dart b/lib/modules/sonarr/routes/edit_series/widgets/button_update_series.dart index 51c8ca09..0747d1dc 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/button_update_series.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/button_update_series.dart @@ -34,11 +34,9 @@ class SonarrEditSeriesBottomActionBar extends StatelessWidget { // Update internal series list, show snackbar, pop route _globalState.resetSeries(); await _globalState.series.then((_) { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: 'Updated Series', message: _series.title, - type: SNACKBAR_TYPE.success, ); Navigator.of(context).pop(); }); diff --git a/lib/modules/sonarr/routes/edit_series/widgets/tile_language_profile.dart b/lib/modules/sonarr/routes/edit_series/widgets/tile_language_profile.dart index 1b6ed8de..994d45ea 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/tile_language_profile.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/tile_language_profile.dart @@ -11,15 +11,18 @@ class SonarrSeriesEditLanguageProfileTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Language Profile'), - subtitle: LSSubtitle(text: context.watch().languageProfile.name), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Language Profile'), + subtitle: LunaText.subtitle(text: context.watch().languageProfile.name), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { - List _values = await SonarrDialogs.editLanguageProfiles(context, profiles); - if(_values[0]) context.read().languageProfile = _values[1]; + Tuple2 result = await SonarrDialogs().editLanguageProfiles(context, profiles); + if(result.item1) context.read().languageProfile = result.item2; } } diff --git a/lib/modules/sonarr/routes/edit_series/widgets/tile_monitored.dart b/lib/modules/sonarr/routes/edit_series/widgets/tile_monitored.dart index 0c60a024..1a57d338 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/tile_monitored.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/tile_monitored.dart @@ -4,12 +4,15 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesEditMonitoredTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Monitored'), - subtitle: LSSubtitle(text: 'Monitor series for new releases'), - trailing: LunaSwitch( - value: context.watch().monitored, - onChanged: (value) => context.read().monitored = value, - ), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Monitored'), + subtitle: LunaText.subtitle(text: 'Monitor series for new releases'), + trailing: LunaSwitch( + value: context.watch().monitored, + onChanged: (value) => context.read().monitored = value, + ), + ); + } } diff --git a/lib/modules/sonarr/routes/edit_series/widgets/tile_quality_profile.dart b/lib/modules/sonarr/routes/edit_series/widgets/tile_quality_profile.dart index b2f28abf..39016349 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/tile_quality_profile.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/tile_quality_profile.dart @@ -11,15 +11,18 @@ class SonarrSeriesEditQualityProfileTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Quality Profile'), - subtitle: LSSubtitle(text: context.watch().qualityProfile?.name ?? Constants.TEXT_EMDASH), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Quality Profile'), + subtitle: LunaText.subtitle(text: context.watch().qualityProfile?.name ?? LunaUI.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { - List _values = await SonarrDialogs.editQualityProfile(context, profiles); - if(_values[0]) context.read().qualityProfile = _values[1]; + Tuple2 result = await SonarrDialogs().editQualityProfile(context, profiles); + if(result.item1) context.read().qualityProfile = result.item2; } } diff --git a/lib/modules/sonarr/routes/edit_series/widgets/tile_series_path.dart b/lib/modules/sonarr/routes/edit_series/widgets/tile_series_path.dart index 06430e08..0e806650 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/tile_series_path.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/tile_series_path.dart @@ -4,12 +4,15 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesEditSeriesPathTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Series Path'), - subtitle: LSSubtitle(text: context.watch().seriesPath ?? Constants.TEXT_EMDASH), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Series Path'), + subtitle: LunaText.subtitle(text: context.watch().seriesPath ?? LunaUI.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { Tuple2 _values = await LunaDialogs().editText( diff --git a/lib/modules/sonarr/routes/edit_series/widgets/tile_series_type.dart b/lib/modules/sonarr/routes/edit_series/widgets/tile_series_type.dart index ec23e805..7054e58b 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/tile_series_type.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/tile_series_type.dart @@ -4,15 +4,18 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesEditSeriesTypeTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Series Type'), - subtitle: LSSubtitle(text: context.watch().seriesType.value?.lunaCapitalizeFirstLetters() ?? Constants.TEXT_EMDASH), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Series Type'), + subtitle: LunaText.subtitle(text: context.watch().seriesType.value?.lunaCapitalizeFirstLetters() ?? LunaUI.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) async { - List _values = await SonarrDialogs.editSeriesType(context); - if(_values[0]) context.read().seriesType = _values[1]; + Tuple2 result = await SonarrDialogs().editSeriesType(context); + if(result.item1) context.read().seriesType = result.item2; } } diff --git a/lib/modules/sonarr/routes/edit_series/widgets/tile_tags.dart b/lib/modules/sonarr/routes/edit_series/widgets/tile_tags.dart index c09eeea9..fc54b070 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/tile_tags.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/tile_tags.dart @@ -4,14 +4,15 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesEditTagsTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Tags'), - subtitle: LSSubtitle(text: context.watch().tags.length == 0 - ? 'Not Set' - : context.watch().tags.map((e) => e.label).join(', ')), - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => await SonarrDialogs.setEditTags(context); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Tags'), + subtitle: LunaText.subtitle(text: context.watch().tags.length == 0 + ? 'Not Set' + : context.watch().tags.map((e) => e.label).join(', ')), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => await SonarrDialogs().setEditTags(context), + ); + } } diff --git a/lib/modules/sonarr/routes/edit_series/widgets/tile_use_season_folders.dart b/lib/modules/sonarr/routes/edit_series/widgets/tile_use_season_folders.dart index c38382b1..8df81a51 100644 --- a/lib/modules/sonarr/routes/edit_series/widgets/tile_use_season_folders.dart +++ b/lib/modules/sonarr/routes/edit_series/widgets/tile_use_season_folders.dart @@ -4,12 +4,15 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesEditSeasonFoldersTile extends StatelessWidget { @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Use Season Folders'), - subtitle: LSSubtitle(text: 'Sort episodes into season folders'), - trailing: LunaSwitch( - value: context.watch().useSeasonFolders, - onChanged: (value) => context.read().useSeasonFolders = value, - ), - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Use Season Folders'), + subtitle: LunaText.subtitle(text: 'Sort episodes into season folders'), + trailing: LunaSwitch( + value: context.watch().useSeasonFolders, + onChanged: (value) => context.read().useSeasonFolders = value, + ), + ); + } } diff --git a/lib/modules/sonarr/routes/history/route.dart b/lib/modules/sonarr/routes/history/route.dart index 93c0e828..9653f2ab 100644 --- a/lib/modules/sonarr/routes/history/route.dart +++ b/lib/modules/sonarr/routes/history/route.dart @@ -32,46 +32,44 @@ class _State extends State with AutomaticKeepAliveClientMixi super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>( - selector: (_, state) => state.history, - builder: (context, future, _) => FutureBuilder( - future: future, - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr history', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: Selector>( + selector: (_, state) => state.history, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr history', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) { - return snapshot.data.records.length == 0 - ? _noHistory() - : _history(snapshot.data); - } - return LSLoader(); - }, + if(snapshot.hasData) return _history(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); + ); + } - Widget _noHistory() => LSGenericMessage( - text: 'No History Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _history(SonarrHistory history) => LSListView( - children: List.generate( - history.records.length, - (index) => SonarrHistoryTile(record: history.records[index]), - ), - ); -} \ No newline at end of file + Widget _history(SonarrHistory history) { + if((history?.records?.length ?? 0) == 0) return LunaMessage( + text: 'No History Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: SonarrNavigationBar.scrollControllers[3], + itemCount: history.records.length, + itemBuilder: (context, index) => SonarrHistoryTile(record: history.records[index]), + ); + } +} diff --git a/lib/modules/sonarr/routes/history/widgets/history_tile.dart b/lib/modules/sonarr/routes/history/widgets/history_tile.dart index 7732d3b3..6c875718 100644 --- a/lib/modules/sonarr/routes/history/widgets/history_tile.dart +++ b/lib/modules/sonarr/routes/history/widgets/history_tile.dart @@ -19,25 +19,28 @@ class _State extends State { final double _padding = 8.0; @override - Widget build(BuildContext context) => LSCard( - child: InkWell( - child: Row( - children: [ - Expanded(child: _information), - ], - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + child: Row( + children: [ + Expanded(child: _information), + ], + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + ), + onTap: _onTap, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), ), - onTap: _onTap, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - ), - ); + ); + } Widget get _information => Padding( child: Container( child: Column( children: [ - LSTitle(text: widget.record.series.title, maxLines: 1), + LunaText.title(text: widget.record.series.title, maxLines: 1), _subtitleOne, _subtitleTwo, _subtitleThree, @@ -54,12 +57,12 @@ class _State extends State { Widget get _subtitleOne => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: Colors.white70, ), children: [ TextSpan(text: 'Season ${widget.record.episode.seasonNumber}'), - TextSpan(text: ' ${Constants.TEXT_EMDASH} '), + TextSpan(text: ' ${LunaUI.TEXT_EMDASH} '), TextSpan(text: 'Episode ${widget.record.episode.episodeNumber}'), ], ), @@ -71,7 +74,7 @@ class _State extends State { Widget get _subtitleTwo => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: Colors.white70, ), children: [ @@ -86,7 +89,7 @@ class _State extends State { Widget get _subtitleThree => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: widget.record.eventType.lunaColour, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), diff --git a/lib/modules/sonarr/routes/missing/route.dart b/lib/modules/sonarr/routes/missing/route.dart index 1557d6a1..e4b02527 100644 --- a/lib/modules/sonarr/routes/missing/route.dart +++ b/lib/modules/sonarr/routes/missing/route.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; @@ -8,20 +7,14 @@ class SonarrMissingRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { SonarrState _state = Provider.of(context, listen: false); _state.resetMissing(); await _state.missing; @@ -32,46 +25,46 @@ class _State extends State with AutomaticKeepAliveClientMixi super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>( - selector: (_, state) => state.missing, - builder: (context, future, _) => FutureBuilder( - future: future, - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr missing episodes', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector>( + selector: (_, state) => state.missing, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr missing episodes', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) { - return snapshot.data.records.length == 0 - ? _noEpisodes() - : _episodes(snapshot.data); - } - return LSLoader(); - }, + if(snapshot.hasData) return _episodes(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); + ); + } - Widget _noEpisodes() => LSGenericMessage( - text: 'No Episodes Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _episodes(SonarrMissing missing) => LSListView( - children: List.generate( - missing.records.length, - (index) => SonarrMissingTile(record: missing.records[index]), - ), - ); + Widget _episodes(SonarrMissing missing) { + if((missing?.records?.length ?? 0) == 0) return LunaMessage( + text: 'No Episodes Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListView( + controller: SonarrNavigationBar.scrollControllers[2], + children: List.generate( + missing.records.length, + (index) => SonarrMissingTile(record: missing.records[index]), + ), + ); + } } \ No newline at end of file diff --git a/lib/modules/sonarr/routes/missing/widgets/missing_tile.dart b/lib/modules/sonarr/routes/missing/widgets/missing_tile.dart index 3a9e2d57..829f004f 100644 --- a/lib/modules/sonarr/routes/missing/widgets/missing_tile.dart +++ b/lib/modules/sonarr/routes/missing/widgets/missing_tile.dart @@ -22,7 +22,8 @@ class _State extends State { @override Widget build(BuildContext context) => Selector>( selector: (_, state) => state.missing, - builder: (context, series, _) => LSCard( + builder: (context, series, _) => LunaCard( + context: context, child: InkWell( child: Row( children: [ @@ -33,7 +34,7 @@ class _State extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), onTap: _tileOnTap, onLongPress: _tileOnLongPress, ), @@ -44,9 +45,9 @@ class _State extends State { ), ); - Widget get _poster => LSNetworkImage( + Widget get _poster => LunaNetworkImage( url: Provider.of(context, listen: false).getPosterURL(widget.record.seriesId), - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', height: _height, width: _width, headers: Provider.of(context, listen: false).headers.cast(), @@ -56,7 +57,7 @@ class _State extends State { child: Container( child: Column( children: [ - LSTitle(text: widget.record.series.title, darken: !widget.record.monitored, maxLines: 1), + LunaText.title(text: widget.record.series.title, darken: !widget.record.monitored, maxLines: 1), _subtitleOne, _subtitleTwo, _subtitleThree, @@ -72,7 +73,7 @@ class _State extends State { Widget get _trailing => Container( child: Padding( - child: LSIconButton( + child: LunaIconButton( icon: Icons.search, onPressed: _trailingOnPressed, onLongPress: _trailingOnLongPress, @@ -85,12 +86,12 @@ class _State extends State { Widget get _subtitleOne => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: widget.record.monitored ? Colors.white70 : Colors.white30, ), children: [ TextSpan(text: widget.record.seasonNumber == 0 ? 'Specials ' : 'Season ${widget.record.seasonNumber} '), - TextSpan(text: Constants.TEXT_EMDASH), + TextSpan(text: LunaUI.TEXT_EMDASH), TextSpan(text: ' Episode ${widget.record.episodeNumber}'), ], ), @@ -102,7 +103,7 @@ class _State extends State { Widget get _subtitleTwo => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: widget.record.monitored ? Colors.white70 : Colors.white30, ), children: [ @@ -122,7 +123,7 @@ class _State extends State { Widget get _subtitleThree => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: LunaColours.red, fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), @@ -150,18 +151,15 @@ class _State extends State { Future _trailingOnPressed() async { Provider.of(context, listen: false).api.command.episodeSearch(episodeIds: [widget.record.id]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Searching for Episode...', message: widget.record.title, - type: SNACKBAR_TYPE.success, )) .catchError((error, stack) { LunaLogger().error('Failed to search for episode: ${widget.record.id}', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Search', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } diff --git a/lib/modules/sonarr/routes/queue/route.dart b/lib/modules/sonarr/routes/queue/route.dart index 39867f82..2c5d74ed 100644 --- a/lib/modules/sonarr/routes/queue/route.dart +++ b/lib/modules/sonarr/routes/queue/route.dart @@ -16,7 +16,7 @@ class _SonarrQueueRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_SonarrQueueRoute> { +class _State extends State<_SonarrQueueRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @@ -34,45 +34,51 @@ class _State extends State<_SonarrQueueRoute> { @override Widget build(BuildContext context) => Scaffold( key: _scaffoldKey, - appBar: _appBar, - body: _body, + appBar: _appBar(), + body: _body(), ); - Widget get _appBar => LunaAppBar(title: 'Queue'); + Widget _appBar() { + return LunaAppBar( + title: 'Queue', + scrollControllers: [scrollController], + ); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().queue, - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr queue', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: context.watch().queue, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr queue', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _emptyQueue - : _queue(snapshot.data); - return LSLoader(); - }, - ), - ); + if(snapshot.hasData) return _queue(snapshot.data); + return LunaLoader(); + }, + ), + ); + } - Widget get _emptyQueue => LunaMessage( - text: 'Empty Queue', - buttonText: 'Refresh', - onTap: _refreshKey.currentState.show, - ); - - Widget _queue(List queue) => LSListView( - children: List.generate( - queue.length, - (index) => SonarrQueueQueueTile( + Widget _queue(List queue) { + if((queue?.length ?? 0) == 0) return LunaMessage( + text: 'Empty Queue', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: queue.length, + itemBuilder: (context, index) => SonarrQueueQueueTile( key: ObjectKey(queue[index].id), record: queue[index], ), - ), - ); + ); + } } diff --git a/lib/modules/sonarr/routes/queue/widgets/queue_tile.dart b/lib/modules/sonarr/routes/queue/widgets/queue_tile.dart index 88a26325..130131e6 100644 --- a/lib/modules/sonarr/routes/queue/widgets/queue_tile.dart +++ b/lib/modules/sonarr/routes/queue/widgets/queue_tile.dart @@ -42,9 +42,9 @@ class _State extends State { ), children: [ TextSpan(text: widget?.record?.series?.title ?? 'Unknown Series'), - TextSpan(text: '\nSeason ${widget.record.episode.seasonNumber} ${Constants.TEXT_EMDASH} Episode ${widget.record.episode.episodeNumber}\n'), + TextSpan(text: '\nSeason ${widget.record.episode.seasonNumber} ${LunaUI.TEXT_EMDASH} Episode ${widget.record.episode.episodeNumber}\n'), TextSpan( - text: '${widget.record?.quality?.quality?.name ?? 'Unknown'} ${Constants.TEXT_EMDASH} ${widget.record?.lunaPercentageComplete ?? Constants.TEXT_EMDASH}%', + text: '${widget.record?.quality?.quality?.name ?? 'Unknown'} ${LunaUI.TEXT_EMDASH} ${widget.record?.lunaPercentageComplete ?? LunaUI.TEXT_EMDASH}%', style: TextStyle( fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.accent, @@ -90,7 +90,7 @@ class _State extends State { runSpacing: 6.0, children: [ LunaHighlightedNode( - text: widget.record?.protocol?.lunaCapitalizeFirstLetters() ?? Constants.TEXT_EMDASH, + text: widget.record?.protocol?.lunaCapitalizeFirstLetters() ?? LunaUI.TEXT_EMDASH, backgroundColor: widget.record.protocol == 'torrent' ? LunaColours.purple : LunaColours.blue, ), LunaHighlightedNode( @@ -98,7 +98,7 @@ class _State extends State { backgroundColor: LunaColours.accent, ), LunaHighlightedNode( - text: '${widget.record?.lunaPercentageComplete ?? Constants.TEXT_EMDASH}%', + text: '${widget.record?.lunaPercentageComplete ?? LunaUI.TEXT_EMDASH}%', backgroundColor: LunaColours.blueGrey, ), LunaHighlightedNode( @@ -123,8 +123,8 @@ class _State extends State { ), TextSpan( text: widget.record.episode.seasonNumber == 0 - ? 'Specials ${Constants.TEXT_EMDASH} Episode ${widget.record.episode.episodeNumber}\n' - : 'Season ${widget.record.episode.seasonNumber} ${Constants.TEXT_EMDASH} Episode ${widget.record.episode.episodeNumber}\n', + ? 'Specials ${LunaUI.TEXT_EMDASH} Episode ${widget.record.episode.episodeNumber}\n' + : 'Season ${widget.record.episode.seasonNumber} ${LunaUI.TEXT_EMDASH} Episode ${widget.record.episode.episodeNumber}\n', style: TextStyle( color: LunaColours.accent, fontWeight: LunaUI.FONT_WEIGHT_BOLD, @@ -168,7 +168,7 @@ class _State extends State { icon: Icons.messenger_outline_rounded, onTap: () async { String messages = widget.record.statusMessages.fold('', (warnings, status) { - warnings += status.messages.fold('', (message, element) => message += '\n${Constants.TEXT_BULLET} $element'); + warnings += status.messages.fold('', (message, element) => message += '\n${LunaUI.TEXT_BULLET} $element'); return warnings.trim(); }); LunaDialogs().textPreview(context, 'Messages', messages); @@ -200,11 +200,11 @@ class _State extends State { blacklist: context.read().removeQueueBlacklist, ).then((value) { context.read().resetQueue(); - showLunaSuccessSnackBar(context: context, title: 'Removed From Queue', message: widget.record.title); + showLunaSuccessSnackBar(title: 'Removed From Queue', message: widget.record.title); }) .catchError((error, stack) { LunaLogger().error('Failed to remove item from queue: ${widget.record.id}', error, stack); - showLunaErrorSnackBar(context: context, title: 'Failed to Delete From Queue'); + showLunaErrorSnackBar(title: 'Failed to Delete From Queue', error: error); }); } } diff --git a/lib/modules/sonarr/routes/releases/route.dart b/lib/modules/sonarr/routes/releases/route.dart index ba50dfa9..bed8c138 100644 --- a/lib/modules/sonarr/routes/releases/route.dart +++ b/lib/modules/sonarr/routes/releases/route.dart @@ -1,5 +1,4 @@ import 'package:fluro/fluro.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; @@ -71,19 +70,12 @@ class _SonarrReleasesRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_SonarrReleasesRoute> { +class _State extends State<_SonarrReleasesRoute> with LunaLoadCallbackMixin, LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - final ScrollController _scrollController = ScrollController(); Future> _future; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { if(context.read().api != null && mounted) setState(() { if(widget.episodeId != null) { _future = context.read().api.release.getReleases(episodeId: widget.episodeId); @@ -98,36 +90,68 @@ class _State extends State<_SonarrReleasesRoute> { ); } }); + if(_future != null) await _future; } @override Widget build(BuildContext context) => Scaffold( key: _scaffoldKey, - appBar: _appBar, - body: _body, + appBar: _appBar(), + body: _body(), ); - Widget get _appBar => SonarrReleasesAppBar(scrollController: _scrollController); + Widget _appBar() { + return SonarrReleasesAppBar(scrollController: scrollController); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr releases: ${widget.episodeId}', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: _future, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr releases: ${widget.episodeId}', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.connectionState == ConnectionState.done && snapshot.hasData) return snapshot.data.length == 0 - ? _noReleases() - : _releases(snapshot.data); - return LunaLoader(); - }, - ), - ); + if(snapshot.hasData) return _releases(snapshot.data); + return LunaLoader(); + }, + ), + ); + } + + Widget _releases(List releases) { + if((releases?.length ?? 0) == 0) return LunaMessage( + text: 'No Releases Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return Consumer( + builder: (context, state, _) { + List _filtered = _filterAndSort(releases); + if((_filtered?.length ?? 0) == 0) return LunaListView( + controller: scrollController, + children: [ + LunaMessage.inList(text: 'No Releases Found'), + ], + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: _filtered.length, + itemBuilder: (context, index) => SonarrReleasesReleaseTile( + key: ObjectKey(_filtered[index].guid), + release: _filtered[index], + isSeasonRelease: widget.episodeId == null, + ), + ); + } + ); + } List _filterAndSort(List releases) { if(releases == null || releases.length == 0) return releases; @@ -144,30 +168,4 @@ class _State extends State<_SonarrReleasesRoute> { _filtered = _state.releasesSortType.sort(_filtered, _state.releasesSortAscending); return _filtered; } - - Widget _noReleases({ bool showButton = true }) => LSGenericMessage( - text: 'No Releases Found', - showButton: showButton, - buttonText: 'Refresh', - onTapHandler: _refresh, - ); - - Widget _releases(List releases) => Consumer( - builder: (context, state, _) { - List _filtered = _filterAndSort(releases); - return LSListView( - controller: _scrollController, - children: _filtered.length ==0 - ? [_noReleases(showButton: false)] - : List.generate( - _filtered.length, - (index) => SonarrReleasesReleaseTile( - key: ObjectKey(_filtered[index].guid), - release: _filtered[index], - isSeasonRelease: widget.episodeId == null, - ), - ), - ); - } - ); } diff --git a/lib/modules/sonarr/routes/releases/widgets/appbar.dart b/lib/modules/sonarr/routes/releases/widgets/appbar.dart index f900ff00..c0f32d38 100644 --- a/lib/modules/sonarr/routes/releases/widgets/appbar.dart +++ b/lib/modules/sonarr/routes/releases/widgets/appbar.dart @@ -8,6 +8,7 @@ Widget SonarrReleasesAppBar({ @required ScrollController scrollController, }) => LunaAppBar( title: 'Releases', + scrollControllers: [scrollController], bottom: _SearchBar(scrollController: scrollController), ); @@ -20,7 +21,7 @@ class _SearchBar extends StatefulWidget implements PreferredSizeWidget { }) : super(key: key); @override - Size get preferredSize => Size.fromHeight(62.0); + Size get preferredSize => Size.fromHeight(LunaTextInputBar.appBarHeight); @override State<_SearchBar> createState() => _State(scrollController: scrollController); @@ -43,24 +44,25 @@ class _State extends State<_SearchBar> { @override Widget build(BuildContext context) => Consumer( - builder: (context, state, widget) => Row( - children: [ - Expanded( - child: LSTextInputBar( - controller: _controller, - autofocus: false, - onChanged: (text, updateController) => _onChange(text, updateController), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + builder: (context, state, widget) => Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: LunaTextInputBar( + controller: _controller, + scrollController: scrollController, + autofocus: false, + onChanged: (value) => context.read().releasesSearchQuery = value, + margin: LunaTextInputBar.appBarMargin, + ), ), - ), - SonarrReleasesAppBarFilterButton(controller: scrollController), - SonarrReleasesAppBarSortButton(controller: scrollController), - ], + SonarrReleasesAppBarFilterButton(controller: scrollController), + SonarrReleasesAppBarSortButton(controller: scrollController), + ], + ), + height: LunaTextInputBar.appBarHeight, ), ); - - void _onChange(String text, bool updateController) { - context.read().releasesSearchQuery = text; - if(updateController) _controller.text = text; - } } diff --git a/lib/modules/sonarr/routes/releases/widgets/appbar_filter_button.dart b/lib/modules/sonarr/routes/releases/widgets/appbar_filter_button.dart index 90797cd1..284d83dc 100644 --- a/lib/modules/sonarr/routes/releases/widgets/appbar_filter_button.dart +++ b/lib/modules/sonarr/routes/releases/widgets/appbar_filter_button.dart @@ -33,7 +33,7 @@ class _State extends State { child: Text( SonarrReleasesFilter.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: state.releasesHidingType == SonarrReleasesFilter.values[index] ? LunaColours.accent : Colors.white, @@ -43,7 +43,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + margin: LunaTextInputBar.appBarMargin.subtract(EdgeInsets.only(left: 12.0)), color: Theme.of(context).canvasColor, ); } \ No newline at end of file diff --git a/lib/modules/sonarr/routes/releases/widgets/appbar_sort_button.dart b/lib/modules/sonarr/routes/releases/widgets/appbar_sort_button.dart index c78683ec..c6f608a3 100644 --- a/lib/modules/sonarr/routes/releases/widgets/appbar_sort_button.dart +++ b/lib/modules/sonarr/routes/releases/widgets/appbar_sort_button.dart @@ -41,7 +41,7 @@ class _State extends State { Text( SonarrReleasesSorting.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: state.releasesSortType == SonarrReleasesSorting.values[index] ? LunaColours.accent : Colors.white, @@ -51,7 +51,7 @@ class _State extends State { state.releasesSortAscending ? Icons.arrow_upward : Icons.arrow_downward, - size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, + size: LunaUI.FONT_SIZE_SUBTITLE+2.0, color: LunaColours.accent, ), ], @@ -60,7 +60,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 13.5), + margin: LunaTextInputBar.appBarMargin.subtract(EdgeInsets.only(left: 12.0)), color: Theme.of(context).canvasColor, ); } \ No newline at end of file diff --git a/lib/modules/sonarr/routes/releases/widgets/tile_release.dart b/lib/modules/sonarr/routes/releases/widgets/tile_release.dart index e8ac37d4..d0fc7cb5 100644 --- a/lib/modules/sonarr/routes/releases/widgets/tile_release.dart +++ b/lib/modules/sonarr/routes/releases/widgets/tile_release.dart @@ -123,18 +123,15 @@ class _State extends State { indexerId: widget.release.indexerId, useVersion3: widget.isSeasonRelease, ) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Downloading Release...', message: widget.release.title, - type: SNACKBAR_TYPE.success, )) .catchError((error, stack) { LunaLogger().error('Unable to download release: ${widget.release.guid}', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Download Release', - type: SNACKBAR_TYPE.failure, + error: error, ); }); if(mounted) setState(() => _loadingState = LunaLoadingState.INACTIVE); diff --git a/lib/modules/sonarr/routes/season_details/route.dart b/lib/modules/sonarr/routes/season_details/route.dart index 64c4e349..ced956f5 100644 --- a/lib/modules/sonarr/routes/season_details/route.dart +++ b/lib/modules/sonarr/routes/season_details/route.dart @@ -41,7 +41,7 @@ class _SonarrSeasonDetailsRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_SonarrSeasonDetailsRoute> { +class _State extends State<_SonarrSeasonDetailsRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @@ -68,77 +68,85 @@ class _State extends State<_SonarrSeasonDetailsRoute> { if(widget.seriesId <= 0) return LunaInvalidRoute(title: 'Season Details', message: 'Series Not Found'); return Scaffold( key: _scaffoldKey, - appBar: _appBar, - body: _body, - floatingActionButton: _floatingActionButton, + appBar: _appBar(), + body: _body(), + floatingActionButton: _floatingActionButton(), ); } - Widget get _appBar => LunaAppBar(title: 'Season Details'); + Widget _appBar() { + return LunaAppBar( + title: 'Season Details', + scrollControllers: [scrollController], + ); + } - Widget get _floatingActionButton => context.watch().selectedEpisodes.length == 0 - ? null - : LSFloatingActionButtonExtended( - label: context.watch().selectedEpisodes.length == 1 + Widget _floatingActionButton() { + int _episodes = context.watch().selectedEpisodes?.length ?? 0; + if(_episodes == 0) return null; + return LunaFloatingActionButtonExtended( + label: _episodes == 1 ? '1 Episode' - : '${context.watch().selectedEpisodes.length} Episodes', + : '$_episodes Episodes', icon: Icons.search, onPressed: () => _searchSelected(), ); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().episodes[widget.seriesId], - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) return LSErrorMessage(onTapHandler: () => _refresh()); - if(snapshot.hasData) { - if(widget.seasonNumber == -1) return SonarrSeasonDetailsAllSeasons( - episodes: snapshot.data, - seriesId: widget.seriesId, - ); - List _episodes = snapshot.data.where( - (episode) => episode.seasonNumber == widget.seasonNumber, - ).toList(); - if(_episodes != null && _episodes.length > 0) { - _episodes.sort((a,b) => (b.episodeNumber ?? 0).compareTo(a.episodeNumber ?? 0)); - return SonarrSeasonDetailsSeason( - episodes: _episodes, + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: context.watch().episodes[widget.seriesId], + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) return LunaMessage.error(onTap: _refreshKey.currentState?.show); + if(snapshot.hasData) { + if(widget.seasonNumber == -1) return SonarrSeasonDetailsAllSeasons( + episodes: snapshot.data, seriesId: widget.seriesId, - seasonNumber: widget.seasonNumber, + scrollController: scrollController, ); + List _episodes = snapshot.data.where( + (episode) => episode.seasonNumber == widget.seasonNumber, + ).toList(); + if(_episodes != null && _episodes.length > 0) { + _episodes.sort((a,b) => (b.episodeNumber ?? 0).compareTo(a.episodeNumber ?? 0)); + return SonarrSeasonDetailsSeason( + episodes: _episodes, + seriesId: widget.seriesId, + seasonNumber: widget.seasonNumber, + scrollController: scrollController, + ); + } + return _unknown(); } - return _unknown; - } - return LSLoader(); - }, - ), - ); + return LunaLoader(); + }, + ), + ); + } - Widget get _unknown => LSGenericMessage(text: 'No Episodes Found'); + Widget _unknown() { + return LunaMessage(text: 'No Episodes Found'); + } Future _searchSelected() async { if(context.read().api != null) context.read().api.command.episodeSearch( episodeIds: context.read().selectedEpisodes, ).then((_) { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: 'Searching for Episodes...', message: context.read().selectedEpisodes.length == 1 ? '1 Episode' : '${context.read().selectedEpisodes.length} Episodes', - type: SNACKBAR_TYPE.success, ); context.read().selectedEpisodes = []; }) .catchError((error, stack) { LunaLogger().error('Failed to search for episodes: ${context.read().selectedEpisodes.join(', ')}', error, stack); - LSSnackBar( - context: context, - title: 'Failed to Search For Episodes', - type: SNACKBAR_TYPE.failure, - ); + showLunaErrorSnackBar(title: 'Failed to Search For Episodes', error: error); }); } } diff --git a/lib/modules/sonarr/routes/season_details/widgets/all_seasons.dart b/lib/modules/sonarr/routes/season_details/widgets/all_seasons.dart index d6e622db..0e52bdc6 100644 --- a/lib/modules/sonarr/routes/season_details/widgets/all_seasons.dart +++ b/lib/modules/sonarr/routes/season_details/widgets/all_seasons.dart @@ -5,17 +5,22 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeasonDetailsAllSeasons extends StatelessWidget { final int seriesId; final List episodes; + final ScrollController scrollController; SonarrSeasonDetailsAllSeasons({ Key key, @required this.seriesId, @required this.episodes, + @required this.scrollController, }) : super(key: key); @override - Widget build(BuildContext context) => LSListView( - children: _buildSeasons, - ); + Widget build(BuildContext context) { + return LunaListView( + controller: scrollController, + children: _buildSeasons, + ); + } List get _buildSeasons { // Put episodes into a map with the key being the season number diff --git a/lib/modules/sonarr/routes/season_details/widgets/season.dart b/lib/modules/sonarr/routes/season_details/widgets/season.dart index bf0d75e0..c2dcbc32 100644 --- a/lib/modules/sonarr/routes/season_details/widgets/season.dart +++ b/lib/modules/sonarr/routes/season_details/widgets/season.dart @@ -6,26 +6,31 @@ class SonarrSeasonDetailsSeason extends StatelessWidget { final int seasonNumber; final int seriesId; final List episodes; + final ScrollController scrollController; SonarrSeasonDetailsSeason({ Key key, @required this.seasonNumber, @required this.seriesId, @required this.episodes, + @required this.scrollController, }) : super(key: key); @override - Widget build(BuildContext context) => LSListView( - children: [ - SonarrSeasonDetailsSeasonHeader( - seriesId: seriesId, - seasonNumber: seasonNumber, - episodes: episodes, - ), - ...List.generate( - episodes.length, - (index) => SonarrSeasonDetailsEpisodeTile(episode: episodes[index]), - ), - ], - ); + Widget build(BuildContext context) { + return LunaListView( + controller: scrollController, + children: [ + SonarrSeasonDetailsSeasonHeader( + seriesId: seriesId, + seasonNumber: seasonNumber, + episodes: episodes, + ), + ...List.generate( + episodes.length, + (index) => SonarrSeasonDetailsEpisodeTile(episode: episodes[index]), + ), + ], + ); + } } diff --git a/lib/modules/sonarr/routes/season_details/widgets/season_header.dart b/lib/modules/sonarr/routes/season_details/widgets/season_header.dart index ba904acd..c0abffd7 100644 --- a/lib/modules/sonarr/routes/season_details/widgets/season_header.dart +++ b/lib/modules/sonarr/routes/season_details/widgets/season_header.dart @@ -15,15 +15,13 @@ class SonarrSeasonDetailsSeasonHeader extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => GestureDetector( - child: LSHeader( - text: seasonNumber == 0 - ? 'Specials' - : 'Season $seasonNumber', - ), - onTap: () async => _onTap(context), - onLongPress: () async => handler(context, seriesId, seasonNumber), - ); + Widget build(BuildContext context) { + return GestureDetector( + child: LunaHeader(text: seasonNumber == 0 ? 'Specials' : 'Season $seasonNumber'), + onTap: () async => _onTap(context), + onLongPress: () async => handler(context, seriesId, seasonNumber), + ); + } Future _onTap(BuildContext context) async { bool _allSelected = true; @@ -52,21 +50,13 @@ class SonarrSeasonDetailsSeasonHeader extends StatelessWidget { seriesId: seriesId, seasonNumber: seasonNumber, ) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Searching for Season...', - message: seasonNumber == 0 - ? 'Specials' - : 'Season $seasonNumber', - type: SNACKBAR_TYPE.success, + message: seasonNumber == 0 ? 'Specials' : 'Season $seasonNumber', )) .catchError((error, stack) { LunaLogger().error('Failed season search: $seriesId, season $seasonNumber', error, stack); - LSSnackBar( - context: context, - title: 'Failed to Season Search', - type: SNACKBAR_TYPE.failure, - ); + showLunaErrorSnackBar(title: 'Failed to Season Search', error: error); }); } diff --git a/lib/modules/sonarr/routes/season_details/widgets/tile_episode.dart b/lib/modules/sonarr/routes/season_details/widgets/tile_episode.dart index f2628b20..6ac19e64 100644 --- a/lib/modules/sonarr/routes/season_details/widgets/tile_episode.dart +++ b/lib/modules/sonarr/routes/season_details/widgets/tile_episode.dart @@ -104,8 +104,8 @@ class _State extends State { children: [ TextSpan( text: widget.episode.seasonNumber == 0 - ? 'Specials ${Constants.TEXT_EMDASH} Episode ${widget.episode.episodeNumber}' - : 'Season ${widget.episode.seasonNumber} ${Constants.TEXT_EMDASH} Episode ${widget.episode.episodeNumber}', + ? 'Specials ${LunaUI.TEXT_EMDASH} Episode ${widget.episode.episodeNumber}' + : 'Season ${widget.episode.seasonNumber} ${LunaUI.TEXT_EMDASH} Episode ${widget.episode.episodeNumber}', style: TextStyle( color: LunaColours.accent, fontWeight: LunaUI.FONT_WEIGHT_BOLD, @@ -178,7 +178,7 @@ class _State extends State { ), children: [ TextSpan(text: queue?.quality?.quality?.name ?? LunaUI.TEXT_EMDASH), - TextSpan(text: Constants.TEXT_EMDASH.lunaPad()), + TextSpan(text: LunaUI.TEXT_EMDASH.lunaPad()), TextSpan(text: '${queue.lunaPercentageComplete}%'), ], ), @@ -191,7 +191,7 @@ class _State extends State { ), children: [ TextSpan(text: widget.episode.episodeFile.quality.quality.name), - TextSpan(text: Constants.TEXT_EMDASH.lunaPad()), + TextSpan(text: LunaUI.TEXT_EMDASH.lunaPad()), TextSpan(text: widget.episode.episodeFile.size.lunaBytesToString()), ], ), diff --git a/lib/modules/sonarr/routes/series/route.dart b/lib/modules/sonarr/routes/series/route.dart index e4cc5c15..58163107 100644 --- a/lib/modules/sonarr/routes/series/route.dart +++ b/lib/modules/sonarr/routes/series/route.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; @@ -5,13 +7,6 @@ import 'package:lunasea/modules/sonarr.dart'; import 'package:tuple/tuple.dart'; class SonarrSeriesRoute extends StatefulWidget { - final ScrollController scrollController; - - SonarrSeriesRoute({ - Key key, - @required this.scrollController, - }): super(key: key); - @override State createState() => _State(); } @@ -48,54 +43,59 @@ class _State extends State with AutomaticKeepAliveClientMixin super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, - appBar: _appBar, + body: _body(), + appBar: _appBar(), ); } - Widget get _appBar => LunaAppBar.empty( - child: SonarrSeriesSearchBar(scrollController: widget.scrollController), - height: 62.0, - ); + Widget _appBar() { + return LunaAppBar.empty( + child: SonarrSeriesSearchBar(scrollController: SonarrNavigationBar.scrollControllers[0]), + height: LunaTextInputBar.appBarHeight, + ); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>, - Future>, - Future>, - bool - >>( - selector: (_, state) => Tuple4( - state.series, - state.qualityProfiles, - state.languageProfiles, - state.enableVersion3, - ), - builder: (context, tuple, _) => FutureBuilder( - future: Future.wait([ - tuple.item1, - tuple.item2, - if(tuple.item4) tuple.item3, - ]), - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr series', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: Selector>, + Future>, + Future>, + bool + >>( + selector: (_, state) => Tuple4( + state.series, + state.qualityProfiles, + state.languageProfiles, + state.enableVersion3, + ), + builder: (context, tuple, _) => FutureBuilder( + future: Future.wait([ + tuple.item1, + tuple.item2, + if(tuple.item4) tuple.item3, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr series', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noSeries() - : snapshot.data.length > 2 - ? _series(snapshot.data[0], snapshot.data[1], snapshot.data[2]) - : _series(snapshot.data[0], snapshot.data[1], null); - return LSLoader(); - }, + if(snapshot.hasData) { + return snapshot.data.length > 2 + ? _series(snapshot.data[0], snapshot.data[1], snapshot.data[2]) + : _series(snapshot.data[0], snapshot.data[1], null); + } + return LunaLoader(); + }, + ), ), - ), - ); + ); + } List _filterAndSort(List series, List profiles, String query) { if(series == null || series.length == 0) return series; @@ -115,29 +115,42 @@ class _State extends State with AutomaticKeepAliveClientMixin List series, List qualities, List languages, - ) => Selector( - selector: (_, state) => state.seriesSearchQuery, - builder: (context, query, _) { - List _filtered = _filterAndSort(series, qualities, query); - return LSListView( - controller: widget.scrollController, - children: _filtered.length == 0 - ? [_noSeries(showButton: false)] - : List.generate( - _filtered.length, - (index) => SonarrSeriesTile( - series: _filtered[index], - profile: qualities.firstWhere((element) => element.id == _filtered[index].profileId, orElse: null), + ) { + if((series?.length ?? 0) == 0) return LunaMessage( + text: 'No Series Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return Selector( + selector: (_, state) => state.seriesSearchQuery, + builder: (context, query, _) { + List _filtered = _filterAndSort(series, qualities, query); + if((_filtered?.length ?? 0) == 0) return LunaListView( + controller: SonarrNavigationBar.scrollControllers[0], + children: [ + LunaMessage.inList(text: 'No Series Found'), + if((query ?? '').isNotEmpty) LunaButtonContainer( + children: [ + LunaButton.text( + text: query.length > 20 + ? 'Search for "${query.substring(0, min(20, query.length))}${LunaUI.TEXT_ELLIPSIS}"' + : 'Search for "$query"', + backgroundColor: LunaColours.accent, + onTap: () async => SonarrAddSeriesRouter().navigateTo(context, query: query ?? ''), + ), + ], ), + ], + ); + return LunaListViewBuilder( + controller: SonarrNavigationBar.scrollControllers[0], + itemCount: _filtered.length, + itemBuilder: (context, index) => SonarrSeriesTile( + series: _filtered[index], + profile: qualities.firstWhere((element) => element.id == _filtered[index].profileId, orElse: null), ), - ); - }, - ); - - Widget _noSeries({ bool showButton = true }) => LSGenericMessage( - text: 'No Series Found', - showButton: showButton, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + ); + }, + ); + } } diff --git a/lib/modules/sonarr/routes/series/widgets/search_bar.dart b/lib/modules/sonarr/routes/series/widgets/search_bar.dart index b4a7f76e..22cfeae9 100644 --- a/lib/modules/sonarr/routes/series/widgets/search_bar.dart +++ b/lib/modules/sonarr/routes/series/widgets/search_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; -class SonarrSeriesSearchBar extends StatefulWidget implements PreferredSizeWidget { +class SonarrSeriesSearchBar extends StatefulWidget { final ScrollController scrollController; SonarrSeriesSearchBar({ @@ -10,9 +10,6 @@ class SonarrSeriesSearchBar extends StatefulWidget implements PreferredSizeWidge @required this.scrollController, }) : super(key: key); - @override - Size get preferredSize => Size.fromHeight(62.0); - @override State createState() => _State(); } @@ -27,28 +24,25 @@ class _State extends State { } @override - Widget build(BuildContext context) => Padding( - child: Row( + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: Consumer( - builder: (context, state, _) => LSTextInputBar( + builder: (context, state, _) => LunaTextInputBar( controller: _controller, + scrollController: widget.scrollController, autofocus: false, - onChanged: (text, updateController) => _onChange(text, updateController), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + onChanged: (value) => context.read().seriesSearchQuery = value, + margin: EdgeInsets.zero, ), ), ), SonarrSeriesSearchBarFilterButton(controller: widget.scrollController), SonarrSeriesSearchBarSortButton(controller: widget.scrollController), ], - ), - padding: EdgeInsets.only(top: 1.0, bottom: 1.0), - ); - - void _onChange(String text, bool updateController) { - context.read().seriesSearchQuery = text; - if(updateController) _controller.text = text; + ); } } diff --git a/lib/modules/sonarr/routes/series/widgets/search_bar_filter_button.dart b/lib/modules/sonarr/routes/series/widgets/search_bar_filter_button.dart index 891e683b..9b2f3cb4 100644 --- a/lib/modules/sonarr/routes/series/widgets/search_bar_filter_button.dart +++ b/lib/modules/sonarr/routes/series/widgets/search_bar_filter_button.dart @@ -33,7 +33,7 @@ class _State extends State { child: Text( SonarrSeriesFilter.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: state.seriesHidingType == SonarrSeriesFilter.values[index] ? LunaColours.accent : Colors.white, @@ -43,7 +43,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + margin: EdgeInsets.symmetric(horizontal: 12.0), color: Theme.of(context).canvasColor, ); } \ No newline at end of file diff --git a/lib/modules/sonarr/routes/series/widgets/search_bar_sort_button.dart b/lib/modules/sonarr/routes/series/widgets/search_bar_sort_button.dart index 756efc27..4301626d 100644 --- a/lib/modules/sonarr/routes/series/widgets/search_bar_sort_button.dart +++ b/lib/modules/sonarr/routes/series/widgets/search_bar_sort_button.dart @@ -41,7 +41,7 @@ class _State extends State { Text( SonarrSeriesSorting.values[index].readable, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: state.seriesSortType == SonarrSeriesSorting.values[index] ? LunaColours.accent : Colors.white, @@ -51,7 +51,7 @@ class _State extends State { state.seriesSortAscending ? Icons.arrow_upward : Icons.arrow_downward, - size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, + size: LunaUI.FONT_SIZE_SUBTITLE+2.0, color: LunaColours.accent, ), ], @@ -60,7 +60,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 13.5), + margin: EdgeInsets.zero, color: Theme.of(context).canvasColor, ); } \ No newline at end of file diff --git a/lib/modules/sonarr/routes/series/widgets/series_tile.dart b/lib/modules/sonarr/routes/series/widgets/series_tile.dart index 971fb9c8..9e6ecec4 100644 --- a/lib/modules/sonarr/routes/series/widgets/series_tile.dart +++ b/lib/modules/sonarr/routes/series/widgets/series_tile.dart @@ -24,7 +24,8 @@ class _State extends State { @override Widget build(BuildContext context) => Selector>>( selector: (_, state) => state.series, - builder: (context, series, _) => LSCard( + builder: (context, series, _) => LunaCard( + context: context, child: InkWell( child: Row( children: [ @@ -34,7 +35,7 @@ class _State extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), onTap: () async => _tileOnTap(), onLongPress: () async => SonarrAppBarSeriesSettingsAction.handler(context, widget.series), ), @@ -45,9 +46,9 @@ class _State extends State { ), ); - Widget get _poster => LSNetworkImage( + Widget get _poster => LunaNetworkImage( url: Provider.of(context, listen: false).getPosterURL(widget.series.id), - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', height: _height, width: _width, headers: Provider.of(context, listen: false).headers.cast(), @@ -57,7 +58,7 @@ class _State extends State { child: Container( child: Column( children: [ - LSTitle(text: widget.series.title, darken: !widget.series.monitored, maxLines: 1), + LunaText.title(text: widget.series.title, darken: !widget.series.monitored, maxLines: 1), _subtitleOne, _subtitleTwo, _subtitleThree, @@ -74,7 +75,7 @@ class _State extends State { Widget get _subtitleOne => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: widget.series.monitored ? Colors.white70 : Colors.white30, ), children: [ @@ -91,9 +92,9 @@ class _State extends State { : null, ), ), - TextSpan(text: ' ${Constants.TEXT_BULLET} '), + TextSpan(text: ' ${LunaUI.TEXT_BULLET} '), TextSpan(text: widget.series.lunaSeasonCount), - TextSpan(text: ' ${Constants.TEXT_BULLET} '), + TextSpan(text: ' ${LunaUI.TEXT_BULLET} '), TextSpan( text: widget.series.lunaSizeOnDisk, style: TextStyle( @@ -117,7 +118,7 @@ class _State extends State { Widget get _subtitleTwo => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: widget.series.monitored ? Colors.white70 : Colors.white30, ), children: [ @@ -134,7 +135,7 @@ class _State extends State { : null, ), ), - TextSpan(text: ' ${Constants.TEXT_BULLET} '), + TextSpan(text: ' ${LunaUI.TEXT_BULLET} '), TextSpan( text: widget.profile?.name ?? 'Unknown', style: TextStyle( @@ -158,7 +159,7 @@ class _State extends State { Widget get _subtitleThree => RichText( text: TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: widget.series.monitored ? Colors.white70 : Colors.white30, ), children: [ @@ -180,7 +181,7 @@ class _State extends State { ), ), TextSpan( - text: ' ${Constants.TEXT_BULLET} ', + text: ' ${LunaUI.TEXT_BULLET} ', style: TextStyle( color: Provider.of(context).seriesSortType == SonarrSeriesSorting.NEXT_AIRING ? widget.series.monitored diff --git a/lib/modules/sonarr/routes/series_details/route.dart b/lib/modules/sonarr/routes/series_details/route.dart index e26eb866..692008de 100644 --- a/lib/modules/sonarr/routes/series_details/route.dart +++ b/lib/modules/sonarr/routes/series_details/route.dart @@ -96,6 +96,8 @@ class _State extends State<_SonarrSeriesDetailsRoute> with LunaLoadCallbackMixin Widget get _appBar => LunaAppBar( title: 'Series Details', + scrollControllers: SonarrSeriesDetailsNavigationBar.scrollControllers, + pageController: _pageController, actions: [ SonarrAppBarSeriesEditAction(seriesId: widget.seriesId), SonarrAppBarSeriesSettingsAction(seriesId: widget.seriesId), @@ -132,7 +134,7 @@ class _State extends State<_SonarrSeriesDetailsRoute> with LunaLoadCallbackMixin ); if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to pull Sonarr series details', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to pull Sonarr series details', snapshot.error, snapshot.stackTrace); } return LunaMessage.error(onTap: loadCallback); } @@ -143,19 +145,18 @@ class _State extends State<_SonarrSeriesDetailsRoute> with LunaLoadCallbackMixin ? _findLanguageProfile(series.languageProfileId, snapshot.data[3]) : null; List tags = _findTags(series.tags, snapshot.data[1]); - return series == null - ? _unknown - : PageView( - controller: _pageController, - children: _tabs( - series: series, - quality: quality, - language: language, - tags: tags, - ), - ); + if(series == null) return _unknown(); + return PageView( + controller: _pageController, + children: _tabs( + series: series, + quality: quality, + language: language, + tags: tags, + ), + ); } - return LSLoader(); + return LunaLoader(); }, ), ); @@ -170,5 +171,7 @@ class _State extends State<_SonarrSeriesDetailsRoute> with LunaLoadCallbackMixin SonarrSeriesDetailsSeasonList(series: series), ]; - Widget get _unknown => LSGenericMessage(text: 'Series Not Found'); + Widget _unknown() { + return LunaMessage(text: 'Series Not Found'); + } } diff --git a/lib/modules/sonarr/routes/series_details/widgets/appbar_edit_action.dart b/lib/modules/sonarr/routes/series_details/widgets/appbar_edit_action.dart index 7d3090cc..2a0ece14 100644 --- a/lib/modules/sonarr/routes/series_details/widgets/appbar_edit_action.dart +++ b/lib/modules/sonarr/routes/series_details/widgets/appbar_edit_action.dart @@ -19,11 +19,11 @@ class SonarrAppBarSeriesEditAction extends StatelessWidget { if(snapshot.hasError) return Container(); if(snapshot.hasData) { SonarrSeries series = snapshot.data.firstWhere((element) => element.id == seriesId, orElse: () => null); - if(series != null) return LSIconButton( + if(series != null) return LunaIconButton( icon: Icons.edit, onPressed: () async => SonarrEditSeriesRouter().navigateTo(context, seriesId: series.id), ); - } + } return Container(); }, ), diff --git a/lib/modules/sonarr/routes/series_details/widgets/appbar_settings_action.dart b/lib/modules/sonarr/routes/series_details/widgets/appbar_settings_action.dart index abfb9783..775ce198 100644 --- a/lib/modules/sonarr/routes/series_details/widgets/appbar_settings_action.dart +++ b/lib/modules/sonarr/routes/series_details/widgets/appbar_settings_action.dart @@ -19,7 +19,7 @@ class SonarrAppBarSeriesSettingsAction extends StatelessWidget { if(snapshot.hasError) return Container(); if(snapshot.hasData) { SonarrSeries series = snapshot.data.firstWhere((element) => element.id == seriesId, orElse: () => null); - if(series != null) return LSIconButton( + if(series != null) return LunaIconButton( icon: Icons.more_vert, onPressed: () async => handler(context, series), ); @@ -33,13 +33,13 @@ class SonarrAppBarSeriesSettingsAction extends StatelessWidget { BuildContext context, SonarrSeries series, ) async { - List values = await SonarrDialogs.seriesSettings(context, series); - if(values[0]) switch(values[1] as SonarrSeriesSettingsType) { + Tuple2 result = await SonarrDialogs().seriesSettings(context, series); + if(result.item1) switch(result.item2) { case SonarrSeriesSettingsType.EDIT: _edit(context, series); break; case SonarrSeriesSettingsType.DELETE: _delete(context, series); break; case SonarrSeriesSettingsType.REFRESH: _refresh(context, series); break; case SonarrSeriesSettingsType.MONITORED: _monitored(context, series); break; - default: LunaLogger().warning('SonarrAppBarSeriesSettingsAction', '_handler', 'Unknown case: ${(values[1] as SonarrSeriesSettingsType)}'); + default: LunaLogger().warning('SonarrAppBarSeriesSettingsAction', '_handler', 'Unknown case: ${result.item2}'); } } @@ -60,23 +60,20 @@ class SonarrAppBarSeriesSettingsAction extends StatelessWidget { .then((_) { series.monitored = !series.monitored; _state.notify(); - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: series.monitored ? 'Monitoring' : 'No Longer Monitoring', message: series.title, - type: SNACKBAR_TYPE.success, ); }) .catchError((error, stack) { LunaLogger().error('Failed to toggle monitored state for series: ${series.id} / ${series.monitored}', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: series.monitored ? 'Failed to Unmonitor Series' : 'Failed to Monitor Series', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } @@ -89,18 +86,16 @@ class SonarrAppBarSeriesSettingsAction extends StatelessWidget { Sonarr _sonarr = Provider.of(context, listen: false).api; if(_sonarr != null) _sonarr.command.refreshSeries(seriesId: series.id) .then((_) { - LSSnackBar( - context: context, + showLunaInfoSnackBar( title: 'Refreshing...', message: series.title, ); }) .catchError((error, stack) { LunaLogger().error('Unable to refresh series: ${series.id}', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Refresh', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } @@ -116,23 +111,20 @@ class SonarrAppBarSeriesSettingsAction extends StatelessWidget { deleteFiles: _state.removeSeriesDeleteFiles, ) .then((_) { - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: _state.removeSeriesDeleteFiles ? 'Series Removed (With Data)' : 'Series Removed', message: series.title, - type: SNACKBAR_TYPE.success, ); _state.reset(); if(Navigator.of(context).canPop()) Navigator.of(context).pop(); }) .catchError((error, stack) { LunaLogger().error('Failed to remove series: ${series.id}', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Remove Series', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } diff --git a/lib/modules/sonarr/routes/series_details/widgets/navigation_bar.dart b/lib/modules/sonarr/routes/series_details/widgets/navigation_bar.dart index be875841..45dd8d46 100644 --- a/lib/modules/sonarr/routes/series_details/widgets/navigation_bar.dart +++ b/lib/modules/sonarr/routes/series_details/widgets/navigation_bar.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/sonarr.dart'; -class SonarrSeriesDetailsNavigationBar extends StatefulWidget { +class SonarrSeriesDetailsNavigationBar extends StatelessWidget { + final PageController pageController; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + static const List icons = [ Icons.subject_rounded, LunaIcons.television, @@ -13,46 +15,18 @@ class SonarrSeriesDetailsNavigationBar extends StatefulWidget { 'Seasons', ]; - final PageController pageController; - SonarrSeriesDetailsNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - int _index = SonarrDatabaseValue.NAVIGATION_INDEX_SERIES_DETAILS.data; - - @override - void initState() { - super.initState(); - widget.pageController?.addListener(_pageControllerListener); - } - - @override - void dispose() { - widget.pageController?.removeListener(_pageControllerListener); - super.dispose(); - } - - void _pageControllerListener() { - if(widget.pageController.page.round() == _index) return; - setState(() => _index = widget.pageController.page.round()); - } - - @override - Widget build(BuildContext context) => LSBottomNavigationBar( - index: _index, - onTap: _navOnTap, - icons: SonarrSeriesDetailsNavigationBar.icons, - titles: SonarrSeriesDetailsNavigationBar.titles, - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/sonarr/routes/series_details/widgets/overview.dart b/lib/modules/sonarr/routes/series_details/widgets/overview.dart index f0c568ec..7ae07c13 100644 --- a/lib/modules/sonarr/routes/series_details/widgets/overview.dart +++ b/lib/modules/sonarr/routes/series_details/widgets/overview.dart @@ -21,141 +21,147 @@ class SonarrSeriesDetailsOverview extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSListView( - children: [ - _description(context), - _links(), - _information(context), - ], - ); - - Widget _information(BuildContext context) => LSTableBlock( - children: [ - LSTableContent(title: 'path', body: series.path ?? 'Unknown'), - LSTableContent(title: 'size', body: series.sizeOnDisk?.lunaBytesToString(decimals: 1) ?? 'Unknown'), - LSTableContent(title: 'type', body: series.seriesType?.value?.lunaCapitalizeFirstLetters() ?? 'Unknown'), - LSTableContent(title: 'quality', body: quality?.name ?? 'Unknown'), - if(Provider.of(context, listen: false).enableVersion3) LSTableContent(title: 'language', body: language?.name ?? Constants.TEXT_EMDASH), - if(tags != null && tags.length > 0) LSTableContent( - title: 'tags', - body: tags.fold('', (string, tag) => string += ', ${tag.label}').substring(2), - ), - LSTableContent(title: '', body: ''), - LSTableContent(title: 'status', body: series.status?.lunaCapitalizeFirstLetters() ?? 'Unknown'), - LSTableContent(title: 'runtime', body: series.lunaRuntime), - LSTableContent(title: 'network', body: series.network ?? 'Unknown'), - if(series.nextAiring != null) LSTableContent(title: 'next airing', body: series.lunaNextAiring), - if(series.nextAiring != null) LSTableContent(title: 'air time', body: series.lunaAirTime), - ], - ); - - Widget _description(BuildContext context) => LSCard( - child: InkWell( - child: Row( - children: [ - LSNetworkImage( - url: Provider.of(context, listen: false).getPosterURL(series.id), - headers: Provider.of(context, listen: false).headers.cast(), - placeholder: 'assets/images/blanks/video.png', - height: _height, - width: _width, - ), - Expanded( - child: Padding( - child: Container( - child: Column( - children: [ - LSTitle(text: series.title, maxLines: 1), - Text( - series.overview == null || series.overview.isEmpty ? 'No summary is available.\n\n\n' : series.overview, - maxLines: 4, - overflow: TextOverflow.fade, - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - ], - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - ), - height: (_height-(_padding*2)), - ), - padding: EdgeInsets.all(_padding), - ), - ), - ], - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => LunaDialogs().textPreview(context, series.title, series.overview), - ), - decoration: LunaCardDecoration( - uri: Provider.of(context, listen: false).getFanartURL(series.id), - headers: Provider.of(context, listen: false).headers, - ), - ); - - Widget _links() { - return LSContainerRow( + Widget build(BuildContext context) { + return LunaListView( + controller: SonarrSeriesDetailsNavigationBar.scrollControllers[0], children: [ - if(series.imdbId != '') Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset('assets/images/services/imdb.png'), - padding: EdgeInsets.all(12.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await series?.imdbId?.lunaOpenIMDB(), - ), - height: 50.0, - reducedMargin: true, - ), + _description(context), + _links(context), + _information(context), + ], + ); + } + + Widget _information(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'path', body: series.path ?? 'Unknown'), + LunaTableContent(title: 'size', body: series.sizeOnDisk?.lunaBytesToString(decimals: 1) ?? 'Unknown'), + LunaTableContent(title: 'type', body: series.seriesType?.value?.lunaCapitalizeFirstLetters() ?? 'Unknown'), + LunaTableContent(title: 'quality', body: quality?.name ?? 'Unknown'), + if(Provider.of(context, listen: false).enableVersion3) LunaTableContent(title: 'language', body: language?.name ?? LunaUI.TEXT_EMDASH), + LunaTableContent( + title: 'tags', + body: tags != null && tags.length > 0 + ? tags.fold('', (string, tag) => string += ', ${tag.label}').substring(2) + : LunaUI.TEXT_EMDASH, ), - if(series.tvdbId != 0) Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset('assets/images/services/trakt.png'), - padding: EdgeInsets.all(12.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await series?.tvdbId?.toString()?.lunaOpenTraktSeries(), + LunaTableContent(title: '', body: ''), + LunaTableContent(title: 'status', body: series.status?.lunaCapitalizeFirstLetters() ?? 'Unknown'), + LunaTableContent(title: 'runtime', body: series.lunaRuntime), + LunaTableContent(title: 'network', body: series.network ?? 'Unknown'), + if(series.nextAiring != null) LunaTableContent(title: 'next airing', body: series.lunaNextAiring), + if(series.nextAiring != null) LunaTableContent(title: 'air time', body: series.lunaAirTime), + ], + ); + } + + Widget _description(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + child: Row( + children: [ + LunaNetworkImage( + url: Provider.of(context, listen: false).getPosterURL(series.id), + headers: Provider.of(context, listen: false).headers.cast(), + placeholderAsset: 'assets/images/blanks/video.png', + height: _height, + width: _width, ), - height: 50.0, - reducedMargin: true, - ), + Expanded( + child: Padding( + child: Container( + child: Column( + children: [ + LunaText.title(text: series.title, maxLines: 1), + Text( + series.overview == null || series.overview.isEmpty ? 'No summary is available.\n\n\n' : series.overview, + maxLines: 4, + overflow: TextOverflow.fade, + style: TextStyle( + color: Colors.white70, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + ), + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + ), + height: (_height-(_padding*2)), + ), + padding: EdgeInsets.all(_padding), + ), + ), + ], + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, ), - if(series.tvdbId != 0) Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset('assets/images/services/thetvdb.png'), - padding: EdgeInsets.all(12.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await series?.tvdbId?.toString()?.lunaOpenTheTVDB(), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => LunaDialogs().textPreview(context, series.title, series.overview), + ), + decoration: LunaCardDecoration( + uri: Provider.of(context, listen: false).getFanartURL(series.id), + headers: Provider.of(context, listen: false).headers, + ), + ); + } + + Widget _links(BuildContext context) { + return LunaButtonContainer( + children: [ + if(series.imdbId != '') LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset('assets/images/services/imdb.png'), + padding: EdgeInsets.all(12.0), ), - height: 50.0, - reducedMargin: true, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => await series?.imdbId?.lunaOpenIMDB(), ), + height: 50.0, + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), ), - if(series.tvMazeId != 0) Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset('assets/images/services/tvmaze.png'), - padding: EdgeInsets.all(12.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await series?.tvMazeId?.toString()?.lunaOpenTVMaze(), + if(series.tvdbId != 0) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset('assets/images/services/trakt.png'), + padding: EdgeInsets.all(12.0), ), - height: 50.0, - reducedMargin: true, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => await series?.tvdbId?.toString()?.lunaOpenTraktSeries(), ), + height: 50.0, + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + if(series.tvdbId != 0) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset('assets/images/services/thetvdb.png'), + padding: EdgeInsets.all(12.0), + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => await series?.tvdbId?.toString()?.lunaOpenTheTVDB(), + ), + height: 50.0, + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + if(series.tvMazeId != 0) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset('assets/images/services/tvmaze.png'), + padding: EdgeInsets.all(12.0), + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => await series?.tvMazeId?.toString()?.lunaOpenTVMaze(), + ), + height: 50.0, + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), ), ], ); diff --git a/lib/modules/sonarr/routes/series_details/widgets/season_all_tile.dart b/lib/modules/sonarr/routes/series_details/widgets/season_all_tile.dart index c6b68594..742f6468 100644 --- a/lib/modules/sonarr/routes/series_details/widgets/season_all_tile.dart +++ b/lib/modules/sonarr/routes/series_details/widgets/season_all_tile.dart @@ -11,37 +11,43 @@ class SonarrSeriesDetailsSeasonAllTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'All Seasons', darken: !series.monitored), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: series.monitored ? Colors.white70 : Colors.white30, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - TextSpan(text: series?.sizeOnDisk?.lunaBytesToString(decimals: 1) ?? '0.0 B'), - TextSpan(text: '\n'), - TextSpan( - style: TextStyle( - color: series.lunaPercentageComplete == 100 - ? series.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30) - : series.monitored ? LunaColours.red : LunaColours.red.withOpacity(0.30), - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - ), - text: '${series.lunaPercentageComplete}% ${Constants.TEXT_EMDASH} ${series.episodeFileCount ?? 0}/${series.episodeCount ?? 0} Episodes Available', + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'All Seasons', darken: !series.monitored), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + color: series.monitored ? Colors.white70 : Colors.white30, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), - ], + children: [ + TextSpan(text: series?.sizeOnDisk?.lunaBytesToString(decimals: 1) ?? '0.0 B'), + TextSpan(text: '\n'), + TextSpan( + style: TextStyle( + color: series.lunaPercentageComplete == 100 + ? series.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30) + : series.monitored ? LunaColours.red : LunaColours.red.withOpacity(0.30), + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + text: [ + '${series.lunaPercentageComplete}%', + LunaUI.TEXT_EMDASH.lunaPad(), + '${series.episodeFileCount ?? 0}/${series.episodeCount ?? 0}', + 'Episodes Available', + ].join(' '), + ), + ], + ), ), - ), - padContent: true, - trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => SonarrSeasonDetailsRouter().navigateTo( - context, - seriesId: series.id, - seasonNumber: -1, - ); + contentPadding: true, + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => SonarrSeasonDetailsRouter().navigateTo( + context, + seriesId: series.id, + seasonNumber: -1, + ), + ); + } } diff --git a/lib/modules/sonarr/routes/series_details/widgets/season_list.dart b/lib/modules/sonarr/routes/series_details/widgets/season_list.dart index e283d22d..d4aedc2f 100644 --- a/lib/modules/sonarr/routes/series_details/widgets/season_list.dart +++ b/lib/modules/sonarr/routes/series_details/widgets/season_list.dart @@ -12,10 +12,11 @@ class SonarrSeriesDetailsSeasonList extends StatelessWidget { @override Widget build(BuildContext context) { - if(series.seasons.length == 0) return LSGenericMessage(text: 'No Seasons Found'); + if(series.seasons.length == 0) return LunaMessage(text: 'No Seasons Found'); List _seasons = series.seasons; _seasons.sort((a,b) => a.seasonNumber.compareTo(b.seasonNumber)); - return LSListView( + return LunaListView( + controller: SonarrSeriesDetailsNavigationBar.scrollControllers[1], children: [ if(_seasons.length > 1) SonarrSeriesDetailsSeasonAllTile(series: series), ...List.generate( diff --git a/lib/modules/sonarr/routes/series_details/widgets/season_tile.dart b/lib/modules/sonarr/routes/series_details/widgets/season_tile.dart index e8c644e0..292060fb 100644 --- a/lib/modules/sonarr/routes/series_details/widgets/season_tile.dart +++ b/lib/modules/sonarr/routes/series_details/widgets/season_tile.dart @@ -18,15 +18,21 @@ class SonarrSeriesDetailsSeasonTile extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => widget.season == null - ? Container() - : LSCardTile( - title: LSTitle(text: widget.season.seasonNumber == 0 ? 'Specials' : 'Season ${widget.season.seasonNumber}', darken: !widget.season.monitored), + Widget build(BuildContext context) { + if(widget.season == null) return SizedBox(height: 0.0); + return LunaListTile( + context: context, + title: LunaText.title( + text: widget.season.seasonNumber == 0 + ? 'Specials' + : 'Season ${widget.season.seasonNumber}', + darken: !widget.season.monitored, + ), subtitle: RichText( text: TextSpan( style: TextStyle( color: widget.season.monitored ? Colors.white70 : Colors.white30, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan(text: widget.season?.statistics?.sizeOnDisk?.lunaBytesToString(decimals: 1) ?? '0.0 B'), @@ -38,22 +44,34 @@ class _State extends State { : widget.season.monitored ? LunaColours.red : LunaColours.red.withOpacity(0.30), fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), - text: '${widget.season.lunaPercentageComplete}% ${Constants.TEXT_EMDASH} ${widget?.season?.statistics?.episodeFileCount ?? 0}/${widget?.season?.statistics?.totalEpisodeCount ?? 0} Episodes Available', + text: [ + '${widget.season.lunaPercentageComplete}%', + LunaUI.TEXT_EMDASH.lunaPad(), + '${widget?.season?.statistics?.episodeFileCount ?? 0}/${widget?.season?.statistics?.totalEpisodeCount ?? 0}', + 'Episodes Available', + ].join(' '), ), ], ), ), trailing: _trailing(context), - onTap: () async => _onTap(context), + onTap: () async => SonarrSeasonDetailsRouter().navigateTo( + context, + seriesId: widget.seriesId, + seasonNumber: widget.season.seasonNumber, + ), onLongPress: () async => SonarrSeasonDetailsSeasonHeader.handler(context, widget.seriesId, widget.season.seasonNumber), - padContent: true, + contentPadding: true, ); + } - Widget _trailing(BuildContext context) => LSIconButton( - icon: widget.season.monitored ? Icons.turned_in : Icons.turned_in_not, - color: widget.season.monitored ? Colors.white : Colors.white30, - onPressed: _trailingOnPressed, - ); + Widget _trailing(BuildContext context) { + return LunaIconButton( + icon: widget.season.monitored ? Icons.turned_in : Icons.turned_in_not, + color: widget.season.monitored ? Colors.white : Colors.white30, + onPressed: _trailingOnPressed, + ); + } Future _trailingOnPressed() async { SonarrState _state = Provider.of(context, listen: false); @@ -76,15 +94,13 @@ class _State extends State { .then((series) => _state.api.series.updateSeries(series: series)) .then((_) { setState(() {}); - LSSnackBar( - context: context, + showLunaSuccessSnackBar( title: widget.season.monitored ? 'Monitoring' : 'No Longer Monitoring', message: widget.season.seasonNumber == 0 ? 'Specials' : 'Season ${widget.season.seasonNumber}', - type: SNACKBAR_TYPE.success, ); }) .catchError((error, stack) { @@ -92,10 +108,4 @@ class _State extends State { LunaLogger().error('Failed to toggle monitored state: ${widget.seriesId} / ${widget.season.seasonNumber}', error, stack); }); } - - Future _onTap(BuildContext context) async => SonarrSeasonDetailsRouter().navigateTo( - context, - seriesId: widget.seriesId, - seasonNumber: widget.season.seasonNumber, - ); } diff --git a/lib/modules/sonarr/routes/sonarr/route.dart b/lib/modules/sonarr/routes/sonarr/route.dart index 8d651b75..9799447e 100644 --- a/lib/modules/sonarr/routes/sonarr/route.dart +++ b/lib/modules/sonarr/routes/sonarr/route.dart @@ -17,38 +17,25 @@ class _SonarrHomeRoute extends StatefulWidget { class _State extends State<_SonarrHomeRoute> { final GlobalKey _scaffoldKey = GlobalKey(); - final ScrollController _catalogueScrollController = ScrollController(); - PageController _pageController; + LunaPageController _pageController; @override void initState() { super.initState(); - _pageController = PageController(initialPage: SonarrDatabaseValue.NAVIGATION_INDEX.data); + _pageController = LunaPageController(initialPage: SonarrDatabaseValue.NAVIGATION_INDEX.data); } @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ LunaDatabaseValue.ENABLED_PROFILE.key ]), - builder: (context, box, _) => Scaffold( - key: _scaffoldKey, - drawer: _drawer(), - appBar: _appBar(), - bottomNavigationBar: _bottomNavigationBar(), - body: _body(), - ), - ), + return LunaScaffold( + scaffoldKey: _scaffoldKey, + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), ); } - Future _onWillPop() async { - if(_scaffoldKey.currentState.isDrawerOpen) return true; - _scaffoldKey.currentState.openDrawer(); - return false; - } - Widget _drawer() => LunaDrawer(page: LunaModule.SONARR.key); Widget _bottomNavigationBar() { @@ -68,6 +55,7 @@ class _State extends State<_SonarrHomeRoute> { ]; return LunaAppBar.dropdown( title: LunaModule.SONARR.name, + useDrawer: true, profiles: profiles, actions: actions, pageController: _pageController, @@ -83,7 +71,7 @@ class _State extends State<_SonarrHomeRoute> { return PageView( controller: _pageController, children: [ - SonarrSeriesRoute(scrollController: _catalogueScrollController), + SonarrSeriesRoute(), SonarrUpcomingRoute(), SonarrMissingRoute(), SonarrHistoryRoute(), diff --git a/lib/modules/sonarr/routes/sonarr/widgets/appbar_add_series_action.dart b/lib/modules/sonarr/routes/sonarr/widgets/appbar_add_series_action.dart index cad68ede..8011c9d3 100644 --- a/lib/modules/sonarr/routes/sonarr/widgets/appbar_add_series_action.dart +++ b/lib/modules/sonarr/routes/sonarr/widgets/appbar_add_series_action.dart @@ -4,13 +4,13 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrAppBarAddSeriesAction extends StatelessWidget { @override - Widget build(BuildContext context) => LunaIconButton( - icon: Icons.add, - onPressed: () async => _onPressed(context), - ); - - Future _onPressed(BuildContext context) async => SonarrAddSeriesRouter().navigateTo( - context, - query: '', - ); + Widget build(BuildContext context) { + return LunaIconButton( + icon: Icons.add, + onPressed: () async => SonarrAddSeriesRouter().navigateTo( + context, + query: '', + ), + ); + } } diff --git a/lib/modules/sonarr/routes/sonarr/widgets/appbar_global_settings_action.dart b/lib/modules/sonarr/routes/sonarr/widgets/appbar_global_settings_action.dart index dd4d27b5..af658a46 100644 --- a/lib/modules/sonarr/routes/sonarr/widgets/appbar_global_settings_action.dart +++ b/lib/modules/sonarr/routes/sonarr/widgets/appbar_global_settings_action.dart @@ -4,7 +4,7 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrAppBarGlobalSettingsAction extends StatelessWidget { @override - Widget build(BuildContext context) => LSIconButton( + Widget build(BuildContext context) => LunaIconButton( icon: Icons.more_vert, onPressed: () async => _handler(context), ); @@ -32,18 +32,16 @@ class SonarrAppBarGlobalSettingsAction extends StatelessWidget { Sonarr _sonarr = Provider.of(context, listen: false).api; if(_sonarr != null) _sonarr.command.refreshSeries() .then((_) { - LSSnackBar( - context: context, - title: 'Updating Library${Constants.TEXT_ELLIPSIS}', + showLunaInfoSnackBar( + title: 'Updating Library${LunaUI.TEXT_ELLIPSIS}', message: 'Updating library in the background', ); }) .catchError((error, stack) { LunaLogger().error('Unable to update library', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Update Library', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } @@ -52,18 +50,16 @@ class SonarrAppBarGlobalSettingsAction extends StatelessWidget { Sonarr _sonarr = Provider.of(context, listen: false).api; if(_sonarr != null) _sonarr.command.rssSync() .then((_) { - LSSnackBar( - context: context, - title: 'Running RSS Sync${Constants.TEXT_ELLIPSIS}', + showLunaInfoSnackBar( + title: 'Running RSS Sync${LunaUI.TEXT_ELLIPSIS}', message: 'Running RSS sync in the background', ); }) .catchError((error, stack) { LunaLogger().error('Unable to run RSS sync', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Run RSS Sync', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } @@ -74,18 +70,16 @@ class SonarrAppBarGlobalSettingsAction extends StatelessWidget { List _values = await SonarrDialogs.searchAllMissingEpisodes(context); if(_values[0]) _sonarr.command.missingEpisodeSearch() .then((_) { - LSSnackBar( - context: context, - title: 'Searching${Constants.TEXT_ELLIPSIS}', + showLunaInfoSnackBar( + title: 'Searching${LunaUI.TEXT_ELLIPSIS}', message: 'Searching for all missing episodes', ); }) .catchError((error, stack) { LunaLogger().error('Unable to search for all missing episodes', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Search', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } @@ -95,18 +89,16 @@ class SonarrAppBarGlobalSettingsAction extends StatelessWidget { Sonarr _sonarr = Provider.of(context, listen: false).api; if(_sonarr != null) _sonarr.command.backup() .then((_) { - LSSnackBar( - context: context, - title: 'Backing Up Database${Constants.TEXT_ELLIPSIS}', + showLunaInfoSnackBar( + title: 'Backing Up Database${LunaUI.TEXT_ELLIPSIS}', message: 'Backing up the database in the background', ); }) .catchError((error, stack) { LunaLogger().error('Unable to backup database', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Backup Database', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } diff --git a/lib/modules/sonarr/routes/sonarr/widgets/navigation_bar.dart b/lib/modules/sonarr/routes/sonarr/widgets/navigation_bar.dart index 3adc124d..a4cb347d 100644 --- a/lib/modules/sonarr/routes/sonarr/widgets/navigation_bar.dart +++ b/lib/modules/sonarr/routes/sonarr/widgets/navigation_bar.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/sonarr.dart'; -class SonarrNavigationBar extends StatefulWidget { +class SonarrNavigationBar extends StatelessWidget { + final PageController pageController; static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + static const List icons = [ LunaIcons.television, LunaIcons.upcoming, @@ -19,46 +20,18 @@ class SonarrNavigationBar extends StatefulWidget { 'History', ]; - final PageController pageController; - SonarrNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - int _index = SonarrDatabaseValue.NAVIGATION_INDEX.data; - - @override - void initState() { - super.initState(); - widget.pageController?.addListener(_pageControllerListener); - } - - @override - void dispose() { - widget.pageController?.removeListener(_pageControllerListener); - super.dispose(); - } - - void _pageControllerListener() { - if(widget.pageController.page.round() == _index) return; - setState(() => _index = widget.pageController.page.round()); - } - - @override - Widget build(BuildContext context) => LSBottomNavigationBar( - index: _index, - onTap: _navOnTap, - icons: SonarrNavigationBar.icons, - titles: SonarrNavigationBar.titles, - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/sonarr/routes/tags/route.dart b/lib/modules/sonarr/routes/tags/route.dart index a1d89c60..a30e9c16 100644 --- a/lib/modules/sonarr/routes/tags/route.dart +++ b/lib/modules/sonarr/routes/tags/route.dart @@ -16,7 +16,7 @@ class _SonarrTagsRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_SonarrTagsRoute> { +class _State extends State<_SonarrTagsRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @@ -32,53 +32,58 @@ class _State extends State<_SonarrTagsRoute> { } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } - Widget get _appBar => LunaAppBar( - title: 'Tags', - actions: [ - SonarrTagsAppBarActionAddTag(), - ], - ); + Widget _appBar() { + return LunaAppBar( + title: 'Tags', + scrollControllers: [scrollController], + actions: [ + SonarrTagsAppBarActionAddTag(), + ], + ); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().tags, - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr tags', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: context.watch().tags, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr tags', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noTags - : _tags(snapshot.data); - return LSLoader(); - }, - ), - ); + if(snapshot.hasData) return _tags(snapshot.data); + return LunaLoader(); + }, + ), + ); + } - Widget get _noTags => LSGenericMessage( - text: 'No Tags Found', - buttonText: 'Refresh', - showButton: true, - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _tags(List tags) => LSListView( - children: List.generate( - tags.length, - (index) => SonarrTagsTagTile( + Widget _tags(List tags) { + if((tags?.length ?? 0) == 0) return LunaMessage( + text: 'No Tags Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: tags.length, + itemBuilder: (context, index) => SonarrTagsTagTile( key: ObjectKey(tags[index].id), tag: tags[index], ), - ), - ); + ); + } } diff --git a/lib/modules/sonarr/routes/tags/widgets/appbar_action_add_tag.dart b/lib/modules/sonarr/routes/tags/widgets/appbar_action_add_tag.dart index f3b4dbff..0d778109 100644 --- a/lib/modules/sonarr/routes/tags/widgets/appbar_action_add_tag.dart +++ b/lib/modules/sonarr/routes/tags/widgets/appbar_action_add_tag.dart @@ -11,28 +11,28 @@ class SonarrTagsAppBarActionAddTag extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => asDialogButton - ? LSDialog.button(text: 'Add', textColor: Colors.white, onPressed: () async => _onPressed(context)) - : LSIconButton(icon: Icons.add, onPressed: () async => _onPressed(context)); + Widget build(BuildContext context) { + if(asDialogButton) return LunaDialog.button( + text: 'Add', + textColor: Colors.white, + onPressed: () async => _onPressed(context), + ); + return LunaIconButton( + icon: Icons.add, + onPressed: () async => _onPressed(context), + ); + } Future _onPressed(BuildContext context) async { - List _values = await SonarrDialogs.addNewTag(context); - if(_values[0]) context.read().api.tag.addTag(label: _values[1]) + Tuple2 result = await SonarrDialogs().addNewTag(context); + if(result.item1) context.read().api.tag.addTag(label: result.item2) .then((tag) { - showLunaSuccessSnackBar( - context: context, - title: 'Added Tag', - message: tag.label, - ); + showLunaSuccessSnackBar(title: 'Added Tag', message: tag.label); context.read().resetTags(); }) .catchError((error, stack) { - LunaLogger().error('Failed to add tag: ${_values[1]}', error, stack); - showLunaErrorSnackBar( - context: context, - title: 'Failed to Add Tag', - error: error, - ); + LunaLogger().error('Failed to add tag: ${result.item2}', error, stack); + showLunaErrorSnackBar(title: 'Failed to Add Tag', error: error); }); } } diff --git a/lib/modules/sonarr/routes/tags/widgets/tag_tile.dart b/lib/modules/sonarr/routes/tags/widgets/tag_tile.dart index 6b40fa9e..4e42c0d9 100644 --- a/lib/modules/sonarr/routes/tags/widgets/tag_tile.dart +++ b/lib/modules/sonarr/routes/tags/widgets/tag_tile.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; @@ -15,83 +14,72 @@ class SonarrTagsTagTile extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State with LunaLoadCallbackMixin { List seriesList; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _loadSeries()); + Future loadCallback() async { + await context.read().series.then((series) { + List _series = []; + series.forEach((element) { + if(element.tags.contains(widget.tag.id)) _series.add(element.title); + }); + _series.sort(); + if(mounted) setState(() { + seriesList = _series; + }); + }) + .catchError((error) { + if(mounted) setState(() { + seriesList = null; + }); + }); } - Future _loadSeries() async => await context.read().series.then((series) { - List _series = []; - series.forEach((element) { - if(element.tags.contains(widget.tag.id)) _series.add(element.title); - }); - _series.sort(); - if(mounted) setState(() { - seriesList = _series; - }); - }) - .catchError((_) { - if(mounted) setState(() { - seriesList = null; - }); - }); - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.tag.label), - subtitle: LSSubtitle(text: subtitle), - trailing: seriesList == null || seriesList.length != 0 - ? null - : LSIconButton( - icon: Icons.delete, - color: LunaColours.red, - onPressed: _handleDelete, - ), - onTap: _handleInfo, - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: widget.tag.label), + subtitle: LunaText.subtitle(text: subtitle()), + trailing: seriesList == null || seriesList.length != 0 + ? null + : LunaIconButton( + icon: Icons.delete, + color: LunaColours.red, + onPressed: _handleDelete, + ), + onTap: _handleInfo, + ); + } - String get subtitle { + String subtitle() { if(seriesList == null) return 'Loading...'; if(seriesList.length == 0) return 'No Series'; return '${seriesList.length} Series'; } - Future _handleInfo() async => LunaDialogs().textPreview( - context, - 'Series List', - seriesList == null || seriesList.length == 0 ? 'No Series' : seriesList.join('\n'), - ); + Future _handleInfo() async { + return LunaDialogs().textPreview( + context, + 'Series List', + seriesList == null || seriesList.length == 0 ? 'No Series' : seriesList.join('\n'), + ); + } Future _handleDelete() async { if(seriesList == null || seriesList.length != 0) { - showLunaErrorSnackBar( - context: context, - title: 'Cannot Delete Tag', - message: 'The tag must not be attached to any series', - ); + showLunaErrorSnackBar(title: 'Cannot Delete Tag', message: 'The tag must not be attached to any series'); } else { - List _values = await SonarrDialogs.deleteTag(context); - if(_values[0]) context.read().api.tag.deleteTag(id: widget.tag.id) + bool result = await SonarrDialogs().deleteTag(context); + if(result) context.read().api.tag.deleteTag(id: widget.tag.id) .then((_) { - showLunaSuccessSnackBar( - context: context, - title: 'Deleted Tag', - message: widget.tag.label, - ); + showLunaSuccessSnackBar(title: 'Deleted Tag', message: widget.tag.label); context.read().resetTags(); }) .catchError((error, stack) { LunaLogger().error('Failed to delete tag: ${widget.tag.id}', error, stack); - showLunaErrorSnackBar( - context: context, - title: 'Failed to Delete Tag', - error: error, - ); + showLunaErrorSnackBar(title: 'Failed to Delete Tag', error: error); }); } } -} \ No newline at end of file +} diff --git a/lib/modules/sonarr/routes/upcoming/route.dart b/lib/modules/sonarr/routes/upcoming/route.dart index cb7172fa..400d5135 100644 --- a/lib/modules/sonarr/routes/upcoming/route.dart +++ b/lib/modules/sonarr/routes/upcoming/route.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; @@ -9,20 +8,14 @@ class SonarrUpcomingRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { context.read().resetUpcoming(); await context.read().upcoming; } @@ -32,43 +25,40 @@ class _State extends State with AutomaticKeepAliveClientMix super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>>( - selector: (_, state) => state.upcoming, - builder: (context, future, _) => FutureBuilder( - future: future, - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Sonarr upcoming episodes', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector>>( + selector: (_, state) => state.upcoming, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Sonarr upcoming episodes', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) { - return snapshot.data.length == 0 - ? _noEpisodes() - : _episodes(snapshot.data); - } - return LSLoader(); - }, + if(snapshot.hasData) return _episodes(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); - - Widget _noEpisodes() => LSGenericMessage( - text: 'No Episodes Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + ); + } Widget _episodes(List upcoming) { + if((upcoming?.length ?? 0) == 0) return LunaMessage( + text: 'No Episodes Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); // Split episodes into days into a map Map> _episodeMap = upcoming.fold({}, (map, entry) { if(entry.airDateUtc == null) return map; @@ -89,13 +79,14 @@ class _State extends State with AutomaticKeepAliveClientMix )), }); // Return the list - return LSListView( + return LunaListView( + controller: SonarrNavigationBar.scrollControllers[1], children: _episodeWidgets.expand((e) => e).toList(), ); } List _buildDay(String date, List upcoming) => [ - LSHeader(text: date), + LunaHeader(text: date), ...List.generate( upcoming.length, (index) => SonarrUpcomingTile(record: upcoming[index]), diff --git a/lib/modules/sonarr/routes/upcoming/widgets/upcoming_tile.dart b/lib/modules/sonarr/routes/upcoming/widgets/upcoming_tile.dart index 7493c22f..f9f84a32 100644 --- a/lib/modules/sonarr/routes/upcoming/widgets/upcoming_tile.dart +++ b/lib/modules/sonarr/routes/upcoming/widgets/upcoming_tile.dart @@ -46,7 +46,7 @@ class _State extends State { return TextSpan( children: [ TextSpan(text: widget.record.seasonNumber == 0 ? 'Specials ' : 'Season ${widget.record.seasonNumber} '), - TextSpan(text: Constants.TEXT_EMDASH), + TextSpan(text: LunaUI.TEXT_EMDASH), TextSpan(text: ' Episode ${widget.record.episodeNumber}'), ], ); @@ -88,18 +88,15 @@ class _State extends State { Future _trailingOnPressed() async { Provider.of(context, listen: false).api.command.episodeSearch(episodeIds: [widget.record.id]) - .then((_) => LSSnackBar( - context: context, + .then((_) => showLunaSuccessSnackBar( title: 'Searching for Episode...', message: widget.record.title, - type: SNACKBAR_TYPE.success, )) .catchError((error, stack) { LunaLogger().error('Failed to search for episode: ${widget.record.id}', error, stack); - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'Failed to Search', - type: SNACKBAR_TYPE.failure, + error: error, ); }); } diff --git a/lib/modules/tautulli/core/dialogs.dart b/lib/modules/tautulli/core/dialogs.dart index fcba98cd..e30c2571 100644 --- a/lib/modules/tautulli/core/dialogs.dart +++ b/lib/modules/tautulli/core/dialogs.dart @@ -13,19 +13,19 @@ class TautulliDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'lunasea.Settings'.tr(), content: List.generate( TautulliGlobalSettingsType.values.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: TautulliGlobalSettingsType.values[index].name, icon: TautulliGlobalSettingsType.values[index].icon, iconColor: LunaColours.list(index), onTap: () => _setValues(true, TautulliGlobalSettingsType.values[index]), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return Tuple2(_flag, _value); } @@ -43,19 +43,19 @@ class TautulliDialogs { Navigator.of(context, rootNavigator: true).pop(); } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Page', content: List.generate( titles.length, - (index) => LSDialog.tile( + (index) => LunaDialog.tile( text: titles[index], icon: icons[index], iconColor: LunaColours.list(index), onTap: () => _setValues(true, index), ), ), - contentPadding: LSDialog.listDialogContentPadding(), + contentPadding: LunaDialog.listDialogContentPadding(), ); return [_flag, _index]; @@ -73,22 +73,22 @@ class TautulliDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'tautulli.TerminateSession'.tr(), buttons: [ - LSDialog.button( + LunaDialog.button( text: 'tautulli.Terminate'.tr(), textColor: LunaColours.red, onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'tautulli.TerminationConfirmMessage'.tr()+'\n'), - LSDialog.textContent(text: 'tautulli.TerminationAttachMessage'.tr()), + LunaDialog.textContent(text: 'tautulli.TerminationConfirmMessage'.tr()+'\n'), + LunaDialog.textContent(text: 'tautulli.TerminationAttachMessage'.tr()), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'tautulli.TerminationMessage'.tr(), onSubmitted: (_) => _setValues(true), @@ -96,7 +96,7 @@ class TautulliDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return Tuple2(_flag, _textController.text); @@ -114,20 +114,20 @@ class TautulliDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Refresh Rate', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Set the rate at which the activity information will refresh at in seconds.'), + LunaDialog.textContent(text: 'Set the rate at which the activity information will refresh at in seconds.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Refresh Rate', onSubmitted: (_) => _setValues(true), @@ -140,7 +140,7 @@ class TautulliDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, int.tryParse(_textController.text) ?? 10]; @@ -158,20 +158,20 @@ class TautulliDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Statistics Item Count', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Set the amount of items fetched for each category in the statistics.'), + LunaDialog.textContent(text: 'Set the amount of items fetched for each category in the statistics.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Item Count', onSubmitted: (_) => _setValues(true), @@ -184,7 +184,7 @@ class TautulliDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return [_flag, int.tryParse(_textController.text) ?? 3]; @@ -202,20 +202,20 @@ class TautulliDialogs { } } - await LSDialog.dialog( + await LunaDialog.dialog( context: context, title: 'Termination Message', buttons: [ - LSDialog.button( + LunaDialog.button( text: 'Set', onPressed: () => _setValues(true), ), ], content: [ - LSDialog.textContent(text: 'Set a default, prefilled message for terminating sessions.'), + LunaDialog.textContent(text: 'Set a default, prefilled message for terminating sessions.'), Form( key: _formKey, - child: LSDialog.textFormInput( + child: LunaDialog.textFormInput( controller: _textController, title: 'Termination Message', onSubmitted: (_) => _setValues(true), @@ -223,7 +223,7 @@ class TautulliDialogs { ), ), ], - contentPadding: LSDialog.inputTextDialogContentPadding(), + contentPadding: LunaDialog.inputTextDialogContentPadding(), ); return Tuple2(_flag, _textController.text); diff --git a/lib/modules/tautulli/core/extensions/tautulli_history_record.dart b/lib/modules/tautulli/core/extensions/tautulli_history_record.dart index 67ebcb62..c4384628 100644 --- a/lib/modules/tautulli/core/extensions/tautulli_history_record.dart +++ b/lib/modules/tautulli/core/extensions/tautulli_history_record.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; import 'package:lunasea/core.dart'; extension TautulliHistoryRecordExtension on TautulliHistoryRecord { diff --git a/lib/modules/tautulli/core/extensions/tautulli_session.dart b/lib/modules/tautulli/core/extensions/tautulli_session.dart index 51302ae3..24771b94 100644 --- a/lib/modules/tautulli/core/extensions/tautulli_session.dart +++ b/lib/modules/tautulli/core/extensions/tautulli_session.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; extension TautulliSessionExtension on TautulliSession { String get lunaContainer { diff --git a/lib/modules/tautulli/core/extensions/tautulli_table_library.dart b/lib/modules/tautulli/core/extensions/tautulli_table_library.dart index d5ed1f9c..0a06598b 100644 --- a/lib/modules/tautulli/core/extensions/tautulli_table_library.dart +++ b/lib/modules/tautulli/core/extensions/tautulli_table_library.dart @@ -1,5 +1,5 @@ import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; extension TautulliTableLibraryExtension on TautulliTableLibrary { String get readableCount { @@ -17,7 +17,7 @@ extension TautulliTableLibraryExtension on TautulliTableLibrary { String _episodes = this.childCount == 1 ? '1 Episode' : '${this.childCount} Episodes'; - return '$_shows ${Constants.TEXT_BULLET} $_seasons ${Constants.TEXT_BULLET} $_episodes'; + return '$_shows ${LunaUI.TEXT_BULLET} $_seasons ${LunaUI.TEXT_BULLET} $_episodes'; case TautulliSectionType.ARTIST: String _artists = this.count == 1 ? '1 Artist' @@ -28,7 +28,7 @@ extension TautulliTableLibraryExtension on TautulliTableLibrary { String _tracks = this.childCount == 1 ? '1 Track' : '${this.childCount} Tracks'; - return '$_artists ${Constants.TEXT_BULLET} $_albums ${Constants.TEXT_BULLET} $_tracks'; + return '$_artists ${LunaUI.TEXT_BULLET} $_albums ${LunaUI.TEXT_BULLET} $_tracks'; case TautulliSectionType.PHOTO: return this.count == 1 ? '${this.count} Photo' : '${this.count} Photos'; diff --git a/lib/modules/tautulli/core/graphs/bar_graph.dart b/lib/modules/tautulli/core/graphs/bar_graph.dart index 51001230..08748357 100644 --- a/lib/modules/tautulli/core/graphs/bar_graph.dart +++ b/lib/modules/tautulli/core/graphs/bar_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliBarGraphHelper { static const int BAR_COUNT = 7; @@ -19,8 +18,8 @@ class TautulliBarGraphHelper { y: data.series.fold(0, (value, data) => value+data.data[cIndex]), width: BAR_WIDTH, borderRadius: BorderRadius.only( - topLeft: Radius.circular(Constants.UI_BORDER_RADIUS/3), - topRight: Radius.circular(Constants.UI_BORDER_RADIUS/3), + topLeft: Radius.circular(LunaUI.BORDER_RADIUS/3), + topRight: Radius.circular(LunaUI.BORDER_RADIUS/3), ), rodStackItems: List.generate( data.series.length, @@ -39,7 +38,7 @@ class TautulliBarGraphHelper { enabled: true, touchTooltipData: BarTouchTooltipData( tooltipBgColor: LunaTheme.isAMOLEDTheme ? Colors.black : LunaColours.primary, - tooltipRoundedRadius: Constants.UI_BORDER_RADIUS, + tooltipRoundedRadius: LunaUI.BORDER_RADIUS, tooltipPadding: EdgeInsets.all(8.0), maxContentWidth: MediaQuery.of(context).size.width/1.25, fitInsideVertically: true, @@ -59,7 +58,7 @@ class TautulliBarGraphHelper { (_header + _body).trim(), TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBHEADER, + fontSize: LunaUI.FONT_SIZE_SUBHEADER, ), ); }, diff --git a/lib/modules/tautulli/core/graphs/graph.dart b/lib/modules/tautulli/core/graphs/graph.dart index af747565..ddee4318 100644 --- a/lib/modules/tautulli/core/graphs/graph.dart +++ b/lib/modules/tautulli/core/graphs/graph.dart @@ -1,27 +1,24 @@ import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliGraphHelper { static const GRAPH_HEIGHT = 225.0; static const LEGEND_HEIGHT = 26.0; static const DEFAULT_MAX_TITLE_LENGTH = 5; - TautulliGraphHelper._(); + BarChartAlignment chartAlignment() => BarChartAlignment.spaceEvenly; - static BarChartAlignment chartAlignment() => BarChartAlignment.spaceEvenly; + FlGridData gridData() => FlGridData(show: false); - static FlGridData gridData() => FlGridData(show: false); - - static FlBorderData borderData() => FlBorderData( + FlBorderData borderData() => FlBorderData( show: true, border: Border.all(color: Colors.white12), ); - static FlTitlesData titlesData(TautulliGraphData data, { + FlTitlesData titlesData(TautulliGraphData data, { int maxTitleLength = DEFAULT_MAX_TITLE_LENGTH, bool titleOverFlowShowEllipsis = true, }) => FlTitlesData( @@ -35,70 +32,78 @@ class TautulliGraphHelper { getTitles: (value) => data.categories[value.truncate()].length > maxTitleLength+1 ? [ data.categories[value.truncate()].substring(0, min(maxTitleLength, data.categories[value.truncate()].length)).toUpperCase(), - if(titleOverFlowShowEllipsis) Constants.TEXT_ELLIPSIS, + if(titleOverFlowShowEllipsis) LunaUI.TEXT_ELLIPSIS, ].join() : data.categories[value.truncate()].toUpperCase(), getTextStyles: (_) => TextStyle( color: Colors.white30, - fontSize: Constants.UI_FONT_SIZE_GRAPH_LEGEND, + fontSize: LunaUI.FONT_SIZE_GRAPH_LEGEND, ), ), ); - static Widget createLegend(List data) => Container( - child: Row( - children: List.generate( - data.length, - (index) => Padding( - child: Row( - children: [ - Padding( - child: Container( - height: Constants.UI_FONT_SIZE_GRAPH_LEGEND, - width: Constants.UI_FONT_SIZE_GRAPH_LEGEND, - decoration: BoxDecoration( + Widget createLegend(List data) { + return Container( + child: Row( + children: List.generate( + data.length, + (index) => Padding( + child: Row( + children: [ + Padding( + child: Container( + height: LunaUI.FONT_SIZE_GRAPH_LEGEND, + width: LunaUI.FONT_SIZE_GRAPH_LEGEND, + decoration: BoxDecoration( + color: LunaColours.graph(index), + borderRadius: BorderRadius.circular(8.0), + ), + ), + padding: EdgeInsets.only(right: 6.0), + ), + Text( + data[index].name, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_GRAPH_LEGEND, color: LunaColours.graph(index), - borderRadius: BorderRadius.circular(8.0), ), ), - padding: EdgeInsets.only(right: 6.0), - ), - Text( - data[index].name, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_GRAPH_LEGEND, - color: LunaColours.graph(index), - ), - ), - ], - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, + ], + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + ), + padding: EdgeInsets.symmetric(horizontal: 6.0), ), - padding: EdgeInsets.symmetric(horizontal: 6.0), + ), + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + ), + height: LEGEND_HEIGHT, + ); + } + + Widget loadingContainer(BuildContext context) { + return LunaCard( + context: context, + child: Container( + height: GRAPH_HEIGHT+LEGEND_HEIGHT, + child: LunaLoader(), + ), + ); + } + + Widget errorContainer(BuildContext context) { + return LunaCard( + context: context, + child: Container( + height: GRAPH_HEIGHT+LEGEND_HEIGHT, + alignment: Alignment.center, + child: LunaIconButton( + icon: Icons.error, + iconSize: 60.0, + color: Colors.white12, ), ), - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - ), - height: LEGEND_HEIGHT, - ); - - static Widget get loadingContainer => LSCard( - child: Container( - height: GRAPH_HEIGHT+LEGEND_HEIGHT, - child: LSLoader(), - ), - ); - - static Widget get errorContainer => LSCard( - child: Container( - height: GRAPH_HEIGHT+LEGEND_HEIGHT, - alignment: Alignment.center, - child: LSIconButton( - icon: Icons.error, - iconSize: 60.0, - color: Colors.white12, - ), - ), - ); + ); + } } diff --git a/lib/modules/tautulli/core/graphs/line_graph.dart b/lib/modules/tautulli/core/graphs/line_graph.dart index 223e2150..6596dddb 100644 --- a/lib/modules/tautulli/core/graphs/line_graph.dart +++ b/lib/modules/tautulli/core/graphs/line_graph.dart @@ -3,7 +3,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLineGraphHelper { TautulliLineGraphHelper._(); @@ -21,7 +20,7 @@ class TautulliLineGraphHelper { : '??', getTextStyles: (_) => TextStyle( color: Colors.white30, - fontSize: Constants.UI_FONT_SIZE_GRAPH_LEGEND, + fontSize: LunaUI.FONT_SIZE_GRAPH_LEGEND, ), ), ); @@ -56,7 +55,7 @@ class TautulliLineGraphHelper { enabled: true, touchTooltipData: LineTouchTooltipData( tooltipBgColor: LunaTheme.isAMOLEDTheme ? Colors.black : LunaColours.primary, - tooltipRoundedRadius: Constants.UI_BORDER_RADIUS, + tooltipRoundedRadius: LunaUI.BORDER_RADIUS, tooltipPadding: EdgeInsets.all(8.0), maxContentWidth: MediaQuery.of(context).size.width/1.25, fitInsideVertically: true, @@ -72,7 +71,7 @@ class TautulliLineGraphHelper { ].join().trim(), TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBHEADER, + fontSize: LunaUI.FONT_SIZE_SUBHEADER, ), ), ), diff --git a/lib/modules/tautulli/core/state.dart b/lib/modules/tautulli/core/state.dart index 72184271..4c0933d2 100644 --- a/lib/modules/tautulli/core/state.dart +++ b/lib/modules/tautulli/core/state.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliState extends LunaModuleState { TautulliState() { @@ -24,12 +23,6 @@ class TautulliState extends LunaModuleState { _search = null; _statistics = null; _recentlyAdded = null; - _loginLogs = null; - _newsletterLogs = null; - _notificationLogs = null; - _plexMediaScannerLogs = null; - _plexMediaServerLogs = null; - _tautulliLogs = null; _dailyPlayCountGraph = null; _playsByMonthGraph = null; _playCountByDayOfWeekGraph = null; @@ -40,8 +33,6 @@ class TautulliState extends LunaModuleState { _playCountByStreamResolutionGraph = null; _playCountByPlatformStreamTypeGraph = null; _playCountByUserStreamTypeGraph = null; - _updatePlexMediaServer = null; - _updateTautulli = null; _librariesTable = null; _searchQuery = ''; @@ -55,8 +46,6 @@ class TautulliState extends LunaModuleState { _metadata = {}; _libraryWatchTimeStats = {}; _libraryUserStats = {}; - _geolocationInformation = {}; - _whoisInformation = {}; // Reset global data resetProfile(); @@ -347,103 +336,6 @@ class TautulliState extends LunaModuleState { notifyListeners(); } - //////////// - /// LOGS /// - //////////// - - Future _loginLogs; - Future get loginLogs => _loginLogs; - set loginLogs(Future loginLogs) { - assert(loginLogs != null); - _loginLogs = loginLogs; - notifyListeners(); - } - - void resetLoginLogs() { - if(_api != null) _loginLogs = _api.users.getUserLogins( - length: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, - ); - notifyListeners(); - } - - Future _newsletterLogs; - Future get newsletterLogs => _newsletterLogs; - set newsletterLogs(Future newsletterLogs) { - assert(newsletterLogs != null); - _newsletterLogs = newsletterLogs; - notifyListeners(); - } - - void resetNewsletterLogs() { - if(_api != null) _newsletterLogs = _api.notifications.getNewsletterLog( - length: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, - ); - notifyListeners(); - } - - Future _notificationLogs; - Future get notificationLogs => _notificationLogs; - set notificationLogs(Future notificationLogs) { - assert(notificationLogs != null); - _notificationLogs = notificationLogs; - notifyListeners(); - } - - void resetNotificationLogs() { - if(_api != null) _notificationLogs = _api.notifications.getNotificationLog( - length: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, - ); - notifyListeners(); - } - - Future> _plexMediaScannerLogs; - Future> get plexMediaScannerLogs => _plexMediaScannerLogs; - set plexMediaScannerLogs(Future> plexMediaScannerLogs) { - assert(plexMediaScannerLogs != null); - _plexMediaScannerLogs = plexMediaScannerLogs; - notifyListeners(); - } - - void resetPlexMediaScannerLogs() { - if(_api != null) _plexMediaScannerLogs = _api.miscellaneous.getPlexLog( - window: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, - logType: TautulliPlexLogType.SCANNER, - ); - notifyListeners(); - } - - Future> _plexMediaServerLogs; - Future> get plexMediaServerLogs => _plexMediaServerLogs; - set plexMediaServerLogs(Future> plexMediaServerLogs) { - assert(plexMediaServerLogs != null); - _plexMediaServerLogs = plexMediaServerLogs; - notifyListeners(); - } - - void resetPlexMediaServerLogs() { - if(_api != null) _plexMediaServerLogs = _api.miscellaneous.getPlexLog( - window: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, - logType: TautulliPlexLogType.SERVER, - ); - notifyListeners(); - } - - Future> _tautulliLogs; - Future> get tautulliLogs => _tautulliLogs; - set tautulliLogs(Future> tautulliLogs) { - assert(tautulliLogs != null); - _tautulliLogs = tautulliLogs; - notifyListeners(); - } - - void resetTautulliLogs() { - if(_api != null) _tautulliLogs = _api.miscellaneous.getLogs( - start: 0, - end: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, - ); - notifyListeners(); - } - ////////////// /// GRAPHS /// ////////////// @@ -633,41 +525,6 @@ class TautulliState extends LunaModuleState { resetPlayCountByUserStreamTypeGraph(); } - /////////////// - /// UPDATES /// - /////////////// - - Future _updatePlexMediaServer; - Future get updatePlexMediaServer => _updatePlexMediaServer; - set updatePlexMediaServer(Future updatePlexMediaServer) { - assert(updatePlexMediaServer != null); - _updatePlexMediaServer = updatePlexMediaServer; - notifyListeners(); - } - - void resetUpdatePlexMediaServer() { - if(_api != null) _updatePlexMediaServer = _api.system.getPMSUpdate(); - notifyListeners(); - } - - Future _updateTautulli; - Future get updateTautulli => _updateTautulli; - set updateTautulli(Future updateTautulli) { - assert(updateTautulli != null); - _updateTautulli = updateTautulli; - notifyListeners(); - } - - void resetUpdateTautulli() { - if(_api != null) _updateTautulli = _api.system.updateCheck(); - notifyListeners(); - } - - void resetAllUpdates() { - resetUpdatePlexMediaServer(); - resetUpdateTautulli(); - } - ///////////////// /// LIBRARIES /// ///////////////// @@ -744,26 +601,6 @@ class TautulliState extends LunaModuleState { notifyListeners(); } - ////////////////// - /// IP ADDRESS /// - ////////////////// - - Map> _geolocationInformation = {}; - Map> get geolocationInformation => _geolocationInformation; - void fetchGeolocationInformation(String ipAddress) { - assert(ipAddress != null); - _geolocationInformation[ipAddress] = _api.miscellaneous.getGeoIPLookup(ipAddress: ipAddress); - notifyListeners(); - } - - Map> _whoisInformation = {}; - Map> get whoisInformation => _whoisInformation; - void fetchWHOISInformation(String ipAddress) { - assert(ipAddress != null); - _whoisInformation[ipAddress] = _api.miscellaneous.getWHOISLookup(ipAddress: ipAddress); - notifyListeners(); - } - /********* * IMAGES * *********/ diff --git a/lib/modules/tautulli/core/types/media_details_switcher.dart b/lib/modules/tautulli/core/types/media_details_switcher.dart index 3ea0a7cd..4fd5a5a1 100644 --- a/lib/modules/tautulli/core/types/media_details_switcher.dart +++ b/lib/modules/tautulli/core/types/media_details_switcher.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; enum TautulliMediaDetailsSwitcherType { GO_TO_SERIES, diff --git a/lib/modules/tautulli/routes/activity/route.dart b/lib/modules/tautulli/routes/activity/route.dart index b7e16209..4a1c0f76 100644 --- a/lib/modules/tautulli/routes/activity/route.dart +++ b/lib/modules/tautulli/routes/activity/route.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliActivityRoute extends StatefulWidget { TautulliActivityRoute({ diff --git a/lib/modules/tautulli/routes/activity/widgets/activity_tile.dart b/lib/modules/tautulli/routes/activity/widgets/activity_tile.dart index 60da5350..cadcf6b5 100644 --- a/lib/modules/tautulli/routes/activity/widgets/activity_tile.dart +++ b/lib/modules/tautulli/routes/activity/widgets/activity_tile.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:tautulli/tautulli.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; diff --git a/lib/modules/tautulli/routes/activity_details/route.dart b/lib/modules/tautulli/routes/activity_details/route.dart index 204bdfe2..985b981a 100644 --- a/lib/modules/tautulli/routes/activity_details/route.dart +++ b/lib/modules/tautulli/routes/activity_details/route.dart @@ -2,7 +2,6 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliActivityDetailsRouter extends TautulliPageRouter { TautulliActivityDetailsRouter() : super('/tautulli/activity/:sessionid'); diff --git a/lib/modules/tautulli/routes/activity_details/widgets/appbar_metadata_action.dart b/lib/modules/tautulli/routes/activity_details/widgets/appbar_metadata_action.dart index 69712eb7..a75d0f67 100644 --- a/lib/modules/tautulli/routes/activity_details/widgets/appbar_metadata_action.dart +++ b/lib/modules/tautulli/routes/activity_details/widgets/appbar_metadata_action.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliActivityDetailsMetadataAction extends StatelessWidget { final String sessionId; diff --git a/lib/modules/tautulli/routes/activity_details/widgets/appbar_user_action.dart b/lib/modules/tautulli/routes/activity_details/widgets/appbar_user_action.dart index 527de9cb..7633248d 100644 --- a/lib/modules/tautulli/routes/activity_details/widgets/appbar_user_action.dart +++ b/lib/modules/tautulli/routes/activity_details/widgets/appbar_user_action.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliActivityDetailsUserAction extends StatelessWidget { final String sessionId; diff --git a/lib/modules/tautulli/routes/activity_details/widgets/bottom_action_bar.dart b/lib/modules/tautulli/routes/activity_details/widgets/bottom_action_bar.dart index 49fe2067..43c889e5 100644 --- a/lib/modules/tautulli/routes/activity_details/widgets/bottom_action_bar.dart +++ b/lib/modules/tautulli/routes/activity_details/widgets/bottom_action_bar.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliActivityDetailsBottomActionBar extends StatelessWidget { final String sessionId; diff --git a/lib/modules/tautulli/routes/check_for_updates.dart b/lib/modules/tautulli/routes/check_for_updates.dart index 83ccaf33..85a9590d 100644 --- a/lib/modules/tautulli/routes/check_for_updates.dart +++ b/lib/modules/tautulli/routes/check_for_updates.dart @@ -1,2 +1,3 @@ export 'check_for_updates/route.dart'; +export 'check_for_updates/state.dart'; export 'check_for_updates/widgets.dart'; diff --git a/lib/modules/tautulli/routes/check_for_updates/route.dart b/lib/modules/tautulli/routes/check_for_updates/route.dart index 94afb352..75a04728 100644 --- a/lib/modules/tautulli/routes/check_for_updates/route.dart +++ b/lib/modules/tautulli/routes/check_for_updates/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliCheckForUpdatesRouter extends TautulliPageRouter { TautulliCheckForUpdatesRouter() : super('/tautulli/more/checkforupdates'); @@ -17,66 +15,62 @@ class _TautulliCheckForUpdatesRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliCheckForUpdatesRoute> { +class _State extends State<_TautulliCheckForUpdatesRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - bool _initialLoad = false; - - Future _refresh() async { - context.read().resetAllUpdates(); - setState(() => _initialLoad = true); - await Future.wait([ - context.read().updatePlexMediaServer, - context.read().updateTautulli, - ]); - } @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliCheckForUpdatesState(context), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _initialLoad ? _body : LSLoader(), - ); + Widget _appBar() { + return LunaAppBar( + title: 'Check for Updates', + scrollControllers: [scrollController], + ); + } - Widget get _appBar => LunaAppBar(title: 'Check for Updates'); - - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: Future.wait([ - context.watch().updatePlexMediaServer, - context.watch().updateTautulli, - ]), - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch updates', snapshot.error, StackTrace.current); + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchAllUpdates(context), + child: FutureBuilder( + future: Future.wait([ + context.watch().plexMediaServer, + context.watch().tautulli, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch updates', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return _list( - pms: snapshot.data[0], - tautulli: snapshot.data[1], - ); - return LSLoader(); - }, - ), - ); + if(snapshot.hasData) return _list(snapshot.data[0], snapshot.data[1]); + return LunaLoader(); + }, + ), + ); + } - Widget _list({ - @required TautulliPMSUpdate pms, - @required TautulliUpdateCheck tautulli, - }) => LSListView( - children: [ - TautulliCheckForUpdatesPMSTile(update: pms), - TautulliCheckForUpdatesTautulliTile(update: tautulli), - ], - ); + Widget _list(TautulliPMSUpdate pms, TautulliUpdateCheck tautulli) { + return LunaListView( + controller: scrollController, + children: [ + TautulliCheckForUpdatesPMSTile(update: pms), + TautulliCheckForUpdatesTautulliTile(update: tautulli), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/check_for_updates/state.dart b/lib/modules/tautulli/routes/check_for_updates/state.dart new file mode 100644 index 00000000..48cba6ba --- /dev/null +++ b/lib/modules/tautulli/routes/check_for_updates/state.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliCheckForUpdatesState extends ChangeNotifier { + TautulliCheckForUpdatesState(BuildContext context) { + fetchAllUpdates(context); + } + + Future fetchAllUpdates(BuildContext context) async { + fetchPlexMediaServer(context); + fetchTautulli(context); + await Future.wait([ + if(_plexMediaServer != null) _plexMediaServer, + if(_tautulli != null) _tautulli, + ]); + } + + Future _plexMediaServer; + Future get plexMediaServer => _plexMediaServer; + void fetchPlexMediaServer(BuildContext context) { + if(context.read().enabled) { + _plexMediaServer = context.read().api.system.getPMSUpdate(); + } + notifyListeners(); + } + + Future _tautulli; + Future get tautulli => _tautulli; + void fetchTautulli(BuildContext context) { + if(context.read().enabled) { + _tautulli = context.read().api.system.updateCheck(); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/check_for_updates/widgets/pms_tile.dart b/lib/modules/tautulli/routes/check_for_updates/widgets/pms_tile.dart index 5b01122e..fb546c44 100644 --- a/lib/modules/tautulli/routes/check_for_updates/widgets/pms_tile.dart +++ b/lib/modules/tautulli/routes/check_for_updates/widgets/pms_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliCheckForUpdatesPMSTile extends StatelessWidget { final TautulliPMSUpdate update; @@ -11,50 +11,57 @@ class TautulliCheckForUpdatesPMSTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Plex Media Server'), - subtitle: _subtitle, - trailing: _trailing, - padContent: true, - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Plex Media Server'), + subtitle: _subtitle(), + trailing: _trailing(), + contentPadding: true, + ); + } - Widget get _trailing => Column( - children: [ - LSIconButton( - icon: LunaIcons.plex, - color: LunaColours.list(0), - ), - ], - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - ); - - Widget get _subtitle => RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - if(!update.updateAvailable) TextSpan( - text: 'No Updates Available\n', - style: TextStyle( - color: LunaColours.accent, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - ), + Widget _trailing() { + return Column( + children: [ + LunaIconButton( + icon: LunaIcons.plex, + color: LunaColours.list(0), ), - if(!update.updateAvailable) TextSpan(text: 'Current Version: ${update.version}'), - if(update.updateAvailable) TextSpan( - text: 'Update Available\n', - style: TextStyle( - color: LunaColours.orange, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - ), - ), - if(update.updateAvailable) TextSpan(text: 'Latest Version: ${update.version}'), ], - ), - overflow: TextOverflow.fade, - maxLines: 3, - ); + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + ); + } + + Widget _subtitle() { + return RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + children: [ + if(!update.updateAvailable) TextSpan( + text: 'No Updates Available\n', + style: TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + if(!update.updateAvailable) TextSpan(text: 'Current Version: ${update.version}'), + if(update.updateAvailable) TextSpan( + text: 'Update Available\n', + style: TextStyle( + color: LunaColours.orange, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + if(update.updateAvailable) TextSpan(text: 'Latest Version: ${update.version}'), + ], + ), + overflow: TextOverflow.fade, + maxLines: 3, + ); + } } diff --git a/lib/modules/tautulli/routes/check_for_updates/widgets/tautulli_tile.dart b/lib/modules/tautulli/routes/check_for_updates/widgets/tautulli_tile.dart index d74a55c2..6f5a4bf6 100644 --- a/lib/modules/tautulli/routes/check_for_updates/widgets/tautulli_tile.dart +++ b/lib/modules/tautulli/routes/check_for_updates/widgets/tautulli_tile.dart @@ -1,8 +1,7 @@ import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliCheckForUpdatesTautulliTile extends StatelessWidget { final TautulliUpdateCheck update; @@ -13,55 +12,62 @@ class TautulliCheckForUpdatesTautulliTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Tautulli'), - subtitle: _subtitle, - trailing: _trailing, - padContent: true, - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Tautulli'), + subtitle: _subtitle(), + trailing: _trailing(), + contentPadding: true, + ); + } - Widget get _trailing => Column( - children: [ - LSIconButton( - icon: LunaIcons.tautulli, - color: LunaColours.list(1), - ), - ], - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - ); - - Widget get _subtitle => RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - if(!update.update) TextSpan( - text: 'No Updates Available\n', - style: TextStyle( - color: LunaColours.accent, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - ), + Widget _trailing() { + return Column( + children: [ + LunaIconButton( + icon: LunaIcons.tautulli, + color: LunaColours.list(1), ), - if(update.update) TextSpan( - text: 'Update Available\n', - style: TextStyle( - color: LunaColours.orange, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, - ), - ), - if(update.update) TextSpan( - text: 'Current Version: ${update.currentRelease?? update.currentVersion?.substring(0, min(7, update.currentVersion?.length ?? 0)) ?? 'Unknown'}\n', - ), - if(update.update) TextSpan( - text: 'Latest Version: ${update.latestRelease?? update.latestVersion?.substring(0, min(7, update.latestVersion?.length ?? 0)) ?? 'Unknown'}\n', - ), - TextSpan(text: 'Install Type: ${update.installType}'), ], - ), - overflow: TextOverflow.fade, - maxLines: 4, - ); + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + ); + } + + Widget _subtitle() { + return RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + children: [ + if(!update.update) TextSpan( + text: 'No Updates Available\n', + style: TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + if(update.update) TextSpan( + text: 'Update Available\n', + style: TextStyle( + color: LunaColours.orange, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + if(update.update) TextSpan( + text: 'Current Version: ${update.currentRelease?? update.currentVersion?.substring(0, min(7, update.currentVersion?.length ?? 0)) ?? 'Unknown'}\n', + ), + if(update.update) TextSpan( + text: 'Latest Version: ${update.latestRelease?? update.latestVersion?.substring(0, min(7, update.latestVersion?.length ?? 0)) ?? 'Unknown'}\n', + ), + TextSpan(text: 'Install Type: ${update.installType}'), + ], + ), + overflow: TextOverflow.fade, + maxLines: 4, + ); + } } diff --git a/lib/modules/tautulli/routes/graphs/route.dart b/lib/modules/tautulli/routes/graphs/route.dart index 8e02dfe0..f52508a7 100644 --- a/lib/modules/tautulli/routes/graphs/route.dart +++ b/lib/modules/tautulli/routes/graphs/route.dart @@ -16,38 +16,46 @@ class _TautulliGraphsRoute extends StatefulWidget { class _State extends State<_TautulliGraphsRoute> { final GlobalKey _scaffoldKey = GlobalKey(); - PageController _pageController; + LunaPageController _pageController; @override void initState() { super.initState(); - _pageController = PageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_GRAPHS.data); + _pageController = LunaPageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_GRAPHS.data); } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - body: _body, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), + ); + } - Widget get _appBar => LunaAppBar( - title: 'Graphs', - actions: [ - TautulliGraphsTypeButton(), - ], - ); + Widget _appBar() { + return LunaAppBar( + pageController: _pageController, + scrollControllers: TautulliGraphsNavigationBar.scrollControllers, + title: 'Graphs', + actions: [ + TautulliGraphsTypeButton(), + ], + ); + } - Widget get _bottomNavigationBar => TautulliGraphsNavigationBar(pageController: _pageController); + Widget _bottomNavigationBar() { + return TautulliGraphsNavigationBar(pageController: _pageController); + } - Widget get _body => PageView( - controller: _pageController, - children: _tabs, - ); - - List get _tabs => [ - TautulliGraphsPlayByPeriodRoute(), - TautulliGraphsStreamInformationRoute(), - ]; + Widget _body() { + return PageView( + controller: _pageController, + children: [ + TautulliGraphsPlayByPeriodRoute(), + TautulliGraphsStreamInformationRoute(), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/graphs/widgets/navigation_bar.dart b/lib/modules/tautulli/routes/graphs/widgets/navigation_bar.dart index 46383f89..bc34c5fc 100644 --- a/lib/modules/tautulli/routes/graphs/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/routes/graphs/widgets/navigation_bar.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; -class TautulliGraphsNavigationBar extends StatefulWidget { +class TautulliGraphsNavigationBar extends StatelessWidget { + final PageController pageController; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + static const List icons = [ LunaIcons.history, Icons.videocam, @@ -13,7 +15,6 @@ class TautulliGraphsNavigationBar extends StatefulWidget { 'Stream Information', ]; - final PageController pageController; TautulliGraphsNavigationBar({ Key key, @@ -21,38 +22,12 @@ class TautulliGraphsNavigationBar extends StatefulWidget { }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - int _index = TautulliDatabaseValue.NAVIGATION_INDEX_GRAPHS.data; - - @override - void initState() { - super.initState(); - widget.pageController?.addListener(_pageControllerListener); - } - - @override - void dispose() { - widget.pageController?.removeListener(_pageControllerListener); - super.dispose(); - } - - void _pageControllerListener() { - if(widget.pageController.page.round() == _index) return; - setState(() => _index = widget.pageController.page.round()); - } - - @override - Widget build(BuildContext context) => LSBottomNavigationBar( - index: _index, - icons: TautulliGraphsNavigationBar.icons, - titles: TautulliGraphsNavigationBar.titles, - onTap: _navOnTap, - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/tautulli/routes/graphs/widgets/type_button.dart b/lib/modules/tautulli/routes/graphs/widgets/type_button.dart index ae029ae7..fe278e7c 100644 --- a/lib/modules/tautulli/routes/graphs/widgets/type_button.dart +++ b/lib/modules/tautulli/routes/graphs/widgets/type_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsTypeButton extends StatelessWidget { @override @@ -22,7 +21,7 @@ class TautulliGraphsTypeButton extends StatelessWidget { child: Text( TautulliStatsType.values[index].value.lunaCapitalizeFirstLetters(), style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: type == TautulliGraphYAxis.values[index] ? LunaColours.accent : Colors.white, diff --git a/lib/modules/tautulli/routes/graphs_play_by_period/route.dart b/lib/modules/tautulli/routes/graphs_play_by_period/route.dart index 61f39566..5fa1682b 100644 --- a/lib/modules/tautulli/routes/graphs_play_by_period/route.dart +++ b/lib/modules/tautulli/routes/graphs_play_by_period/route.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; @@ -12,20 +11,14 @@ class TautulliGraphsPlayByPeriodRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - Future _refresh() async { + Future loadCallback() async { context.read().resetAllPlayPeriodGraphs(); await Future.wait([ context.read().dailyPlayCountGraph, @@ -41,61 +34,65 @@ class _State extends State with AutomaticKeepAl super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: LSListView( - children: [ - LSHeader( - text: 'Daily', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data} Days', - '\n\n', - 'The total play count or duration of television, movies, and music played per day.' - ].join(), - ), - TautulliGraphsDailyPlayCountGraph(), - LSHeader( - text: 'Monthly', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_MONTHS.data} Months', - '\n\n', - 'The combined total of television, movies, and music by month.', - ].join(), - ), - TautulliGraphsPlaysByMonthGraph(), - LSHeader( - text: 'By Day Of Week', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', - '\n\n', - 'The combined total of television, movies, and music played per day of the week.', - ].join(), - ), - TautulliGraphsPlayCountByDayOfWeekGraph(), - LSHeader( - text: 'By Top Platforms', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', - '\n\n', - 'The combined total of television, movies, and music played by the top most active platforms.', - ].join(), - ), - TautulliGraphsPlayCountByTopPlatformsGraph(), - LSHeader( - text: 'By Top Users', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', - '\n\n', - 'The combined total of television, movies, and music played by the top most active users.', - ].join(), - ), - TautulliGraphsPlayCountByTopUsersGraph(), - ], - ), - ); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: LunaListView( + controller: TautulliGraphsNavigationBar.scrollControllers[0], + children: [ + LunaHeader( + text: 'Daily', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data} Days', + '\n\n', + 'The total play count or duration of television, movies, and music played per day.' + ].join(), + ), + TautulliGraphsDailyPlayCountGraph(), + LunaHeader( + text: 'Monthly', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_MONTHS.data} Months', + '\n\n', + 'The combined total of television, movies, and music by month.', + ].join(), + ), + TautulliGraphsPlaysByMonthGraph(), + LunaHeader( + text: 'By Day Of Week', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', + '\n\n', + 'The combined total of television, movies, and music played per day of the week.', + ].join(), + ), + TautulliGraphsPlayCountByDayOfWeekGraph(), + LunaHeader( + text: 'By Top Platforms', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', + '\n\n', + 'The combined total of television, movies, and music played by the top most active platforms.', + ].join(), + ), + TautulliGraphsPlayCountByTopPlatformsGraph(), + LunaHeader( + text: 'By Top Users', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', + '\n\n', + 'The combined total of television, movies, and music played by the top most active users.', + ].join(), + ), + TautulliGraphsPlayCountByTopUsersGraph(), + ], + ), + ); + } } diff --git a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/daily_play_count_graph.dart b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/daily_play_count_graph.dart index 272f78ab..e3084fa5 100644 --- a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/daily_play_count_graph.dart +++ b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/daily_play_count_graph.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsDailyPlayCountGraph extends StatelessWidget { @override @@ -14,18 +13,19 @@ class TautulliGraphsDailyPlayCountGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByDate', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByDate', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -34,9 +34,9 @@ class TautulliGraphsDailyPlayCountGraph extends StatelessWidget { child: Padding( child: LineChart( LineChartData( - gridData: TautulliGraphHelper.gridData(), + gridData: TautulliGraphHelper().gridData(), titlesData: TautulliLineGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + borderData: TautulliGraphHelper().borderData(), lineBarsData: TautulliLineGraphHelper.lineBarsData(data), lineTouchData: TautulliLineGraphHelper.lineTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsDailyPlayCountGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_day_of_week_graph.dart b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_day_of_week_graph.dart index 0d6cc3a4..f9bf682a 100644 --- a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_day_of_week_graph.dart +++ b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_day_of_week_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlayCountByDayOfWeekGraph extends StatelessWidget { @override @@ -13,18 +12,19 @@ class TautulliGraphsPlayCountByDayOfWeekGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByDayOfWeek', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByDayOfWeek', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +33,10 @@ class TautulliGraphsPlayCountByDayOfWeekGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data, maxTitleLength: 3, titleOverFlowShowEllipsis: false), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data, maxTitleLength: 3, titleOverFlowShowEllipsis: false), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsPlayCountByDayOfWeekGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_platforms_graph.dart b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_platforms_graph.dart index f1bead13..bb389ca7 100644 --- a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_platforms_graph.dart +++ b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_platforms_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlayCountByTopPlatformsGraph extends StatelessWidget { @override @@ -13,18 +12,19 @@ class TautulliGraphsPlayCountByTopPlatformsGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByTopTenPlatforms', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByTopTenPlatforms', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +33,10 @@ class TautulliGraphsPlayCountByTopPlatformsGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsPlayCountByTopPlatformsGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_users_graph.dart b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_users_graph.dart index c29a01d2..75a31841 100644 --- a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_users_graph.dart +++ b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/play_count_by_top_users_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlayCountByTopUsersGraph extends StatelessWidget { @override @@ -13,18 +12,19 @@ class TautulliGraphsPlayCountByTopUsersGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByTopTenUsers', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByTopTenUsers', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +33,10 @@ class TautulliGraphsPlayCountByTopUsersGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsPlayCountByTopUsersGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/plays_by_month_graph.dart b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/plays_by_month_graph.dart index 50d6137e..0aefc7dd 100644 --- a/lib/modules/tautulli/routes/graphs_play_by_period/widgets/plays_by_month_graph.dart +++ b/lib/modules/tautulli/routes/graphs_play_by_period/widgets/plays_by_month_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlaysByMonthGraph extends StatelessWidget { @override @@ -13,18 +12,19 @@ class TautulliGraphsPlaysByMonthGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByMonth', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByMonth', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +33,10 @@ class TautulliGraphsPlaysByMonthGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data, maxTitleLength: 3, titleOverFlowShowEllipsis: false), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data, maxTitleLength: 3, titleOverFlowShowEllipsis: false), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsPlaysByMonthGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_stream_information/route.dart b/lib/modules/tautulli/routes/graphs_stream_information/route.dart index f558719f..c2b9e8b0 100644 --- a/lib/modules/tautulli/routes/graphs_stream_information/route.dart +++ b/lib/modules/tautulli/routes/graphs_stream_information/route.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; @@ -12,20 +11,14 @@ class TautulliGraphsStreamInformationRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - Future _refresh() async { + Future loadCallback() async { context.read().resetAllStreamInformationGraphs(); await Future.wait([ context.read().dailyStreamTypeBreakdownGraph, @@ -41,61 +34,65 @@ class _State extends State with AutomaticK super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: LSListView( - children: [ - LSHeader( - text: 'Daily Stream Type Breakdown', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data} Days', - '\n\n', - 'The total play count or duration of television, movies, and music by the transcode decision.', - ].join(), - ), - TautulliGraphsDailyStreamTypeBreakdownGraph(), - LSHeader( - text: 'By Source Resolution', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', - '\n\n', - 'The combined total of television and movies by their original resolution (pre-transcoding).', - ].join(), - ), - TautulliGraphsPlayCountBySourceResolutionGraph(), - LSHeader( - text: 'By Stream Resolution', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', - '\n\n', - 'The combined total of television and movies by their streamed resolution (post-transcoding).', - ].join(), - ), - TautulliGraphsPlayCountByStreamResolutionGraph(), - LSHeader( - text: 'By Platform Stream Type', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', - '\n\n', - 'The combined total of television, movies, and music by platform and stream type.', - ].join(), - ), - TautulliGraphsPlayCountByPlatformStreamTypeGraph(), - LSHeader( - text: 'By User Stream Type', - subtitle: [ - 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', - '\n\n', - 'The combined total of television, movies, and music by user and stream type.', - ].join(), - ), - TautulliGraphsPlayCountByUserStreamTypeGraph(), - ], - ), - ); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: LunaListView( + controller: TautulliGraphsNavigationBar.scrollControllers[1], + children: [ + LunaHeader( + text: 'Daily Stream Type Breakdown', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data} Days', + '\n\n', + 'The total play count or duration of television, movies, and music by the transcode decision.', + ].join(), + ), + TautulliGraphsDailyStreamTypeBreakdownGraph(), + LunaHeader( + text: 'By Source Resolution', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', + '\n\n', + 'The combined total of television and movies by their original resolution (pre-transcoding).', + ].join(), + ), + TautulliGraphsPlayCountBySourceResolutionGraph(), + LunaHeader( + text: 'By Stream Resolution', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', + '\n\n', + 'The combined total of television and movies by their streamed resolution (post-transcoding).', + ].join(), + ), + TautulliGraphsPlayCountByStreamResolutionGraph(), + LunaHeader( + text: 'By Platform Stream Type', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', + '\n\n', + 'The combined total of television, movies, and music by platform and stream type.', + ].join(), + ), + TautulliGraphsPlayCountByPlatformStreamTypeGraph(), + LunaHeader( + text: 'By User Stream Type', + subtitle: [ + 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', + '\n\n', + 'The combined total of television, movies, and music by user and stream type.', + ].join(), + ), + TautulliGraphsPlayCountByUserStreamTypeGraph(), + ], + ), + ); + } } diff --git a/lib/modules/tautulli/routes/graphs_stream_information/widgets/daily_stream_type_breakdown_graph.dart b/lib/modules/tautulli/routes/graphs_stream_information/widgets/daily_stream_type_breakdown_graph.dart index c47ca48b..5cd1543c 100644 --- a/lib/modules/tautulli/routes/graphs_stream_information/widgets/daily_stream_type_breakdown_graph.dart +++ b/lib/modules/tautulli/routes/graphs_stream_information/widgets/daily_stream_type_breakdown_graph.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsDailyStreamTypeBreakdownGraph extends StatelessWidget { @override @@ -14,18 +13,19 @@ class TautulliGraphsDailyStreamTypeBreakdownGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByDate', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByDate', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -34,9 +34,9 @@ class TautulliGraphsDailyStreamTypeBreakdownGraph extends StatelessWidget { child: Padding( child: LineChart( LineChartData( - gridData: TautulliGraphHelper.gridData(), + gridData: TautulliGraphHelper().gridData(), titlesData: TautulliLineGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + borderData: TautulliGraphHelper().borderData(), lineBarsData: TautulliLineGraphHelper.lineBarsData(data), lineTouchData: TautulliLineGraphHelper.lineTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsDailyStreamTypeBreakdownGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_platform_stream_type_graph.dart b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_platform_stream_type_graph.dart index c22ca9bb..e899d7b9 100644 --- a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_platform_stream_type_graph.dart +++ b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_platform_stream_type_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlayCountByPlatformStreamTypeGraph extends StatelessWidget { @override @@ -13,18 +12,19 @@ class TautulliGraphsPlayCountByPlatformStreamTypeGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getStreamTypeByTopTenPlatforms', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getStreamTypeByTopTenPlatforms', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +33,10 @@ class TautulliGraphsPlayCountByPlatformStreamTypeGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsPlayCountByPlatformStreamTypeGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_source_resolution_graph.dart b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_source_resolution_graph.dart index 1e6577a0..fed2b56c 100644 --- a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_source_resolution_graph.dart +++ b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_source_resolution_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlayCountBySourceResolutionGraph extends StatelessWidget { @override @@ -13,18 +12,19 @@ class TautulliGraphsPlayCountBySourceResolutionGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysBySourceResolution', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysBySourceResolution', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +33,10 @@ class TautulliGraphsPlayCountBySourceResolutionGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsPlayCountBySourceResolutionGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_stream_resolution_graph.dart b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_stream_resolution_graph.dart index 45751d77..d0c8c232 100644 --- a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_stream_resolution_graph.dart +++ b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_stream_resolution_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlayCountByStreamResolutionGraph extends StatelessWidget { @override @@ -13,18 +12,19 @@ class TautulliGraphsPlayCountByStreamResolutionGraph extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByStreamResolution', snapshot.error, StackTrace.current); + LunaLogger().error('Unable to fetch Tautulli graph data: getPlaysByStreamResolution', snapshot.error, snapshot.stackTrace); } - return TautulliGraphHelper.errorContainer; + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +33,10 @@ class TautulliGraphsPlayCountByStreamResolutionGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +44,7 @@ class TautulliGraphsPlayCountByStreamResolutionGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_user_stream_type_graph.dart b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_user_stream_type_graph.dart index ff84dcc2..0503c491 100644 --- a/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_user_stream_type_graph.dart +++ b/lib/modules/tautulli/routes/graphs_stream_information/widgets/play_count_by_user_stream_type_graph.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliGraphsPlayCountByUserStreamTypeGraph extends StatelessWidget { @override @@ -12,19 +11,22 @@ class TautulliGraphsPlayCountByUserStreamTypeGraph extends StatelessWidget { future: future, builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli graph data: getStreamTypeByTopTenUsers', snapshot.error, StackTrace.current); - } - return TautulliGraphHelper.errorContainer; + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli graph data: getStreamTypeByTopTenUsers', + snapshot.error, + snapshot.stackTrace, + ); + return TautulliGraphHelper().errorContainer(context); } if(snapshot.hasData) return _graph(context, snapshot.data); - return TautulliGraphHelper.loadingContainer; + return TautulliGraphHelper().loadingContainer(context); }, ), ); Widget _graph(BuildContext context, TautulliGraphData data) { - return LSCard( + return LunaCard( + context: context, child: Column( children: [ Container( @@ -33,10 +35,10 @@ class TautulliGraphsPlayCountByUserStreamTypeGraph extends StatelessWidget { child: Padding( child: BarChart( BarChartData( - alignment: TautulliGraphHelper.chartAlignment(), - gridData: TautulliGraphHelper.gridData(), - titlesData: TautulliGraphHelper.titlesData(data), - borderData: TautulliGraphHelper.borderData(), + alignment: TautulliGraphHelper().chartAlignment(), + gridData: TautulliGraphHelper().gridData(), + titlesData: TautulliGraphHelper().titlesData(data), + borderData: TautulliGraphHelper().borderData(), barGroups: TautulliBarGraphHelper.barGroups(context, data), barTouchData: TautulliBarGraphHelper.barTouchData(context, data), ), @@ -44,7 +46,7 @@ class TautulliGraphsPlayCountByUserStreamTypeGraph extends StatelessWidget { padding: EdgeInsets.all(14.0), ), ), - TautulliGraphHelper.createLegend(data.series), + TautulliGraphHelper().createLegend(data.series), ], ), ); diff --git a/lib/modules/tautulli/routes/history/route.dart b/lib/modules/tautulli/routes/history/route.dart index 17f70019..25e381fe 100644 --- a/lib/modules/tautulli/routes/history/route.dart +++ b/lib/modules/tautulli/routes/history/route.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliHistoryRoute extends StatefulWidget { TautulliHistoryRoute({ @@ -13,68 +11,63 @@ class TautulliHistoryRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - Future _refresh() async { + Future loadCallback() async { context.read().resetHistory(); await context.read().history; } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - @override Widget build(BuildContext context) { super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>( - selector: (_, state) => state.history, - builder: (context, history, _) => FutureBuilder( - future: history, - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli history', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector>( + selector: (_, state) => state.history, + builder: (context, history, _) => FutureBuilder( + future: history, + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli history', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.records.length == 0 - ? _noHistory() - : _history(snapshot.data); - return LSLoader(); - }, + if(snapshot.hasData) return _history(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); + ); + } - Widget _history(TautulliHistory history) => LSListViewBuilder( - itemCount: history.records.length, - itemBuilder: (context, index) => TautulliHistoryTile( - userId: -1, - history: history.records[index], - ), - ); - - Widget _noHistory() => LSGenericMessage( - text: 'No History Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + Widget _history(TautulliHistory history) { + if((history?.records?.length ?? 0) == 0) return LunaMessage( + text: 'No History Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + controller: TautulliNavigationBar.scrollControllers[2], + itemCount: history.records.length, + itemBuilder: (context, index) => TautulliHistoryTile(history: history.records[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/history/widgets/history_tile.dart b/lib/modules/tautulli/routes/history/widgets/history_tile.dart index dc8d8069..2a4a1cd2 100644 --- a/lib/modules/tautulli/routes/history/widgets/history_tile.dart +++ b/lib/modules/tautulli/routes/history/widgets/history_tile.dart @@ -1,47 +1,47 @@ import 'package:flutter/material.dart'; -import 'package:tautulli/tautulli.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; class TautulliHistoryTile extends StatelessWidget { - final int userId; final TautulliHistoryRecord history; final double _imageDimension = 83.0; final double _padding = 8.0; TautulliHistoryTile({ Key key, - @required this.userId, @required this.history, }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( - child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - child: Row( - children: [ - _poster(context), - _details, - ], + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + child: Row( + children: [ + _poster(context), + _details, + ], + ), + onTap: () async => _onTap(context), ), - onTap: () async => _onTap(context), - ), - decoration: LunaCardDecoration( - uri: context.watch().getImageURLFromRatingKey( - history.grandparentRatingKey ?? history.parentRatingKey ?? history.ratingKey ?? '', - width: MediaQuery.of(context).size.width.truncate(), + decoration: LunaCardDecoration( + uri: context.watch().getImageURLFromRatingKey( + history.grandparentRatingKey ?? history.parentRatingKey ?? history.ratingKey ?? '', + width: MediaQuery.of(context).size.width.truncate(), + ), + headers: context.watch().headers.cast(), ), - headers: context.watch().headers.cast(), - ), - ); + ); + } - Widget _poster(BuildContext context) => LSNetworkImage( + Widget _poster(BuildContext context) => LunaNetworkImage( url: context.watch().getImageURLFromPath(history.thumb), headers: context.watch().headers.cast(), height: _imageDimension, width: _imageDimension/1.5, - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', ); Widget get _details => Expanded( @@ -49,9 +49,9 @@ class TautulliHistoryTile extends StatelessWidget { child: Container( child: Column( children: [ - LSTitle(text: history.lsTitle, maxLines: 1), + LunaText.title(text: history.lsTitle, maxLines: 1), _subtitle, - LSSubtitle(text: history.lsDate), + LunaText.subtitle(text: history.lsDate), _userInfo, ], mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -68,7 +68,7 @@ class TautulliHistoryTile extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ // Television @@ -93,12 +93,12 @@ class TautulliHistoryTile extends StatelessWidget { Padding( child: Icon( history.lsWatchStatusIcon, - size: Constants.UI_FONT_SIZE_SUBHEADER, + size: LunaUI.FONT_SIZE_SUBHEADER, ), padding: EdgeInsets.only(right: 6.0), ), Expanded( - child: LSSubtitle( + child: LunaText.subtitle( text: history.friendlyName ?? 'Unknown User', maxLines: 1, ), diff --git a/lib/modules/tautulli/routes/history_details/route.dart b/lib/modules/tautulli/routes/history_details/route.dart index 4e25f817..17b17dd2 100644 --- a/lib/modules/tautulli/routes/history_details/route.dart +++ b/lib/modules/tautulli/routes/history_details/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliHistoryDetailsRouter extends TautulliPageRouter { TautulliHistoryDetailsRouter() : super('/tautulli/history/:ratingkey/:key/:value'); @@ -59,17 +57,11 @@ class _TautulliHistoryDetailsRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliHistoryDetailsRoute> { +class _State extends State<_TautulliHistoryDetailsRoute> with LunaLoadCallbackMixin, LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { context.read().setIndividualHistory( widget.ratingKey, context.read().api.history.getHistory( @@ -81,44 +73,62 @@ class _State extends State<_TautulliHistoryDetailsRoute> { } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } - Widget get _appBar => LunaAppBar( - title: 'History Details', - actions: [ - TautulliHistoryDetailsUser(ratingKey: widget.ratingKey, sessionKey: widget.sessionKey, referenceId: widget.referenceId), - TautulliHistoryDetailsMetadata(ratingKey: widget.ratingKey, sessionKey: widget.sessionKey, referenceId: widget.referenceId), - ], - ); + Widget _appBar() { + return LunaAppBar( + title: 'History Details', + scrollControllers: [scrollController], + actions: [ + TautulliHistoryDetailsUser(ratingKey: widget.ratingKey, sessionKey: widget.sessionKey, referenceId: widget.referenceId), + TautulliHistoryDetailsMetadata(ratingKey: widget.ratingKey, sessionKey: widget.sessionKey, referenceId: widget.referenceId), + ], + ); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().individualHistory[widget.ratingKey], - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to pull Tautulli history session', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: context.watch().individualHistory[widget.ratingKey], + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to pull Tautulli history session', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () => _refresh()); - } - if(snapshot.hasData) { - TautulliHistoryRecord _record = snapshot.data.records.firstWhere((record) { - if(record.referenceId == (widget.referenceId ?? -1) || record.sessionKey == (widget.sessionKey ?? -1)) return true; - return false; - }, orElse: () => null); - if(_record != null) return TautulliHistoryDetailsInformation(history: _record); - return _unknown; - } - return LSLoader(); - }, - ), - ); + if(snapshot.hasData) { + TautulliHistoryRecord _record = snapshot.data.records.firstWhere((record) { + if(record.referenceId == (widget.referenceId ?? -1) || record.sessionKey == (widget.sessionKey ?? -1)) return true; + return false; + }, orElse: () => null); + if(_record != null) return TautulliHistoryDetailsInformation( + scrollController: scrollController, + history: _record, + ); + return _unknown(); + } + return LunaLoader(); + }, + ), + ); + } - Widget get _unknown => LSGenericMessage(text: 'History Not Found'); + Widget _unknown() { + return LunaMessage.goBack( + context: context, + text: 'History Not Found', + ); + } } diff --git a/lib/modules/tautulli/routes/history_details/widgets/information.dart b/lib/modules/tautulli/routes/history_details/widgets/information.dart index 78c9c85c..29a5bf17 100644 --- a/lib/modules/tautulli/routes/history_details/widgets/information.dart +++ b/lib/modules/tautulli/routes/history_details/widgets/information.dart @@ -2,53 +2,63 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliHistoryDetailsInformation extends StatelessWidget { final TautulliHistoryRecord history; + final ScrollController scrollController; TautulliHistoryDetailsInformation({ Key key, @required this.history, + @required this.scrollController, }): super(key: key); @override - Widget build(BuildContext context) => LSListView( - children: [ - _metadataBlock, - _sessionBlock, - _playerBlock, - ], - ); + Widget build(BuildContext context) { + return LunaListView( + controller: scrollController, + children: [ + LunaHeader(text: 'Metadata'), + _metadataBlock(), + LunaHeader(text: 'Session'), + _sessionBlock(), + LunaHeader(text: 'Player'), + _playerBlock(), + ], + ); + } - Widget get _metadataBlock => LSTableBlock( - title: 'Metadata', - children: [ - LSTableContent(title: 'status', body: history.lsStatus), - LSTableContent(title: 'title', body: history.lsFullTitle), - if(history.year != null) LSTableContent(title: 'year', body: history.year.toString()), - LSTableContent(title: 'user', body: history.friendlyName), - ], - ); + Widget _metadataBlock() { + return LunaTableCard( + content: [ + LunaTableContent(title: 'status', body: history.lsStatus), + LunaTableContent(title: 'title', body: history.lsFullTitle), + if(history.year != null) LunaTableContent(title: 'year', body: history.year.toString()), + LunaTableContent(title: 'user', body: history.friendlyName), + ], + ); + } - Widget get _sessionBlock => LSTableBlock( - title: 'Session', - children: [ - LSTableContent(title: 'state', body: history.lsState), - LSTableContent(title: 'date', body: DateFormat('yyyy-MM-dd').format(history.date)), - LSTableContent(title: 'started', body: history.date.lunaTime), - LSTableContent(title: 'stopped', body: history.state == null ? history.stopped.lunaTime : Constants.TEXT_EMDASH), - LSTableContent(title: 'paused', body: history.pausedCounter.lunaTimestampWords), - ], - ); + Widget _sessionBlock() { + return LunaTableCard( + content: [ + LunaTableContent(title: 'state', body: history.lsState), + LunaTableContent(title: 'date', body: DateFormat('yyyy-MM-dd').format(history.date)), + LunaTableContent(title: 'started', body: history.date.lunaTime), + LunaTableContent(title: 'stopped', body: history.state == null ? history.stopped.lunaTime : LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'paused', body: history.pausedCounter.lunaTimestampWords), + ], + ); + } - Widget get _playerBlock => LSTableBlock( - title: 'Player', - children: [ - LSTableContent(title: 'location', body: history.ipAddress), - LSTableContent(title: 'platform', body: history.platform), - LSTableContent(title: 'product', body: history.product), - LSTableContent(title: 'player', body: history.player), - ], - ); + Widget _playerBlock() { + return LunaTableCard( + content: [ + LunaTableContent(title: 'location', body: history.ipAddress), + LunaTableContent(title: 'platform', body: history.platform), + LunaTableContent(title: 'product', body: history.product), + LunaTableContent(title: 'player', body: history.player), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/history_details/widgets/metadata.dart b/lib/modules/tautulli/routes/history_details/widgets/metadata.dart index d0e03f8a..26a550e3 100644 --- a/lib/modules/tautulli/routes/history_details/widgets/metadata.dart +++ b/lib/modules/tautulli/routes/history_details/widgets/metadata.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliHistoryDetailsMetadata extends StatelessWidget { final int ratingKey; @@ -25,7 +24,7 @@ class TautulliHistoryDetailsMetadata extends StatelessWidget { if(record.referenceId == (referenceId ?? -1) || record.sessionKey == (sessionKey ?? -1)) return true; return false; }, orElse: () => null); - if(_record != null) return LSIconButton( + if(_record != null) return LunaIconButton( icon: Icons.info_outline, onPressed: () async => _onPressed(context, _record), ); @@ -34,5 +33,9 @@ class TautulliHistoryDetailsMetadata extends StatelessWidget { }, ); - Future _onPressed(BuildContext context, TautulliHistoryRecord record) => TautulliMediaDetailsRouter().navigateTo(context, ratingKey: record.ratingKey, mediaType: record.mediaType); + Future _onPressed(BuildContext context, TautulliHistoryRecord record) => TautulliMediaDetailsRouter().navigateTo( + context, + ratingKey: record.ratingKey, + mediaType: record.mediaType, + ); } diff --git a/lib/modules/tautulli/routes/history_details/widgets/user.dart b/lib/modules/tautulli/routes/history_details/widgets/user.dart index 46c4b814..592f847a 100644 --- a/lib/modules/tautulli/routes/history_details/widgets/user.dart +++ b/lib/modules/tautulli/routes/history_details/widgets/user.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliHistoryDetailsUser extends StatelessWidget { final int ratingKey; @@ -25,7 +24,7 @@ class TautulliHistoryDetailsUser extends StatelessWidget { if(record.referenceId == (referenceId ?? -1) || record.sessionKey == (sessionKey ?? -1)) return true; return false; }, orElse: () => null); - if(_record != null) return LSIconButton( + if(_record != null) return LunaIconButton( icon: Icons.person, onPressed: () async => _onPressed(context, _record.userId), ); diff --git a/lib/modules/tautulli/routes/ipaddress_details.dart b/lib/modules/tautulli/routes/ipaddress_details.dart index 49c2d6be..94925037 100644 --- a/lib/modules/tautulli/routes/ipaddress_details.dart +++ b/lib/modules/tautulli/routes/ipaddress_details.dart @@ -1,2 +1,3 @@ export 'ipaddress_details/route.dart'; +export 'ipaddress_details/state.dart'; export 'ipaddress_details/widgets.dart'; diff --git a/lib/modules/tautulli/routes/ipaddress_details/route.dart b/lib/modules/tautulli/routes/ipaddress_details/route.dart index a339c9b3..22c7cda3 100644 --- a/lib/modules/tautulli/routes/ipaddress_details/route.dart +++ b/lib/modules/tautulli/routes/ipaddress_details/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliIPAddressDetailsRouter extends TautulliPageRouter { TautulliIPAddressDetailsRouter() : super('/tautulli/ipaddress/:ipaddress'); @@ -33,57 +31,64 @@ class _TautulliIPAddressRoute extends StatefulWidget { State<_TautulliIPAddressRoute> createState() => _State(); } -class _State extends State<_TautulliIPAddressRoute> { +class _State extends State<_TautulliIPAddressRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - bool _initialLoad = false; @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliIPAddressDetailsState(context, widget.ipAddress), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - Future _refresh() async { - context.read().fetchGeolocationInformation(widget.ipAddress); - context.read().fetchWHOISInformation(widget.ipAddress); - setState(() => _initialLoad = true); - await Future.wait([ - context.read().geolocationInformation[widget.ipAddress], - context.read().whoisInformation[widget.ipAddress], - ]); + Widget _appBar() { + return LunaAppBar( + title: 'IP Address Details', + scrollControllers: [scrollController], + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _initialLoad ? _body : LSLoader(), - ); + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchAll(context), + child: FutureBuilder( + future: Future.wait([ + context.watch().geolocation, + context.watch().whois, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli IP address information', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); + } + if(snapshot.hasData) return _list(snapshot.data[0], snapshot.data[1]); + return LunaLoader(); + }, + ), + ); + } - Widget get _appBar => LunaAppBar(title: 'IP Address Details'); - - Widget get _body => FutureBuilder( - future: Future.wait([ - context.watch().geolocationInformation[widget.ipAddress], - context.watch().whoisInformation[widget.ipAddress], - ]), - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli IP address information', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return _list(snapshot.data[0], snapshot.data[1]); - return LSLoader(); - }, - ); - - Widget _list(TautulliGeolocationInfo geolocation, TautulliWHOISInfo whois) => LSListView( - children: [ - TautulliIPAddressDetailsGeolocationTile(geolocation: geolocation), - TautulliIPAddressDetailsWHOISTile(whois: whois), - ], - ); + Widget _list(TautulliGeolocationInfo geolocation, TautulliWHOISInfo whois) { + return LunaListView( + controller: scrollController, + children: [ + LunaHeader(text: 'Location'), + TautulliIPAddressDetailsGeolocationTile(geolocation: geolocation), + LunaHeader(text: 'Connection'), + TautulliIPAddressDetailsWHOISTile(whois: whois), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/ipaddress_details/state.dart b/lib/modules/tautulli/routes/ipaddress_details/state.dart new file mode 100644 index 00000000..5f9e1c04 --- /dev/null +++ b/lib/modules/tautulli/routes/ipaddress_details/state.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliIPAddressDetailsState extends ChangeNotifier { + final String ipAddress; + TautulliIPAddressDetailsState(BuildContext context, this.ipAddress) { + fetchAll(context); + } + + Future fetchAll(BuildContext context) async { + fetchGeolocation(context); + fetchWHOIS(context); + await Future.wait([ + if(_geolocation != null) _geolocation, + if(_whois != null) _whois, + ]); + } + + Future _geolocation; + Future get geolocation => _geolocation; + void fetchGeolocation(BuildContext context) { + if(context.read().enabled) { + _geolocation = context.read().api.miscellaneous.getGeoIPLookup(ipAddress: ipAddress); + } + notifyListeners(); + } + + Future _whois; + Future get whois => _whois; + void fetchWHOIS(BuildContext context) { + if(context.read().enabled) { + _whois = context.read().api.miscellaneous.getWHOISLookup(ipAddress: ipAddress); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/ipaddress_details/widgets/geolocation_tile.dart b/lib/modules/tautulli/routes/ipaddress_details/widgets/geolocation_tile.dart index e5453a5c..3280fb2b 100644 --- a/lib/modules/tautulli/routes/ipaddress_details/widgets/geolocation_tile.dart +++ b/lib/modules/tautulli/routes/ipaddress_details/widgets/geolocation_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliIPAddressDetailsGeolocationTile extends StatelessWidget { final TautulliGeolocationInfo geolocation; @@ -11,16 +11,17 @@ class TautulliIPAddressDetailsGeolocationTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSTableBlock( - title: 'Location Details', - children: [ - LSTableContent(title: 'country', body: geolocation?.country ?? Constants.TEXT_EMDASH), - LSTableContent(title: 'region', body: geolocation?.region ?? Constants.TEXT_EMDASH), - LSTableContent(title: 'city', body: geolocation?.city ?? Constants.TEXT_EMDASH), - LSTableContent(title: 'postal', body: geolocation?.postalCode ?? Constants.TEXT_EMDASH), - LSTableContent(title: 'timezone', body: geolocation?.timezone ?? Constants.TEXT_EMDASH), - LSTableContent(title: 'latitude', body: '${geolocation?.latitude ?? Constants.TEXT_EMDASH}'), - LSTableContent(title: 'longitude', body: '${geolocation?.longitude ?? Constants.TEXT_EMDASH}'), - ], - ); + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'country', body: geolocation?.country ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'region', body: geolocation?.region ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'city', body: geolocation?.city ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'postal', body: geolocation?.postalCode ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'timezone', body: geolocation?.timezone ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'latitude', body: '${geolocation?.latitude ?? LunaUI.TEXT_EMDASH}'), + LunaTableContent(title: 'longitude', body: '${geolocation?.longitude ?? LunaUI.TEXT_EMDASH}'), + ], + ); + } } \ No newline at end of file diff --git a/lib/modules/tautulli/routes/ipaddress_details/widgets/whois_tile.dart b/lib/modules/tautulli/routes/ipaddress_details/widgets/whois_tile.dart index 888619b6..f767e311 100644 --- a/lib/modules/tautulli/routes/ipaddress_details/widgets/whois_tile.dart +++ b/lib/modules/tautulli/routes/ipaddress_details/widgets/whois_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliIPAddressDetailsWHOISTile extends StatelessWidget { final TautulliWHOISInfo whois; @@ -11,23 +11,26 @@ class TautulliIPAddressDetailsWHOISTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSTableBlock( - title: 'Connection Details', - children: [ - LSTableContent(title: 'host', body: whois?.host ?? Constants.TEXT_EMDASH), - ...whois?.subnets?.fold>([], (list, subnet) { - list.add(LSTableContent( - title: 'isp', - body: [ - subnet?.description ?? Constants.TEXT_EMDASH, - '\n\n${subnet?.address ?? Constants.TEXT_EMDASH}', - '\n${subnet?.city}, ${subnet?.state ?? Constants.TEXT_EMDASH}', - '\n${subnet?.postalCode ?? Constants.TEXT_EMDASH}', - '\n${subnet?.country ?? Constants.TEXT_EMDASH}', - ].join(), - )); - return list; - }), - ], - ); + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'host', body: whois?.host ?? LunaUI.TEXT_EMDASH), + ...whois?.subnets?.fold>([], (list, subnet) { + list.add( + LunaTableContent( + title: 'isp', + body: [ + subnet?.description ?? LunaUI.TEXT_EMDASH, + '\n\n${subnet?.address ?? LunaUI.TEXT_EMDASH}', + '\n${subnet?.city}, ${subnet?.state ?? LunaUI.TEXT_EMDASH}', + '\n${subnet?.postalCode ?? LunaUI.TEXT_EMDASH}', + '\n${subnet?.country ?? LunaUI.TEXT_EMDASH}', + ].join(), + ), + ); + return list; + }), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/libraries/route.dart b/lib/modules/tautulli/routes/libraries/route.dart index 6df12caa..bcc906eb 100644 --- a/lib/modules/tautulli/routes/libraries/route.dart +++ b/lib/modules/tautulli/routes/libraries/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLibrariesRouter extends TautulliPageRouter { TautulliLibrariesRouter() : super('/tautulli/libraries'); @@ -17,62 +15,67 @@ class _TautulliLibrariesRoute extends StatefulWidget { State<_TautulliLibrariesRoute> createState() => _State(); } -class _State extends State<_TautulliLibrariesRoute> { +class _State extends State<_TautulliLibrariesRoute> with LunaScrollControllerMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { + Future loadCallback() async { context.read().resetLibrariesTable(); await context.read().librariesTable; } @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); + Widget _appBar() { + return LunaAppBar( + title: 'Libraries', + scrollControllers: [scrollController], + ); + } - Widget get _appBar => LunaAppBar(title: 'Libraries'); - - Widget get _body => LSRefreshIndicator( - onRefresh: _refresh, - refreshKey: _refreshKey, - child: Selector>( - selector: (_, state) => state.librariesTable, - builder: (context, future, _) => FutureBuilder( - future: future, - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli libraries table', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + onRefresh: loadCallback, + key: _refreshKey, + child: Selector>( + selector: (_, state) => state.librariesTable, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli libraries table', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.libraries.length == 0 - ? _noLibraries() - : _libraries(snapshot.data); - return LSLoader(); - }, + if(snapshot.hasData) return _libraries(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); + ); + } - Widget _noLibraries() => LSGenericMessage( - text: 'No Libraries Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _libraries(TautulliLibrariesTable libraries) => LSListViewBuilder( - itemCount: libraries.libraries.length, - itemBuilder: (context, index) => TautulliLibrariesLibraryTile(library: libraries.libraries[index]), - ); + Widget _libraries(TautulliLibrariesTable libraries) { + if((libraries?.libraries?.length ?? 0) == 0) return LunaMessage( + text: 'No Libraries Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: libraries.libraries.length, + itemBuilder: (context, index) => TautulliLibrariesLibraryTile(library: libraries.libraries[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/libraries/widgets/library_tile.dart b/lib/modules/tautulli/routes/libraries/widgets/library_tile.dart index 9f2c2e54..b5f96f80 100644 --- a/lib/modules/tautulli/routes/libraries/widgets/library_tile.dart +++ b/lib/modules/tautulli/routes/libraries/widgets/library_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLibrariesLibraryTile extends StatelessWidget { final TautulliTableLibrary library; @@ -12,40 +11,43 @@ class TautulliLibrariesLibraryTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: library.sectionName), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - TextSpan(text: '${library.readableCount}\n'), - TextSpan(text: library.plays == 1 - ? '1 Play ${Constants.TEXT_EMDASH} ' - : '${library.plays} Plays ${Constants.TEXT_EMDASH} ', + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: library.sectionName), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), - TextSpan(text: '${library.duration.lunaTimestampWords}\n'), - TextSpan( - style: TextStyle( - color: LunaColours.accent, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, + children: [ + TextSpan(text: '${library.readableCount}\n'), + TextSpan(text: library.plays == 1 + ? '1 Play ${LunaUI.TEXT_EMDASH} ' + : '${library.plays} Plays ${LunaUI.TEXT_EMDASH} ', ), - text: '${library.lastAccessed?.lunaAge ?? 'Unknown'}', - ), - ], + TextSpan(text: '${library.duration.lunaTimestampWords}\n'), + TextSpan( + style: TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + text: '${library.lastAccessed?.lunaAge ?? 'Unknown'}', + ), + ], + ), + overflow: TextOverflow.fade, + maxLines: 3, ), - overflow: TextOverflow.fade, - maxLines: 3, - ), - padContent: true, - decoration: LunaCardDecoration( - uri: context.watch().getImageURLFromPath(library.thumb), - headers: context.watch().headers, - ), - onTap: () async => _onTap(context), - ); + contentPadding: true, + decoration: LunaCardDecoration( + uri: context.watch().getImageURLFromPath(library.thumb), + headers: context.watch().headers, + ), + onTap: () async => _onTap(context), + ); + } Future _onTap(BuildContext context) => TautulliLibrariesDetailsRouter().navigateTo(context, sectionId: library.sectionId); } diff --git a/lib/modules/tautulli/routes/libraries_details/route.dart b/lib/modules/tautulli/routes/libraries_details/route.dart index cf5117cb..406a2c8b 100644 --- a/lib/modules/tautulli/routes/libraries_details/route.dart +++ b/lib/modules/tautulli/routes/libraries_details/route.dart @@ -33,31 +33,39 @@ class _TautulliLibrariesDetailsRoute extends StatefulWidget { class _State extends State<_TautulliLibrariesDetailsRoute> { final GlobalKey _scaffoldKey = GlobalKey(); - PageController _pageController; + LunaPageController _pageController; @override void initState() { super.initState(); - _pageController = PageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_LIBRARIES_DETAILS.data); + _pageController = LunaPageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_LIBRARIES_DETAILS.data); } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - bottomNavigationBar: TautulliLibrariesDetailsNavigationBar(pageController: _pageController), - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + bottomNavigationBar: TautulliLibrariesDetailsNavigationBar(pageController: _pageController), + ); + } - Widget get _appBar => LunaAppBar(title: 'Library Details'); + Widget _appBar() { + return LunaAppBar( + title: 'Library Details', + scrollControllers: TautulliLibrariesDetailsNavigationBar.scrollControllers, + pageController: _pageController, + ); + } - Widget get _body => PageView( - controller: _pageController, - children: _tabs, - ); - - List get _tabs => [ - TautulliLibrariesDetailsInformation(sectionId: widget.sectionId), - TautulliLibrariesDetailsUserStats(sectionId: widget.sectionId), - ]; + Widget _body() { + return PageView( + controller: _pageController, + children: [ + TautulliLibrariesDetailsInformation(sectionId: widget.sectionId), + TautulliLibrariesDetailsUserStats(sectionId: widget.sectionId), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/libraries_details/widgets/information.dart b/lib/modules/tautulli/routes/libraries_details/widgets/information.dart index d440b3b9..f543cef2 100644 --- a/lib/modules/tautulli/routes/libraries_details/widgets/information.dart +++ b/lib/modules/tautulli/routes/libraries_details/widgets/information.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLibrariesDetailsInformation extends StatefulWidget { final int sectionId; @@ -16,7 +14,7 @@ class TautulliLibrariesDetailsInformation extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); bool _initialLoad = false; @@ -24,7 +22,7 @@ class _State extends State with AutomaticKe @override bool get wantKeepAlive => true; - Future _refresh() async { + Future loadCallback() async { context.read().resetLibrariesTable(); context.read().fetchLibraryWatchTimeStats(widget.sectionId); setState(() => _initialLoad = true); @@ -34,59 +32,54 @@ class _State extends State with AutomaticKe ]); } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - @override Widget build(BuildContext context) { super.build(context); return Scaffold( key: _scaffoldKey, - body: _initialLoad ? _body : LSLoader(), + body: _initialLoad ? _body() : LunaLoader(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: Future.wait([ - context.watch().librariesTable, - context.watch().libraryWatchTimeStats[widget.sectionId], - ]), - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - if(snapshot.hasData) { - TautulliTableLibrary library = (snapshot.data[0] as TautulliLibrariesTable).libraries.firstWhere( - (element) => element.sectionId == widget.sectionId, - orElse: () => null, - ); - return library == null - ? _unknown - : _list( - library: library, - watchTimeStats: (snapshot.data[1] as List), + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: Future.wait([ + context.watch().librariesTable, + context.watch().libraryWatchTimeStats[widget.sectionId], + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) return LunaMessage.error(onTap: _refreshKey.currentState.show); + if(snapshot.hasData) { + TautulliTableLibrary library = (snapshot.data[0] as TautulliLibrariesTable).libraries.firstWhere( + (element) => element.sectionId == widget.sectionId, + orElse: () => null, ); - } - return LSLoader(); - }, - ), - ); + return _list(library, snapshot.data[1]); + } + return LunaLoader(); + }, + ), + ); + } - Widget _list({ - @required TautulliTableLibrary library, - @required List watchTimeStats, - }) { - return LSListView( + Widget _list(TautulliTableLibrary library, List watchTimeStats) { + if(library == null) return LunaMessage( + text: 'Library Not Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListView( + controller: TautulliLibrariesDetailsNavigationBar.scrollControllers[0], children: [ + LunaHeader(text: 'Details'), TautulliLibrariesDetailsInformationDetails(library: library), + LunaHeader(text: 'Global Stats'), TautulliLibrariesDetailsInformationGlobalStats(watchtime: watchTimeStats), ], ); } - - Widget get _unknown => LSGenericMessage(text: 'Library Not Found'); } diff --git a/lib/modules/tautulli/routes/libraries_details/widgets/information_details.dart b/lib/modules/tautulli/routes/libraries_details/widgets/information_details.dart index c253ea5e..9e569fa7 100644 --- a/lib/modules/tautulli/routes/libraries_details/widgets/information_details.dart +++ b/lib/modules/tautulli/routes/libraries_details/widgets/information_details.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLibrariesDetailsInformationDetails extends StatelessWidget { final TautulliTableLibrary library; @@ -11,17 +11,23 @@ class TautulliLibrariesDetailsInformationDetails extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSTableBlock( - title: 'Details', - children: [ - LSTableContent(title: 'name', body: library.sectionName), - if(library.count != null) LSTableContent(title: _count(library.count), body: '${library.count} ${_count(library.count)}'), - if(library.parentCount != null) LSTableContent(title: _parentCount(library.parentCount), body: '${library.parentCount} ${_parentCount(library.parentCount)}'), - if(library.childCount != null) LSTableContent(title: _childCount(library.childCount), body: '${library.childCount} ${_childCount(library.childCount)}'), - LSTableContent(title: 'last played', body: library.lastPlayed), - LSTableContent(title: '', body: library.lastAccessed?.lunaAge ?? 'Unknown'), - ], - ); + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'name', body: library.sectionName), + if(library.count != null) LunaTableContent(title: _count(library.count), body: '${library.count} ${_count(library.count)}'), + if(library.parentCount != null) LunaTableContent(title: _parentCount(library.parentCount), body: '${library.parentCount} ${_parentCount(library.parentCount)}'), + if(library.childCount != null) LunaTableContent(title: _childCount(library.childCount), body: '${library.childCount} ${_childCount(library.childCount)}'), + LunaTableContent( + title: 'last played', + body: [ + library?.lastPlayed ?? LunaUI.TEXT_EMDASH, + library.lastAccessed?.lunaAge ?? 'Unknown', + ].join('\n'), + ), + ], + ); + } String _count(int value) { switch(library.sectionType) { diff --git a/lib/modules/tautulli/routes/libraries_details/widgets/information_global_stats.dart b/lib/modules/tautulli/routes/libraries_details/widgets/information_global_stats.dart index 84fbc5d3..cc9cf24a 100644 --- a/lib/modules/tautulli/routes/libraries_details/widgets/information_global_stats.dart +++ b/lib/modules/tautulli/routes/libraries_details/widgets/information_global_stats.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLibrariesDetailsInformationGlobalStats extends StatelessWidget { final List watchtime; @@ -13,16 +13,17 @@ class TautulliLibrariesDetailsInformationGlobalStats extends StatelessWidget { @override - Widget build(BuildContext context) => LSTableBlock( - title: 'Global Stats', - children: List.generate( - watchtime.length, - (index) => LSTableContent( - title: _title(watchtime[index].queryDays), - body: _body(watchtime[index].totalPlays, watchtime[index].totalTime), + Widget build(BuildContext context) { + return LunaTableCard( + content: List.generate( + watchtime.length, + (index) => LunaTableContent( + title: _title(watchtime[index].queryDays), + body: _body(watchtime[index].totalPlays, watchtime[index].totalTime), + ), ), - ), - ); + ); + } String _title(int days) { if(days == 0) return 'All Time'; diff --git a/lib/modules/tautulli/routes/libraries_details/widgets/navigation_bar.dart b/lib/modules/tautulli/routes/libraries_details/widgets/navigation_bar.dart index 564871fd..e83117e0 100644 --- a/lib/modules/tautulli/routes/libraries_details/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/routes/libraries_details/widgets/navigation_bar.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; -class TautulliLibrariesDetailsNavigationBar extends StatefulWidget { +class TautulliLibrariesDetailsNavigationBar extends StatelessWidget { + final PageController pageController; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + static const List icons = [ Icons.info_outline, Icons.people, @@ -13,46 +15,18 @@ class TautulliLibrariesDetailsNavigationBar extends StatefulWidget { 'User Stats', ]; - final PageController pageController; - TautulliLibrariesDetailsNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - int _index = TautulliDatabaseValue.NAVIGATION_INDEX_LIBRARIES_DETAILS.data; - - @override - void initState() { - super.initState(); - widget.pageController?.addListener(_pageControllerListener); - } - - @override - void dispose() { - widget.pageController?.removeListener(_pageControllerListener); - super.dispose(); - } - - void _pageControllerListener() { - if(widget.pageController.page.round() == _index) return; - setState(() => _index = widget.pageController.page.round()); - } - - @override - Widget build(BuildContext context) => LSBottomNavigationBar( - index: _index, - icons: TautulliLibrariesDetailsNavigationBar.icons, - titles: TautulliLibrariesDetailsNavigationBar.titles, - onTap: _navOnTap, - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/tautulli/routes/libraries_details/widgets/user_stats.dart b/lib/modules/tautulli/routes/libraries_details/widgets/user_stats.dart index 41ddde2c..d8cb68bd 100644 --- a/lib/modules/tautulli/routes/libraries_details/widgets/user_stats.dart +++ b/lib/modules/tautulli/routes/libraries_details/widgets/user_stats.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLibrariesDetailsUserStats extends StatefulWidget { final int sectionId; @@ -16,54 +14,60 @@ class TautulliLibrariesDetailsUserStats extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - Future _refresh() async { + Future loadCallback() async { context.read().fetchLibraryUserStats(widget.sectionId); await context.read().libraryUserStats[widget.sectionId]; } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - @override Widget build(BuildContext context) { super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().libraryUserStats[widget.sectionId], - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - if(snapshot.hasData) return snapshot.data.length == 0 ? _noStatsFound : _list(userStats: snapshot.data); - return LSLoader(); - }, - ), - ); - - Widget _list({ @required List userStats }) { - return LSListView( - children: List.generate( - userStats.length, - (index) => TautulliLibrariesDetailsUserStatsTile(user: userStats[index]), + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: context.watch().libraryUserStats[widget.sectionId], + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Failed to fetch library watch stats', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); + } + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, ), ); } - Widget get _noStatsFound => LSGenericMessage(text: 'No User Found'); + Widget _list(List stats) { + if((stats?.length ?? 0) == 0) return LunaMessage( + text: 'No Users Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: TautulliLibrariesDetailsNavigationBar.scrollControllers[1], + itemCount: stats.length, + itemBuilder: (context, index) => TautulliLibrariesDetailsUserStatsTile(user: stats[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/libraries_details/widgets/user_stats_tile.dart b/lib/modules/tautulli/routes/libraries_details/widgets/user_stats_tile.dart index 1a0d8b91..08e3d7da 100644 --- a/lib/modules/tautulli/routes/libraries_details/widgets/user_stats_tile.dart +++ b/lib/modules/tautulli/routes/libraries_details/widgets/user_stats_tile.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLibrariesDetailsUserStatsTile extends StatelessWidget { final TautulliLibraryUserStats user; - final double _imageDimension = 70.0; - final double _padding = 8.0; TautulliLibrariesDetailsUserStatsTile({ Key key, @@ -14,87 +11,59 @@ class TautulliLibrariesDetailsUserStatsTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( - child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - child: Row( - children: [ - _userThumb(context), - _details, - ], - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - ), - onTap: () async => _onPressed(context), - ), - ); + Widget build(BuildContext context) { + return LunaTwoLineCardWithPoster( + posterUrl: context.watch().getImageURLFromPath(user.userThumb), + posterHeaders: context.watch().headers.cast(), + posterPlaceholder: 'assets/images/blanks/user.png', + title: user.friendlyName, + customSubtitle1: _subtitle1(), + customSubtitle2: _subtitle2(), + onTap: () async => _onTap(context), + ); + } - Widget _userThumb(BuildContext context) => LSNetworkImage( - url: context.watch().getImageURLFromPath(user.userThumb), - headers: context.watch().headers.cast(), - placeholder: 'assets/images/blanks/user.png', - height: _imageDimension, - width: _imageDimension, - ); - - Widget get _details => Expanded( - child: Padding( - child: Container( - child: Column( - children: [ - _title, - _userId, - _totalPlays, - ], - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + Widget _subtitle1() { + return Row( + children: [ + Padding( + child: Icon( + Icons.play_arrow_rounded, + size: LunaUI.FONT_SIZE_SUBTITLE, + ), + padding: EdgeInsets.only(right: 6.0), ), - height: (_imageDimension-(_padding*2)), - ), - padding: EdgeInsets.all(_padding), - ), - ); - - Widget get _title => LSTitle(text: user.friendlyName); - - Widget get _totalPlays => Row( - children: [ - Padding( - child: Icon( - Icons.play_arrow, - size: Constants.UI_FONT_SIZE_SUBTITLE, + Expanded( + child: LunaText.subtitle( + text: user.totalPlays == 1 ? '1 Play' : '${user.totalPlays} Plays', + maxLines: 1, + ), ), - padding: EdgeInsets.only(right: 6.0), - ), - Expanded( - child: LSSubtitle( - text: user.totalPlays == 1 ? '1 Play' : '${user.totalPlays} Plays', - maxLines: 1, - ), - ), - ], - ); + ], + ); + } - Widget get _userId => Row( - children: [ - Padding( - child: Icon( - Icons.person, - size: Constants.UI_FONT_SIZE_SUBTITLE, + Widget _subtitle2() { + return Row( + children: [ + Padding( + child: Icon( + Icons.person_rounded, + size: LunaUI.FONT_SIZE_SUBTITLE, + ), + padding: EdgeInsets.only(right: 6.0), ), - padding: EdgeInsets.only(right: 6.0), - ), - Expanded( - child: LSSubtitle( - text: user.userId.toString(), - maxLines: 1, + Expanded( + child: LunaText.subtitle( + text: user.userId.toString(), + maxLines: 1, + ), ), - ), - ], - ); + ], + ); + } - Future _onPressed(BuildContext context) => TautulliUserDetailsRouter().navigateTo( + Future _onTap(BuildContext context) => TautulliUserDetailsRouter().navigateTo( context, userId: user.userId, ); diff --git a/lib/modules/tautulli/routes/logs.dart b/lib/modules/tautulli/routes/logs.dart index 1de871a3..2723fba7 100644 --- a/lib/modules/tautulli/routes/logs.dart +++ b/lib/modules/tautulli/routes/logs.dart @@ -1,2 +1 @@ export 'logs/route.dart'; -export 'logs/widgets.dart'; diff --git a/lib/modules/tautulli/routes/logs/route.dart b/lib/modules/tautulli/routes/logs/route.dart index ac0e463d..28ebd14f 100644 --- a/lib/modules/tautulli/routes/logs/route.dart +++ b/lib/modules/tautulli/routes/logs/route.dart @@ -15,26 +15,90 @@ class _TautulliLogsRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliLogsRoute> { +class _State extends State<_TautulliLogsRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } - Widget get _appBar => LunaAppBar(title: 'Logs'); + Widget _appBar() { + return LunaAppBar( + title: 'Logs', + scrollControllers: [scrollController], + ); + } - Widget get _body => LSListView( - children: [ - TautulliLogsLoginsTile(), - TautulliLogsNewslettersTile(), - TautulliLogsNotificationsTile(), - TautulliLogsPlexMediaScannerTile(), - TautulliLogsPlexMediaServerTile(), - TautulliLogsTautulliTile(), - ], - ); + Widget _body() { + return LunaListView( + controller: scrollController, + children: [ + LunaListTile( + context: context, + title: LunaText.title(text: 'Logins'), + subtitle: LunaText.subtitle(text: 'Tautulli Login Logs'), + trailing: LunaIconButton( + icon: Icons.vpn_key_rounded, + color: LunaColours.list(0), + ), + onTap: () async => TautulliLogsLoginsRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Newsletters'), + subtitle: LunaText.subtitle(text: 'Tautulli Newsletter Logs'), + trailing: LunaIconButton( + icon: Icons.email_rounded, + color: LunaColours.list(1), + ), + onTap: () async => TautulliLogsNewslettersRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Notifications'), + subtitle: LunaText.subtitle(text: 'Tautulli Notification Logs'), + trailing: LunaIconButton( + icon: Icons.notifications_rounded, + color: LunaColours.list(2), + ), + onTap: () async => TautulliLogsNotificationsRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Plex Media Scanner'), + subtitle: LunaText.subtitle(text: 'Plex Media Scanner Logs'), + trailing: LunaIconButton( + icon: Icons.scanner_rounded, + color: LunaColours.list(3), + ), + onTap: () async => TautulliLogsPlexMediaScannerRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Plex Media Server'), + subtitle: LunaText.subtitle(text: 'Plex Media Server Logs'), + trailing: LunaIconButton( + icon: LunaIcons.plex, + color: LunaColours.list(4), + ), + onTap: () async => TautulliLogsPlexMediaServerRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Tautulli'), + subtitle: LunaText.subtitle(text: 'Tautulli Logs'), + trailing: LunaIconButton( + icon: LunaIcons.tautulli, + color: LunaColours.list(5), + ), + onTap: () async => TautulliLogsTautulliRouter().navigateTo(context), + ), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/logs/widgets.dart b/lib/modules/tautulli/routes/logs/widgets.dart deleted file mode 100644 index fb9fe900..00000000 --- a/lib/modules/tautulli/routes/logs/widgets.dart +++ /dev/null @@ -1,6 +0,0 @@ -export 'widgets/logins_tile.dart'; -export 'widgets/newsletters_tile.dart'; -export 'widgets/notifications_tile.dart'; -export 'widgets/plex_media_scanner.dart'; -export 'widgets/plex_media_server_tile.dart'; -export 'widgets/tautulli_tile.dart'; diff --git a/lib/modules/tautulli/routes/logs/widgets/logins_tile.dart b/lib/modules/tautulli/routes/logs/widgets/logins_tile.dart deleted file mode 100644 index 23fe2ad3..00000000 --- a/lib/modules/tautulli/routes/logs/widgets/logins_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliLogsLoginsTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Logins'), - subtitle: LSSubtitle(text: 'Tautulli Login Logs'), - trailing: LSIconButton( - icon: Icons.vpn_key, - color: LunaColours.list(0), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLogsLoginsRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/logs/widgets/newsletters_tile.dart b/lib/modules/tautulli/routes/logs/widgets/newsletters_tile.dart deleted file mode 100644 index c6f2cef0..00000000 --- a/lib/modules/tautulli/routes/logs/widgets/newsletters_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliLogsNewslettersTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Newsletters'), - subtitle: LSSubtitle(text: 'Tautulli Newsletter Logs'), - trailing: LSIconButton( - icon: Icons.email, - color: LunaColours.list(1), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLogsNewslettersRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/logs/widgets/notifications_tile.dart b/lib/modules/tautulli/routes/logs/widgets/notifications_tile.dart deleted file mode 100644 index 15fea7ca..00000000 --- a/lib/modules/tautulli/routes/logs/widgets/notifications_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliLogsNotificationsTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Notifications'), - subtitle: LSSubtitle(text: 'Tautulli Notification Logs'), - trailing: LSIconButton( - icon: Icons.notifications, - color: LunaColours.list(2), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLogsNotificationsRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/logs/widgets/plex_media_scanner.dart b/lib/modules/tautulli/routes/logs/widgets/plex_media_scanner.dart deleted file mode 100644 index 9fc35a8e..00000000 --- a/lib/modules/tautulli/routes/logs/widgets/plex_media_scanner.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliLogsPlexMediaScannerTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Plex Media Scanner'), - subtitle: LSSubtitle(text: 'Plex Media Scanner Logs'), - trailing: LSIconButton( - icon: Icons.scanner, - color: LunaColours.list(3), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLogsPlexMediaScannerRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/logs/widgets/plex_media_server_tile.dart b/lib/modules/tautulli/routes/logs/widgets/plex_media_server_tile.dart deleted file mode 100644 index a6f75156..00000000 --- a/lib/modules/tautulli/routes/logs/widgets/plex_media_server_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliLogsPlexMediaServerTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Plex Media Server'), - subtitle: LSSubtitle(text: 'Plex Media Server Logs'), - trailing: LSIconButton( - icon: LunaIcons.plex, - color: LunaColours.list(4), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLogsPlexMediaServerRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/logs/widgets/tautulli_tile.dart b/lib/modules/tautulli/routes/logs/widgets/tautulli_tile.dart deleted file mode 100644 index 7f216ff7..00000000 --- a/lib/modules/tautulli/routes/logs/widgets/tautulli_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliLogsTautulliTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Tautulli'), - subtitle: LSSubtitle(text: 'Tautulli Logs'), - trailing: LSIconButton( - icon: LunaIcons.tautulli, - color: LunaColours.list(5), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLogsTautulliRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/logs_logins.dart b/lib/modules/tautulli/routes/logs_logins.dart index 02e3965c..2ce8b60e 100644 --- a/lib/modules/tautulli/routes/logs_logins.dart +++ b/lib/modules/tautulli/routes/logs_logins.dart @@ -1,2 +1,3 @@ export 'logs_logins/route.dart'; +export 'logs_logins/state.dart'; export 'logs_logins/widgets.dart'; diff --git a/lib/modules/tautulli/routes/logs_logins/route.dart b/lib/modules/tautulli/routes/logs_logins/route.dart index 9ec861ec..4594fbd9 100644 --- a/lib/modules/tautulli/routes/logs_logins/route.dart +++ b/lib/modules/tautulli/routes/logs_logins/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLogsLoginsRouter extends TautulliPageRouter { TautulliLogsLoginsRouter() : super('/tautulli/logs/logins'); @@ -17,62 +15,62 @@ class _TautulliLogsLoginsRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliLogsLoginsRoute> { +class _State extends State<_TautulliLogsLoginsRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { - context.read().resetLoginLogs(); - await context.read().loginLogs; + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliLogsLoginsState(context), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget _appBar() { + return LunaAppBar( + title: 'Login Logs', + scrollControllers: [scrollController], + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar(title: 'Login Logs'); - - Widget get _body => LSRefreshIndicator( - onRefresh: _refresh, - refreshKey: _refreshKey, - child: Selector>( - selector: (_, state) => state.loginLogs, - builder: (context, logs, _) => FutureBuilder( - future: logs, + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchLogs(context), + child: FutureBuilder( + future: context.select((TautulliLogsLoginsState state) => state.logs), builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli login logs', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli login logs', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - if(snapshot.hasData) return snapshot.data.logins.length == 0 - ? _noLogs() - : _logs(snapshot.data); - return LSLoader(); + if(snapshot.hasData) return _logs(snapshot.data); + return LunaLoader(); }, ), - ), - ); + ); + } - Widget _noLogs() => LSGenericMessage( - text: 'No Logs Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _logs(TautulliUserLogins logs) => LSListViewBuilder( - itemCount: logs.logins.length, - itemBuilder: (context, index) => TautulliLogsLoginsLogTile(login: logs.logins[index]), - ); + Widget _logs(TautulliUserLogins logs) { + if((logs?.logins?.length ?? 0) == 0) return LunaMessage( + text: 'No Logs Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: logs.logins.length, + itemBuilder: (context, index) => TautulliLogsLoginsLogTile(login: logs.logins[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/logs_logins/state.dart b/lib/modules/tautulli/routes/logs_logins/state.dart new file mode 100644 index 00000000..1ac2e5f9 --- /dev/null +++ b/lib/modules/tautulli/routes/logs_logins/state.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsLoginsState extends ChangeNotifier { + TautulliLogsLoginsState(BuildContext context) { + fetchLogs(context); + } + + Future _logs; + Future get logs => _logs; + Future fetchLogs(BuildContext context) async { + if(context.read().enabled) { + _logs = context.read().api.users.getUserLogins( + length: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, + ); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/logs_logins/widgets/log_tile.dart b/lib/modules/tautulli/routes/logs_logins/widgets/log_tile.dart index 772bc68e..761f8c63 100644 --- a/lib/modules/tautulli/routes/logs_logins/widgets/log_tile.dart +++ b/lib/modules/tautulli/routes/logs_logins/widgets/log_tile.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLogsLoginsLogTile extends StatelessWidget { final TautulliUserLoginRecord login; @@ -12,47 +12,54 @@ class TautulliLogsLoginsLogTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: login.friendlyName), - subtitle: _subtitle, - trailing: _trailing, - padContent: true, - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: login.friendlyName), + subtitle: _subtitle(), + trailing: _trailing(), + contentPadding: true, + ); + } - Widget get _subtitle => RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - TextSpan(text: '${login.ipAddress}\n'), - TextSpan(text: '${login.os}\n'), - TextSpan(text: '${login.host}\n'), - TextSpan( - text: LunaDatabaseValue.USE_24_HOUR_TIME.data - ? DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} HH:mm').format(login.timestamp) - : DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} hh:mm a').format(login.timestamp), - style: TextStyle( - color: LunaColours.accent, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, + Widget _subtitle() { + return RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + children: [ + TextSpan(text: '${login.ipAddress}\n'), + TextSpan(text: '${login.os}\n'), + TextSpan(text: '${login.host}\n'), + TextSpan( + text: LunaDatabaseValue.USE_24_HOUR_TIME.data + ? DateFormat('MMMM dd, yyyy ${LunaUI.TEXT_EMDASH} HH:mm').format(login.timestamp) + : DateFormat('MMMM dd, yyyy ${LunaUI.TEXT_EMDASH} hh:mm a').format(login.timestamp), + style: TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), ), + ], + ), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 5, + ); + } + + Widget _trailing() { + return Column( + children: [ + LunaIconButton( + icon: login.success ? Icons.check_circle : Icons.cancel, + color: login.success ? Colors.white : LunaColours.red, ), ], - ), - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 5, - ); - - Widget get _trailing => Column( - children: [ - LSIconButton( - icon: login.success ? Icons.check_circle : Icons.cancel, - color: login.success ? Colors.white : LunaColours.red, - ), - ], - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - ); + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + ); + } } diff --git a/lib/modules/tautulli/routes/logs_newsletters.dart b/lib/modules/tautulli/routes/logs_newsletters.dart index a51c97a8..182ad5c0 100644 --- a/lib/modules/tautulli/routes/logs_newsletters.dart +++ b/lib/modules/tautulli/routes/logs_newsletters.dart @@ -1,2 +1,3 @@ export 'logs_newsletters/route.dart'; +export 'logs_newsletters/state.dart'; export 'logs_newsletters/widgets.dart'; diff --git a/lib/modules/tautulli/routes/logs_newsletters/route.dart b/lib/modules/tautulli/routes/logs_newsletters/route.dart index 4c0c9cd0..ae7a05c1 100644 --- a/lib/modules/tautulli/routes/logs_newsletters/route.dart +++ b/lib/modules/tautulli/routes/logs_newsletters/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLogsNewslettersRouter extends TautulliPageRouter { TautulliLogsNewslettersRouter() : super('/tautulli/logs/newsletters'); @@ -17,62 +15,62 @@ class _TautulliLogsNewslettersRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliLogsNewslettersRoute> { +class _State extends State<_TautulliLogsNewslettersRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { - context.read().resetNewsletterLogs(); - await context.read().newsletterLogs; + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliLogsNewslettersState(context), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget _appBar() { + return LunaAppBar( + title: 'Newsletter Logs', + scrollControllers: [scrollController], + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar(title: 'Newsletter Logs'); - - Widget get _body => LSRefreshIndicator( - onRefresh: _refresh, - refreshKey: _refreshKey, - child: Selector>( - selector: (_, state) => state.newsletterLogs, - builder: (context, logs, _) => FutureBuilder( - future: logs, + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchLogs(context), + child: FutureBuilder( + future: context.select((TautulliLogsNewslettersState state) => state.logs), builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli newsletter logs', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli newsletter logs', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - if(snapshot.hasData) return snapshot.data.logs.length == 0 - ? _noLogs() - : _logs(snapshot.data); - return LSLoader(); + if(snapshot.hasData) return _logs(snapshot.data); + return LunaLoader(); }, ), - ), - ); + ); + } - Widget _noLogs() => LSGenericMessage( - text: 'No Logs Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _logs(TautulliNewsletterLogs logs) => LSListViewBuilder( - itemCount: logs.logs.length, - itemBuilder: (context, index) => TautulliLogsNewsletterLogTile(newsletter: logs.logs[index]), - ); + Widget _logs(TautulliNewsletterLogs logs) { + if((logs?.logs?.length ?? 0) == 0) return LunaMessage( + text: 'No Logs Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: logs.logs.length, + itemBuilder: (context, index) => TautulliLogsNewsletterLogTile(newsletter: logs.logs[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/logs_newsletters/state.dart b/lib/modules/tautulli/routes/logs_newsletters/state.dart new file mode 100644 index 00000000..3cfcb1b6 --- /dev/null +++ b/lib/modules/tautulli/routes/logs_newsletters/state.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsNewslettersState extends ChangeNotifier { + TautulliLogsNewslettersState(BuildContext context) { + fetchLogs(context); + } + + Future _logs; + Future get logs => _logs; + Future fetchLogs(BuildContext context) async { + if(context.read().enabled) { + _logs = context.read().api.notifications.getNewsletterLog( + length: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, + ); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/logs_newsletters/widgets/log_tile.dart b/lib/modules/tautulli/routes/logs_newsletters/widgets/log_tile.dart index 47f87aed..b3f50546 100644 --- a/lib/modules/tautulli/routes/logs_newsletters/widgets/log_tile.dart +++ b/lib/modules/tautulli/routes/logs_newsletters/widgets/log_tile.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLogsNewsletterLogTile extends StatelessWidget { final TautulliNewsletterLogRecord newsletter; @@ -12,18 +12,21 @@ class TautulliLogsNewsletterLogTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: newsletter.agentName), - subtitle: _subtitle, - trailing: _trailing, - padContent: true, - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: newsletter.agentName), + subtitle: _subtitle(), + trailing: _trailing(), + contentPadding: true, + ); + } - Widget get _subtitle => RichText( + Widget _subtitle() => RichText( text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan(text: '${newsletter.notifyAction}\n'), @@ -31,8 +34,8 @@ class TautulliLogsNewsletterLogTile extends StatelessWidget { TextSpan(text: '${newsletter.bodyText}\n'), TextSpan( text: LunaDatabaseValue.USE_24_HOUR_TIME.data - ? DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} HH:mm').format(newsletter.timestamp) - : DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} hh:mm a').format(newsletter.timestamp), + ? DateFormat('MMMM dd, yyyy ${LunaUI.TEXT_EMDASH} HH:mm').format(newsletter.timestamp) + : DateFormat('MMMM dd, yyyy ${LunaUI.TEXT_EMDASH} hh:mm a').format(newsletter.timestamp), style: TextStyle( color: LunaColours.accent, fontWeight: LunaUI.FONT_WEIGHT_BOLD, @@ -45,9 +48,9 @@ class TautulliLogsNewsletterLogTile extends StatelessWidget { maxLines: 4, ); - Widget get _trailing => Column( + Widget _trailing() => Column( children: [ - LSIconButton( + LunaIconButton( icon: newsletter.success ? Icons.check_circle : Icons.cancel, color: newsletter.success ? Colors.white : LunaColours.red, ), diff --git a/lib/modules/tautulli/routes/logs_notifications.dart b/lib/modules/tautulli/routes/logs_notifications.dart index c04b9c9c..543a4c35 100644 --- a/lib/modules/tautulli/routes/logs_notifications.dart +++ b/lib/modules/tautulli/routes/logs_notifications.dart @@ -1,2 +1,3 @@ export 'logs_notifications/route.dart'; +export 'logs_notifications/state.dart'; export 'logs_notifications/widgets.dart'; diff --git a/lib/modules/tautulli/routes/logs_notifications/route.dart b/lib/modules/tautulli/routes/logs_notifications/route.dart index e9926e63..169ac37b 100644 --- a/lib/modules/tautulli/routes/logs_notifications/route.dart +++ b/lib/modules/tautulli/routes/logs_notifications/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLogsNotificationsRouter extends TautulliPageRouter { TautulliLogsNotificationsRouter() : super('/tautulli/logs/notifications'); @@ -17,62 +15,62 @@ class _TautulliLogsNotificationsRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliLogsNotificationsRoute> { +class _State extends State<_TautulliLogsNotificationsRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { - context.read().resetNotificationLogs(); - await context.read().notificationLogs; + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliLogsNotificationsState(context), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget _appBar() { + return LunaAppBar( + title: 'Notification Logs', + scrollControllers: [scrollController], + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar(title: 'Notification Logs'); - - Widget get _body => LSRefreshIndicator( - onRefresh: _refresh, - refreshKey: _refreshKey, - child: Selector>( - selector: (_, state) => state.notificationLogs, - builder: (context, logs, _) => FutureBuilder( - future: logs, + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchLogs(context), + child: FutureBuilder( + future: context.select((TautulliLogsNotificationsState state) => state.logs), builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli notification logs', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli notification logs', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - if(snapshot.hasData) return snapshot.data.logs.length == 0 - ? _noLoginLogs() - : _loginLogs(snapshot.data); - return LSLoader(); + if(snapshot.hasData) return _logs(snapshot.data); + return LunaLoader(); }, ), - ), - ); + ); + } - Widget _noLoginLogs() => LSGenericMessage( - text: 'No Logs Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _loginLogs(TautulliNotificationLogs logs) => LSListViewBuilder( - itemCount: logs.logs.length, - itemBuilder: (context, index) => TautulliLogsNotificationLogTile(notification: logs.logs[index]), - ); + Widget _logs(TautulliNotificationLogs logs) { + if((logs?.logs?.length ?? 0) == 0) return LunaMessage( + text: 'No Logs Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: logs.logs.length, + itemBuilder: (context, index) => TautulliLogsNotificationLogTile(notification: logs.logs[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/logs_notifications/state.dart b/lib/modules/tautulli/routes/logs_notifications/state.dart new file mode 100644 index 00000000..dbbc98b2 --- /dev/null +++ b/lib/modules/tautulli/routes/logs_notifications/state.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsNotificationsState extends ChangeNotifier { + TautulliLogsNotificationsState(BuildContext context) { + fetchLogs(context); + } + + Future _logs; + Future get logs => _logs; + Future fetchLogs(BuildContext context) async { + if(context.read().enabled) { + _logs = context.read().api.notifications.getNotificationLog( + length: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, + ); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/logs_notifications/widgets/log_tile.dart b/lib/modules/tautulli/routes/logs_notifications/widgets/log_tile.dart index 38021f49..c9d8cd20 100644 --- a/lib/modules/tautulli/routes/logs_notifications/widgets/log_tile.dart +++ b/lib/modules/tautulli/routes/logs_notifications/widgets/log_tile.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLogsNotificationLogTile extends StatelessWidget { final TautulliNotificationLogRecord notification; @@ -12,18 +12,21 @@ class TautulliLogsNotificationLogTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: notification.agentName), - subtitle: _subtitle, - trailing: _trailing, - padContent: true, - ); + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: notification.agentName), + subtitle: _subtitle(), + trailing: _trailing(), + contentPadding: true, + ); + } - Widget get _subtitle => RichText( + Widget _subtitle() => RichText( text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan(text: '${notification.notifyAction}\n'), @@ -31,8 +34,8 @@ class TautulliLogsNotificationLogTile extends StatelessWidget { TextSpan(text: '${notification.bodyText}\n'), TextSpan( text: LunaDatabaseValue.USE_24_HOUR_TIME.data - ? DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} HH:mm').format(notification.timestamp) - : DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} hh:mm a').format(notification.timestamp), + ? DateFormat('MMMM dd, yyyy ${LunaUI.TEXT_EMDASH} HH:mm').format(notification.timestamp) + : DateFormat('MMMM dd, yyyy ${LunaUI.TEXT_EMDASH} hh:mm a').format(notification.timestamp), style: TextStyle( color: LunaColours.accent, fontWeight: LunaUI.FONT_WEIGHT_BOLD, @@ -45,9 +48,9 @@ class TautulliLogsNotificationLogTile extends StatelessWidget { maxLines: 4, ); - Widget get _trailing => Column( + Widget _trailing() => Column( children: [ - LSIconButton( + LunaIconButton( icon: notification.success ? Icons.check_circle : Icons.cancel, color: notification.success ? Colors.white : LunaColours.red, ), diff --git a/lib/modules/tautulli/routes/logs_plex_media_scanner.dart b/lib/modules/tautulli/routes/logs_plex_media_scanner.dart index fd3c7e2a..c2d7ec59 100644 --- a/lib/modules/tautulli/routes/logs_plex_media_scanner.dart +++ b/lib/modules/tautulli/routes/logs_plex_media_scanner.dart @@ -1,2 +1,3 @@ export 'logs_plex_media_scanner/route.dart'; +export 'logs_plex_media_scanner/state.dart'; export 'logs_plex_media_scanner/widgets.dart'; diff --git a/lib/modules/tautulli/routes/logs_plex_media_scanner/route.dart b/lib/modules/tautulli/routes/logs_plex_media_scanner/route.dart index 878e1692..6ce59790 100644 --- a/lib/modules/tautulli/routes/logs_plex_media_scanner/route.dart +++ b/lib/modules/tautulli/routes/logs_plex_media_scanner/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLogsPlexMediaScannerRouter extends TautulliPageRouter { TautulliLogsPlexMediaScannerRouter() : super('/tautulli/logs/plexmediascanner'); @@ -17,62 +15,63 @@ class _TautulliLogsPlexMediaScannerRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliLogsPlexMediaScannerRoute> { +class _State extends State<_TautulliLogsPlexMediaScannerRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { - context.read().resetPlexMediaScannerLogs(); - await context.read().plexMediaScannerLogs; + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliLogsPlexMediaScannerState(context), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget _appBar() { + return LunaAppBar( + title: 'Plex Media Scanner Logs', + scrollControllers: [scrollController], + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar(title: 'Plex Media Scanner Logs'); - - Widget get _body => LSRefreshIndicator( - onRefresh: _refresh, - refreshKey: _refreshKey, - child: Selector>>( - selector: (_, state) => state.plexMediaScannerLogs, - builder: (context, logs, _) => FutureBuilder( - future: logs, + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchLogs(context), + child: FutureBuilder( + future: context.select((TautulliLogsPlexMediaScannerState state) => state.logs), builder: (context, AsyncSnapshot> snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli plex media scanner logs', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Plex Media Scanner logs', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noLogs() - : _logs(snapshot.data); - return LSLoader(); + if(snapshot.hasData) return _logs(snapshot.data); + return LunaLoader(); }, ), - ), - ); + ); + } - Widget _noLogs() => LSGenericMessage( - text: 'No Logs Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _logs(List logs) => LSListViewBuilder( - itemCount: logs.length, - itemBuilder: (context, index) => TautulliLogsPlexMediaScannerLogTile(log: logs[index]), - ); + Widget _logs(List logs) { + if((logs?.length ?? 0) == 0) return LunaMessage( + text: 'No Logs Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + List _reversed = logs.reversed.toList(); + return LunaListViewBuilder( + controller: scrollController, + itemCount: _reversed.length, + itemBuilder: (context, index) => TautulliLogsPlexMediaScannerLogTile(log: _reversed[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/logs_plex_media_scanner/state.dart b/lib/modules/tautulli/routes/logs_plex_media_scanner/state.dart new file mode 100644 index 00000000..512fef36 --- /dev/null +++ b/lib/modules/tautulli/routes/logs_plex_media_scanner/state.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsPlexMediaScannerState extends ChangeNotifier { + TautulliLogsPlexMediaScannerState(BuildContext context) { + fetchLogs(context); + } + + Future> _logs; + Future> get logs => _logs; + Future fetchLogs(BuildContext context) async { + if(context.read().enabled) { + _logs = context.read().api.miscellaneous.getPlexLog( + window: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, + logType: TautulliPlexLogType.SCANNER, + ); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/logs_plex_media_scanner/widgets/log_tile.dart b/lib/modules/tautulli/routes/logs_plex_media_scanner/widgets/log_tile.dart index 0495a5ad..87abfc71 100644 --- a/lib/modules/tautulli/routes/logs_plex_media_scanner/widgets/log_tile.dart +++ b/lib/modules/tautulli/routes/logs_plex_media_scanner/widgets/log_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLogsPlexMediaScannerLogTile extends StatelessWidget { final TautulliPlexLog log; diff --git a/lib/modules/tautulli/routes/logs_plex_media_server.dart b/lib/modules/tautulli/routes/logs_plex_media_server.dart index 8e3f94e7..3b3eca21 100644 --- a/lib/modules/tautulli/routes/logs_plex_media_server.dart +++ b/lib/modules/tautulli/routes/logs_plex_media_server.dart @@ -1,2 +1,3 @@ export 'logs_plex_media_server/route.dart'; +export 'logs_plex_media_server/state.dart'; export 'logs_plex_media_server/widgets.dart'; diff --git a/lib/modules/tautulli/routes/logs_plex_media_server/route.dart b/lib/modules/tautulli/routes/logs_plex_media_server/route.dart index 785a4a10..8ac58b17 100644 --- a/lib/modules/tautulli/routes/logs_plex_media_server/route.dart +++ b/lib/modules/tautulli/routes/logs_plex_media_server/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLogsPlexMediaServerRouter extends TautulliPageRouter { TautulliLogsPlexMediaServerRouter() : super('/tautulli/logs/plexmediaserver'); @@ -17,62 +15,63 @@ class _TautulliLogsPlexMediaServerRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliLogsPlexMediaServerRoute> { +class _State extends State<_TautulliLogsPlexMediaServerRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { - context.read().resetPlexMediaServerLogs(); - await context.read().plexMediaServerLogs; + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliLogsPlexMediaServerState(context), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget _appBar() { + return LunaAppBar( + title: 'Plex Media Server Logs', + scrollControllers: [scrollController], + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar(title: 'Plex Media Server Logs'); - - Widget get _body => LSRefreshIndicator( - onRefresh: _refresh, - refreshKey: _refreshKey, - child: Selector>>( - selector: (_, state) => state.plexMediaServerLogs, - builder: (context, logs, _) => FutureBuilder( - future: logs, + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchLogs(context), + child: FutureBuilder( + future: context.select((TautulliLogsPlexMediaServerState state) => state.logs), builder: (context, AsyncSnapshot> snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli plex media server logs', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Plex Media Server logs', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noLogs() - : _logs(snapshot.data); - return LSLoader(); + if(snapshot.hasData) return _logs(snapshot.data); + return LunaLoader(); }, ), - ), - ); + ); + } - Widget _noLogs() => LSGenericMessage( - text: 'No Logs Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _logs(List logs) => LSListViewBuilder( - itemCount: logs.length, - itemBuilder: (context, index) => TautulliLogsPlexMediaServerLogTile(log: logs[index]), - ); + Widget _logs(List logs) { + if((logs?.length ?? 0) == 0) return LunaMessage( + text: 'No Logs Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + List _reversed = logs.reversed.toList(); + return LunaListViewBuilder( + controller: scrollController, + itemCount: _reversed.length, + itemBuilder: (context, index) => TautulliLogsPlexMediaServerLogTile(log: _reversed[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/logs_plex_media_server/state.dart b/lib/modules/tautulli/routes/logs_plex_media_server/state.dart new file mode 100644 index 00000000..77d63c18 --- /dev/null +++ b/lib/modules/tautulli/routes/logs_plex_media_server/state.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsPlexMediaServerState extends ChangeNotifier { + TautulliLogsPlexMediaServerState(BuildContext context) { + fetchLogs(context); + } + + Future> _logs; + Future> get logs => _logs; + Future fetchLogs(BuildContext context) async { + if(context.read().enabled) { + _logs = context.read().api.miscellaneous.getPlexLog( + window: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, + logType: TautulliPlexLogType.SERVER, + ); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/logs_plex_media_server/widgets/log_tile.dart b/lib/modules/tautulli/routes/logs_plex_media_server/widgets/log_tile.dart index 0a0a7b09..90a2426f 100644 --- a/lib/modules/tautulli/routes/logs_plex_media_server/widgets/log_tile.dart +++ b/lib/modules/tautulli/routes/logs_plex_media_server/widgets/log_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLogsPlexMediaServerLogTile extends StatelessWidget { final TautulliPlexLog log; diff --git a/lib/modules/tautulli/routes/logs_tautulli.dart b/lib/modules/tautulli/routes/logs_tautulli.dart index 4010c236..3c078fc4 100644 --- a/lib/modules/tautulli/routes/logs_tautulli.dart +++ b/lib/modules/tautulli/routes/logs_tautulli.dart @@ -1,2 +1,3 @@ export 'logs_tautulli/route.dart'; +export 'logs_tautulli/state.dart'; export 'logs_tautulli/widgets.dart'; diff --git a/lib/modules/tautulli/routes/logs_tautulli/route.dart b/lib/modules/tautulli/routes/logs_tautulli/route.dart index 321bdd01..47b3288b 100644 --- a/lib/modules/tautulli/routes/logs_tautulli/route.dart +++ b/lib/modules/tautulli/routes/logs_tautulli/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliLogsTautulliRouter extends TautulliPageRouter { TautulliLogsTautulliRouter() : super('/tautulli/logs/tautulli'); @@ -17,62 +15,62 @@ class _TautulliLogsTautulliRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliLogsTautulliRoute> { +class _State extends State<_TautulliLogsTautulliRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async { - context.read().resetTautulliLogs(); - await context.read().tautulliLogs; + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => TautulliLogsTautulliState(context), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ), + ); } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget _appBar() { + return LunaAppBar( + title: 'Plex Media Server Logs', + scrollControllers: [scrollController], + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar(title: 'Tautulli Logs'); - - Widget get _body => LSRefreshIndicator( - onRefresh: _refresh, - refreshKey: _refreshKey, - child: Selector>>( - selector: (_, state) => state.tautulliLogs, - builder: (context, logs, _) => FutureBuilder( - future: logs, + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchLogs(context), + child: FutureBuilder( + future: context.select((TautulliLogsTautulliState state) => state.logs), builder: (context, AsyncSnapshot> snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli Tautulli logs', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli logs', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noLogs() - : _logs(snapshot.data); - return LSLoader(); + if(snapshot.hasData) return _logs(snapshot.data); + return LunaLoader(); }, ), - ), - ); + ); + } - Widget _noLogs() => LSGenericMessage( - text: 'No Logs Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _logs(List logs) => LSListViewBuilder( - itemCount: logs.length, - itemBuilder: (context, index) => TautulliLogsTautulliLogTile(log: logs[index]), - ); + Widget _logs(List logs) { + if((logs?.length ?? 0) == 0) return LunaMessage( + text: 'No Logs Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: logs.length, + itemBuilder: (context, index) => TautulliLogsTautulliLogTile(log: logs[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/logs_tautulli/state.dart b/lib/modules/tautulli/routes/logs_tautulli/state.dart new file mode 100644 index 00000000..ecc9db1b --- /dev/null +++ b/lib/modules/tautulli/routes/logs_tautulli/state.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsTautulliState extends ChangeNotifier { + TautulliLogsTautulliState(BuildContext context) { + fetchLogs(context); + } + + Future> _logs; + Future> get logs => _logs; + Future fetchLogs(BuildContext context) async { + if(context.read().enabled) { + _logs = context.read().api.miscellaneous.getLogs( + start: 0, + end: TautulliDatabaseValue.CONTENT_LOAD_LENGTH.data, + ); + } + notifyListeners(); + } +} diff --git a/lib/modules/tautulli/routes/logs_tautulli/widgets/log_tile.dart b/lib/modules/tautulli/routes/logs_tautulli/widgets/log_tile.dart index d9d2a161..00e1b78c 100644 --- a/lib/modules/tautulli/routes/logs_tautulli/widgets/log_tile.dart +++ b/lib/modules/tautulli/routes/logs_tautulli/widgets/log_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliLogsTautulliLogTile extends StatelessWidget { final TautulliLog log; diff --git a/lib/modules/tautulli/routes/media_details/route.dart b/lib/modules/tautulli/routes/media_details/route.dart index 452685e8..6d597c0d 100644 --- a/lib/modules/tautulli/routes/media_details/route.dart +++ b/lib/modules/tautulli/routes/media_details/route.dart @@ -2,7 +2,6 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliMediaDetailsRouter extends TautulliPageRouter { TautulliMediaDetailsRouter() : super('/tautulli/media/:mediatype/:ratingkey'); @@ -43,27 +42,33 @@ class _TautulliMediaDetailsRoute extends StatefulWidget { class _State extends State<_TautulliMediaDetailsRoute> { final GlobalKey _scaffoldKey = GlobalKey(); - PageController _pageController; + LunaPageController _pageController; @override void initState() { super.initState(); - _pageController = PageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_MEDIA_DETAILS.data); + _pageController = LunaPageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_MEDIA_DETAILS.data); } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - body: widget.mediaType != null && widget.mediaType != TautulliMediaType.NULL && widget.ratingKey != null - ? _body - : _contentNotFound, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), + ); + } - Widget get _appBar => LunaAppBar(title: 'Media Details'); + Widget _appBar() { + return LunaAppBar( + title: 'Media Details', + scrollControllers: TautulliMediaDetailsNavigationBar.scrollControllers, + pageController: _pageController, + ); + } - Widget get _bottomNavigationBar { + Widget _bottomNavigationBar() { if( widget.mediaType != null && widget.mediaType != TautulliMediaType.NULL && @@ -74,17 +79,28 @@ class _State extends State<_TautulliMediaDetailsRoute> { } - Widget get _body => widget.mediaType != TautulliMediaType.COLLECTION - ? PageView( + Widget _body() { + if( + widget.mediaType == null || + widget.mediaType == TautulliMediaType.NULL || + widget.ratingKey == null + ) return LunaMessage(text: 'No Content Found'); + if(widget.mediaType == TautulliMediaType.COLLECTION) return TautulliMediaDetailsMetadata( + ratingKey: widget.ratingKey, + type: widget.mediaType, + ); + return PageView( controller: _pageController, - children: _tabs, - ) - : TautulliMediaDetailsMetadata(ratingKey: widget.ratingKey, type: widget.mediaType); - - List get _tabs => [ - TautulliMediaDetailsMetadata(ratingKey: widget.ratingKey, type: widget.mediaType), - TautulliMediaDetailsHistory(ratingKey: widget.ratingKey, type: widget.mediaType), - ]; - - Widget get _contentNotFound => LSGenericMessage(text: 'No Content Found'); + children: [ + TautulliMediaDetailsMetadata( + ratingKey: widget.ratingKey, + type: widget.mediaType, + ), + TautulliMediaDetailsHistory( + ratingKey: widget.ratingKey, + type: widget.mediaType, + ), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/media_details/widgets/history.dart b/lib/modules/tautulli/routes/media_details/widgets/history.dart index d1a53c6a..4c14a614 100644 --- a/lib/modules/tautulli/routes/media_details/widgets/history.dart +++ b/lib/modules/tautulli/routes/media_details/widgets/history.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliMediaDetailsHistory extends StatefulWidget { final TautulliMediaType type; @@ -75,42 +74,43 @@ class _State extends State with AutomaticKeepAliveC super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().individualHistory[widget.ratingKey], - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli history: ${widget.ratingKey}', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: context.watch().individualHistory[widget.ratingKey], + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli history: ${widget.ratingKey}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.records.length == 0 - ? _noHistory() - : _history(snapshot.data); - return LSLoader(); - }, - ), - ); + if(snapshot.hasData) return _history(snapshot.data); + return LunaLoader(); + }, + ), + ); + } - Widget _history(TautulliHistory history) => LSListViewBuilder( - itemCount: history.records.length, - itemBuilder: (context, index) => TautulliHistoryTile( - userId: history.records[index].userId, - history: history.records[index], - ), - ); - - Widget _noHistory() => LSGenericMessage( - text: 'No History Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + Widget _history(TautulliHistory history) { + if((history?.records?.length ?? 0) == 0) return LunaMessage( + text: 'No History Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: TautulliMediaDetailsNavigationBar.scrollControllers[1], + itemCount: history.records.length, + itemBuilder: (context, index) => TautulliHistoryTile(history: history.records[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/media_details/widgets/metadata.dart b/lib/modules/tautulli/routes/media_details/widgets/metadata.dart index 4c80ebea..e9b7e2b4 100644 --- a/lib/modules/tautulli/routes/media_details/widgets/metadata.dart +++ b/lib/modules/tautulli/routes/media_details/widgets/metadata.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliMediaDetailsMetadata extends StatefulWidget { final TautulliMediaType type; @@ -44,33 +43,41 @@ class _State extends State with AutomaticKeepAlive super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().metadata[widget.ratingKey], - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli metadata: ${widget.ratingKey}', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: context.watch().metadata[widget.ratingKey], + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli metadata: ${widget.ratingKey}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return _metadata(snapshot.data); - return LSLoader(); - }, - ), - ); + if(snapshot.hasData) return _metadata(snapshot.data); + return LunaLoader(); + }, + ), + ); + } - Widget _metadata(TautulliMetadata metadata) => LSListView( - children: [ - TautulliMediaDetailsMetadataHeaderTile(metadata: metadata), - TautulliMediaDetailsMetadataSummary(metadata: metadata, type: widget.type), - TautulliMediaDetailsMetadataMetadata(metadata: metadata), - ], - ); + Widget _metadata(TautulliMetadata metadata) { + return LunaListView( + controller: TautulliMediaDetailsNavigationBar.scrollControllers[0], + children: [ + TautulliMediaDetailsMetadataHeaderTile(metadata: metadata), + TautulliMediaDetailsMetadataSummary(metadata: metadata, type: widget.type), + TautulliMediaDetailsMetadataMetadata(metadata: metadata), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/media_details/widgets/metadata_header.dart b/lib/modules/tautulli/routes/media_details/widgets/metadata_header.dart index 6201a550..7b947f40 100644 --- a/lib/modules/tautulli/routes/media_details/widgets/metadata_header.dart +++ b/lib/modules/tautulli/routes/media_details/widgets/metadata_header.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:tautulli/tautulli.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; @@ -15,27 +14,32 @@ class TautulliMediaDetailsMetadataHeaderTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( - child: _body(context), - decoration: metadata.art != null && metadata.art.isNotEmpty - ? LunaCardDecoration( - uri: context.watch().getImageURLFromPath( - metadata.art, - width: MediaQuery.of(context).size.width.truncate(), - ), - headers: context.watch().headers, - ) - : null, - ); + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: _body(context), + decoration: metadata.art != null && metadata.art.isNotEmpty + ? LunaCardDecoration( + uri: context.watch().getImageURLFromPath( + metadata.art, + width: MediaQuery.of(context).size.width.truncate(), + ), + headers: context.watch().headers, + ) + : null, + ); + } - Widget _body(BuildContext context) => Row( - children: [ - _poster(context), - Expanded(child: _mediaInfo), - ], - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - ); + Widget _body(BuildContext context) { + return Row( + children: [ + _poster(context), + Expanded(child: _mediaInfo), + ], + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + ); + } String get _posterLink { switch(metadata.mediaType) { @@ -54,9 +58,9 @@ class TautulliMediaDetailsMetadataHeaderTile extends StatelessWidget { } Widget _poster(BuildContext context) { - return LSNetworkImage( + return LunaNetworkImage( url: context.watch().getImageURLFromPath(_posterLink), - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', height: _height, width: _width, headers: context.watch().headers.cast(), @@ -67,7 +71,7 @@ class TautulliMediaDetailsMetadataHeaderTile extends StatelessWidget { child: Container( child: Column( children: [ - LSTitle(text: _title, maxLines: 1), + LunaText.title(text: _title, maxLines: 1), SizedBox(height: 4.0), ..._subtitles, ], @@ -114,12 +118,12 @@ class TautulliMediaDetailsMetadataHeaderTile extends StatelessWidget { case TautulliMediaType.SEASON: case TautulliMediaType.LIVE: case TautulliMediaType.COLLECTION: _text = metadata.title; break; - case TautulliMediaType.EPISODE: _text = '${metadata.parentTitle} ${Constants.TEXT_BULLET} Episode ${metadata.mediaIndex}'; break; - case TautulliMediaType.TRACK: _text = '${metadata.parentTitle} ${Constants.TEXT_BULLET} Track ${metadata.mediaIndex}'; break; + case TautulliMediaType.EPISODE: _text = '${metadata.parentTitle} ${LunaUI.TEXT_BULLET} Episode ${metadata.mediaIndex}'; break; + case TautulliMediaType.TRACK: _text = '${metadata.parentTitle} ${LunaUI.TEXT_BULLET} Track ${metadata.mediaIndex}'; break; case TautulliMediaType.NULL: default: _text = 'Unknown'; break; } - return LSSubtitle(text: _text); + return LunaText.subtitle(text: _text); } Widget get _subtitleTwo { @@ -142,7 +146,7 @@ class TautulliMediaDetailsMetadataHeaderTile extends StatelessWidget { maxLines: 3, style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ); } diff --git a/lib/modules/tautulli/routes/media_details/widgets/metadata_metadata.dart b/lib/modules/tautulli/routes/media_details/widgets/metadata_metadata.dart index 24267059..bb75395a 100644 --- a/lib/modules/tautulli/routes/media_details/widgets/metadata_metadata.dart +++ b/lib/modules/tautulli/routes/media_details/widgets/metadata_metadata.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/modules/tautulli.dart'; class TautulliMediaDetailsMetadataMetadata extends StatelessWidget { final TautulliMetadata metadata; @@ -11,71 +11,30 @@ class TautulliMediaDetailsMetadataMetadata extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => Column( - children: [ - ..._block( - title: 'Metadata', - children: [ - if(metadata.originallyAvailableAt != null && metadata.originallyAvailableAt.isNotEmpty) _content('released', metadata.originallyAvailableAt), - if(metadata.addedAt != null) _content('added', metadata.addedAt.lunaDate), - if(metadata.duration != null) _content('duration', metadata.duration.lunaTimestamp), - if(metadata.mediaInfo != null && metadata.mediaInfo.length != 0) _content('bitrate', '${metadata.mediaInfo[0].bitrate ?? '??'} kbps'), - if(metadata.rating != null) _content('rating', '${((metadata.rating*10).truncate())}%'), - if(metadata.studio != null && metadata.studio.isNotEmpty) _content('studio', metadata.studio), - if(metadata.genres != null && metadata.genres.length != 0) _content('genres', metadata.genres.take(5).join('\n')), - if(metadata.directors != null && metadata.directors.length != 0) _content('directors', metadata.directors.take(5).join('\n')), - if(metadata.writers != null && metadata.writers.length != 0) _content('writers', metadata.writers.take(5).join('\n')), - if(metadata.actors != null && metadata.actors.length != 0) _content('actors', metadata.actors.take(5).join('\n')), - ], - ), - ], - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - ); - - List _block({ - @required String title, - @required List children, - }) => [ - LSHeader(text: title), - LSCard( - child: Padding( - child: Column( - children: children, - ), - padding: EdgeInsets.symmetric(vertical: 8.0), - ), - ), - ]; - - Widget _content(String header, String body) => Padding( - child: Row( - children: [ - Expanded( - child: Text( - header.toUpperCase(), - textAlign: TextAlign.end, - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - flex: 2, - ), - Container(width: 16.0, height: 0.0), - Expanded( - child: Text( - body, - textAlign: TextAlign.start, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - flex: 5, - ), + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + if(metadata.originallyAvailableAt != null && metadata.originallyAvailableAt.isNotEmpty) + LunaTableContent(title: 'released', body: metadata.originallyAvailableAt), + if(metadata.addedAt != null) + LunaTableContent(title: 'added', body: metadata.addedAt.lunaDate), + if(metadata.duration != null) + LunaTableContent(title: 'duration', body: metadata.duration.lunaTimestamp), + if(metadata.mediaInfo != null && metadata.mediaInfo.length != 0) + LunaTableContent(title: 'bitrate', body: '${metadata.mediaInfo[0].bitrate ?? LunaUI.TEXT_EMDASH} kbps'), + if(metadata.rating != null) + LunaTableContent(title: 'rating', body: '${(((metadata?.rating ?? 0)*10).truncate())}%'), + if(metadata.studio != null && metadata.studio.isNotEmpty) + LunaTableContent(title: 'studio', body: metadata.studio), + if(metadata.genres != null && metadata.genres.length != 0) + LunaTableContent(title: 'genres', body: metadata.genres.take(5).join('\n')), + if(metadata.directors != null && metadata.directors.length != 0) + LunaTableContent(title: 'directors', body: metadata.directors.take(5).join('\n')), + if(metadata.writers != null && metadata.writers.length != 0) + LunaTableContent(title: 'writers', body: metadata.writers.take(5).join('\n')), + if(metadata.actors != null && metadata.actors.length != 0) + LunaTableContent(title: 'actors', body: metadata.actors.take(5).join('\n')), ], - crossAxisAlignment: CrossAxisAlignment.start, - ), - padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - ); + ); + } } \ No newline at end of file diff --git a/lib/modules/tautulli/routes/media_details/widgets/navigation_bar.dart b/lib/modules/tautulli/routes/media_details/widgets/navigation_bar.dart index 30fc453d..c616439e 100644 --- a/lib/modules/tautulli/routes/media_details/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/routes/media_details/widgets/navigation_bar.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; -class TautulliMediaDetailsNavigationBar extends StatefulWidget { +class TautulliMediaDetailsNavigationBar extends StatelessWidget { + final PageController pageController; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + static const List icons = [ Icons.info_outline, LunaIcons.history, @@ -13,46 +15,18 @@ class TautulliMediaDetailsNavigationBar extends StatefulWidget { 'History', ]; - final PageController pageController; - TautulliMediaDetailsNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - int _index = TautulliDatabaseValue.NAVIGATION_INDEX_MEDIA_DETAILS.data; - - @override - void initState() { - super.initState(); - widget.pageController?.addListener(_pageControllerListener); - } - - @override - void dispose() { - widget.pageController?.removeListener(_pageControllerListener); - super.dispose(); - } - - void _pageControllerListener() { - if(widget.pageController.page.round() == _index) return; - setState(() => _index = widget.pageController.page.round()); - } - - @override - Widget build(BuildContext context) => LSBottomNavigationBar( - index: _index, - icons: TautulliMediaDetailsNavigationBar.icons, - titles: TautulliMediaDetailsNavigationBar.titles, - onTap: _navOnTap, - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } } diff --git a/lib/modules/tautulli/routes/media_details/widgets/switcher_buttons.dart b/lib/modules/tautulli/routes/media_details/widgets/switcher_buttons.dart index bc5c51e1..19e06c08 100644 --- a/lib/modules/tautulli/routes/media_details/widgets/switcher_buttons.dart +++ b/lib/modules/tautulli/routes/media_details/widgets/switcher_buttons.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliMediaDetailsSwitcherButton extends StatefulWidget { final TautulliMetadata metadata; diff --git a/lib/modules/tautulli/routes/more.dart b/lib/modules/tautulli/routes/more.dart index 28884860..56a2ef26 100644 --- a/lib/modules/tautulli/routes/more.dart +++ b/lib/modules/tautulli/routes/more.dart @@ -1,2 +1 @@ export 'more/route.dart'; -export 'more/widgets.dart'; diff --git a/lib/modules/tautulli/routes/more/route.dart b/lib/modules/tautulli/routes/more/route.dart index 567b9124..74b43c13 100644 --- a/lib/modules/tautulli/routes/more/route.dart +++ b/lib/modules/tautulli/routes/more/route.dart @@ -26,16 +26,91 @@ class _State extends State with AutomaticKeepAliveClientMixin ); } - Widget get _body => LSListView( - children: [ - TautulliMoreCheckForUpdatesTile(), - TautulliMoreGraphsTile(), - TautulliMoreLibrariesTile(), - TautulliMoreLogsTile(), - TautulliMoreRecentlyAddedTile(), - TautulliMoreSearchTile(), - TautulliMoreStatisticsTile(), - TautulliMoreSyncedItemsTile(), - ], - ); + Widget get _body { + return LunaListView( + controller: TautulliNavigationBar.scrollControllers[3], + children: [ + LunaListTile( + context: context, + title: LunaText.title(text: 'Check for Updates'), + subtitle: LunaText.subtitle(text: 'Tautulli & Plex Updates'), + trailing: LunaIconButton( + icon: Icons.system_update_rounded, + color: LunaColours.list(0), + ), + onTap: () async => TautulliCheckForUpdatesRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Graphs'), + subtitle: LunaText.subtitle(text: 'Play Count & Duration Graphs'), + trailing: LunaIconButton( + icon: Icons.insert_chart_rounded, + color: LunaColours.list(1), + ), + onTap: () async => TautulliGraphsRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Libraries'), + subtitle: LunaText.subtitle(text: 'Plex Library Information'), + trailing: LunaIconButton( + icon: Icons.video_library_rounded, + color: LunaColours.list(2), + ), + onTap: () async => TautulliLibrariesRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Logs'), + subtitle: LunaText.subtitle(text: 'Tautulli & Plex Logs'), + trailing: LunaIconButton( + icon: Icons.developer_mode_rounded, + color: LunaColours.list(3), + ), + onTap: () async => TautulliLogsRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Recently Added'), + subtitle: LunaText.subtitle(text: 'Recently Added Content to Plex'), + trailing: LunaIconButton( + icon: Icons.recent_actors_rounded, + color: LunaColours.list(4), + ), + onTap: () async => TautulliRecentlyAddedRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Search'), + subtitle: LunaText.subtitle(text: 'Search Your Libraries'), + trailing: LunaIconButton( + icon: Icons.search_rounded, + color: LunaColours.list(5), + ), + onTap: () async => TautulliSearchRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Statistics'), + subtitle: LunaText.subtitle(text: 'User & Library Statistics'), + trailing: LunaIconButton( + icon: Icons.format_list_numbered_rounded, + color: LunaColours.list(6), + ), + onTap: () async => TautulliStatisticsRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Synced Items'), + subtitle: LunaText.subtitle(text: 'Synced Content on Devices'), + trailing: LunaIconButton( + icon: Icons.sync_rounded, + color: LunaColours.list(7), + ), + onTap: () async => TautulliSyncedItemsRouter().navigateTo(context), + ), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/more/widgets.dart b/lib/modules/tautulli/routes/more/widgets.dart deleted file mode 100644 index c64818b4..00000000 --- a/lib/modules/tautulli/routes/more/widgets.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'widgets/check_for_updates_tile.dart'; -export 'widgets/graphs_tile.dart'; -export 'widgets/libraries_tile.dart'; -export 'widgets/logs_tile.dart'; -export 'widgets/recently_added_tile.dart'; -export 'widgets/search_tile.dart'; -export 'widgets/statistics_tile.dart'; -export 'widgets/synced_items_tile.dart'; diff --git a/lib/modules/tautulli/routes/more/widgets/check_for_updates_tile.dart b/lib/modules/tautulli/routes/more/widgets/check_for_updates_tile.dart deleted file mode 100644 index dba7d848..00000000 --- a/lib/modules/tautulli/routes/more/widgets/check_for_updates_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreCheckForUpdatesTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Check For Updates'), - subtitle: LSSubtitle(text: 'Tautulli & Plex Updates'), - trailing: LSIconButton( - icon: Icons.system_update, - color: LunaColours.list(0), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliCheckForUpdatesRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/more/widgets/graphs_tile.dart b/lib/modules/tautulli/routes/more/widgets/graphs_tile.dart deleted file mode 100644 index 5c3e6376..00000000 --- a/lib/modules/tautulli/routes/more/widgets/graphs_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreGraphsTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Graphs'), - subtitle: LSSubtitle(text: 'Play Count & Duration Graphs'), - trailing: LSIconButton( - icon: Icons.insert_chart, - color: LunaColours.list(1), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliGraphsRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/more/widgets/libraries_tile.dart b/lib/modules/tautulli/routes/more/widgets/libraries_tile.dart deleted file mode 100644 index 9afb1af0..00000000 --- a/lib/modules/tautulli/routes/more/widgets/libraries_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreLibrariesTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Libraries'), - subtitle: LSSubtitle(text: 'Plex Library Information'), - trailing: LSIconButton( - icon: Icons.video_library, - color: LunaColours.list(2), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLibrariesRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/more/widgets/logs_tile.dart b/lib/modules/tautulli/routes/more/widgets/logs_tile.dart deleted file mode 100644 index 90eccbee..00000000 --- a/lib/modules/tautulli/routes/more/widgets/logs_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreLogsTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Logs'), - subtitle: LSSubtitle(text: 'Tautulli & Plex Logs'), - trailing: LSIconButton( - icon: Icons.developer_mode, - color: LunaColours.list(3), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliLogsRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/more/widgets/recently_added_tile.dart b/lib/modules/tautulli/routes/more/widgets/recently_added_tile.dart deleted file mode 100644 index 3ab3bbc7..00000000 --- a/lib/modules/tautulli/routes/more/widgets/recently_added_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreRecentlyAddedTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Recently Added'), - subtitle: LSSubtitle(text: 'Recently Added Content to Plex'), - trailing: LSIconButton( - icon: Icons.recent_actors, - color: LunaColours.list(4), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliRecentlyAddedRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/more/widgets/search_tile.dart b/lib/modules/tautulli/routes/more/widgets/search_tile.dart deleted file mode 100644 index 3d294271..00000000 --- a/lib/modules/tautulli/routes/more/widgets/search_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreSearchTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Search'), - subtitle: LSSubtitle(text: 'Search Your Libraries'), - trailing: LSIconButton( - icon: Icons.search, - color: LunaColours.list(5), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliSearchRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/more/widgets/statistics_tile.dart b/lib/modules/tautulli/routes/more/widgets/statistics_tile.dart deleted file mode 100644 index 1ebedc69..00000000 --- a/lib/modules/tautulli/routes/more/widgets/statistics_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreStatisticsTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Statistics'), - subtitle: LSSubtitle(text: 'User & Library Statistics'), - trailing: LSIconButton( - icon: Icons.format_list_numbered, - color: LunaColours.list(6), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliStatisticsRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/more/widgets/synced_items_tile.dart b/lib/modules/tautulli/routes/more/widgets/synced_items_tile.dart deleted file mode 100644 index 5043528f..00000000 --- a/lib/modules/tautulli/routes/more/widgets/synced_items_tile.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; - -class TautulliMoreSyncedItemsTile extends StatelessWidget { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: 'Synced Items'), - subtitle: LSSubtitle(text: 'Synced Content on Devices'), - trailing: LSIconButton( - icon: Icons.sync, - color: LunaColours.list(7), - ), - onTap: () async => _onTap(context), - ); - - Future _onTap(BuildContext context) async => TautulliSyncedItemsRouter().navigateTo(context); -} diff --git a/lib/modules/tautulli/routes/recently_added/route.dart b/lib/modules/tautulli/routes/recently_added/route.dart index f7c59777..1dcaa0aa 100644 --- a/lib/modules/tautulli/routes/recently_added/route.dart +++ b/lib/modules/tautulli/routes/recently_added/route.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliRecentlyAddedRouter extends TautulliPageRouter { TautulliRecentlyAddedRouter() : super('/tautulli/recentlyadded'); @@ -17,7 +16,7 @@ class _TautulliRecentlyAddedRoute extends StatefulWidget { State<_TautulliRecentlyAddedRoute> createState() => _State(); } -class _State extends State<_TautulliRecentlyAddedRoute> { +class _State extends State<_TautulliRecentlyAddedRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @@ -33,46 +32,57 @@ class _State extends State<_TautulliRecentlyAddedRoute> { } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } - Widget get _appBar => LunaAppBar(title: 'Recently Added'); + Widget _appBar() { + return LunaAppBar( + title: 'Recently Added', + scrollControllers: [scrollController], + ); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>>( - selector: (_, state) => state.recentlyAdded, - builder: (context, stats, _) => FutureBuilder( - future: stats, - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli recently added', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: Selector>>( + selector: (_, state) => state.recentlyAdded, + builder: (context, stats, _) => FutureBuilder( + future: stats, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli recently added', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noRecentlyAdded() - : _recentlyAdded(snapshot.data); - return LSLoader(); - }, + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); + ); + } - Widget _noRecentlyAdded() => LSGenericMessage( - text: 'No Statistics Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _recentlyAdded(List added) => LSListViewBuilder( - itemCount: added.length, - itemBuilder: (context, index) => TautulliRecentlyAddedContentTile(recentlyAdded: added[index]), - ); + Widget _list(List added) { + if((added?.length ?? 0) == 0) return LunaMessage( + text: 'No Content Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: added.length, + itemBuilder: (context, index) => TautulliRecentlyAddedContentTile(recentlyAdded: added[index]), + ); + } } \ No newline at end of file diff --git a/lib/modules/tautulli/routes/recently_added/widgets/content_tile.dart b/lib/modules/tautulli/routes/recently_added/widgets/content_tile.dart index f6a1cce8..5eb1ea4a 100644 --- a/lib/modules/tautulli/routes/recently_added/widgets/content_tile.dart +++ b/lib/modules/tautulli/routes/recently_added/widgets/content_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliRecentlyAddedContentTile extends StatelessWidget { final TautulliRecentlyAdded recentlyAdded; @@ -14,27 +13,30 @@ class TautulliRecentlyAddedContentTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( - child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - child: Row( - children: [ - _poster(context), - _details(context), - ], - ), - onTap: () async => _onTap(context), - ), - decoration: recentlyAdded.art != null && recentlyAdded.art.isNotEmpty - ? LunaCardDecoration( - uri: context.watch().getImageURLFromPath( - recentlyAdded.art, - width: MediaQuery.of(context).size.width.truncate(), + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + child: Row( + children: [ + _poster(context), + _details(context), + ], ), - headers: context.watch().headers.cast(), - ) - : null, - ); + onTap: () async => _onTap(context), + ), + decoration: recentlyAdded.art != null && recentlyAdded.art.isNotEmpty + ? LunaCardDecoration( + uri: context.watch().getImageURLFromPath( + recentlyAdded.art, + width: MediaQuery.of(context).size.width.truncate(), + ), + headers: context.watch().headers.cast(), + ) + : null, + ); + } String get _posterLink { switch(recentlyAdded.mediaType) { @@ -53,10 +55,10 @@ class TautulliRecentlyAddedContentTile extends StatelessWidget { } Widget _poster(BuildContext context) { - return LSNetworkImage( + return LunaNetworkImage( url: context.watch().getImageURLFromPath(_posterLink), headers: context.watch().headers.cast(), - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', height: _imageDimension, width: _imageDimension/1.5, ); @@ -87,7 +89,7 @@ class TautulliRecentlyAddedContentTile extends StatelessWidget { if(recentlyAdded.title != null && recentlyAdded.title.isNotEmpty) title = recentlyAdded.title; if(recentlyAdded.parentTitle != null && recentlyAdded.parentTitle.isNotEmpty) title = recentlyAdded.parentTitle; if(recentlyAdded.grandparentTitle != null && recentlyAdded.grandparentTitle.isNotEmpty) title = recentlyAdded.grandparentTitle; - return LSTitle( + return LunaText.title( text: title, maxLines: 1, ); @@ -97,24 +99,24 @@ class TautulliRecentlyAddedContentTile extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ // Television - if(recentlyAdded.mediaType == TautulliMediaType.EPISODE) TextSpan(text: 'S${recentlyAdded.parentMediaIndex}\t${Constants.TEXT_BULLET}\tE${recentlyAdded.mediaIndex}'), - if(recentlyAdded.mediaType == TautulliMediaType.EPISODE) TextSpan(text: '\t${Constants.TEXT_EMDASH}\t${recentlyAdded.title}'), + if(recentlyAdded.mediaType == TautulliMediaType.EPISODE) TextSpan(text: 'S${recentlyAdded.parentMediaIndex}\t${LunaUI.TEXT_BULLET}\tE${recentlyAdded.mediaIndex}'), + if(recentlyAdded.mediaType == TautulliMediaType.EPISODE) TextSpan(text: '\t${LunaUI.TEXT_EMDASH}\t${recentlyAdded.title}'), if(recentlyAdded.mediaType == TautulliMediaType.SHOW) TextSpan(text: recentlyAdded.year.toString()), if(recentlyAdded.mediaType == TautulliMediaType.SEASON) TextSpan(text: recentlyAdded.fullTitle), // Movie if(recentlyAdded.mediaType == TautulliMediaType.MOVIE) TextSpan(text: recentlyAdded.year.toString()), // Music - if(recentlyAdded.mediaType == TautulliMediaType.ARTIST) TextSpan(text: Constants.TEXT_EMDASH), + if(recentlyAdded.mediaType == TautulliMediaType.ARTIST) TextSpan(text: LunaUI.TEXT_EMDASH), if(recentlyAdded.mediaType == TautulliMediaType.ALBUM) TextSpan(text: recentlyAdded.title), if(recentlyAdded.mediaType == TautulliMediaType.TRACK) TextSpan(text: recentlyAdded.title), - if(recentlyAdded.mediaType == TautulliMediaType.TRACK) TextSpan(text: '\t${Constants.TEXT_EMDASH}\t${recentlyAdded.parentTitle}'), + if(recentlyAdded.mediaType == TautulliMediaType.TRACK) TextSpan(text: '\t${LunaUI.TEXT_EMDASH}\t${recentlyAdded.parentTitle}'), // Other - if(recentlyAdded.mediaType == TautulliMediaType.LIVE) TextSpan(text: Constants.TEXT_EMDASH), - if(recentlyAdded.mediaType == TautulliMediaType.COLLECTION) TextSpan(text: Constants.TEXT_EMDASH), + if(recentlyAdded.mediaType == TautulliMediaType.LIVE) TextSpan(text: LunaUI.TEXT_EMDASH), + if(recentlyAdded.mediaType == TautulliMediaType.COLLECTION) TextSpan(text: LunaUI.TEXT_EMDASH), ], ), softWrap: false, @@ -122,9 +124,9 @@ class TautulliRecentlyAddedContentTile extends StatelessWidget { overflow: TextOverflow.fade, ); - Widget get _addedAt => LSSubtitle(text: recentlyAdded.addedAt?.lunaAge ?? 'Unknown'); + Widget get _addedAt => LunaText.subtitle(text: recentlyAdded.addedAt?.lunaAge ?? 'Unknown'); - Widget get _library => LSSubtitle(text: recentlyAdded.libraryName); + Widget get _library => LunaText.subtitle(text: recentlyAdded.libraryName); Future _onTap(BuildContext context) async => TautulliMediaDetailsRouter().navigateTo(context, ratingKey: recentlyAdded.ratingKey, mediaType: recentlyAdded.mediaType); } diff --git a/lib/modules/tautulli/routes/search/route.dart b/lib/modules/tautulli/routes/search/route.dart index 7bbedfb3..dcc86382 100644 --- a/lib/modules/tautulli/routes/search/route.dart +++ b/lib/modules/tautulli/routes/search/route.dart @@ -15,13 +15,13 @@ class _TautulliSearchRoute extends StatefulWidget { State<_TautulliSearchRoute> createState() => _State(); } -class _State extends State<_TautulliSearchRoute> { +class _State extends State<_TautulliSearchRoute> with LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); @override Widget build(BuildContext context) => Scaffold( key: _scaffoldKey, - appBar: TautulliSearchAppBar(), - body: TautulliSearchSearchResults(), + appBar: TautulliSearchAppBar(scrollController: scrollController), + body: TautulliSearchSearchResults(scrollController: scrollController), ); } \ No newline at end of file diff --git a/lib/modules/tautulli/routes/search/widgets/search_appbar.dart b/lib/modules/tautulli/routes/search/widgets/search_appbar.dart index 2a6500db..978d6c49 100644 --- a/lib/modules/tautulli/routes/search/widgets/search_appbar.dart +++ b/lib/modules/tautulli/routes/search/widgets/search_appbar.dart @@ -3,12 +3,22 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; // ignore: non_constant_identifier_names -Widget TautulliSearchAppBar() => LunaAppBar( +Widget TautulliSearchAppBar({ + @required ScrollController scrollController, +}) => LunaAppBar( title: 'Search', - bottom: _SearchBar(), + scrollControllers: [scrollController], + bottom: _SearchBar(scrollController: scrollController), ); class _SearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; + + _SearchBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + @override Size get preferredSize => Size.fromHeight(62.0); @@ -26,30 +36,27 @@ class _State extends State<_SearchBar> { } @override - Widget build(BuildContext context) => Container( - child: Consumer( - builder: (context, state, widget) => Row( + Widget build(BuildContext context) => Consumer( + builder: (context, state, _) => Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( - child: LSTextInputBar( + child: LunaTextInputBar( controller: _controller, - autofocus: state.searchQuery.isEmpty, - onChanged: (text, updateController) => _onChange(text, updateController), - onSubmitted: _onSubmit, - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + scrollController: widget.scrollController, + autofocus: (state.searchQuery ?? '').isEmpty, + onChanged: (value) => context.read().searchQuery = value, + onSubmitted: (value) { + if(value.isNotEmpty) context.read().fetchSearch(); + }, + margin: LunaTextInputBar.appBarMargin, ), ), ], ), + height: LunaTextInputBar.appBarHeight, ), ); - - void _onChange(String text, bool updateController) { - context.read().searchQuery = text; - if(updateController) _controller.text = text; - } - - Future _onSubmit(String value) async { - if(value.isNotEmpty) context.read().fetchSearch(); - } } diff --git a/lib/modules/tautulli/routes/search/widgets/search_result_tile.dart b/lib/modules/tautulli/routes/search/widgets/search_result_tile.dart index 0d4fbbce..83642bfa 100644 --- a/lib/modules/tautulli/routes/search/widgets/search_result_tile.dart +++ b/lib/modules/tautulli/routes/search/widgets/search_result_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliSearchResultTile extends StatelessWidget { final TautulliSearchResult result; @@ -16,30 +15,33 @@ class TautulliSearchResultTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( - child: InkWell( - child: Row( - children: [ - _poster(context), - _details, - ], + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + child: Row( + children: [ + _poster(context), + _details, + ], + ), + onTap: () async => _onTap(context), ), - onTap: () async => _onTap(context), - ), - decoration: result.art != null && result.art.isNotEmpty - ? LunaCardDecoration( - uri: context.watch().getImageURLFromPath(result.art), - headers: context.watch().headers, - ) - : null, - ); + decoration: result.art != null && result.art.isNotEmpty + ? LunaCardDecoration( + uri: context.watch().getImageURLFromPath(result.art), + headers: context.watch().headers, + ) + : null, + ); + } - Widget _poster(BuildContext context) => LSNetworkImage( + Widget _poster(BuildContext context) => LunaNetworkImage( url: context.watch().getImageURLFromPath(result.thumb), headers: context.watch().headers.cast(), height: _imageDimension, width: _imageDimension/1.5, - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', ); Widget get _details => Expanded( @@ -64,7 +66,7 @@ class TautulliSearchResultTile extends StatelessWidget { ), ); - Widget get _title => LSTitle(text: result.title, maxLines: 1); + Widget get _title => LunaText.title(text: result.title, maxLines: 1); Widget get _subtitleOne { String _text = ''; @@ -76,13 +78,13 @@ class TautulliSearchResultTile extends StatelessWidget { case TautulliMediaType.EPISODE: case TautulliMediaType.ALBUM: case TautulliMediaType.TRACK: _text = result?.parentTitle ?? ''; break; - case TautulliMediaType.COLLECTION: _text = '${result?.minYear ?? 0} ${Constants.TEXT_EMDASH} ${result?.maxYear ?? 0}'; break; + case TautulliMediaType.COLLECTION: _text = '${result?.minYear ?? 0} ${LunaUI.TEXT_EMDASH} ${result?.maxYear ?? 0}'; break; default: break; } - return LSSubtitle(text: _text, maxLines: 1); + return LunaText.subtitle(text: _text, maxLines: 1); } - Widget get _subtitleTwo => LSSubtitle(text: result.grandparentTitle, maxLines: 1); + Widget get _subtitleTwo => LunaText.subtitle(text: result.grandparentTitle, maxLines: 1); Widget get _library => Text( result.libraryName, @@ -95,5 +97,9 @@ class TautulliSearchResultTile extends StatelessWidget { ), ); - Future _onTap(BuildContext context) async => TautulliMediaDetailsRouter().navigateTo(context, ratingKey: result.ratingKey, mediaType: mediaType); + Future _onTap(BuildContext context) async => TautulliMediaDetailsRouter().navigateTo( + context, + ratingKey: result.ratingKey, + mediaType: mediaType, + ); } diff --git a/lib/modules/tautulli/routes/search/widgets/search_results.dart b/lib/modules/tautulli/routes/search/widgets/search_results.dart index 51091811..573912c1 100644 --- a/lib/modules/tautulli/routes/search/widgets/search_results.dart +++ b/lib/modules/tautulli/routes/search/widgets/search_results.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliSearchSearchResults extends StatefulWidget { + final ScrollController scrollController; + + TautulliSearchSearchResults({ + Key key, + @required this.scrollController, + }): super(key: key); + @override State createState() => _State(); } @@ -11,8 +17,6 @@ class TautulliSearchSearchResults extends StatefulWidget { class _State extends State { final GlobalKey _refreshKey = GlobalKey(); - Future _refresh() async => context.read().fetchSearch(); - @override Widget build(BuildContext context) => Selector>( selector: (_, state) => state.search, @@ -22,52 +26,55 @@ class _State extends State { }, ); - Widget _futureBuilder(Future future) => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: future, - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli search results', snapshot.error, StackTrace.current); + Widget _futureBuilder(Future future) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchSearch(), + child: FutureBuilder( + future: future, + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.connectionState == ConnectionState.none) return Container(); + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli search results', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - switch(snapshot.connectionState) { - case ConnectionState.none: return Container(); - case ConnectionState.done: - if(snapshot.hasData) return snapshot.data.count == 0 - ? _noResults() - : _results(snapshot.data.results); - break; - case ConnectionState.waiting: - case ConnectionState.active: - default: break; - } - return LSLoader(); - }, - ), - ); + if(snapshot.hasData && snapshot.connectionState == ConnectionState.done) return _results(snapshot.data); + return LunaLoader(); + }, + ), + ); + } - Widget _noResults() => LSGenericMessage(text: 'No Results Found'); - Widget _results(TautulliSearchResults results) => LSListView( - children: [ - ..._movies(results.movies), - ..._series(results.shows), - ..._seasons(results.seasons), - ..._episodes(results.episodes), - ..._artists(results.artists), - ..._albums(results.albums), - ..._tracks(results.tracks), - ..._collections(results.collections), - ], - ); + Widget _results(TautulliSearch search) { + if((search?.count ?? 0) == 0) return LunaMessage( + text: 'No Results Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListView( + controller: widget.scrollController, + children: [ + ..._movies(search.results.movies), + ..._series(search.results.shows), + ..._seasons(search.results.seasons), + ..._episodes(search.results.episodes), + ..._artists(search.results.artists), + ..._albums(search.results.albums), + ..._tracks(search.results.tracks), + ..._collections(search.results.collections), + ], + ); + } List _movies(List movies) => [ - LSHeader(text: 'movies'), - if(movies.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'movies'), + if(movies.length == 0) LunaMessage(text: 'No Results Found'), ...movies.map((movie) => TautulliSearchResultTile( result: movie, mediaType: TautulliMediaType.MOVIE, @@ -75,8 +82,8 @@ class _State extends State { ]; List _series(List series) => [ - LSHeader(text: 'series'), - if(series.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'series'), + if(series.length == 0) LunaMessage(text: 'No Results Found'), ...series.map((show) => TautulliSearchResultTile( result: show, mediaType: TautulliMediaType.SHOW, @@ -84,8 +91,8 @@ class _State extends State { ]; List _seasons(List seasons) => [ - LSHeader(text: 'seasons'), - if(seasons.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'seasons'), + if(seasons.length == 0) LunaMessage(text: 'No Results Found'), ...seasons.map((show) => TautulliSearchResultTile( result: show, mediaType: TautulliMediaType.SEASON, @@ -93,8 +100,8 @@ class _State extends State { ]; List _episodes(List episodes) => [ - LSHeader(text: 'episodes'), - if(episodes.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'episodes'), + if(episodes.length == 0) LunaMessage(text: 'No Results Found'), ...episodes.map((show) => TautulliSearchResultTile( result: show, mediaType: TautulliMediaType.EPISODE, @@ -102,8 +109,8 @@ class _State extends State { ]; List _artists(List artists) => [ - LSHeader(text: 'artists'), - if(artists.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'artists'), + if(artists.length == 0) LunaMessage(text: 'No Results Found'), ...artists.map((show) => TautulliSearchResultTile( result: show, mediaType: TautulliMediaType.ARTIST, @@ -111,8 +118,8 @@ class _State extends State { ]; List _albums(List albums) => [ - LSHeader(text: 'albums'), - if(albums.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'albums'), + if(albums.length == 0) LunaMessage(text: 'No Results Found'), ...albums.map((show) => TautulliSearchResultTile( result: show, mediaType: TautulliMediaType.ALBUM, @@ -120,8 +127,8 @@ class _State extends State { ]; List _tracks(List tracks) => [ - LSHeader(text: 'tracks'), - if(tracks.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'tracks'), + if(tracks.length == 0) LunaMessage(text: 'No Results Found'), ...tracks.map((show) => TautulliSearchResultTile( result: show, mediaType: TautulliMediaType.TRACK, @@ -129,11 +136,11 @@ class _State extends State { ]; List _collections(List collections) => [ - LSHeader(text: 'collections'), - if(collections.length == 0) LSGenericMessage(text: 'No Results Found'), + LunaHeader(text: 'collections'), + if(collections.length == 0) LunaMessage(text: 'No Results Found'), ...collections.map((show) => TautulliSearchResultTile( result: show, mediaType: TautulliMediaType.COLLECTION, )), ]; -} \ No newline at end of file +} diff --git a/lib/modules/tautulli/routes/statistics/route.dart b/lib/modules/tautulli/routes/statistics/route.dart index 214ae975..9253909e 100644 --- a/lib/modules/tautulli/routes/statistics/route.dart +++ b/lib/modules/tautulli/routes/statistics/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliStatisticsRouter extends TautulliPageRouter { TautulliStatisticsRouter() : super('/tautulli/statistics'); @@ -17,77 +15,84 @@ class _TautulliStatisticsRoute extends StatefulWidget { State<_TautulliStatisticsRoute> createState() => _State(); } -class _State extends State<_TautulliStatisticsRoute> { +class _State extends State<_TautulliStatisticsRoute> with LunaLoadCallbackMixin, LunaScrollControllerMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); + final List denylist = [ + 'top_libraries', + 'popular_music', + 'top_music', + ]; - Future _refresh() async { + Future loadCallback() async { context.read().resetStatistics(); await context.read().statistics; } @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); } - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); + Widget _appBar() { + return LunaAppBar( + title: 'Statistics', + scrollControllers: [scrollController], + actions: [ + TautulliStatisticsTypeButton(), + TautulliStatisticsTimeRangeButton(), + ], + ); + } - Widget get _appBar => LunaAppBar( - title: 'Statistics', - actions: [ - TautulliStatisticsTypeButton(), - TautulliStatisticsTimeRangeButton(), - ], - ); - - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>>( - selector: (_, state) => state.statistics, - builder: (context, stats, _) => FutureBuilder( - future: stats, - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli statistics', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector>>( + selector: (_, state) => state.statistics, + builder: (context, stats, _) => FutureBuilder( + future: stats, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli statistics', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noStatistics() - : _statistics(snapshot.data); - return LSLoader(); - }, + if(snapshot.hasData) return _statistics(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); - - Widget _noStatistics() => LSGenericMessage( - text: 'No Statistics Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + ); + } Widget _statistics(List stats) { + if((stats?.length ?? 0) == 0) return LunaMessage( + text: 'No Statistics Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); List> list = []; stats.forEach((element) => list.add(_builder(element))); - return LSListView( + return LunaListView( + controller: scrollController, children: list.expand((e) => e).toList(), ); } - List _builder(TautulliHomeStats stats) => stats.data.length > 0 - ? [ - LSHeader(text: stats.title), + List _builder(TautulliHomeStats stats) { + if((stats?.data ?? 0) == 0 || denylist.contains(stats.id)) return []; + return [ + LunaHeader(text: stats.title), ...List.generate( stats.data.length, (index) { @@ -115,6 +120,6 @@ class _State extends State<_TautulliStatisticsRoute> { } } ) - ] - : []; + ]; + } } diff --git a/lib/modules/tautulli/routes/statistics/widgets/media_tile.dart b/lib/modules/tautulli/routes/statistics/widgets/media_tile.dart index 59c6da07..afe58428 100644 --- a/lib/modules/tautulli/routes/statistics/widgets/media_tile.dart +++ b/lib/modules/tautulli/routes/statistics/widgets/media_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliStatisticsMediaTile extends StatelessWidget { final Map data; @@ -16,32 +15,35 @@ class TautulliStatisticsMediaTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( - child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - child: Row( - children: [ - _poster(context), - _details(context), - ], - ), - onTap: () async => _onTap(context), - ), - decoration: data['art'] != null && (data['art'] as String).isNotEmpty - ? LunaCardDecoration( - uri: context.watch().getImageURLFromPath( - data['art'], - width: MediaQuery.of(context).size.width.truncate(), + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + child: Row( + children: [ + _poster(context), + _details(context), + ], ), - headers: context.watch().headers.cast(), - ) - : null, - ); + onTap: () async => _onTap(context), + ), + decoration: data['art'] != null && (data['art'] as String).isNotEmpty + ? LunaCardDecoration( + uri: context.watch().getImageURLFromPath( + data['art'], + width: MediaQuery.of(context).size.width.truncate(), + ), + headers: context.watch().headers.cast(), + ) + : null, + ); + } - Widget _poster(BuildContext context) => LSNetworkImage( + Widget _poster(BuildContext context) => LunaNetworkImage( url: context.watch().getImageURLFromPath(data['thumb']), headers: context.watch().headers.cast(), - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', height: _imageDimension, width: _imageDimension/1.5, ); @@ -64,7 +66,7 @@ class TautulliStatisticsMediaTile extends StatelessWidget { ), ); - Widget get _title => LSTitle( + Widget get _title => LunaText.title( text: data['title'], maxLines: 1, ); @@ -73,7 +75,7 @@ class TautulliStatisticsMediaTile extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -100,11 +102,11 @@ class TautulliStatisticsMediaTile extends StatelessWidget { : null, ), ) - : TextSpan(text: '${Constants.TEXT_EMDASH}'), + : TextSpan(text: '${LunaUI.TEXT_EMDASH}'), TextSpan(text: '\n'), data['last_play'] != null ? TextSpan(text: 'Last Played ' + DateTime.fromMillisecondsSinceEpoch(data['last_play']*1000)?.lunaAge ?? 'Unknown') - : TextSpan(text: Constants.TEXT_EMDASH) + : TextSpan(text: LunaUI.TEXT_EMDASH) ], ), softWrap: false, diff --git a/lib/modules/tautulli/routes/statistics/widgets/platform_tile.dart b/lib/modules/tautulli/routes/statistics/widgets/platform_tile.dart index 2c834157..8cbbaeb8 100644 --- a/lib/modules/tautulli/routes/statistics/widgets/platform_tile.dart +++ b/lib/modules/tautulli/routes/statistics/widgets/platform_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliStatisticsPlatformTile extends StatelessWidget { final Map data; @@ -14,7 +13,8 @@ class TautulliStatisticsPlatformTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Row( children: [ _placeholder, @@ -27,7 +27,7 @@ class TautulliStatisticsPlatformTile extends StatelessWidget { height: _imageDimension, width: _imageDimension/1.5, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), image: DecorationImage( image: AssetImage('assets/images/blanks/platform.png'), fit: BoxFit.cover, @@ -53,7 +53,7 @@ class TautulliStatisticsPlatformTile extends StatelessWidget { ), ); - Widget get _title => LSTitle( + Widget get _title => LunaText.title( text: data['platform'] ?? 'Unknown Platform', maxLines: 1, ); @@ -62,7 +62,7 @@ class TautulliStatisticsPlatformTile extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -89,11 +89,11 @@ class TautulliStatisticsPlatformTile extends StatelessWidget { : null, ), ) - : TextSpan(text: '${Constants.TEXT_EMDASH}'), + : TextSpan(text: '${LunaUI.TEXT_EMDASH}'), TextSpan(text: '\n'), data['last_play'] != null ? TextSpan(text: 'Last Used ' + DateTime.fromMillisecondsSinceEpoch(data['last_play']*1000)?.lunaAge ?? 'Unknown') - : TextSpan(text: Constants.TEXT_EMDASH) + : TextSpan(text: LunaUI.TEXT_EMDASH) ], ), softWrap: false, diff --git a/lib/modules/tautulli/routes/statistics/widgets/recently_watched_tile.dart b/lib/modules/tautulli/routes/statistics/widgets/recently_watched_tile.dart index 759d1959..e21f8cbf 100644 --- a/lib/modules/tautulli/routes/statistics/widgets/recently_watched_tile.dart +++ b/lib/modules/tautulli/routes/statistics/widgets/recently_watched_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliStatisticsRecentlyWatchedTile extends StatelessWidget { final Map data; @@ -14,9 +13,10 @@ class TautulliStatisticsRecentlyWatchedTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), child: Row( children: [ _poster(context), @@ -36,10 +36,10 @@ class TautulliStatisticsRecentlyWatchedTile extends StatelessWidget { : null, ); - Widget _poster(BuildContext context) => LSNetworkImage( + Widget _poster(BuildContext context) => LunaNetworkImage( url: context.watch().getImageURLFromPath(data['thumb']), headers: context.watch().headers.cast(), - placeholder: 'assets/images/blanks/video.png', + placeholderAsset: 'assets/images/blanks/video.png', height: _imageDimension, width: _imageDimension/1.5, ); @@ -62,7 +62,7 @@ class TautulliStatisticsRecentlyWatchedTile extends StatelessWidget { ), ); - Widget get _title => LSTitle( + Widget get _title => LunaText.title( text: data['title'], maxLines: 1, ); @@ -71,18 +71,18 @@ class TautulliStatisticsRecentlyWatchedTile extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan(text: data['friendly_name'] ?? 'Unknown User'), TextSpan(text: '\n'), data['player'] != null ? TextSpan(text: data['player']) - : TextSpan(text: '${Constants.TEXT_EMDASH}'), + : TextSpan(text: '${LunaUI.TEXT_EMDASH}'), TextSpan(text: '\n'), data['last_watch'] != null ? TextSpan(text: 'Watched ' + DateTime.fromMillisecondsSinceEpoch(data['last_watch']*1000)?.lunaAge ?? 'Unknown') - : TextSpan(text: Constants.TEXT_EMDASH) + : TextSpan(text: LunaUI.TEXT_EMDASH) ], ), softWrap: false, diff --git a/lib/modules/tautulli/routes/statistics/widgets/stream_tile.dart b/lib/modules/tautulli/routes/statistics/widgets/stream_tile.dart index b9d632cd..24145736 100644 --- a/lib/modules/tautulli/routes/statistics/widgets/stream_tile.dart +++ b/lib/modules/tautulli/routes/statistics/widgets/stream_tile.dart @@ -13,7 +13,8 @@ class TautulliStatisticsStreamTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Row( children: [ _placeholder, @@ -26,7 +27,7 @@ class TautulliStatisticsStreamTile extends StatelessWidget { height: _imageDimension, width: _imageDimension/1.5, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), image: DecorationImage( image: AssetImage('assets/images/blanks/stream.png'), fit: BoxFit.cover, @@ -52,7 +53,7 @@ class TautulliStatisticsStreamTile extends StatelessWidget { ), ); - Widget get _title => LSTitle( + Widget get _title => LunaText.title( text: data['title'] ?? 'Unknown Title', maxLines: 1, ); @@ -61,7 +62,7 @@ class TautulliStatisticsStreamTile extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -78,11 +79,11 @@ class TautulliStatisticsStreamTile extends StatelessWidget { ? DateFormat('yyyy-MM-dd HH:mm').format(DateTime.fromMillisecondsSinceEpoch(int.tryParse(data['started']) * 1000)) : DateFormat('yyyy-MM-dd hh:mm a').format(DateTime.fromMillisecondsSinceEpoch(int.tryParse(data['started']) * 1000)), ) - : TextSpan(text: '${Constants.TEXT_EMDASH}'), + : TextSpan(text: '${LunaUI.TEXT_EMDASH}'), TextSpan(text: '\n'), data['last_play'] != null ? TextSpan(text: 'Last Used ' + DateTime.fromMillisecondsSinceEpoch(data['last_play']*1000)?.lunaAge ?? 'Unknown') - : TextSpan(text: Constants.TEXT_EMDASH) + : TextSpan(text: LunaUI.TEXT_EMDASH) ], ), softWrap: false, diff --git a/lib/modules/tautulli/routes/statistics/widgets/time_range_button.dart b/lib/modules/tautulli/routes/statistics/widgets/time_range_button.dart index 6bbf6d91..43a40db7 100644 --- a/lib/modules/tautulli/routes/statistics/widgets/time_range_button.dart +++ b/lib/modules/tautulli/routes/statistics/widgets/time_range_button.dart @@ -20,7 +20,7 @@ class TautulliStatisticsTimeRangeButton extends StatelessWidget { child: Text( TautulliStatisticsTimeRange.values[index].name, style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: range == TautulliStatisticsTimeRange.values[index] ? LunaColours.accent : Colors.white, diff --git a/lib/modules/tautulli/routes/statistics/widgets/type_button.dart b/lib/modules/tautulli/routes/statistics/widgets/type_button.dart index 65689478..b2d0f319 100644 --- a/lib/modules/tautulli/routes/statistics/widgets/type_button.dart +++ b/lib/modules/tautulli/routes/statistics/widgets/type_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliStatisticsTypeButton extends StatelessWidget { @override @@ -21,7 +20,7 @@ class TautulliStatisticsTypeButton extends StatelessWidget { child: Text( TautulliStatsType.values[index].value.lunaCapitalizeFirstLetters(), style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, color: type == TautulliStatsType.values[index] ? LunaColours.accent : Colors.white, diff --git a/lib/modules/tautulli/routes/statistics/widgets/user_tile.dart b/lib/modules/tautulli/routes/statistics/widgets/user_tile.dart index 87b6d7f2..c5e8308c 100644 --- a/lib/modules/tautulli/routes/statistics/widgets/user_tile.dart +++ b/lib/modules/tautulli/routes/statistics/widgets/user_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliStatisticsUserTile extends StatelessWidget { final Map data; @@ -14,9 +13,10 @@ class TautulliStatisticsUserTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), child: Row( children: [ _poster(context), @@ -27,10 +27,10 @@ class TautulliStatisticsUserTile extends StatelessWidget { ), ); - Widget _poster(BuildContext context) => LSNetworkImage( + Widget _poster(BuildContext context) => LunaNetworkImage( url: context.watch().getImageURLFromPath(data['user_thumb']), headers: context.watch().headers.cast(), - placeholder: 'assets/images/blanks/user.png', + placeholderAsset: 'assets/images/blanks/user.png', height: _imageDimension, width: _imageDimension/1.5, ); @@ -53,7 +53,7 @@ class TautulliStatisticsUserTile extends StatelessWidget { ), ); - Widget get _title => LSTitle( + Widget get _title => LunaText.title( text: data['friendly_name'] ?? 'Unknown User', maxLines: 1, ); @@ -62,7 +62,7 @@ class TautulliStatisticsUserTile extends StatelessWidget { text: TextSpan( style: TextStyle( color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), children: [ TextSpan( @@ -89,11 +89,11 @@ class TautulliStatisticsUserTile extends StatelessWidget { : null, ), ) - : TextSpan(text: '${Constants.TEXT_EMDASH}'), + : TextSpan(text: '${LunaUI.TEXT_EMDASH}'), TextSpan(text: '\n'), data['last_play'] != null ? TextSpan(text: 'Last Streamed ' + DateTime.fromMillisecondsSinceEpoch(data['last_play']*1000)?.lunaAge ?? 'Unknown') - : TextSpan(text: Constants.TEXT_EMDASH) + : TextSpan(text: LunaUI.TEXT_EMDASH) ], ), softWrap: false, @@ -109,11 +109,9 @@ class TautulliStatisticsUserTile extends StatelessWidget { ), ); if(_user == null) { - LSSnackBar( - context: context, + showLunaErrorSnackBar( title: 'User Not Found', message: 'Unable to find the Tautulli user', - type: SNACKBAR_TYPE.failure, ); } else { TautulliUserDetailsRouter().navigateTo(context, userId: _user.userId); diff --git a/lib/modules/tautulli/routes/synced_items/route.dart b/lib/modules/tautulli/routes/synced_items/route.dart index 93f3ab12..a9825812 100644 --- a/lib/modules/tautulli/routes/synced_items/route.dart +++ b/lib/modules/tautulli/routes/synced_items/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliSyncedItemsRouter extends TautulliPageRouter { TautulliSyncedItemsRouter() : super('/tautulli/synceditems'); @@ -17,62 +15,67 @@ class _TautulliSyncedItemsRoute extends StatefulWidget { State<_TautulliSyncedItemsRoute> createState() => _State(); } -class _State extends State<_TautulliSyncedItemsRoute> { +class _State extends State<_TautulliSyncedItemsRoute> with LunaScrollControllerMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); - @override - void initState() { - super.initState(); - SchedulerBinding.instance.addPostFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { context.read().resetSyncedItems(); await context.read().syncedItems; } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } - Widget get _appBar => LunaAppBar(title: 'Synced Items'); + Widget _appBar() { + return LunaAppBar( + title: 'Synced Items', + scrollControllers: [scrollController], + ); + } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>>( - selector: (_, state) => state.syncedItems, - builder: (context, synced, _) => FutureBuilder( - future: synced, - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli synced items', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector>>( + selector: (_, state) => state.syncedItems, + builder: (context, synced, _) => FutureBuilder( + future: synced, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli synced items', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noSyncedItems() - : _syncedItems(snapshot.data); - return LSLoader(); - }, + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); + ); + } - Widget _syncedItems(List syncedItems) => LSListViewBuilder( - itemCount: syncedItems.length, - itemBuilder: (context, index) => TautulliSyncedItemTile(syncedItem: syncedItems[index]), - ); - - Widget _noSyncedItems() => LSGenericMessage( - text: 'No Synced Items Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + Widget _list(List syncedItems) { + if((syncedItems?.length ?? 0) == 0) return LunaMessage( + text: 'No Synced Items Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: syncedItems.length, + itemBuilder: (context, index) => TautulliSyncedItemTile(syncedItem: syncedItems[index]), + ); + } } diff --git a/lib/modules/tautulli/routes/synced_items/widgets/synced_item_tile.dart b/lib/modules/tautulli/routes/synced_items/widgets/synced_item_tile.dart index 603b4213..acf6324f 100644 --- a/lib/modules/tautulli/routes/synced_items/widgets/synced_item_tile.dart +++ b/lib/modules/tautulli/routes/synced_items/widgets/synced_item_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliSyncedItemTile extends StatelessWidget { final TautulliSyncedItem syncedItem; @@ -12,52 +11,55 @@ class TautulliSyncedItemTile extends StatelessWidget { }): super(key: key); @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: syncedItem.syncTitle), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - TextSpan( - text: (syncedItem.state ?? 'Unknown').lunaCapitalizeFirstLetters(), - style: TextStyle( - color: LunaColours.accent, - fontWeight: LunaUI.FONT_WEIGHT_BOLD, + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: syncedItem.syncTitle), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + children: [ + TextSpan( + text: (syncedItem.state ?? 'Unknown').lunaCapitalizeFirstLetters(), + style: TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), ), - ), - TextSpan(text: '\t${Constants.TEXT_EMDASH}\t'), - TextSpan(text: (syncedItem.metadataType ?? 'Unknown').lunaCapitalizeFirstLetters()), - TextSpan(text: '\t${Constants.TEXT_BULLET}\t'), - TextSpan(text: (syncedItem.itemCompleteCount ?? 0) == 1 - ? '1 Item' - : '${(syncedItem.itemCompleteCount ?? 0)} Items', - ), - TextSpan(text: '\t${Constants.TEXT_BULLET}\t'), - TextSpan(text: (syncedItem.totalSize ?? 0).lunaBytesToString(decimals: 1)), - TextSpan(text: '\n'), - TextSpan(text: syncedItem.user ?? 'Unknown User'), - TextSpan(text: '\n'), - TextSpan(text: syncedItem.platform ?? 'Unknown Platform'), - TextSpan(text: '\t${Constants.TEXT_BULLET}\t'), - TextSpan(text: syncedItem.deviceName ?? 'Unknown Device'), - ], - ), - ), - decoration: syncedItem.ratingKey != null - ? LunaCardDecoration( - uri: context.watch().getImageURLFromRatingKey( - syncedItem.ratingKey, - width: MediaQuery.of(context).size.width.truncate(), + TextSpan(text: '\t${LunaUI.TEXT_EMDASH}\t'), + TextSpan(text: (syncedItem.metadataType ?? 'Unknown').lunaCapitalizeFirstLetters()), + TextSpan(text: '\t${LunaUI.TEXT_BULLET}\t'), + TextSpan(text: (syncedItem.itemCompleteCount ?? 0) == 1 + ? '1 Item' + : '${(syncedItem.itemCompleteCount ?? 0)} Items', + ), + TextSpan(text: '\t${LunaUI.TEXT_BULLET}\t'), + TextSpan(text: (syncedItem.totalSize ?? 0).lunaBytesToString(decimals: 1)), + TextSpan(text: '\n'), + TextSpan(text: syncedItem.user ?? 'Unknown User'), + TextSpan(text: '\n'), + TextSpan(text: syncedItem.platform ?? 'Unknown Platform'), + TextSpan(text: '\t${LunaUI.TEXT_BULLET}\t'), + TextSpan(text: syncedItem.deviceName ?? 'Unknown Device'), + ], ), - headers: context.watch().headers, - ) - : null, - onTap: () async => _onTap(context), - padContent: true, - ); + ), + decoration: syncedItem.ratingKey != null + ? LunaCardDecoration( + uri: context.watch().getImageURLFromRatingKey( + syncedItem.ratingKey, + width: MediaQuery.of(context).size.width.truncate(), + ), + headers: context.watch().headers, + ) + : null, + onTap: () async => _onTap(context), + contentPadding: true, + ); + } Future _onTap(BuildContext context) async => TautulliMediaDetailsRouter().navigateTo(context, ratingKey: syncedItem.ratingKey, mediaType: TautulliMediaType.NULL.from(syncedItem.metadataType)); } diff --git a/lib/modules/tautulli/routes/tautulli/route.dart b/lib/modules/tautulli/routes/tautulli/route.dart index c23ba82e..e6e7ba16 100644 --- a/lib/modules/tautulli/routes/tautulli/route.dart +++ b/lib/modules/tautulli/routes/tautulli/route.dart @@ -27,27 +27,15 @@ class _State extends State<_TautulliHomeRoute> { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ LunaDatabaseValue.ENABLED_PROFILE.key ]), - builder: (context, box, _) => Scaffold( - key: _scaffoldKey, - drawer: _drawer(), - appBar: _appBar(), - bottomNavigationBar: _bottomNavigationBar(), - body: _body(), - ), - ), + return LunaScaffold( + scaffoldKey: _scaffoldKey, + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), ); } - Future _onWillPop() async { - if(_scaffoldKey.currentState.isDrawerOpen) return true; - _scaffoldKey.currentState.openDrawer(); - return false; - } - Widget _drawer() => LunaDrawer(page: LunaModule.TAUTULLI.key); Widget _bottomNavigationBar() { @@ -66,6 +54,7 @@ class _State extends State<_TautulliHomeRoute> { ]; return LunaAppBar.dropdown( title: LunaModule.TAUTULLI.name, + useDrawer: true, profiles: profiles, actions: actions, pageController: _pageController, diff --git a/lib/modules/tautulli/routes/tautulli/widgets/navigation_bar.dart b/lib/modules/tautulli/routes/tautulli/widgets/navigation_bar.dart index 27653968..74bd75ef 100644 --- a/lib/modules/tautulli/routes/tautulli/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/routes/tautulli/widgets/navigation_bar.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliNavigationBar extends StatefulWidget { final PageController pageController; diff --git a/lib/modules/tautulli/routes/users/route.dart b/lib/modules/tautulli/routes/users/route.dart index 2e12663a..fc0ec7ce 100644 --- a/lib/modules/tautulli/routes/users/route.dart +++ b/lib/modules/tautulli/routes/users/route.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliUsersRoute extends StatefulWidget { TautulliUsersRoute({ @@ -13,65 +11,63 @@ class TautulliUsersRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - Future _refresh() async { + Future loadCallback() async { context.read().resetUsers(); await context.read().users; } - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - @override Widget build(BuildContext context) { super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: Selector>( - selector: (_, state) => state.users, - builder: (context, users, _) => FutureBuilder( - future: users, - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli users', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector>( + selector: (_, state) => state.users, + builder: (context, users, _) => FutureBuilder( + future: users, + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli users', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.users.length == 0 - ? _noUsers() - : _users(snapshot.data); - return LSLoader(); - }, + if(snapshot.hasData) return _users(snapshot.data); + return LunaLoader(); + }, + ), ), - ), - ); + ); + } - Widget _users(TautulliUsersTable users) => LSListViewBuilder( + Widget _users(TautulliUsersTable users) { + if((users?.users?.length ?? 0) == 0) return LunaMessage( + text: 'No Users Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: TautulliNavigationBar.scrollControllers[1], itemCount: users.users.length, itemBuilder: (context, index) => TautulliUserTile(user: users.users[index]), ); - - Widget _noUsers() => LSGenericMessage( - text: 'No Users Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + } } diff --git a/lib/modules/tautulli/routes/users/widgets/user_tile.dart b/lib/modules/tautulli/routes/users/widgets/user_tile.dart index 5fa44529..f0af3c47 100644 --- a/lib/modules/tautulli/routes/users/widgets/user_tile.dart +++ b/lib/modules/tautulli/routes/users/widgets/user_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliUserTile extends StatelessWidget { final TautulliTableUser user; @@ -14,9 +13,10 @@ class TautulliUserTile extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: InkWell( - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), child: Row( children: [ _userThumb(context), @@ -64,19 +64,19 @@ class TautulliUserTile extends StatelessWidget { ), ); - Widget get _title => LSTitle(text: user.friendlyName); + Widget get _title => LunaText.title(text: user.friendlyName); Widget get _lastPlayedTime => Row( children: [ Padding( child: Icon( Icons.visibility, - size: Constants.UI_FONT_SIZE_SUBTITLE, + size: LunaUI.FONT_SIZE_SUBTITLE, ), padding: EdgeInsets.only(right: 6.0), ), Expanded( - child: LSSubtitle( + child: LunaText.subtitle( text: user.lastSeen?.lunaAge ?? 'Never', maxLines: 1, ), @@ -89,12 +89,12 @@ class TautulliUserTile extends StatelessWidget { Padding( child: Icon( Icons.play_arrow, - size: Constants.UI_FONT_SIZE_SUBTITLE, + size: LunaUI.FONT_SIZE_SUBTITLE, ), padding: EdgeInsets.only(right: 6.0), ), Expanded( - child: LSSubtitle( + child: LunaText.subtitle( text: user.lastPlayed ?? 'Never', maxLines: 1, ), diff --git a/lib/modules/tautulli/routes/users_details/route.dart b/lib/modules/tautulli/routes/users_details/route.dart index 1ec875f8..6bedb097 100644 --- a/lib/modules/tautulli/routes/users_details/route.dart +++ b/lib/modules/tautulli/routes/users_details/route.dart @@ -1,9 +1,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliUserDetailsRouter extends TautulliPageRouter { TautulliUserDetailsRouter() : super('/tautulli/user/:userid'); @@ -33,18 +31,17 @@ class _TautulliUserDetailsRoute extends StatefulWidget { State createState() => _State(); } -class _State extends State<_TautulliUserDetailsRoute> { +class _State extends State<_TautulliUserDetailsRoute> with LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); - PageController _pageController; + LunaPageController _pageController; @override void initState() { super.initState(); - _pageController = PageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_USER_DETAILS.data); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + _pageController = LunaPageController(initialPage: TautulliDatabaseValue.NAVIGATION_INDEX_USER_DETAILS.data); } - Future _refresh() async { + Future loadCallback() async { context.read().resetUsers(); await context.read().users; } @@ -57,23 +54,24 @@ class _State extends State<_TautulliUserDetailsRoute> { } @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - body: _body, - ); + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body, + ); + } - Widget get _appBar => LunaAppBar(title: 'User Details'); + Widget _appBar() { + return LunaAppBar( + title: 'User Details', + pageController: _pageController, + scrollControllers: TautulliUserDetailsNavigationBar.scrollControllers, + ); + } - Widget get _bottomNavigationBar => TautulliUserDetailsNavigationBar(pageController: _pageController); - - List _tabs(TautulliTableUser user) => [ - TautulliUserDetailsProfile(user: user), - TautulliUserDetailsHistory(user: user), - TautulliUserDetailsSyncedItems(user: user), - TautulliUserDetailsIPAddresses(user: user), - ]; + Widget _bottomNavigationBar() => TautulliUserDetailsNavigationBar(pageController: _pageController); Widget get _body => Selector>( selector: (_, state) => state.users, @@ -81,24 +79,35 @@ class _State extends State<_TautulliUserDetailsRoute> { future: future, builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to pull Tautulli user table', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () => _refresh()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to pull Tautulli user table', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: loadCallback); } if(snapshot.hasData) { TautulliTableUser user = _findUser(snapshot.data); - return user == null - ? _unknown - : PageView( - controller: _pageController, - children: _tabs(user), - ); + if(user == null) return LunaMessage.goBack( + context: context, + text: 'User Not Found', + ); + return _page(user); } - return LSLoader(); + return LunaLoader(); }, ), ); - Widget get _unknown => LSGenericMessage(text: 'User Record Not Found'); + Widget _page(TautulliTableUser user) { + return PageView( + controller: _pageController, + children: [ + TautulliUserDetailsProfile(user: user), + TautulliUserDetailsHistory(user: user), + TautulliUserDetailsSyncedItems(user: user), + TautulliUserDetailsIPAddresses(user: user), + ], + ); + } } diff --git a/lib/modules/tautulli/routes/users_details/widgets/history.dart b/lib/modules/tautulli/routes/users_details/widgets/history.dart index 029506df..ac96e02e 100644 --- a/lib/modules/tautulli/routes/users_details/widgets/history.dart +++ b/lib/modules/tautulli/routes/users_details/widgets/history.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:tautulli/tautulli.dart'; class TautulliUserDetailsHistory extends StatefulWidget { final TautulliTableUser user; @@ -16,20 +14,14 @@ class TautulliUserDetailsHistory extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { context.read().setUserHistory( widget.user.userId, context.read().api.history.getHistory( @@ -45,42 +37,43 @@ class _State extends State with AutomaticKeepAliveCl super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, + Widget _body() => LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, child: FutureBuilder( future: context.watch().userHistory[widget.user.userId], builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli user history: ${widget.user.userId}', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli user history: ${widget.user.userId}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - if(snapshot.hasData) return snapshot.data.records.length == 0 - ? _noHistory() - : _history(snapshot.data); - return LSLoader(); + if(snapshot.hasData) return _history(snapshot.data); + return LunaLoader(); }, ), ); - Widget _history(TautulliHistory history) => LSListViewBuilder( - itemCount: history.records.length, - itemBuilder: (context, index) => TautulliHistoryTile( - userId: history.records[index].userId, - history: history.records[index], - ), - ); - - Widget _noHistory() => LSGenericMessage( - text: 'No History Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + Widget _history(TautulliHistory history) { + if((history?.records ?? 0) == 0) return LunaMessage( + text: 'No History Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: TautulliUserDetailsNavigationBar.scrollControllers[1], + itemCount: history.records.length, + itemBuilder: (context, index) => TautulliHistoryTile( + history: history.records[index], + ), + ); + } } diff --git a/lib/modules/tautulli/routes/users_details/widgets/ip_addresses.dart b/lib/modules/tautulli/routes/users_details/widgets/ip_addresses.dart index 97a15000..ebd43ccc 100644 --- a/lib/modules/tautulli/routes/users_details/widgets/ip_addresses.dart +++ b/lib/modules/tautulli/routes/users_details/widgets/ip_addresses.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:tautulli/tautulli.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; @@ -16,20 +14,14 @@ class TautulliUserDetailsIPAddresses extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { context.read().setUserIPs( widget.user.userId, context.read().api.users.getUserIPs(userId: widget.user.userId), @@ -42,75 +34,82 @@ class _State extends State with AutomaticKeepAli super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().userIPs[widget.user.userId], - builder: (context, AsyncSnapshot snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli user IP addresses: ${widget.user.userId}', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: context.watch().userIPs[widget.user.userId], + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli user IP addresses: ${widget.user.userId}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.ips.length == 0 - ? _noIPs() - : _ips(snapshot.data); - return LSLoader(); - }, - ), - ); - - Widget _ips(TautulliUserIPs ips) => LSListViewBuilder( - itemCount: ips.ips.length, - itemBuilder: (context, index) => _tile(ips.ips[index]), - ); - - Widget _noIPs() => LSGenericMessage( - text: 'No IPs Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); - - Widget _tile(TautulliUserIPRecord record) => LSCardTile( - title: LSTitle(text: record.ipAddress), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - TextSpan(text: record.lastSeen?.lunaAge ?? 'Unknown'), - TextSpan(text: '\t${Constants.TEXT_EMDASH}\t'), - TextSpan(text: record.playCount == 1 - ? '1 Play' - : '${record.playCount} Plays', - ), - TextSpan(text: '\n'), - TextSpan(text: record.lastPlayed), - ], + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, ), - maxLines: 2, - softWrap: false, - overflow: TextOverflow.fade, - ), - decoration: record.thumb != null && record.thumb.isNotEmpty - ? LunaCardDecoration( - uri: context.read().getImageURLFromPath( - record.thumb ?? '', - width: MediaQuery.of(context).size.width.truncate(), + ); + } + + Widget _list(TautulliUserIPs ips) { + if((ips?.ips?.length ?? 0) == 0) return LunaMessage( + text: 'No IPs Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: TautulliUserDetailsNavigationBar.scrollControllers[3], + itemCount: ips.ips.length, + itemBuilder: (context, index) => _tile(ips.ips[index]), + ); + } + + Widget _tile(TautulliUserIPRecord record) { + return LunaListTile( + context: context, + title: LunaText.title(text: record.ipAddress), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + children: [ + TextSpan(text: record.lastSeen?.lunaAge ?? 'Unknown'), + TextSpan(text: '\t${LunaUI.TEXT_EMDASH}\t'), + TextSpan(text: record.playCount == 1 + ? '1 Play' + : '${record.playCount} Plays', + ), + TextSpan(text: '\n'), + TextSpan(text: record.lastPlayed), + ], ), - headers: context.read().headers, - ) - : null, - padContent: true, - onTap: () async => TautulliIPAddressDetailsRouter().navigateTo(context, ipAddress: record.ipAddress), - ); + maxLines: 2, + softWrap: false, + overflow: TextOverflow.fade, + ), + decoration: record.thumb != null && record.thumb.isNotEmpty + ? LunaCardDecoration( + uri: context.read().getImageURLFromPath( + record.thumb ?? '', + width: MediaQuery.of(context).size.width.truncate(), + ), + headers: context.read().headers, + ) + : null, + contentPadding: true, + onTap: () async => TautulliIPAddressDetailsRouter().navigateTo(context, ipAddress: record.ipAddress), + ); + } } diff --git a/lib/modules/tautulli/routes/users_details/widgets/navigation_bar.dart b/lib/modules/tautulli/routes/users_details/widgets/navigation_bar.dart index 404629e8..342aae8a 100644 --- a/lib/modules/tautulli/routes/users_details/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/routes/users_details/widgets/navigation_bar.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/tautulli.dart'; -class TautulliUserDetailsNavigationBar extends StatefulWidget { +class TautulliUserDetailsNavigationBar extends StatelessWidget { + final PageController pageController; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + static const List icons = [ LunaIcons.user, LunaIcons.history, @@ -17,46 +19,18 @@ class TautulliUserDetailsNavigationBar extends StatefulWidget { 'IPs', ]; - final PageController pageController; - TautulliUserDetailsNavigationBar({ Key key, @required this.pageController, }): super(key: key); @override - State createState() => _State(); -} - -class _State extends State { - int _index = TautulliDatabaseValue.NAVIGATION_INDEX_USER_DETAILS.data; - - @override - void initState() { - super.initState(); - widget.pageController?.addListener(_pageControllerListener); + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); } - - @override - void dispose() { - widget.pageController?.removeListener(_pageControllerListener); - super.dispose(); - } - - void _pageControllerListener() { - if(widget.pageController.page.round() == _index) return; - setState(() => _index = widget.pageController.page.round()); - } - - @override - Widget build(BuildContext context) => LSBottomNavigationBar( - index: _index, - icons: TautulliUserDetailsNavigationBar.icons, - titles: TautulliUserDetailsNavigationBar.titles, - onTap: _navOnTap, - ); - - Future _navOnTap(int index) async { - widget.pageController.lunaJumpToPage(index); - } -} +} \ No newline at end of file diff --git a/lib/modules/tautulli/routes/users_details/widgets/profile.dart b/lib/modules/tautulli/routes/users_details/widgets/profile.dart index bc76ca3f..19690d37 100644 --- a/lib/modules/tautulli/routes/users_details/widgets/profile.dart +++ b/lib/modules/tautulli/routes/users_details/widgets/profile.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:tautulli/tautulli.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; @@ -16,7 +14,7 @@ class TautulliUserDetailsProfile extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @@ -26,13 +24,7 @@ class _State extends State with AutomaticKeepAliveCl // Tracks the initial load to ensure the futures have been initialized bool _initialLoad = false; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { // Initial load or refresh of the user profile data context.read().setUserProfile( widget.user.userId, @@ -62,14 +54,15 @@ class _State extends State with AutomaticKeepAliveCl super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _initialLoad ? _body() : LunaLoader(), ); } - Widget get _body => _initialLoad - ? LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, child: FutureBuilder( future: Future.wait([ context.watch().userProfile[widget.user.userId], @@ -78,60 +71,70 @@ class _State extends State with AutomaticKeepAliveCl ]), builder: (context, AsyncSnapshot> snapshot) { if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli user: ${widget.user.userId}', snapshot.error, StackTrace.current); - } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli user: ${widget.user.userId}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); } if(snapshot.hasData) return _list( user: snapshot.data[0], watchtime: snapshot.data[1], player: snapshot.data[2], ); - return LSLoader(); + return LunaLoader(); }, ), - ) - : LSLoader(); + ); + } Widget _list({ @required TautulliUser user, @required List watchtime, @required List player, - }) => LSListView( - children: [ - _profile(user), - _globalStats(watchtime), - if(player.length > 0) ..._playerStats(player), - ], - ); + }) { + return LunaListView( + controller: TautulliUserDetailsNavigationBar.scrollControllers[0], + children: [ + LunaHeader(text: 'Profile'), + _profile(user), + LunaHeader(text: 'Global Stats'), + _globalStats(watchtime), + if(player.length > 0) LunaHeader(text: 'Player Stats'), + if(player.length > 0) ..._playerStats(player), + ], + ); + } - Widget _profile(TautulliUser user) => LSTableBlock( - title: 'Profile', - children: [ - LSTableContent(title: 'email', body: user.email), - LSTableContent(title: 'last seen', body: widget.user.lastSeen != null - ? widget.user.lastSeen?.lunaAge ?? 'Unknown' - : 'Never', - ), - LSTableContent(title: '', body: ''), - LSTableContent(title: 'title', body: widget.user.lastPlayed ?? 'None'), - LSTableContent(title: 'platform', body: widget.user.platform ?? 'None'), - LSTableContent(title: 'player', body: widget.user.player ?? 'None'), - LSTableContent(title: 'location', body: widget.user.ipAddress ?? 'None'), - ], - ); + Widget _profile(TautulliUser user) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'email', body: user.email), + LunaTableContent(title: 'last seen', body: widget.user.lastSeen != null + ? widget.user.lastSeen?.lunaAge ?? 'Unknown' + : 'Never', + ), + LunaTableContent(title: '', body: ''), + LunaTableContent(title: 'title', body: widget.user.lastPlayed ?? 'None'), + LunaTableContent(title: 'platform', body: widget.user.platform ?? 'None'), + LunaTableContent(title: 'player', body: widget.user.player ?? 'None'), + LunaTableContent(title: 'location', body: widget.user.ipAddress ?? 'None'), + ], + ); + } - Widget _globalStats(List watchtime) => LSTableBlock( - title: 'Global Stats', - children: List.generate( - watchtime.length, - (index) => LSTableContent( - title: _globalStatsTitle(watchtime[index].queryDays), - body: _globalStatsContent(watchtime[index].totalPlays, watchtime[index].totalTime), + Widget _globalStats(List watchtime) { + return LunaTableCard( + content: List.generate( + watchtime.length, + (index) => LunaTableContent( + title: _globalStatsTitle(watchtime[index].queryDays), + body: _globalStatsContent(watchtime[index].totalPlays, watchtime[index].totalTime), + ), ), - ), - ); + ); + } String _globalStatsTitle(int days) { if(days == 0) return 'All Time'; @@ -144,22 +147,14 @@ class _State extends State with AutomaticKeepAliveCl return '$_plays\n${duration.lunaTimestampWords}'; } - List _playerStats(List player) => [ - LSHeader(text: 'Player Stats'), - ...List.generate( - player.length, - (index) => LSCard( - child: Padding( - child: Column( - children: [ - LSTableContent(title: 'player', body: player[index].playerName), - LSTableContent(title: 'platform', body: player[index].platform), - LSTableContent(title: 'plays', body: player[index].totalPlays == 1 ? '1 Play' : '${player[index].totalPlays} Plays'), - ], - ), - padding: EdgeInsets.symmetric(vertical: 8.0), - ), - ), + List _playerStats(List player) => List.generate( + player.length, + (index) => LunaTableCard( + content: [ + LunaTableContent(title: 'player', body: player[index].playerName), + LunaTableContent(title: 'platform', body: player[index].platform), + LunaTableContent(title: 'plays', body: player[index].totalPlays == 1 ? '1 Play' : '${player[index].totalPlays} Plays'), + ], ), - ]; + ); } diff --git a/lib/modules/tautulli/routes/users_details/widgets/synced_items.dart b/lib/modules/tautulli/routes/users_details/widgets/synced_items.dart index 6b874e3a..8839b652 100644 --- a/lib/modules/tautulli/routes/users_details/widgets/synced_items.dart +++ b/lib/modules/tautulli/routes/users_details/widgets/synced_items.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:tautulli/tautulli.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; @@ -16,20 +14,14 @@ class TautulliUserDetailsSyncedItems extends StatefulWidget { State createState() => _State(); } -class _State extends State with AutomaticKeepAliveClientMixin { +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _refreshKey = GlobalKey(); @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { + Future loadCallback() async { context.read().setUserSyncedItems( widget.user.userId, context.read().api.libraries.getSyncedItems(userId: widget.user.userId), @@ -42,39 +34,43 @@ class _State extends State with AutomaticKeepAli super.build(context); return Scaffold( key: _scaffoldKey, - body: _body, + body: _body(), ); } - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: _refresh, - child: FutureBuilder( - future: context.watch().userSyncedItems[widget.user.userId], - builder: (context, AsyncSnapshot> snapshot) { - if(snapshot.hasError) { - if(snapshot.connectionState != ConnectionState.waiting) { - LunaLogger().error('Unable to fetch Tautulli user synced items: ${widget.user.userId}', snapshot.error, StackTrace.current); + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: context.watch().userSyncedItems[widget.user.userId], + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Tautulli user synced items: ${widget.user.userId}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState?.show); } - return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); - } - if(snapshot.hasData) return snapshot.data.length == 0 - ? _noSyncedItems() - : _syncedItems(snapshot.data); - return LSLoader(); - }, - ), - ); + if(snapshot.hasData) return _syncedItems(snapshot.data); + return LunaLoader(); + }, + ), + ); + } - Widget _syncedItems(List items) => LSListViewBuilder( - itemCount: items.length, - itemBuilder: (context, index) => TautulliSyncedItemTile(syncedItem: items[index]), - ); - - Widget _noSyncedItems() => LSGenericMessage( - text: 'No Synced Items Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () async => _refreshKey.currentState.show(), - ); + Widget _syncedItems(List items) { + if((items?.length ?? 0) == 0) return LunaMessage( + text: 'No Synced Items Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + controller: TautulliUserDetailsNavigationBar.scrollControllers[2], + itemCount: items.length, + itemBuilder: (context, index) => TautulliSyncedItemTile(syncedItem: items[index]), + ); + } } diff --git a/localization/lunasea/en.json b/localization/lunasea/en.json index 9524ecb4..b154d00f 100644 --- a/localization/lunasea/en.json +++ b/localization/lunasea/en.json @@ -6,7 +6,9 @@ "lunasea.ComingSoon": "Coming Soon", "lunasea.Dashboard": "Dashboard", "lunasea.GoBack": "Go Back", + "lunasea.GoToSettings": "Go to Settings", "lunasea.ModuleIsNotEnabled": "{} Is Not Enabled", + "lunasea.NoModulesEnabled": "No Modules Enabled", "lunasea.Refresh": "Refresh", "lunasea.ReturnToDashboard": "Return to Dashboard", "lunasea.SearchTextBar": "Search…", diff --git a/localization/radarr/en.json b/localization/radarr/en.json index c799db19..1ffa1cab 100644 --- a/localization/radarr/en.json +++ b/localization/radarr/en.json @@ -31,7 +31,7 @@ "radarr.InCinemasIn": "In Cinemas in {}", "radarr.InCinemasToday": "In Cinemas Today", "radarr.Interactive": "Interactive", - "radarr.Language": "Language", + "radarr.Languages": "Languages", "radarr.ManualImport": "Manual Import", "radarr.ManualImportDescription": "Import Movies from the Filesystem", "radarr.MinimumAvailability": "Minimum Availability", @@ -73,6 +73,9 @@ "radarr.SearchAllMissing": "Search All Missing", "radarr.SearchFor": "Search for {}", "radarr.Seeders": "Seeders", + "radarr.SelectLanguage": "Select Languages", + "radarr.SelectMovie": "Select Movie", + "radarr.SelectQuality": "Select Quality", "radarr.Size": "Size", "radarr.SortCatalogue": "Sort Catalogue", "radarr.Studio": "Studio", diff --git a/pubspec.lock b/pubspec.lock index 1a456d21..b79d708a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,7 +9,7 @@ packages: source: hosted version: "19.0.0" analyzer: - dependency: "direct dev" + dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" @@ -49,14 +49,14 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.3" + version: "2.0.0" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.6" + version: "0.4.7" build_daemon: dependency: transitive description: @@ -70,21 +70,21 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "2.0.0" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.11.5" + version: "1.12.2" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.10" + version: "6.1.12" built_collection: dependency: transitive description: @@ -119,7 +119,7 @@ packages: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" cli_util: dependency: transitive description: @@ -224,14 +224,14 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.14" + version: "2.0.0" dbus: dependency: transitive description: @@ -245,7 +245,7 @@ packages: name: dio url: "https://pub.dartlang.org" source: hosted - version: "4.0.0-prev3" + version: "4.0.0" easy_localization: dependency: "direct main" description: @@ -301,7 +301,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" firebase: dependency: transitive description: @@ -441,7 +441,7 @@ packages: name: fl_chart url: "https://pub.dartlang.org" source: hosted - version: "0.30.0" + version: "0.35.0" flash: dependency: "direct main" description: @@ -472,7 +472,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" flutter_spinkit: dependency: "direct main" description: @@ -480,13 +480,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.0" - flutter_sticky_header: - dependency: "direct main" - description: - name: flutter_sticky_header - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" flutter_web_plugins: dependency: transitive description: flutter @@ -512,14 +505,14 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "1.0.0" hive: dependency: "direct main" description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" hive_flutter: dependency: "direct main" description: @@ -533,7 +526,7 @@ packages: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" http: dependency: transitive description: @@ -547,7 +540,7 @@ packages: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.0.0" http_parser: dependency: transitive description: @@ -582,7 +575,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "1.0.0" js: dependency: transitive description: @@ -813,7 +806,7 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.8" + version: "1.0.0" quick_actions: dependency: "direct main" description: @@ -838,10 +831,10 @@ packages: radarr: dependency: "direct main" description: - path: "/Users/jagandeepbrar/Git/Packages/radarr" - relative: false - source: path - version: "2.0.0-pre.3" + name: radarr + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0-pre.4" share: dependency: "direct main" description: @@ -904,7 +897,7 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.4+1" + version: "1.0.1" simple_gesture_detector: dependency: transitive description: @@ -923,21 +916,21 @@ packages: name: sliver_tools url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.1" sonarr: dependency: "direct main" description: name: sonarr url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-pre.3" + version: "2.0.0" source_gen: dependency: transitive description: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+3" + version: "1.0.0" source_span: dependency: transitive description: @@ -1000,7 +993,7 @@ packages: name: tautulli url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-pre.3" + version: "2.0.0" term_glyph: dependency: transitive description: @@ -1014,7 +1007,7 @@ packages: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+3" + version: "1.0.0" tuple: dependency: "direct main" description: @@ -1077,14 +1070,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" - value_layout_builder: - dependency: transitive - description: - name: value_layout_builder - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0" + version: "3.0.3" vector_math: dependency: transitive description: @@ -1098,7 +1084,7 @@ packages: name: wake_on_lan url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.1+1" watcher: dependency: transitive description: @@ -1112,7 +1098,7 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 55d75b73..2e5944b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,11 +12,11 @@ dependencies: cloud_firestore: ^1.0.3 # ANDROID IOS MACOS WEB connectivity_plus: ^1.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS convert: ^3.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS - dio: ^4.0.0-prev3 # ANDROID IOS LINUX MACOS WEB WINDOWS + dio: ^4.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS easy_localization: ^3.0.0 # ANDROID IOS LINUX MACOS WEB encrypt: ^5.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS expandable: ^5.0.1 # ANDROID IOS LINUX MACOS WEB WINDOWS - file_picker: ^3.0.0 # ANDROID IOS WEB + file_picker: ^3.0.1 # ANDROID IOS WEB firebase_analytics: ^7.1.1 # ANDROID IOS WEB firebase_auth: ^1.0.1 # ANDROID IOS MACOS WEB firebase_core: ^1.0.2 # ANDROID IOS MACOS WEB @@ -26,10 +26,9 @@ dependencies: flash: ^1.5.1 # ANDROID IOS LINUX MACOS WEB WINDOWS fluro: ^2.0.3 # ANDROID IOS LINUX MACOS WEB WINDOWS flutter_spinkit: ^5.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS - flutter_sticky_header: ^0.5.0 # ANDROID IOS LINUX MACOS WEB WINDOWS - fl_chart: ^0.30.0 # ANDROID IOS LINUX MACOS WEB WINDOWS + fl_chart: ^0.35.0 # ANDROID IOS LINUX MACOS WEB WINDOWS google_nav_bar: ^5.0.3 # ANDROID IOS LINUX MACOS WEB WINDOWS - hive: ^2.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS + hive: ^2.0.2 # ANDROID IOS LINUX MACOS WEB WINDOWS hive_flutter: ^1.0.0 # ANDROID IOS LINUX MACOS WINDOWS in_app_purchase: ^0.5.1+1 # ANDROID IOS infinite_scroll_pagination: ^3.0.1 # ANDROID IOS LINUX MACOS WEB WINDOWS @@ -41,24 +40,22 @@ dependencies: percent_indicator: ^3.0.1 # ANDROID IOS LINUX MACOS WEB WINDOWS provider: ^5.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS quick_actions: ^0.6.0 # ANDROID IOS - radarr: # ANDROID IOS LINUX MACOS WEB WINDOWS - path: /Users/jagandeepbrar/Git/Packages/radarr + radarr: ^2.0.0-pre.4 # ANDROID IOS LINUX MACOS WEB WINDOWS share: ^2.0.1 # ANDROID IOS - sonarr: ^2.0.0-pre.3 # ANDROID IOS LINUX MACOS WEB WINDOWS + sonarr: ^2.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS stack_trace: ^1.10.0 # ANDROID IOS LINUX MACOS WEB WINDOWS supercharged: ^2.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS table_calendar: ^2.3.3 # ANDROID IOS LINUX MACOS WEB WINDOWS - tautulli: ^2.0.0-pre.3 # ANDROID IOS LINUX MACOS WEB WINDOWS + tautulli: ^2.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS tuple: ^2.0.0 # ANDROID IOS LINUX MACOS WEB WINDOWS url_launcher: ^6.0.3 # ANDROID IOS LINUX MACOS WEB WINDOWS - uuid: ^3.0.2 # ANDROID IOS LINUX MACOS WEB WINDOWS - wake_on_lan: ^2.0.1 # ANDROID IOS LINUX MACOS WINDOWS + uuid: ^3.0.3 # ANDROID IOS LINUX MACOS WEB WINDOWS + wake_on_lan: ^2.0.1+1 # ANDROID IOS LINUX MACOS WINDOWS xml: ^5.0.2 # ANDROID IOS LINUX MACOS WEB WINDOWS dev_dependencies: - analyzer: ^1.2.0 - build_runner: ^1.11.5 - hive_generator: ^1.0.0 + build_runner: ^1.12.2 + hive_generator: ^1.0.1 flutter: uses-material-design: true