diff --git a/mobile/lib/ui/home/landing_page_widget.dart b/mobile/lib/ui/home/landing_page_widget.dart index 119610ffa..546751eb4 100644 --- a/mobile/lib/ui/home/landing_page_widget.dart +++ b/mobile/lib/ui/home/landing_page_widget.dart @@ -19,7 +19,10 @@ import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/payment/subscription.dart'; +import "package:photos/ui/settings/developer_settings_page.dart"; +import "package:photos/ui/settings/developer_settings_widget.dart"; import "package:photos/ui/settings/language_picker.dart"; +import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; class LandingPageWidget extends StatefulWidget { @@ -30,7 +33,10 @@ class LandingPageWidget extends StatefulWidget { } class _LandingPageWidgetState extends State { + static const kDeveloperModeTapCountThreshold = 7; + double _featureIndex = 0; + int _developerModeTapCount = 0; @override void initState() { @@ -40,7 +46,35 @@ class _LandingPageWidgetState extends State { @override Widget build(BuildContext context) { - return Scaffold(body: _getBody(), resizeToAvoidBottomInset: false); + return Scaffold( + body: GestureDetector( + onTap: () async { + _developerModeTapCount++; + if (_developerModeTapCount >= kDeveloperModeTapCountThreshold) { + _developerModeTapCount = 0; + final result = await showChoiceDialog( + context, + title: S.of(context).developerSettings, + firstButtonLabel: S.of(context).yes, + body: S.of(context).developerSettingsWarning, + isDismissible: false, + ); + if (result?.action == ButtonAction.first) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const DeveloperSettingsPage(); + }, + ), + ); + setState(() {}); + } + } + }, + child: _getBody(), + ), + resizeToAvoidBottomInset: false, + ); } Widget _getBody() { @@ -131,6 +165,7 @@ class _LandingPageWidgetState extends State { ), ), ), + const DeveloperSettingsWidget(), const Padding( padding: EdgeInsets.all(20), ), @@ -195,7 +230,9 @@ class _LandingPageWidgetState extends State { // No key if (Configuration.instance.getKeyAttributes() == null) { // Never had a key - page = const PasswordEntryPage(mode: PasswordEntryMode.set,); + page = const PasswordEntryPage( + mode: PasswordEntryMode.set, + ); } else if (Configuration.instance.getKey() == null) { // Yet to decrypt the key page = const PasswordReentryPage(); @@ -223,7 +260,9 @@ class _LandingPageWidgetState extends State { // No key if (Configuration.instance.getKeyAttributes() == null) { // Never had a key - page = const PasswordEntryPage(mode: PasswordEntryMode.set,); + page = const PasswordEntryPage( + mode: PasswordEntryMode.set, + ); } else if (Configuration.instance.getKey() == null) { // Yet to decrypt the key page = const PasswordReentryPage(); diff --git a/mobile/lib/ui/settings/developer_settings_page.dart b/mobile/lib/ui/settings/developer_settings_page.dart new file mode 100644 index 000000000..c9f6357f2 --- /dev/null +++ b/mobile/lib/ui/settings/developer_settings_page.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import "package:photos/core/configuration.dart"; +import "package:photos/core/network/network.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/ui/common/gradient_button.dart"; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/toast_util.dart"; + +class DeveloperSettingsPage extends StatefulWidget { + const DeveloperSettingsPage({super.key}); + + @override + State createState() => _DeveloperSettingsPageState(); +} + +class _DeveloperSettingsPageState extends State { + final _logger = Logger('DeveloperSettingsPage'); + final _urlController = TextEditingController(); + + @override + void dispose() { + _urlController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _logger.info( + "Current endpoint is: ${Configuration.instance.getHttpEndpoint()}", + ); + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).developerSettings), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField( + controller: _urlController, + decoration: InputDecoration( + labelText: S.of(context).serverEndpoint, + hintText: Configuration.instance.getHttpEndpoint(), + ), + autofocus: true, + ), + const SizedBox(height: 40), + GradientButton( + onTap: () async { + final url = _urlController.text; + _logger.info("Entered endpoint: $url"); + try { + final uri = Uri.parse(url); + if ((uri.scheme == "http" || uri.scheme == "https")) { + await _ping(url); + await Configuration.instance.setHttpEndpoint(url); + showToast(context, S.of(context).endpointUpdatedMessage); + Navigator.of(context).pop(); + } else { + throw const FormatException(); + } + } catch (e) { + // ignore: unawaited_futures + showErrorDialog( + context, + S.of(context).invalidEndpoint, + S.of(context).invalidEndpointMessage, + ); + } + }, + text: S.of(context).save, + ), + ], + ), + ), + ); + } + + Future _ping(String endpoint) async { + try { + final response = + await NetworkClient.instance.getDio().get('$endpoint/ping'); + if (response.data['message'] != 'pong') { + throw Exception('Invalid response'); + } + } catch (e) { + throw Exception('Error occurred: $e'); + } + } +}