From 11c85f4a07e69625ac92614f0c0729c66f192e50 Mon Sep 17 00:00:00 2001 From: istalri Date: Tue, 2 Jun 2026 09:29:52 +0200 Subject: [PATCH] Server selection: now only the cards are scrollable and some cleanup --- lib/controllers/client_state_controller.dart | 8 + lib/main.dart | 5 +- lib/pages/login_page.dart | 204 ++++--------------- lib/pages/select_server_page.dart | 187 +++++++++++++++++ pubspec.lock | 16 +- 5 files changed, 247 insertions(+), 173 deletions(-) create mode 100644 lib/pages/select_server_page.dart diff --git a/lib/controllers/client_state_controller.dart b/lib/controllers/client_state_controller.dart index 998d4a1..0e9a505 100644 --- a/lib/controllers/client_state_controller.dart +++ b/lib/controllers/client_state_controller.dart @@ -9,6 +9,14 @@ class ClientStateController extends Notifier { state = newState; } + void setHomeServer(String homeserver){ + state = .new(isInitialized: state?.isInitialized ?? false, + isLoggedIn: state?.isLoggedIn ?? false, + isVerified: state?.isVerified ?? false, + userId: state?.userId, + homeserverUrl: homeserver); + } + static final provider = NotifierProvider( ClientStateController.new, ); diff --git a/lib/main.dart b/lib/main.dart index dcf7b67..377582c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/pages/chat_page.dart"; import "package:nexus/pages/login_page.dart"; +import "package:nexus/pages/select_server_page.dart"; import "package:nexus/pages/verify_page.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; @@ -123,7 +124,9 @@ class App extends StatelessWidget { return Loading(); } - if (!clientState.isLoggedIn) { + if ((!clientState.isLoggedIn) && (clientState.homeserverUrl == null || clientState.homeserverUrl?.isEmpty == true)) { + return SelectServerPage(); + } else if(!clientState.isLoggedIn) { return LoginPage(); } else if (!clientState.isVerified) { return VerifyPage(); diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index d2153eb..8f8c6a4 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -1,185 +1,64 @@ import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_svg/flutter_svg.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/helpers/launch_helper.dart"; -import "package:nexus/models/homeserver.dart"; +import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/models/requests/login_request.dart"; import "package:nexus/widgets/appbar.dart"; -import "package:nexus/widgets/divider_text.dart"; -import "package:nexus/widgets/loading.dart"; class LoginPage extends HookConsumerWidget { const LoginPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); final client = ref.watch(ClientController.provider.notifier); + final isLoggingIn = useState(false); + final homeserverUrl = ref.watch(ClientStateController.provider)?.homeserverUrl; - final isLoading = useState(false); - final homeserver = useState(null); - - final launch = ref.watch(LaunchHelper.provider).launchUrl; - - Future setHomeserver(Uri? newHomeserver) async { - isLoading.value = true; - - homeserver.value = newHomeserver == null - ? null - : await client.discoverHomeserver( - newHomeserver.hasScheme - ? newHomeserver - : Uri.https(newHomeserver.path), - ); - - if (homeserver.value == null && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Homeserver verification failed. Is your homeserver down?", - style: TextStyle(color: theme.colorScheme.onErrorContainer), - ), - backgroundColor: theme.colorScheme.errorContainer, - ), - ); - } - isLoading.value = false; + if(homeserverUrl == null || homeserverUrl.trim().isEmpty) { + throw Exception("Homeserver URL must be set for login."); } - final homeserverUrl = useTextEditingController(); + final theme = Theme.of(context); + final username = useTextEditingController(); final password = useTextEditingController(); return Scaffold( appBar: Appbar(), - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 600), - child: ListView( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 64), - children: [ - Row( - children: [ - SvgPicture.asset("assets/icon.svg", width: 128), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Nexus", style: theme.textTheme.displayMedium), - Text( - "A Simple Matrix Client", - style: theme.textTheme.headlineMedium, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - Padding( - padding: EdgeInsetsGeometry.symmetric(vertical: 12), - child: Divider(), - ), - - DividerText("Enter a homeserver domain:"), - Row( - spacing: 8, - children: [ - Expanded( - child: TextField( - controller: homeserverUrl, - decoration: InputDecoration( - labelText: "Homeserver URL (e.g. matrix.org)", - ), - ), - ), - IconButton.filled( - tooltip: "Confirm homeserver choice", - onPressed: isLoading.value - ? null - : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), - icon: Icon(Icons.check), - ), - ], - ), - - DividerText("Or, choose from some popular homeservers:"), - ...([ - Homeserver( - name: "Matrix.org", - description: - "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", - url: Uri.https("matrix.org"), - iconUrl: - "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", - ), - Homeserver( - name: "Federated Nexus", - description: - "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", - url: Uri.https("federated.nexus"), - iconUrl: "https://federated.nexus/images/icon.png", - ), - Homeserver( - name: "Unredacted", - description: - "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", - url: Uri.https("unredacted.org", "services/si/matrix"), - iconUrl: "https://unredacted.org/favicon.ico", - ), - ].map( - (homeserver) => Card( - child: ListTile( - title: Text(homeserver.name), - leading: Image.network( - homeserver.iconUrl, - errorBuilder: (_, _, _) => SizedBox.shrink(), - height: 32, - ), - subtitle: Text(homeserver.description), - onTap: isLoading.value - ? null - : () => setHomeserver(homeserver.url), - trailing: IconButton( - tooltip: "Launch homeserver info page", - onPressed: () => launch(homeserver.url), - icon: Icon(Icons.info_outline), - ), - ), - ), - )), - SizedBox(height: 8), - TextButton( - onPressed: () => launch(Uri.https("servers.joinmatrix.org")), - child: Text("See more homeservers..."), - ), - if (isLoading.value) - Padding(padding: EdgeInsets.only(top: 32), child: Loading()) - else if (homeserver.value != null) ...[ - DividerText("Then, sign in:"), - SizedBox(height: 4), - TextField( - decoration: InputDecoration(label: Text("Username")), - controller: username, - ), - SizedBox(height: 12), - TextField( - decoration: InputDecoration(label: Text("Password")), - controller: password, - obscureText: true, - ), - SizedBox(height: 12), - ElevatedButton( - onPressed: () async { - isLoading.value = true; + body: AlertDialog( + title: Text("Login"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter your login credentials:", + ), + SizedBox(height: 12), + TextField( + decoration: InputDecoration(label: Text("Username")), + controller: username, + ), + SizedBox(height: 12), + TextField( + decoration: InputDecoration(label: Text("Password")), + controller: password, + obscureText: true, + ), + ], + ), + actions: [ + TextButton( + onPressed: isLoggingIn.value + ? null + : () async { + isLoggingIn.value = true; final error = await client.login( LoginRequest( username: username.text, password: password.text, - homeserverUrl: homeserver.value!, + homeserverUrl: homeserverUrl, ), ); @@ -195,15 +74,12 @@ class LoginPage extends HookConsumerWidget { backgroundColor: theme.colorScheme.errorContainer, ), ); - isLoading.value = false; + isLoggingIn.value = false; } - }, - child: Text("Sign In"), - ), - ], - ], + }, + child: Text("Sign In"), ), - ), + ], ), ); } diff --git a/lib/pages/select_server_page.dart b/lib/pages/select_server_page.dart new file mode 100644 index 0000000..e38d595 --- /dev/null +++ b/lib/pages/select_server_page.dart @@ -0,0 +1,187 @@ +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/helpers/launch_helper.dart"; +import "package:nexus/models/homeserver.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/loading.dart"; + +class SelectServerPage extends HookConsumerWidget { + const SelectServerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final client = ref.watch(ClientController.provider.notifier); + + final isLoading = useState(false); + + final launch = ref.watch(LaunchHelper.provider).launchUrl; + + final homeserverUrl = useTextEditingController(); + + Future setHomeserver(Uri? newHomeserver) async { + isLoading.value = true; + + if(newHomeserver?.hasScheme == false){ + newHomeserver = Uri.https(newHomeserver!.path); + } + + final newUrl = newHomeserver == null + ? null + : await client.discoverHomeserver(newHomeserver); + + if (context.mounted) { + if (newUrl == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Homeserver verification failed. Is your homeserver down?", + style: TextStyle(color: theme.colorScheme.onErrorContainer), + ), + backgroundColor: theme.colorScheme.errorContainer, + ), + ); + } else { + homeserverUrl.text = newHomeserver!.origin; + ref.watch(ClientStateController.provider.notifier).setHomeServer(newUrl); + } + } + isLoading.value = false; + } + + return Scaffold( + appBar: Appbar(), + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Column( + children: [ + Row( + children: [ + SvgPicture.asset("assets/icon.svg", width: 128), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Nexus", style: theme.textTheme.displayMedium), + Text( + "A Simple Matrix Client", + style: theme.textTheme.headlineMedium, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 12), + child: Divider(), + ), + DividerText("Enter a homeserver domain:"), + Row( + spacing: 8, + children: [ + Expanded( + child: TextField( + controller: homeserverUrl, + decoration: InputDecoration( + labelText: "Homeserver URL (e.g. matrix.org)", + hintText: "e.g. matrix.org", + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: (homeserverUrl.text.trim().isEmpty) + ? theme.colorScheme.error + : theme.colorScheme.primary) + ), + ), + ), + ), + IconButton.filled( + tooltip: "Confirm homeserver choice", + onPressed: isLoading.value + ? null + : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), + icon: Icon(Icons.check), + ), + ], + ), + Expanded( + child: ListView( + padding: EdgeInsets.only(top: 12), + children: [ + DividerText("Or, choose from some popular homeservers:"), + ...([ + Homeserver( + name: "Matrix.org", + description: + "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", + url: Uri.https("matrix.org"), + iconUrl: + "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", + ), + Homeserver( + name: "Federated Nexus", + description: + "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", + url: Uri.https("federated.nexus"), + iconUrl: "https://federated.nexus/images/icon.png", + ), + Homeserver( + name: "Unredacted", + description: + "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", + url: Uri.https("unredacted.org", "services/si/matrix"), + iconUrl: "https://unredacted.org/favicon.ico", + ), + Homeserver( + name: "Lorem ipsum", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + url: Uri.https("loremipsum.io"), + iconUrl: "https://loremipsum.io/favicon.ico" + ), + ].map( + (homeserver) => Card( + child: ListTile( + title: Text(homeserver.name), + leading: Image.network( + homeserver.iconUrl, + errorBuilder: (_, _, _) => SizedBox.shrink(), + height: 32, + ), + subtitle: Text(homeserver.description), + onTap: isLoading.value + ? null + : () => setHomeserver(homeserver.url), + trailing: IconButton( + tooltip: "Launch homeserver info page", + onPressed: () => launch(homeserver.url), + icon: Icon(Icons.info_outline), + ), + ), + ), + )), + ], + ), + ), + SizedBox(height: 5), + TextButton( + onPressed: () => launch(Uri.https("servers.joinmatrix.org")), + child: Text("See more homeservers..."), + ), + if (isLoading.value) + Padding(padding: EdgeInsets.only(top: 32), child: Loading()) + else + Padding(padding: EdgeInsets.only(top: 12)) + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 108474b..a7aa53e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -836,10 +836,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1321,26 +1321,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" timeago: dependency: "direct main" description: