Skip to content

Commit

Permalink
Dev mode for app news: custom URI + manual refresh (#5416)
Browse files Browse the repository at this point in the history
  • Loading branch information
g123k committed Jun 20, 2024
1 parent 65dece6 commit 274d797
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ class AppNewsProvider extends ChangeNotifier {
AppNewsProvider(UserPreferences preferences)
: _state = const AppNewsStateLoading(),
_preferences = preferences,
_uriOverride = preferences.getDevModeString(
UserPreferencesDevMode.userPreferencesCustomNewsJSONURI),
_domain = preferences.getDevModeString(
UserPreferencesDevMode.userPreferencesTestEnvDomain) ??
'',
UserPreferencesDevMode.userPreferencesTestEnvDomain),
_prodEnv = preferences
.getFlag(UserPreferencesDevMode.userPreferencesFlagProd) ??
true {
.getFlag(UserPreferencesDevMode.userPreferencesFlagProd) {
_preferences.addListener(_onPreferencesChanged);
loadLatestNews();
}
Expand Down Expand Up @@ -67,13 +67,13 @@ class AppNewsProvider extends ChangeNotifier {
return;
}

final AppNews? tagLine = await Isolate.run(
final AppNews? appNews = await Isolate.run(
() => _parseJSONAndGetLocalizedContent(jsonString!, locale));
if (tagLine == null) {
if (appNews == null) {
_emit(const AppNewsStateError('Unable to parse the JSON news file'));
Logs.e('Unable to parse the JSON news file');
} else {
_emit(AppNewsStateLoaded(tagLine));
_emit(AppNewsStateLoaded(appNews, cacheFile.lastModifiedSync()));
Logs.i('News ${forceUpdate ? 're' : ''}loaded');
}
}
Expand Down Expand Up @@ -106,7 +106,13 @@ class AppNewsProvider extends ChangeNotifier {
try {
final UriProductHelper uriProductHelper = ProductQuery.uriProductHelper;
final Map<String, String> headers = <String, String>{};
final Uri uri = uriProductHelper.getUri(path: _newsUrl);
final Uri uri;

if (_uriOverride?.isNotEmpty == true) {
uri = Uri.parse(_uriOverride!);
} else {
uri = uriProductHelper.getUri(path: _newsUrl);
}

if (uriProductHelper.userInfoForPatch != null) {
headers['Authorization'] =
Expand Down Expand Up @@ -158,20 +164,25 @@ class AppNewsProvider extends ChangeNotifier {

bool? _prodEnv;
String? _domain;
String? _uriOverride;

/// [ProductQuery.uriProductHelper] is not synced yet,
/// so we have to check it manually
Future<void> _onPreferencesChanged() async {
final String jsonURI = _preferences.getDevModeString(
UserPreferencesDevMode.userPreferencesCustomNewsJSONURI) ??
'';
final String domain = _preferences.getDevModeString(
UserPreferencesDevMode.userPreferencesTestEnvDomain) ??
'';
final bool prodEnv =
_preferences.getFlag(UserPreferencesDevMode.userPreferencesFlagProd) ??
true;

if (domain != _domain || prodEnv != _prodEnv) {
if (domain != _domain || prodEnv != _prodEnv || jsonURI != _uriOverride) {
_domain = domain;
_prodEnv = prodEnv;
_uriOverride = jsonURI;
loadLatestNews(forceUpdate: true);
}
}
Expand All @@ -192,9 +203,10 @@ final class AppNewsStateLoading extends AppNewsState {
}

class AppNewsStateLoaded extends AppNewsState {
const AppNewsStateLoaded(this.content);
const AppNewsStateLoaded(this.content, this.lastUpdate);

final AppNews content;
final DateTime lastUpdate;
}

class AppNewsStateError extends AppNewsState {
Expand Down
26 changes: 26 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,31 @@
"@dev_preferences_import_history_subtitle": {
"description": "User dev preferences - Import history - Subtitle"
},
"dev_preferences_news_custom_url_title": "Custom URL for news",
"@dev_preferences_news_custom_url_title": {
"description": "News dev preferences - Custom URL for news - Title"
},
"dev_preferences_news_custom_url_subtitle": "URL of the JSON file:",
"@dev_preferences_news_custom_url_subtitle": {
"description": "News dev preferences - Custom URL for news - Title"
},
"dev_preferences_news_custom_url_empty_value": "Not set",
"@dev_preferences_news_custom_url_empty_value": {
"description": "Message to show when the custom news URL is not set"
},
"dev_preferences_news_provider_status_title": "Status",
"@dev_preferences_news_provider_status_title": {
"description": "News dev preferences - Status - Title"
},
"dev_preferences_news_provider_status_subtitle": "Last refresh: {date}",
"@dev_preferences_news_provider_status_subtitle": {
"description": "News dev preferences - Custom URL for news - Subtitle",
"placeholders": {
"date": {
"type": "String"
}
}
},
"prices_app_dev_mode_flag": "Shortcut to Prices app on product page",
"prices_app_button": "Go to Prices app",
"prices_generic_title": "Prices",
Expand Down Expand Up @@ -1824,6 +1849,7 @@
"description": "User dev preferences - Import history - Result successful"
},
"dev_mode_section_server": "Server configuration",
"dev_mode_section_news": "News provider configuration",
"dev_mode_section_product_page": "Product page",
"dev_mode_section_ui": "User Interface",
"dev_mode_section_data": "Data",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/background/background_task_badge.dart';
import 'package:smooth_app/background/background_task_language_refresh.dart';
import 'package:smooth_app/data_models/continuous_scan_model.dart';
import 'package:smooth_app/data_models/news_feed/newsfeed_provider.dart';
import 'package:smooth_app/data_models/preferences/user_preferences.dart';
import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/database/dao_osm_location.dart';
Expand Down Expand Up @@ -59,6 +61,7 @@ class UserPreferencesDevMode extends AbstractUserPreferences {
static const String userPreferencesFlagUserOrderedKP = '__userOrderedKP';
static const String userPreferencesFlagSpellCheckerOnOcr =
'__spellcheckerOcr';
static const String userPreferencesCustomNewsJSONURI = '__newsJsonURI';

final TextEditingController _textFieldController = TextEditingController();

Expand Down Expand Up @@ -266,6 +269,49 @@ class UserPreferencesDevMode extends AbstractUserPreferences {
),
onTap: () async => _changeTestEnvDomain(),
),
UserPreferencesItemSection(
label: appLocalizations.dev_mode_section_news,
),
UserPreferencesEditableItemTile(
title: appLocalizations.dev_preferences_news_custom_url_title,
subtitleWithEmptyValue:
appLocalizations.dev_preferences_news_custom_url_empty_value,
dialogAction:
appLocalizations.dev_preferences_news_custom_url_subtitle,
value: userPreferences
.getDevModeString(userPreferencesCustomNewsJSONURI),
onNewValue: (String newUrl) => userPreferences.setDevModeString(
userPreferencesCustomNewsJSONURI,
newUrl,
),
validator: (String value) =>
value.isEmpty || Uri.tryParse(value) != null,
),
UserPreferencesItemTileBuilder(
title: appLocalizations.dev_preferences_news_provider_status_title,
subtitleBuilder: (BuildContext context) {
return Consumer<AppNewsProvider>(
builder: (_, AppNewsProvider provider, __) {
return Text(switch (provider.state) {
AppNewsStateLoading() => 'Loading...',
AppNewsStateLoaded(lastUpdate: final DateTime date) =>
appLocalizations
.dev_preferences_news_provider_status_subtitle(
DateFormat.yMd().format(date),
),
AppNewsStateError(exception: final dynamic e) => 'Error $e',
});
});
},
trailingBuilder: (BuildContext context) {
return IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => context
.read<AppNewsProvider>()
.loadLatestNews(forceUpdate: true),
);
},
),
UserPreferencesItemSection(
label: appLocalizations.dev_mode_section_product_page,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/generic_lib/bottom_sheets/smooth_bottom_sheet.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart';
import 'package:smooth_app/pages/preferences/user_preferences_item.dart';
import 'package:smooth_app/themes/smooth_theme_colors.dart';

Expand Down Expand Up @@ -164,6 +167,35 @@ class UserPreferencesItemTile implements UserPreferencesItem {
);
}

/// Same as [UserPreferencesItemTile] but with [WidgetBuilder].
class UserPreferencesItemTileBuilder implements UserPreferencesItem {
const UserPreferencesItemTileBuilder({
required this.title,
required this.subtitleBuilder,
this.onTap,
this.leadingBuilder,
this.trailingBuilder,
});

final String title;
final WidgetBuilder subtitleBuilder;
final VoidCallback? onTap;
final WidgetBuilder? leadingBuilder;
final WidgetBuilder? trailingBuilder;

@override
List<String> get labels => <String>[title];

@override
WidgetBuilder get builder => (final BuildContext context) => ListTile(
title: Text(title),
subtitle: subtitleBuilder.call(context),
onTap: onTap,
leading: leadingBuilder?.call(context),
trailing: trailingBuilder?.call(context),
);
}

class UserPreferencesItemSection implements UserPreferencesItem {
const UserPreferencesItemSection({
required this.label,
Expand Down Expand Up @@ -495,3 +527,128 @@ class UserPreferenceListTile extends StatelessWidget {
);
}
}

class UserPreferencesEditableItemTile extends UserPreferencesItemTile {
const UserPreferencesEditableItemTile({
required super.title,
required String dialogAction,
required this.onNewValue,
this.subtitleWithEmptyValue,
this.validator,
this.hint,
this.value,
}) : assert(dialogAction.length > 0),
super(subtitle: dialogAction);

final String? value;
final String? hint;
final String? subtitleWithEmptyValue;
final bool Function(String)? validator;
final Function(String) onNewValue;

@override
WidgetBuilder get builder => (BuildContext context) {
return ListTile(
title: Text(title),
subtitle: Text(value?.isNotEmpty == true
? value!
: (subtitleWithEmptyValue ?? '-')),
onTap: () async => _showInputTextDialog(context),
);
};

Future<void> _showInputTextDialog(BuildContext context) async {
final TextEditingController controller =
TextEditingController(text: value ?? '');

final dynamic res = await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);

return ChangeNotifierProvider<TextEditingController>.value(
value: controller,
child: Consumer<TextEditingController>(
builder:
(BuildContext context, TextEditingController controller, _) {
return SmoothAlertDialog(
title: title,
close: true,
body: _UserPreferencesEditableDialogContent(
title: subtitle!,
hint: hint,
),
positiveAction: SmoothActionButton(
text: appLocalizations.okay,
onPressed: validator?.call(controller.text) != false
? () => Navigator.of(context).pop(controller.text)
: null,
),
negativeAction: SmoothActionButton(
text: appLocalizations.cancel,
onPressed: () => Navigator.of(context).pop(),
),
);
},
),
);
},
);

if (res is String && res != value) {
onNewValue.call(res);
}
}
}

class _UserPreferencesEditableDialogContent extends StatefulWidget {
const _UserPreferencesEditableDialogContent({
required this.title,
this.hint,
});

final String title;
final String? hint;

@override
State<_UserPreferencesEditableDialogContent> createState() =>
_InputTextDialogBodyState();
}

class _InputTextDialogBodyState
extends State<_UserPreferencesEditableDialogContent> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(widget.title),
const SizedBox(height: 10),
TextField(
controller: Provider.of<TextEditingController>(context),
autocorrect: false,
autofocus: true,
textInputAction: TextInputAction.send,
decoration: InputDecoration(
hintText: widget.hint,
suffix: Semantics(
button: true,
label: MaterialLocalizations.of(context).deleteButtonTooltip,
excludeSemantics: true,
child: InkWell(
onTap: () => context.read<TextEditingController>().clear(),
customBorder: const CircleBorder(),
child: const Padding(
padding: EdgeInsetsDirectional.all(SMALL_SPACE),
child: Icon(Icons.clear),
),
),
),
),
onSubmitted: (String value) => Navigator.of(context).pop(value),
),
],
);
}
}

0 comments on commit 274d797

Please sign in to comment.