diff --git a/lib/helpers/settings_helper.dart b/lib/helpers/settings_helper.dart new file mode 100644 index 0000000..d2dfba4 --- /dev/null +++ b/lib/helpers/settings_helper.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import 'package:comunic/models/account_image_settings.dart'; +import 'package:comunic/models/api_request.dart'; + +/// Settings helper +/// +/// @author Pierre Hubert + +const _APIAccountImageVisibilityAPILevels = { + "open": AccountImageVisibilityLevels.EVERYONE, + "public": AccountImageVisibilityLevels.COMUNIC_USERS, + "friends": AccountImageVisibilityLevels.FRIENDS_ONLY, +}; + +class SettingsHelper { + /// Get & return account image settings + static Future getAccountImageSettings() async { + final response = + (await APIRequest(uri: "settings/get_account_image", needLogin: true) + .exec()) + .assertOk() + .getObject(); + + return AccountImageSettings( + hasImage: response["has_image"], + imageURL: response["image_url"], + visibility: + _APIAccountImageVisibilityAPILevels[response["visibility"]]); + } + + /// Upload a new account image + static Future uploadAccountImage(File newImage) async => + (await APIRequest(uri: "settings/upload_account_image", needLogin: true) + .addFile("picture", newImage) + .execWithFiles()) + .isOK; +} diff --git a/lib/models/account_image_settings.dart b/lib/models/account_image_settings.dart new file mode 100644 index 0000000..90adcaf --- /dev/null +++ b/lib/models/account_image_settings.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; + +/// Account image settings +/// +/// @author Pierre Hubert + +enum AccountImageVisibilityLevels { EVERYONE, COMUNIC_USERS, FRIENDS_ONLY } + +class AccountImageSettings { + final bool hasImage; + final String imageURL; + final AccountImageVisibilityLevels visibility; + + const AccountImageSettings({ + @required this.hasImage, + @required this.imageURL, + @required this.visibility, + }) : assert(hasImage != null), + assert(imageURL != null), + assert(visibility != null); +} diff --git a/lib/models/api_request.dart b/lib/models/api_request.dart index f268c3d..8f429a2 100644 --- a/lib/models/api_request.dart +++ b/lib/models/api_request.dart @@ -22,14 +22,25 @@ class APIRequest { if (this.args == null) this.args = Map(); } - void addString(String name, String value) => args[name] = value; + APIRequest addString(String name, String value) { + args[name] = value; + return this; + } - void addInt(String name, int value) => args[name] = value.toString(); + APIRequest addInt(String name, int value) { + args[name] = value.toString(); + return this; + } - void addBool(String name, bool value) => - args[name] = value ? "true" : "false"; + APIRequest addBool(String name, bool value) { + args[name] = value ? "true" : "false"; + return this; + } - void addFile(String name, File file) => files[name] = file; + APIRequest addFile(String name, File file) { + files[name] = file; + return this; + } void addArgs(Map newArgs) => args.addAll(newArgs); diff --git a/lib/ui/routes/account_settings/account_image_settings.dart b/lib/ui/routes/account_settings/account_image_settings.dart index 5fdcb15..3748c73 100644 --- a/lib/ui/routes/account_settings/account_image_settings.dart +++ b/lib/ui/routes/account_settings/account_image_settings.dart @@ -1,11 +1,18 @@ +import 'package:comunic/helpers/settings_helper.dart'; +import 'package:comunic/models/account_image_settings.dart'; +import 'package:comunic/ui/widgets/async_screen_widget.dart'; +import 'package:comunic/ui/widgets/network_image_widget.dart'; +import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; +import 'package:settings_ui/settings_ui.dart'; /// Account image settings section /// /// @author Pierre Hubert -class AccountImageSettings extends StatelessWidget { +class AccountImageSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( @@ -17,15 +24,75 @@ class AccountImageSettings extends StatelessWidget { } } - class _AccountImageSettingsBody extends StatefulWidget { @override - __AccountImageSettingsBodyState createState() => __AccountImageSettingsBodyState(); + __AccountImageSettingsBodyState createState() => + __AccountImageSettingsBodyState(); } class __AccountImageSettingsBodyState extends State<_AccountImageSettingsBody> { + AccountImageSettings _settings; + + final _key = GlobalKey(); + @override Widget build(BuildContext context) { - return Container(); + return AsyncScreenWidget( + key: _key, + onReload: () async => + _settings = await SettingsHelper.getAccountImageSettings(), + onBuild: () => _buildLayout(), + errorMessage: tr("Could not get account image settings!"), + ); + } + + Widget _buildLayout() { + return SettingsList( + sections: [ + SettingsSection( + title: tr("General"), + tiles: _settings.hasImage + ? _buildHasAccountImageTiles() + : _buildNoAccountImageTiles(), + ) + ], + ); + } + + /// When user has no account image yet + List _buildNoAccountImageTiles() {} + + /// When the user has an account image + List _buildHasAccountImageTiles() { + return [ + // Current account image (if any) + SettingsTile( + title: tr("Current account image"), + leading: NetworkImageWidget( + url: _settings.imageURL, + width: 40, + ), + ), + SettingsTile( + title: tr("Upload new account image"), + onTap: () => _uploadAccountImage(), + ), + SettingsTile(title: tr("Change account image visibility")), + SettingsTile(title: tr("Delete account image")) + ]; + } + + /// Upload a new account image + void _uploadAccountImage() async { + final image = await pickImage(context); + + if (image == null) return; + + if (!await SettingsHelper.uploadAccountImage(image)) { + showSimpleSnack(context, tr("Could not upload your account image!")); + return; + } + + _key.currentState.refresh(); } } diff --git a/lib/ui/routes/account_settings/account_settings_route.dart b/lib/ui/routes/account_settings/account_settings_route.dart index 2396372..a35759e 100644 --- a/lib/ui/routes/account_settings/account_settings_route.dart +++ b/lib/ui/routes/account_settings/account_settings_route.dart @@ -36,7 +36,7 @@ class __AccountSettingsBodyState extends State<_AccountSettingsBody> { title: tr("Account image"), subtitle: tr("Customize your account image"), leading: Icon(Icons.account_circle), - onTap: () => _openSection(AccountImageSettings()), + onTap: () => _openSection(AccountImageSettingsScreen()), ) ], ) diff --git a/lib/ui/widgets/async_screen_widget.dart b/lib/ui/widgets/async_screen_widget.dart new file mode 100644 index 0000000..034b8a3 --- /dev/null +++ b/lib/ui/widgets/async_screen_widget.dart @@ -0,0 +1,96 @@ +import 'package:comunic/ui/widgets/safe_state.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; + +/// Widget that can be used to easily implement fetch of remote ressources +/// +/// @author Pierre Hubert + +class AsyncScreenWidget extends StatefulWidget { + /// Reload function + /// + /// Can be an asynchronous function that takes no arguments and throw an + /// [Exception] in case of failure + /// + /// You do not need to call [State.setState] on this function, it is + /// automatically done by this widget + final Future Function() onReload; + + /// Build function + /// + /// This function will be called whenever [isReady] becomes true + final Widget Function() onBuild; + + /// Error message that will be shown in case of error + final String errorMessage; + + const AsyncScreenWidget({ + Key key, + @required this.onReload, + @required this.onBuild, + @required this.errorMessage, + }) : assert(onReload != null), + assert(onBuild != null), + assert(errorMessage != null), + super(key: key); + + @override + AsyncScreenWidgetState createState() => AsyncScreenWidgetState(); +} + +class AsyncScreenWidgetState extends SafeState { + bool error = false; + bool ready = false; + + @override + void initState() { + refresh(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + // In case of error + if (error) + return buildErrorCard(widget.errorMessage, actions: [ + MaterialButton( + onPressed: () => refresh(), + child: Text(tr("Try again").toUpperCase()), + ) + ]); + + // Show loading states + if (!ready) return buildCenteredProgressBar(); + + // The widget is ready, show it + return RefreshIndicator( + child: widget.onBuild(), + onRefresh: () => refresh(), + ); + } + + /// Refresh this screen + Future refresh() async { + try { + setState(() { + error = false; + ready = false; + }); + + // Call parent method + await widget.onReload(); + + setState(() { + ready = true; + }); + } catch (e, stack) { + print(e); + print(stack); + + setState(() { + error = true; + }); + } + } +}