From 340b341b7f229bd2aada3e36640718be8b176ea7 Mon Sep 17 00:00:00 2001 From: thomas-sterrenburg Date: Thu, 14 May 2020 11:33:42 +0200 Subject: [PATCH] Better sleep management Homepage refresh also refresher connection status Refactoring and splitting widgets Adding updated privacy --- PRIVACY.md | 72 +++++++ lib/constants.dart | 2 +- .../browser/services/browser_service.dart | 5 + lib/features/home/blocs/home_bloc.dart | 10 +- .../pages/clients/clients_page_view.dart | 4 +- .../pages/domains/domains_page_view.dart | 4 +- .../home/presentation/pages/home_page.dart | 40 +--- .../pages/summary/summary_page_view.dart | 29 +-- .../{ => widgets}/query_types_tile.dart | 2 +- .../pages/summary/widgets/summary_tile.dart | 194 +++++++++--------- ...dart => home_page_overflow_refresher.dart} | 17 +- .../widgets/home_trivia_fetcher.dart | 46 +++++ .../datasources/numbers_api_data_source.dart | 2 + .../numbers_api_data_source_dio.dart | 2 + .../widgets/numbers_api_description_tile.dart | 43 ++++ .../pihole_api/blocs/pi_connection_bloc.dart | 11 +- .../pihole_api/data/models/pi_status.dart | 3 - .../widgets/pi_connection_sleep_button.dart | 4 +- .../widgets/pi_connection_status_icon.dart | 11 +- .../widgets/pi_connection_toggle_button.dart | 27 ++- .../presentation/pages/about_page.dart | 13 -- lib/main.dart | 12 +- lib/widgets/layout/animated_opener.dart | 1 - .../blocs/pi_connection_bloc_test.dart | 13 +- .../blocs/settings_bloc_test.dart | 2 - 25 files changed, 347 insertions(+), 222 deletions(-) create mode 100644 PRIVACY.md rename lib/features/home/presentation/pages/summary/{ => widgets}/query_types_tile.dart (100%) rename lib/features/home/presentation/widgets/{home_bloc_overflow_refresher.dart => home_page_overflow_refresher.dart} (70%) create mode 100644 lib/features/home/presentation/widgets/home_trivia_fetcher.dart create mode 100644 lib/features/numbers_api/presentation/widgets/numbers_api_description_tile.dart diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 00000000..694e29aa --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,72 @@ + +**Privacy Policy** + +Sterrenburg built the FlutterHole app as an Open Source app. This SERVICE is provided by Sterrenburg at no cost and is intended for use as is. + +This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service. + +If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy. + +The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at FlutterHole unless otherwise defined in this Privacy Policy. + +**Permissions** + +This application requests permissions for the following reasons: + +* Camera: scanning the QR code for your API token (optional); +* Internet: obtaining data from your Pi-hole. + +**Information Collection and Use** + +For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way. + +The app does use third party services that may collect information used to identify you. + +Link to privacy policy of third party service providers used by the app + +* [Google Play Services](https://www.google.com/policies/privacy/) + +**Log Data** + +I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. + +**Cookies** + +Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory. + +This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service. + +**Service Providers** + +I may employ third-party companies and individuals due to the following reasons: + +* To facilitate our Service; +* To provide the Service on our behalf; +* To perform Service-related services; or +* To assist us in analyzing how our Service is used. + +I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose. + +**Security** + +I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security. + +**Links to Other Sites** + +This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. + +**Children’s Privacy** + +These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13\. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions. + +**Changes to This Privacy Policy** + +I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. + +This policy is effective as of 2020-05-01 + +**Contact Us** + +If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me via https://github.com/sterrenburg/flutterhole. + +This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.firebaseapp.com/) \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index baed399e..eb380408 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -90,7 +90,7 @@ class KColors { static const Color inactive = Colors.grey; static const Color loading = Colors.blue; - static const Color sleeping = Colors.lightBlue; + static const Color sleeping = Colors.blue; static const Color summary = Colors.green; static const Color clients = Colors.blue; static const Color domains = Colors.orange; diff --git a/lib/features/browser/services/browser_service.dart b/lib/features/browser/services/browser_service.dart index 547af56a..c96bb02c 100644 --- a/lib/features/browser/services/browser_service.dart +++ b/lib/features/browser/services/browser_service.dart @@ -1,9 +1,14 @@ abstract class BrowserService { String get privacyUrl; + /// Launch [url] in the browser. + /// + /// Returns true if successful, false otherwise. Future launchUrl(String url); + /// Open the review platform for the current app ID. Future launchReview(); + /// Fetch the plaintext from [privacyUrl]. Future fetchPrivacyReadmeText(); } diff --git a/lib/features/home/blocs/home_bloc.dart b/lib/features/home/blocs/home_bloc.dart index 70815fd7..6d807759 100644 --- a/lib/features/home/blocs/home_bloc.dart +++ b/lib/features/home/blocs/home_bloc.dart @@ -52,11 +52,6 @@ class HomeBloc extends Bloc { @override HomeState get initialState => HomeStateInitial(); - @override - Stream mapEventToState(HomeEvent event) async* { - if (event is HomeEventFetch) yield* _fetch(); - } - Stream _fetch() async* { yield HomeStateLoading(); @@ -121,4 +116,9 @@ class HomeBloc extends Bloc { }, ); } + + @override + Stream mapEventToState(HomeEvent event) async* { + if (event is HomeEventFetch) yield* _fetch(); + } } diff --git a/lib/features/home/presentation/pages/clients/clients_page_view.dart b/lib/features/home/presentation/pages/clients/clients_page_view.dart index 806ea054..8ac0e942 100644 --- a/lib/features/home/presentation/pages/clients/clients_page_view.dart +++ b/lib/features/home/presentation/pages/clients/clients_page_view.dart @@ -4,7 +4,7 @@ import 'package:flutterhole/constants.dart'; import 'package:flutterhole/core/models/failures.dart'; import 'package:flutterhole/features/home/blocs/home_bloc.dart'; import 'package:flutterhole/features/home/presentation/widgets/home_bloc_builder.dart'; -import 'package:flutterhole/features/home/presentation/widgets/home_bloc_overflow_refresher.dart'; +import 'package:flutterhole/features/home/presentation/widgets/home_page_overflow_refresher.dart'; import 'package:flutterhole/features/pihole_api/data/models/pi_client.dart'; import 'package:flutterhole/features/pihole_api/data/models/summary.dart'; import 'package:flutterhole/features/pihole_api/data/models/top_sources.dart'; @@ -44,7 +44,7 @@ class ClientsPageView extends StatelessWidget { (summary) => summary.dnsQueriesToday, ); - return HomeBlocOverflowRefresher( + return HomePageOverflowRefresher( child: ListView.builder( itemCount: topSources.topSources.length, itemBuilder: (context, index) { diff --git a/lib/features/home/presentation/pages/domains/domains_page_view.dart b/lib/features/home/presentation/pages/domains/domains_page_view.dart index aa92ace5..2e6240e5 100644 --- a/lib/features/home/presentation/pages/domains/domains_page_view.dart +++ b/lib/features/home/presentation/pages/domains/domains_page_view.dart @@ -4,7 +4,7 @@ import 'package:flutterhole/constants.dart'; import 'package:flutterhole/core/models/failures.dart'; import 'package:flutterhole/features/home/blocs/home_bloc.dart'; import 'package:flutterhole/features/home/presentation/widgets/home_bloc_builder.dart'; -import 'package:flutterhole/features/home/presentation/widgets/home_bloc_overflow_refresher.dart'; +import 'package:flutterhole/features/home/presentation/widgets/home_page_overflow_refresher.dart'; import 'package:flutterhole/features/pihole_api/data/models/top_items.dart'; import 'package:flutterhole/features/pihole_api/presentation/pages/single_domain_page.dart'; import 'package:flutterhole/widgets/layout/animated_opener.dart'; @@ -35,7 +35,7 @@ class DomainsPageView extends StatelessWidget { return topItemsResult.fold( (failure) => CenteredFailureIndicator(failure), (topItems) { - return HomeBlocOverflowRefresher( + return HomePageOverflowRefresher( child: CustomScrollView( slivers: [ TopQueriesListBuilder(topItems), diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index d5c48d1c..2d175daf 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -2,18 +2,17 @@ import 'package:bottom_navy_bar/bottom_navy_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutterhole/constants.dart'; -import 'package:flutterhole/dependency_injection.dart'; import 'package:flutterhole/features/home/blocs/home_bloc.dart'; import 'package:flutterhole/features/home/presentation/pages/clients/clients_page_view.dart'; import 'package:flutterhole/features/home/presentation/pages/domains/domains_page_view.dart'; import 'package:flutterhole/features/home/presentation/pages/summary/summary_page_view.dart'; +import 'package:flutterhole/features/home/presentation/widgets/home_trivia_fetcher.dart'; import 'package:flutterhole/features/numbers_api/blocs/number_trivia_bloc.dart'; import 'package:flutterhole/features/pihole_api/presentation/widgets/pi_connection_sleep_button.dart'; import 'package:flutterhole/features/pihole_api/presentation/widgets/pi_connection_toggle_button.dart'; import 'package:flutterhole/features/routing/presentation/widgets/default_drawer.dart'; import 'package:flutterhole/features/settings/presentation/widgets/active_pihole_title.dart'; import 'package:flutterhole/features/settings/presentation/widgets/pihole_theme_builder.dart'; -import 'package:flutterhole/features/settings/services/preference_service.dart'; class HomePage extends StatefulWidget { @override @@ -66,47 +65,13 @@ class _HomePageState extends State { ], child: Builder( builder: (context) { - return BlocListener( - listener: (context, state) { - final bool enableTrivia = - getIt().get(KPrefs.useNumbersApi) ?? - true; - - if (enableTrivia) { - state.maybeMap( - success: (state) { - state.summary.fold( - (failure) {}, - (summary) { - BlocProvider.of(context) - .add(NumberTriviaEvent.fetchMany([ - summary.dnsQueriesToday, - summary.adsBlockedToday, - summary.adsPercentageToday.round(), - summary.domainsBeingBlocked, - ])); - }, - ); - }, - orElse: () {}); - } - }, + return HomeTriviaFetcher( child: Scaffold( drawer: DefaultDrawer(), appBar: AppBar( elevation: 0.0, title: ActivePiholeTitle(interactive: true), actions: [ - Visibility( - visible: false, - child: IconButton( - icon: Icon(Icons.refresh), - onPressed: () { - BlocProvider.of(context) - .add(HomeEvent.fetch()); - }, - ), - ), PiConnectionSleepButton(), PiConnectionToggleButton(), ], @@ -137,7 +102,6 @@ class _HomePageState extends State { ], ), body: PageView( -// physics: const BouncingScrollPhysics(), controller: _pageController, onPageChanged: _onPageChanged, children: const [ diff --git a/lib/features/home/presentation/pages/summary/summary_page_view.dart b/lib/features/home/presentation/pages/summary/summary_page_view.dart index 277a264b..68f8b582 100644 --- a/lib/features/home/presentation/pages/summary/summary_page_view.dart +++ b/lib/features/home/presentation/pages/summary/summary_page_view.dart @@ -2,17 +2,15 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutterhole/core/models/failures.dart'; -import 'package:flutterhole/dependency_injection.dart'; import 'package:flutterhole/features/home/blocs/home_bloc.dart'; -import 'package:flutterhole/features/home/presentation/pages/summary/query_types_tile.dart'; import 'package:flutterhole/features/home/presentation/pages/summary/widgets/forward_destinations_tile.dart'; +import 'package:flutterhole/features/home/presentation/pages/summary/widgets/query_types_tile.dart'; import 'package:flutterhole/features/home/presentation/pages/summary/widgets/summary_tile.dart'; import 'package:flutterhole/features/home/presentation/widgets/home_bloc_builder.dart'; -import 'package:flutterhole/features/home/presentation/widgets/home_bloc_overflow_refresher.dart'; +import 'package:flutterhole/features/home/presentation/widgets/home_page_overflow_refresher.dart'; import 'package:flutterhole/features/pihole_api/data/models/dns_query_type.dart'; import 'package:flutterhole/features/pihole_api/data/models/forward_destinations.dart'; import 'package:flutterhole/features/pihole_api/data/models/summary.dart'; -import 'package:flutterhole/features/settings/services/preference_service.dart'; import 'package:flutterhole/widgets/layout/failure_indicators.dart'; import 'package:flutterhole/widgets/layout/loading_indicators.dart'; import 'package:intl/intl.dart'; @@ -33,14 +31,11 @@ class SummaryPageView extends StatelessWidget { ___, Either forwardDestinationsResult, - Either dnsQueryTypesResult,) { - return summaryResult.fold( - (failure) => CenteredFailureIndicator(failure), - (summary) { - final bool enableTrivia = - getIt().get(KPrefs.useNumbersApi) ?? true; - - return HomeBlocOverflowRefresher( + Either dnsQueryTypesResult,) => + summaryResult.fold( + (failure) => CenteredFailureIndicator(failure), + (summary) => + HomePageOverflowRefresher( child: StaggeredGridView.count( crossAxisCount: 4, children: [ @@ -50,7 +45,6 @@ class SummaryPageView extends StatelessWidget { '${_numberFormat.format(summary.dnsQueriesToday)}', integer: summary.dnsQueriesToday, color: Colors.green, - enableTrivia: enableTrivia, ), SummaryTile( title: 'Queries Blocked', @@ -58,7 +52,6 @@ class SummaryPageView extends StatelessWidget { '${_numberFormat.format(summary.adsBlockedToday)}', integer: summary.adsBlockedToday, color: Colors.blue, - enableTrivia: enableTrivia, ), SummaryTile( title: 'Percent Blocked', @@ -66,7 +59,6 @@ class SummaryPageView extends StatelessWidget { '${summary.adsPercentageToday.toStringAsFixed(2)}%', integer: summary.adsPercentageToday.round(), color: Colors.orange, - enableTrivia: enableTrivia, ), SummaryTile( title: 'Domains on Blocklist', @@ -74,7 +66,6 @@ class SummaryPageView extends StatelessWidget { '${_numberFormat.format(summary.domainsBeingBlocked)}', color: Colors.red, integer: summary.domainsBeingBlocked, - enableTrivia: enableTrivia, ), QueryTypesTile(dnsQueryTypesResult), ForwardDestinationsTile(forwardDestinationsResult), @@ -88,10 +79,8 @@ class SummaryPageView extends StatelessWidget { StaggeredTile.count(4, 3), ], ), - ); - }, - ); - }, + ), + ), orElse: () => CenteredLoadingIndicator()); }); } diff --git a/lib/features/home/presentation/pages/summary/query_types_tile.dart b/lib/features/home/presentation/pages/summary/widgets/query_types_tile.dart similarity index 100% rename from lib/features/home/presentation/pages/summary/query_types_tile.dart rename to lib/features/home/presentation/pages/summary/widgets/query_types_tile.dart index eb3514fd..2cb3d861 100644 --- a/lib/features/home/presentation/pages/summary/query_types_tile.dart +++ b/lib/features/home/presentation/pages/summary/widgets/query_types_tile.dart @@ -1,10 +1,10 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutterhole/core/models/failures.dart'; -import 'package:flutterhole/features/pihole_api/data/models/dns_query_type.dart'; import 'package:flutterhole/features/home/presentation/pages/summary/widgets/graph_legend_item.dart'; import 'package:flutterhole/features/home/presentation/pages/summary/widgets/pie_chart_scaffold.dart'; import 'package:flutterhole/features/home/presentation/pages/summary/widgets/query_types_pie_chart.dart'; +import 'package:flutterhole/features/pihole_api/data/models/dns_query_type.dart'; import 'package:flutterhole/widgets/layout/failure_indicators.dart'; class QueryTypesTile extends StatelessWidget { diff --git a/lib/features/home/presentation/pages/summary/widgets/summary_tile.dart b/lib/features/home/presentation/pages/summary/widgets/summary_tile.dart index 77c54d2b..1c6ce116 100644 --- a/lib/features/home/presentation/pages/summary/widgets/summary_tile.dart +++ b/lib/features/home/presentation/pages/summary/widgets/summary_tile.dart @@ -1,15 +1,12 @@ import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutterhole/constants.dart'; import 'package:flutterhole/dependency_injection.dart'; -import 'package:flutterhole/features/browser/services/browser_service.dart'; import 'package:flutterhole/features/numbers_api/blocs/number_trivia_bloc.dart'; +import 'package:flutterhole/features/numbers_api/presentation/widgets/numbers_api_description_tile.dart'; +import 'package:flutterhole/features/settings/services/preference_service.dart'; import 'package:flutterhole/widgets/layout/animate_on_build.dart'; import 'package:flutterhole/widgets/layout/loading_indicators.dart'; -import 'package:flutterhole/widgets/layout/snackbars.dart'; - -const String numbersApiHome = 'http://numbersapi.com/'; class SummaryTile extends StatelessWidget { const SummaryTile({ @@ -18,116 +15,125 @@ class SummaryTile extends StatelessWidget { @required this.subtitle, @required this.integer, @required this.color, - @required this.enableTrivia, }) : super(key: key); final String title; final String subtitle; final int integer; final Color color; - final bool enableTrivia; - String get _url => '$numbersApiHome#$integer'; + bool get _useNumbersApi => + getIt().get(KPrefs.useNumbersApi) ?? true; @override Widget build(BuildContext context) { - if (!enableTrivia) + if (!_useNumbersApi) { return Card( color: color, child: _Tile(title, subtitle), ); + } else { + return Card( + child: OpenContainer( + tappable: false, + closedColor: color, + closedBuilder: (context, VoidCallback openContainer) { + return InkWell( + onTap: openContainer, + child: _Tile(title, subtitle), + ); + }, + openBuilder: (_, __) { + return BlocProvider.value( + value: BlocProvider.of(context), + child: _SummaryTriviaPage( + color: color, + title: title, + subtitle: subtitle, + integer: integer, + ), + ); + }, + ), + ); + } + } +} - return Card( - child: OpenContainer( - tappable: false, - closedColor: color, - closedBuilder: (context, VoidCallback openContainer) { - return InkWell( - onTap: openContainer, - child: _Tile(title, subtitle), - ); - }, - openBuilder: (_, __) { - return Scaffold( - backgroundColor: color, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0.0, - ), - body: AnimateOnBuild( - child: Stack( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _Tile(title, subtitle), - Padding( - padding: const EdgeInsets.all(24.0), - child: BlocBuilder( - bloc: BlocProvider.of(context), - builder: - (BuildContext context, NumberTriviaState state) { - return state.maybeWhen( - success: (trivia) { - if (trivia.containsKey(integer)) { - return Text( - '${trivia[integer]}', - style: Theme.of(context) - .textTheme - .headline6 - .copyWith(color: Colors.white), - textAlign: TextAlign.center, - ); - } +class _SummaryTriviaPage extends StatelessWidget { + const _SummaryTriviaPage({ + Key key, + @required this.color, + @required this.title, + @required this.subtitle, + @required this.integer, + }) : super(key: key); - return Text( - 'No data for ${integer} in $trivia', - style: Theme.of(context) - .textTheme - .bodyText1 - .apply(color: Colors.white), - ); - }, - loading: () => CenteredLoadingIndicator( - color: Colors.white, - ), - orElse: () => Container(), + final Color color; + final String title; + final String subtitle; + final int integer; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: color, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0.0, + ), + body: AnimateOnBuild( + child: Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _Tile(title, subtitle), + Padding( + padding: const EdgeInsets.all(24.0), + child: BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, NumberTriviaState state) { + return state.maybeWhen( + success: (trivia) { + if (trivia.containsKey(integer)) { + return Text( + '${trivia[integer]}', + style: Theme + .of(context) + .textTheme + .headline6 + .copyWith(color: Colors.white), + textAlign: TextAlign.center, ); - }, - ), - ), - ], - ), - Align( - alignment: Alignment.bottomCenter, - child: Tooltip( - message: 'Open $_url', - child: ListTile( - onTap: () async { - final didLaunch = - await getIt().launchUrl(_url); - if (!didLaunch) { - showErrorSnackBar(context, 'Could not open $_url'); } + + return Text( + 'No data for ${integer} in $trivia', + style: Theme + .of(context) + .textTheme + .bodyText1 + .apply(color: Colors.white), + ); }, - leading: Icon(KIcons.info), - title: Row( - children: [ - Text('Powered by the '), - Text( - 'Numbers API', - style: TextStyle(fontWeight: FontWeight.bold), - ) - ], - ), - ), - ), + loading: () => + CenteredLoadingIndicator( + color: Colors.white, + ), + orElse: () => Container(), + ); + }, ), - ], - ), + ), + ], + ), + Align( + alignment: Alignment.bottomCenter, + child: NumbersApiDescriptionTile(integer: integer), ), - ); - }, + ], + ), ), ); } diff --git a/lib/features/home/presentation/widgets/home_bloc_overflow_refresher.dart b/lib/features/home/presentation/widgets/home_page_overflow_refresher.dart similarity index 70% rename from lib/features/home/presentation/widgets/home_bloc_overflow_refresher.dart rename to lib/features/home/presentation/widgets/home_page_overflow_refresher.dart index 1efa0713..2d2cca37 100644 --- a/lib/features/home/presentation/widgets/home_bloc_overflow_refresher.dart +++ b/lib/features/home/presentation/widgets/home_page_overflow_refresher.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutterhole/dependency_injection.dart'; import 'package:flutterhole/features/home/blocs/home_bloc.dart'; +import 'package:flutterhole/features/pihole_api/blocs/pi_connection_bloc.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; typedef void OnRefreshCallback(BuildContext context); -class HomeBlocOverflowRefresher extends StatefulWidget { - const HomeBlocOverflowRefresher({ +class HomePageOverflowRefresher extends StatefulWidget { + const HomePageOverflowRefresher({ Key key, @required this.child, }) : super(key: key); @@ -14,15 +16,20 @@ class HomeBlocOverflowRefresher extends StatefulWidget { final Widget child; @override - _HomeBlocOverflowRefresherState createState() => - _HomeBlocOverflowRefresherState(); + _HomePageOverflowRefresherState createState() => + _HomePageOverflowRefresherState(); } -class _HomeBlocOverflowRefresherState extends State { +class _HomePageOverflowRefresherState extends State { final RefreshController _refreshController = RefreshController(); void _onRefresh() { BlocProvider.of(context).add(HomeEvent.fetch()); + + final connectionBloc = getIt(); + if (!(connectionBloc.state is PiConnectionStateSleeping)) { + connectionBloc.add(PiConnectionEvent.ping()); + } } @override diff --git a/lib/features/home/presentation/widgets/home_trivia_fetcher.dart b/lib/features/home/presentation/widgets/home_trivia_fetcher.dart new file mode 100644 index 00000000..b0ed3758 --- /dev/null +++ b/lib/features/home/presentation/widgets/home_trivia_fetcher.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutterhole/dependency_injection.dart'; +import 'package:flutterhole/features/home/blocs/home_bloc.dart'; +import 'package:flutterhole/features/numbers_api/blocs/number_trivia_bloc.dart'; +import 'package:flutterhole/features/settings/services/preference_service.dart'; + +/// Fetches trivia data when [HomeBloc] is successful. +class HomeTriviaFetcher extends StatelessWidget { + const HomeTriviaFetcher({ + Key key, + this.child, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + return BlocListener( + child: child, + listener: (context, state) { + final bool enableTrivia = + getIt().get(KPrefs.useNumbersApi) ?? true; + + if (enableTrivia) { + state.maybeMap( + success: (state) { + state.summary.fold( + (failure) {}, + (summary) { + BlocProvider.of(context) + .add(NumberTriviaEvent.fetchMany([ + summary.dnsQueriesToday, + summary.adsBlockedToday, + summary.adsPercentageToday.round(), + summary.domainsBeingBlocked, + ])); + }, + ); + }, + orElse: () {}); + } + }, + ); + } +} diff --git a/lib/features/numbers_api/data/datasources/numbers_api_data_source.dart b/lib/features/numbers_api/data/datasources/numbers_api_data_source.dart index b6fc173f..b082fa00 100644 --- a/lib/features/numbers_api/data/datasources/numbers_api_data_source.dart +++ b/lib/features/numbers_api/data/datasources/numbers_api_data_source.dart @@ -2,7 +2,9 @@ abstract class NumbersApiDataSource { static const String baseUrl = 'http://numbersapi.com/'; static const Duration maxAge = Duration(days: 30); + /// http://numbersapi.com/[integer] Future fetchTrivia(int integer); + /// http://numbersapi.com/[integers] Future> fetchManyTrivia(List integers); } diff --git a/lib/features/numbers_api/data/datasources/numbers_api_data_source_dio.dart b/lib/features/numbers_api/data/datasources/numbers_api_data_source_dio.dart index 5d44d915..a0c3e6e7 100644 --- a/lib/features/numbers_api/data/datasources/numbers_api_data_source_dio.dart +++ b/lib/features/numbers_api/data/datasources/numbers_api_data_source_dio.dart @@ -38,6 +38,8 @@ class NumbersApiDataSourceDio implements NumbersApiDataSource { @override Future> fetchManyTrivia(List integers) async { + // TODO perhaps fetch separate requests for each integer. + // This would allow for more robust caching. final response = await _dio.get( '${integers.join(',')}', options: buildCacheOptions(NumbersApiDataSource.maxAge), diff --git a/lib/features/numbers_api/presentation/widgets/numbers_api_description_tile.dart b/lib/features/numbers_api/presentation/widgets/numbers_api_description_tile.dart new file mode 100644 index 00000000..c5dcb562 --- /dev/null +++ b/lib/features/numbers_api/presentation/widgets/numbers_api_description_tile.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutterhole/constants.dart'; +import 'package:flutterhole/dependency_injection.dart'; +import 'package:flutterhole/features/browser/services/browser_service.dart'; +import 'package:flutterhole/widgets/layout/snackbars.dart'; + +const String _numbersApiHome = 'http://numbersapi.com/'; + +class NumbersApiDescriptionTile extends StatelessWidget { + const NumbersApiDescriptionTile({ + Key key, + @required this.integer, + }) : super(key: key); + + final int integer; + + String get url => '$_numbersApiHome#$integer'; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: 'Open $url', + child: ListTile( + onTap: () async { + final didLaunch = await getIt().launchUrl(url); + if (!didLaunch) { + showErrorSnackBar(context, 'Could not open $url'); + } + }, + leading: Icon(KIcons.info), + title: Row( + children: [ + Text('Powered by the '), + Text( + 'Numbers API', + style: TextStyle(fontWeight: FontWeight.bold), + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/pihole_api/blocs/pi_connection_bloc.dart b/lib/features/pihole_api/blocs/pi_connection_bloc.dart index e52f8e20..8cc7b4a4 100644 --- a/lib/features/pihole_api/blocs/pi_connection_bloc.dart +++ b/lib/features/pihole_api/blocs/pi_connection_bloc.dart @@ -110,12 +110,11 @@ class PiConnectionBloc extends Bloc { duration, ); - // TODO disabled for now, since the future.delayed - // prevents any events from landing - - // await Future.delayed(duration); - // // Optimistically guess that the timer elapsed - // yield PiConnectionStateActive(settings, ToggleStatus(PiStatusEnum.enabled)); + // Thank you to felix <3 + // https://github.com/felangel/bloc/issues/1171#issuecomment-628685175 + Future.delayed(duration).whenComplete(() { + add(PiConnectionEvent.ping()); + }); }); }); } diff --git a/lib/features/pihole_api/data/models/pi_status.dart b/lib/features/pihole_api/data/models/pi_status.dart index d2df331f..d1d69c25 100644 --- a/lib/features/pihole_api/data/models/pi_status.dart +++ b/lib/features/pihole_api/data/models/pi_status.dart @@ -3,7 +3,6 @@ import 'package:flutterhole/features/pihole_api/data/models/model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'pi_status.freezed.dart'; - part 'pi_status.g.dart'; enum PiStatusEnum { @@ -17,8 +16,6 @@ abstract class PiStatus extends MapModel with _$PiStatus { const factory PiStatus({ @JsonKey( name: 'status', -// fromJson: _valueToDnsQueryTypes, -// toJson: _dnsQueryTypesToValues, ) PiStatusEnum status, }) = _PiStatus; diff --git a/lib/features/pihole_api/presentation/widgets/pi_connection_sleep_button.dart b/lib/features/pihole_api/presentation/widgets/pi_connection_sleep_button.dart index ba9601bb..9121d121 100644 --- a/lib/features/pihole_api/presentation/widgets/pi_connection_sleep_button.dart +++ b/lib/features/pihole_api/presentation/widgets/pi_connection_sleep_button.dart @@ -7,7 +7,7 @@ import 'package:flutterhole/features/pihole_api/blocs/pi_connection_bloc.dart'; import 'package:supercharged/supercharged.dart'; class PiConnectionSleepButton extends StatelessWidget { - Future _showSleepDialog(BuildContext context) async { + Future _showSleepPickerDialog(BuildContext context) async { final TimeOfDay result = await showTimePicker( context: context, initialTime: TimeOfDay.now(), @@ -46,7 +46,7 @@ class PiConnectionSleepButton extends StatelessWidget { icon: Icon(KIcons.sleep), onPressed: (state is PiConnectionStateActive) ? () async { - _showSleepDialog(context); + _showSleepPickerDialog(context); } : null, ); diff --git a/lib/features/pihole_api/presentation/widgets/pi_connection_status_icon.dart b/lib/features/pihole_api/presentation/widgets/pi_connection_status_icon.dart index 15d11ad3..c431fdb5 100644 --- a/lib/features/pihole_api/presentation/widgets/pi_connection_status_icon.dart +++ b/lib/features/pihole_api/presentation/widgets/pi_connection_status_icon.dart @@ -63,8 +63,8 @@ class PiConnectionStatusIcon extends StatelessWidget { _SleepProgressIndicator(), ], ), - onPressed: interactive ? getIt().state.when< - VoidCallback>( + onPressed: interactive + ? getIt().state.when( initial: () => null, loading: () => null, failure: (failure) => @@ -78,14 +78,15 @@ class PiConnectionStatusIcon extends StatelessWidget { '${settings.title} is ${_$PiStatusEnumEnumMap[toggleStatus .status]}'); }, - sleeping: (settings, start, duration) => + sleeping: (_, start, duration) => () { final dateTime = start.add(duration); showToast( 'Sleeping until ${dateTime.formattedTime} (${dateTime .fromNow})'); }, - ) : null, + ) + : null, ); }); } @@ -121,7 +122,7 @@ class _SleepProgressIndicator extends StatelessWidget { height: 16.0, child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(KColors.sleeping), - value: (percentage), + value: (1 - percentage), ), ); }, diff --git a/lib/features/pihole_api/presentation/widgets/pi_connection_toggle_button.dart b/lib/features/pihole_api/presentation/widgets/pi_connection_toggle_button.dart index 6149a90e..9efd38bb 100644 --- a/lib/features/pihole_api/presentation/widgets/pi_connection_toggle_button.dart +++ b/lib/features/pihole_api/presentation/widgets/pi_connection_toggle_button.dart @@ -9,7 +9,6 @@ import 'package:flutterhole/features/pihole_api/data/models/toggle_status.dart'; import 'package:flutterhole/features/settings/data/models/pihole_settings.dart'; import 'package:flutterhole/widgets/layout/loading_indicators.dart'; import 'package:flutterhole/widgets/layout/snackbars.dart'; -import 'package:flutterhole/widgets/layout/toasts.dart'; class PiConnectionToggleButton extends StatelessWidget { @@ -19,19 +18,19 @@ class PiConnectionToggleButton extends StatelessWidget { bloc: getIt(), listener: (BuildContext context, PiConnectionState state) { state.maybeWhen( - active: (settings, ToggleStatus toggleStatus) { - switch (toggleStatus.status) { - case PiStatusEnum.enabled: - showToast('Enabled'); - break; - case PiStatusEnum.disabled: - showToast('Disabled'); - break; - case PiStatusEnum.unknown: - default: - showToast('Unknown'); - } - }, +// active: (settings, ToggleStatus toggleStatus) { +// switch (toggleStatus.status) { +// case PiStatusEnum.enabled: +// showToast('Enabled'); +// break; +// case PiStatusEnum.disabled: +// showToast('Disabled'); +// break; +// case PiStatusEnum.unknown: +// default: +// showToast('Unknown'); +// } +// }, failure: (failure) { showErrorSnackBar(context, '${failure.toString()}'); }, diff --git a/lib/features/routing/presentation/pages/about_page.dart b/lib/features/routing/presentation/pages/about_page.dart index 22e9158c..479a3c58 100644 --- a/lib/features/routing/presentation/pages/about_page.dart +++ b/lib/features/routing/presentation/pages/about_page.dart @@ -28,12 +28,7 @@ class AboutPage extends StatelessWidget { slivers: [ SliverAppBar( title: Text('About'), -// expandedHeight: kSliverAppBarHeight, flexibleSpace: FlexibleSpaceBar( -// background: SvgPicture.asset( -// 'assets/shared_workspace.svg', -// fit: BoxFit.cover, -// ), ), ), SliverList( @@ -52,14 +47,6 @@ class AboutPage extends StatelessWidget { .copyWith( color: Theme.of(context).accentColor), ), -// Padding( -// padding: -// const EdgeInsets.symmetric(horizontal: 8.0), -// child: ShareQrButton( -// tooltip: 'Share this app with QR', -// data: kAppUrl, -// ), -// ) ], ), subtitle: Text( diff --git a/lib/main.dart b/lib/main.dart index 2b853c6a..fe144c1c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart' as foundation; import 'package:flutter/material.dart'; import 'package:flutterhole/core/debug/bloc_delegate.dart'; import 'package:flutterhole/dependency_injection.dart'; @@ -10,10 +11,14 @@ void main() async { // wait for flutter initialization WidgetsFlutterBinding.ensureInitialized(); + // Configure service injection await configure(Environment.prod); - enableBlocDelegate(); - getIt().createRoutes(); + if (foundation.kReleaseMode) {} else { + enableBlocDelegate(); + } + + getIt().createRoutes(); getIt().add(SettingsEvent.init()); getIt().add(PiConnectionEvent.ping()); @@ -21,11 +26,10 @@ void main() async { } class MyApp extends StatelessWidget { - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'FlutterHole', navigatorKey: getIt().navigatorKey, onGenerateRoute: getIt().onGenerateRoute, initialRoute: RouterService.home, diff --git a/lib/widgets/layout/animated_opener.dart b/lib/widgets/layout/animated_opener.dart index 751019c0..7a38e189 100644 --- a/lib/widgets/layout/animated_opener.dart +++ b/lib/widgets/layout/animated_opener.dart @@ -27,7 +27,6 @@ class AnimatedOpener extends StatelessWidget { ) { return Material( child: InkWell( -// splashColor: Theme.of(context).accentColor.withOpacity(.2), onTap: openContainer, onLongPress: onLongPress, child: closed(context), diff --git a/test/features/pihole_api/blocs/pi_connection_bloc_test.dart b/test/features/pihole_api/blocs/pi_connection_bloc_test.dart index d4b500d8..dca5bde8 100644 --- a/test/features/pihole_api/blocs/pi_connection_bloc_test.dart +++ b/test/features/pihole_api/blocs/pi_connection_bloc_test.dart @@ -202,8 +202,9 @@ void main() { group('$PiConnectionEventSleep', () { final settings = PiholeSettings(title: 'Sleepy pi'); - final Duration tDuration = Duration(seconds: 1); - final ToggleStatus toggleStatus = ToggleStatus(PiStatusEnum.disabled); + final Duration tDuration = Duration(milliseconds: 100); + final ToggleStatus afterSleep = ToggleStatus(PiStatusEnum.disabled); + final ToggleStatus afterWake = ToggleStatus(PiStatusEnum.enabled); final DateTime now = clock.now(); final tFailure = Failure('test #0'); @@ -213,13 +214,15 @@ void main() { when(mockSettingsRepository.fetchActivePiholeSettings()) .thenAnswer((_) async => Right(settings)); when(mockConnectionRepository.sleepPihole(settings, tDuration)) - .thenAnswer((_) async => Right(toggleStatus)); + .thenAnswer((_) async => Right(afterSleep)); + when(mockConnectionRepository.pingPihole(settings)) + .thenAnswer((_) async => Right(afterWake)); return bloc; }, act: (PiConnectionBloc bloc) async => bloc.add(PiConnectionEventSleep(tDuration, now)), - wait: tDuration, + wait: tDuration * 2, expect: [ PiConnectionStateLoading(), PiConnectionStateSleeping( @@ -227,6 +230,8 @@ void main() { now, tDuration, ), + PiConnectionStateLoading(), + PiConnectionStateActive(settings, afterWake), ], ); diff --git a/test/features/settings/presentation/blocs/settings_bloc_test.dart b/test/features/settings/presentation/blocs/settings_bloc_test.dart index 8044e9cf..b465111e 100644 --- a/test/features/settings/presentation/blocs/settings_bloc_test.dart +++ b/test/features/settings/presentation/blocs/settings_bloc_test.dart @@ -65,8 +65,6 @@ void main() { build: () async { when(mockSettingsRepository.fetchAllPiholeSettings()) .thenAnswer((_) async => Left(Failure())); -// when(mockSettingsRepository.fetchActivePiholeSettings()) -// .thenAnswer((_) async => Right(active)); return bloc; },