diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 759323d..c116b11 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 30 (15 per locale) +/// Strings: 48 (24 per locale) /// -/// Built on 2024-02-22 at 01:09 UTC +/// Built on 2024-02-22 at 10:40 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -149,7 +149,6 @@ class Translations implements BaseTranslations { // Translations late final _StringsOnboardingEn onboarding = _StringsOnboardingEn._(_root); - late final _StringsConnectEn connect = _StringsConnectEn._(_root); late final _StringsLinksEn links = _StringsLinksEn._(_root); late final _StringsSettingsEn settings = _StringsSettingsEn._(_root); } @@ -170,15 +169,15 @@ class _StringsOnboardingEn { String get serverRequiredDescription => 'My Linkding it\'s not an standalone app, it requires the Linkding server to work.\nIn order to use this application, you must deploy Linkding on your home server, VPS or any other computer.'; String get installationInstructions => 'Check the installation instructions on the official GitHub repository.'; String get serverRunningConfirmation => 'I confirm that I have an instance of the Linkding server already running.'; -} - -// Path: connect -class _StringsConnectEn { - _StringsConnectEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations + String get createConnection => 'Create a connection'; + String get createConnectionSubtitle => 'Enter all the required details to create a connection to your server.'; + String get ipAddressOrDomain => 'IP address or domain'; + String get port => 'Port'; + String get token => 'Token'; + String get required => 'Required'; + String get serverDetails => 'Server details'; + String get authentication => 'Authentication'; + String get testConnectionUrl => 'Test connection url'; String get connect => 'Connect'; String get connecting => 'Connecting...'; String get cannotConnectToServer => 'Cannot connect to the server.'; @@ -230,7 +229,6 @@ class _StringsEs implements Translations { // Translations @override late final _StringsOnboardingEs onboarding = _StringsOnboardingEs._(_root); - @override late final _StringsConnectEs connect = _StringsConnectEs._(_root); @override late final _StringsLinksEs links = _StringsLinksEs._(_root); @override late final _StringsSettingsEs settings = _StringsSettingsEs._(_root); } @@ -251,15 +249,15 @@ class _StringsOnboardingEs implements _StringsOnboardingEn { @override String get serverRequiredDescription => 'My Linkding no es una aplicación independiente, requiere el servidor Linkding para funcionar.\nPara utilizar esta aplicación, debe instalar Linkding en su servidor doméstico, VPS o cualquier otro ordenador.'; @override String get installationInstructions => 'Mira las instrucciones de instalación en el repositorio oficial en GitHub.'; @override String get serverRunningConfirmation => 'Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.'; -} - -// Path: connect -class _StringsConnectEs implements _StringsConnectEn { - _StringsConnectEs._(this._root); - - @override final _StringsEs _root; // ignore: unused_field - - // Translations + @override String get createConnection => 'Crear una conexión'; + @override String get createConnectionSubtitle => 'Introduce todos los detalles requeridos para crear una conexión con el servidor'; + @override String get ipAddressOrDomain => 'Dirección IP o dominio'; + @override String get port => 'Puerto'; + @override String get token => 'Token'; + @override String get required => 'Requerido'; + @override String get serverDetails => 'Detalles del servidor'; + @override String get authentication => 'Autenticación'; + @override String get testConnectionUrl => 'Probar URL de conexión'; @override String get connect => 'Conectar'; @override String get connecting => 'Conectando...'; @override String get cannotConnectToServer => 'No se puede conectar con el servidor.'; @@ -301,9 +299,18 @@ extension on Translations { case 'onboarding.serverRequiredDescription': return 'My Linkding it\'s not an standalone app, it requires the Linkding server to work.\nIn order to use this application, you must deploy Linkding on your home server, VPS or any other computer.'; case 'onboarding.installationInstructions': return 'Check the installation instructions on the official GitHub repository.'; case 'onboarding.serverRunningConfirmation': return 'I confirm that I have an instance of the Linkding server already running.'; - case 'connect.connect': return 'Connect'; - case 'connect.connecting': return 'Connecting...'; - case 'connect.cannotConnectToServer': return 'Cannot connect to the server.'; + case 'onboarding.createConnection': return 'Create a connection'; + case 'onboarding.createConnectionSubtitle': return 'Enter all the required details to create a connection to your server.'; + case 'onboarding.ipAddressOrDomain': return 'IP address or domain'; + case 'onboarding.port': return 'Port'; + case 'onboarding.token': return 'Token'; + case 'onboarding.required': return 'Required'; + case 'onboarding.serverDetails': return 'Server details'; + case 'onboarding.authentication': return 'Authentication'; + case 'onboarding.testConnectionUrl': return 'Test connection url'; + case 'onboarding.connect': return 'Connect'; + case 'onboarding.connecting': return 'Connecting...'; + case 'onboarding.cannotConnectToServer': return 'Cannot connect to the server.'; case 'links.links': return 'Links'; case 'settings.settings': return 'Settings'; case 'settings.disconnectFromServer': return 'Disconnect from server'; @@ -324,9 +331,18 @@ extension on _StringsEs { case 'onboarding.serverRequiredDescription': return 'My Linkding no es una aplicación independiente, requiere el servidor Linkding para funcionar.\nPara utilizar esta aplicación, debe instalar Linkding en su servidor doméstico, VPS o cualquier otro ordenador.'; case 'onboarding.installationInstructions': return 'Mira las instrucciones de instalación en el repositorio oficial en GitHub.'; case 'onboarding.serverRunningConfirmation': return 'Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.'; - case 'connect.connect': return 'Conectar'; - case 'connect.connecting': return 'Conectando...'; - case 'connect.cannotConnectToServer': return 'No se puede conectar con el servidor.'; + case 'onboarding.createConnection': return 'Crear una conexión'; + case 'onboarding.createConnectionSubtitle': return 'Introduce todos los detalles requeridos para crear una conexión con el servidor'; + case 'onboarding.ipAddressOrDomain': return 'Dirección IP o dominio'; + case 'onboarding.port': return 'Puerto'; + case 'onboarding.token': return 'Token'; + case 'onboarding.required': return 'Requerido'; + case 'onboarding.serverDetails': return 'Detalles del servidor'; + case 'onboarding.authentication': return 'Autenticación'; + case 'onboarding.testConnectionUrl': return 'Probar URL de conexión'; + case 'onboarding.connect': return 'Conectar'; + case 'onboarding.connecting': return 'Conectando...'; + case 'onboarding.cannotConnectToServer': return 'No se puede conectar con el servidor.'; case 'links.links': return 'Enlaces'; case 'settings.settings': return 'Ajustes'; case 'settings.disconnectFromServer': return 'Desconectar del servidor'; diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index dc6b905..dbf2d0b 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -8,9 +8,16 @@ "serverRequired": "Servidor requerido", "serverRequiredDescription": "My Linkding it's not an standalone app, it requires the Linkding server to work.\nIn order to use this application, you must deploy Linkding on your home server, VPS or any other computer.", "installationInstructions": "Check the installation instructions on the official GitHub repository.", - "serverRunningConfirmation": "I confirm that I have an instance of the Linkding server already running." - }, - "connect": { + "serverRunningConfirmation": "I confirm that I have an instance of the Linkding server already running.", + "createConnection": "Create a connection", + "createConnectionSubtitle": "Enter all the required details to create a connection to your server.", + "ipAddressOrDomain": "IP address or domain", + "port": "Port", + "token": "Token", + "required": "Required", + "serverDetails": "Server details", + "authentication": "Authentication", + "testConnectionUrl": "Test connection url", "connect": "Connect", "connecting": "Connecting...", "cannotConnectToServer": "Cannot connect to the server." diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index d431ef4..ea9f22b 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -8,9 +8,16 @@ "serverRequired": "Servidor requerido", "serverRequiredDescription": "My Linkding no es una aplicación independiente, requiere el servidor Linkding para funcionar.\nPara utilizar esta aplicación, debe instalar Linkding en su servidor doméstico, VPS o cualquier otro ordenador.", "installationInstructions": "Mira las instrucciones de instalación en el repositorio oficial en GitHub.", - "serverRunningConfirmation": "Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento." - }, - "connect": { + "serverRunningConfirmation": "Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.", + "createConnection": "Crear una conexión", + "createConnectionSubtitle": "Introduce todos los detalles requeridos para crear una conexión con el servidor", + "ipAddressOrDomain": "Dirección IP o dominio", + "port": "Puerto", + "token": "Token", + "required": "Requerido", + "serverDetails": "Detalles del servidor", + "authentication": "Autenticación", + "testConnectionUrl": "Probar URL de conexión", "connect": "Conectar", "connecting": "Conectando...", "cannotConnectToServer": "No se puede conectar con el servidor." diff --git a/lib/screens/connect/ui/connect.dart b/lib/screens/connect/ui/connect.dart deleted file mode 100644 index c30cb02..0000000 --- a/lib/screens/connect/ui/connect.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:my_linkding/constants/enums.dart'; - -import 'package:my_linkding/screens/connect/provider/connect.provider.dart'; -import 'package:segmented_button_slide/segmented_button_slide.dart'; - -class Connect extends ConsumerWidget { - const Connect({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final provider = ref.watch(connectProvider); - - final validValies = ref.read(connectProvider.notifier).validValues(); - - final connectionMethod = ConnectionMethod.values[provider.method]; - - return Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - margin: const EdgeInsets.only(top: 24, left: 24, right: 24), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.05), - borderRadius: BorderRadius.circular(30), - border: Border.all(color: Theme.of(context).colorScheme.primary), - ), - child: Text( - "${connectionMethod.name}://${provider.ipDomainController.text}${provider.pathController.text != '' ? ':${provider.portController.text}' : ""}${provider.pathController.text}", - textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w500), - ), - ), - const SizedBox(height: 30), - SegmentedButtonSlide( - entries: const [ - SegmentedButtonSlideEntry(label: "HTTP"), - SegmentedButtonSlideEntry(label: "HTTPS"), - ], - selectedEntry: provider.method, - onChange: ref.read(connectProvider.notifier).setConnectionMethod, - colors: SegmentedButtonSlideColors( - barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), - backgroundSelectedColor: Theme.of(context).colorScheme.primary, - foregroundSelectedColor: Theme.of(context).colorScheme.onPrimary, - foregroundUnselectedColor: Theme.of(context).colorScheme.onSurface, - hoverColor: Theme.of(context).colorScheme.onSurfaceVariant, - ), - textOverflow: TextOverflow.ellipsis, - fontSize: 14, - height: 40, - margin: const EdgeInsets.symmetric( - horizontal: 24, - ), - ), - const SizedBox(height: 30), - TextFormField( - controller: provider.ipDomainController, - onChanged: ref.read(connectProvider.notifier).validateIpDomain, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - errorText: provider.ipDomainError, - labelText: "IP address or domain", - ), - ), - const SizedBox(height: 30), - TextFormField( - controller: provider.portController, - onChanged: ref.read(connectProvider.notifier).validatePort, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - errorText: provider.portError, - labelText: "Port", - ), - ), - const SizedBox(height: 30), - TextFormField( - controller: provider.tokenController, - onChanged: ref.read(connectProvider.notifier).validateToken, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.key_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - errorText: provider.tokenError, - labelText: "Token", - ), - obscureText: true, - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilledButton.icon( - onPressed: validValies ? () => ref.read(connectToServerProvider) : null, - icon: const Icon(Icons.login_rounded), - label: Text("Connect"), - ), - ], - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/screens/connect/model/connect.model.dart b/lib/screens/onboarding/model/connect.model.dart similarity index 93% rename from lib/screens/connect/model/connect.model.dart rename to lib/screens/onboarding/model/connect.model.dart index ec81499..637d3fd 100644 --- a/lib/screens/connect/model/connect.model.dart +++ b/lib/screens/onboarding/model/connect.model.dart @@ -12,6 +12,7 @@ class ConnectModel { final String? pathError; final TextEditingController tokenController; final String? tokenError; + final bool validValues; ConnectModel({ required this.isConnecting, @@ -24,6 +25,7 @@ class ConnectModel { required this.pathError, required this.tokenController, required this.tokenError, + required this.validValues, }); ConnectModel copyWidth({ @@ -37,6 +39,7 @@ class ConnectModel { Wrapped? pathError, TextEditingController? tokenController, Wrapped? tokenError, + bool? validValues, }) => ConnectModel( isConnecting: isConnecting ?? this.isConnecting, @@ -49,5 +52,6 @@ class ConnectModel { pathError: pathError != null ? pathError.value : this.pathError, tokenController: tokenController ?? this.tokenController, tokenError: tokenError != null ? tokenError.value : this.tokenError, + validValues: validValues ?? this.validValues, ); } diff --git a/lib/screens/connect/provider/connect.provider.dart b/lib/screens/onboarding/provider/connect.provider.dart similarity index 88% rename from lib/screens/connect/provider/connect.provider.dart rename to lib/screens/onboarding/provider/connect.provider.dart index 670c8dd..185fcec 100644 --- a/lib/screens/connect/provider/connect.provider.dart +++ b/lib/screens/onboarding/provider/connect.provider.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:my_linkding/screens/connect/model/connect.model.dart'; +import 'package:my_linkding/screens/onboarding/model/connect.model.dart'; import 'package:my_linkding/providers/server_instances_provider.dart'; import 'package:my_linkding/i18n/strings.g.dart'; @@ -32,6 +32,7 @@ class Connect extends _$Connect { pathError: null, tokenController: TextEditingController(), tokenError: null, + validValues: false, ); } @@ -45,6 +46,7 @@ class Connect extends _$Connect { } else { state = state.copyWidth(ipDomainError: const Wrapped.value("Error")); } + state = state.copyWidth(validValues: validValues()); } void validatePort(value) { @@ -55,6 +57,7 @@ class Connect extends _$Connect { state = state.copyWidth(portError: const Wrapped.value("Invalid port")); } } + state = state.copyWidth(validValues: validValues()); } void validateToken(String value) { @@ -63,6 +66,7 @@ class Connect extends _$Connect { } else { state = state.copyWidth(tokenError: const Wrapped.value("Error")); } + state = state.copyWidth(validValues: validValues()); } bool validValues() { @@ -87,7 +91,7 @@ FutureOr connectToServer(ConnectToServerRef ref) async { final apiClient = ApiClient(serverInstance: serverInstance); final processModal = ProcessModal(); - processModal.open(t.connect.connecting); + processModal.open(t.onboarding.connecting); final result = await apiClient.checkConnectionInstance(); @@ -97,7 +101,7 @@ FutureOr connectToServer(ConnectToServerRef ref) async { ref.read(serverInstancesProvider.notifier).saveNewInstance(serverInstance); // ref.watch(routerProvider).replace(RoutesPaths.links); } else { - showSnacbkar(label: t.connect.cannotConnectToServer, color: Colors.red); + showSnacbkar(label: t.onboarding.cannotConnectToServer, color: Colors.red); } return false; diff --git a/lib/screens/connect/provider/connect.provider.g.dart b/lib/screens/onboarding/provider/connect.provider.g.dart similarity index 89% rename from lib/screens/connect/provider/connect.provider.g.dart rename to lib/screens/onboarding/provider/connect.provider.g.dart index 049f5b2..b8f0e43 100644 --- a/lib/screens/connect/provider/connect.provider.g.dart +++ b/lib/screens/onboarding/provider/connect.provider.g.dart @@ -6,7 +6,7 @@ part of 'connect.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$connectToServerHash() => r'c40898fd5dd6dd7507140c7a69a6f336a56e30c0'; +String _$connectToServerHash() => r'85925cb547fccb708e657d263e39a46867ac2bca'; /// See also [connectToServer]. @ProviderFor(connectToServer) @@ -21,7 +21,7 @@ final connectToServerProvider = AutoDisposeFutureProvider.internal( ); typedef ConnectToServerRef = AutoDisposeFutureProviderRef; -String _$connectHash() => r'bbdae015118381c538b7a3c15ca8345d30a28995'; +String _$connectHash() => r'2c2e4fff717252259381056b4767b9a4511b5009'; /// See also [Connect]. @ProviderFor(Connect) diff --git a/lib/screens/onboarding/ui/connect.dart b/lib/screens/onboarding/ui/connect.dart new file mode 100644 index 0000000..52da342 --- /dev/null +++ b/lib/screens/onboarding/ui/connect.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:segmented_button_slide/segmented_button_slide.dart'; + +import 'package:my_linkding/screens/onboarding/ui/sliver_top_bar.dart'; +import 'package:my_linkding/screens/onboarding/provider/connect.provider.dart'; +import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart'; +import 'package:my_linkding/widgets/section_label.dart'; + +import 'package:my_linkding/utils/open_url.dart'; +import 'package:my_linkding/constants/enums.dart'; +import 'package:my_linkding/i18n/strings.g.dart'; + +class OnboardingConnect extends ConsumerWidget { + const OnboardingConnect({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CustomScrollView( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: OnboardingSliverTopBar( + icon: Icons.login_rounded, + title: t.onboarding.createConnection, + ), + ), + SliverList.list( + children: const [ + _ConnectForm(), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FilledButton( + onPressed: () => ref.read(onboardingProvider.notifier).previousPage(), + child: Row( + children: [ + const Icon(Icons.arrow_back_rounded), + const SizedBox(width: 8), + Text(t.onboarding.previous), + ], + ), + ), + FilledButton.icon( + onPressed: ref.watch(connectProvider).validValues ? () => ref.read(connectToServerProvider) : null, + icon: const Icon(Icons.login_rounded), + label: Text(t.onboarding.connect), + ), + ], + ), + ], + ), + ); + } +} + +class _ConnectForm extends ConsumerWidget { + const _ConnectForm({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = ref.watch(connectProvider); + + final connectionMethod = ConnectionMethod.values[provider.method]; + + final connectionString = + "${connectionMethod.name}://${provider.ipDomainController.text}${provider.pathController.text != '' ? ':${provider.portController.text}' : ""}${provider.pathController.text}"; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + child: Text( + t.onboarding.createConnectionSubtitle, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + margin: const EdgeInsets.only(top: 30, left: 24, right: 24), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.05), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Theme.of(context).colorScheme.primary), + ), + child: Text( + connectionString, + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w500), + ), + ), + SectionLabel( + label: t.onboarding.serverDetails, + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 24), + ), + SegmentedButtonSlide( + entries: const [ + SegmentedButtonSlideEntry(label: "HTTP"), + SegmentedButtonSlideEntry(label: "HTTPS"), + ], + selectedEntry: provider.method, + onChange: ref.read(connectProvider.notifier).setConnectionMethod, + colors: SegmentedButtonSlideColors( + barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), + backgroundSelectedColor: Theme.of(context).colorScheme.primary, + foregroundSelectedColor: Theme.of(context).colorScheme.onPrimary, + foregroundUnselectedColor: Theme.of(context).colorScheme.onSurface, + hoverColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textOverflow: TextOverflow.ellipsis, + fontSize: 14, + height: 40, + margin: const EdgeInsets.symmetric( + horizontal: 24, + ), + ), + const SizedBox(height: 30), + TextFormField( + controller: provider.ipDomainController, + onChanged: ref.read(connectProvider.notifier).validateIpDomain, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + errorText: provider.ipDomainError, + labelText: t.onboarding.ipAddressOrDomain, + helperText: t.onboarding.required, + ), + ), + const SizedBox(height: 24), + TextFormField( + controller: provider.portController, + onChanged: ref.read(connectProvider.notifier).validatePort, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.tag_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + errorText: provider.portError, + labelText: t.onboarding.port, + ), + ), + SectionLabel( + label: t.onboarding.authentication, + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 24), + ), + TextFormField( + controller: provider.tokenController, + onChanged: ref.read(connectProvider.notifier).validateToken, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.key_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + errorText: provider.tokenError, + labelText: t.onboarding.token, + helperText: t.onboarding.required, + ), + obscureText: true, + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: ref.watch(connectProvider).validValues == true ? () => openUrl(connectionString) : null, + icon: const Icon(Icons.open_in_browser_rounded), + label: Text(t.onboarding.testConnectionUrl), + ), + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/screens/onboarding/ui/onboarding.dart b/lib/screens/onboarding/ui/onboarding.dart index efce192..29a35b6 100644 --- a/lib/screens/onboarding/ui/onboarding.dart +++ b/lib/screens/onboarding/ui/onboarding.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart'; +import 'package:my_linkding/screens/onboarding/ui/connect.dart'; import 'package:my_linkding/screens/onboarding/ui/server_required.dart'; import 'package:my_linkding/screens/onboarding/ui/welcome.dart'; import 'package:my_linkding/widgets/system_overlay_style.dart'; @@ -20,6 +21,7 @@ class Onboarding extends ConsumerWidget { children: const [ OnboardingWelcome(), OnboardingServerRequired(), + OnboardingConnect(), ], ), ), diff --git a/lib/screens/onboarding/ui/server_required.dart b/lib/screens/onboarding/ui/server_required.dart index f51d863..08e3060 100644 --- a/lib/screens/onboarding/ui/server_required.dart +++ b/lib/screens/onboarding/ui/server_required.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart'; +import 'package:my_linkding/screens/onboarding/ui/sliver_top_bar.dart'; import 'package:my_linkding/constants/urls.dart'; import 'package:my_linkding/i18n/strings.g.dart'; @@ -22,9 +23,12 @@ class OnboardingServerRequired extends ConsumerWidget { Expanded( child: CustomScrollView( slivers: [ - const SliverPersistentHeader( + SliverPersistentHeader( pinned: true, - delegate: _Header(), + delegate: OnboardingSliverTopBar( + icon: Icons.dns_rounded, + title: t.onboarding.serverRequired, + ), ), SliverList.list( children: [ @@ -120,69 +124,3 @@ class OnboardingServerRequired extends ConsumerWidget { ); } } - -const _minExent = 50.0; -const _maxExent = 130.0; - -const _iconMaxSize = 50; -const _iconMinSize = 30; - -const _textMaxSize = 28; -const _textMinSize = 22; - -const _textMaxTopPosition = 70; -const _textMinTopPosition = 0; - -const _textMaxLeftPosition = 0; -const _textMinLeftPosition = 44; - -class _Header extends SliverPersistentHeaderDelegate { - const _Header(); - - @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { - final iconPercentage = shrinkOffset.clamp(0, _maxExent - _minExent) / (_maxExent - _minExent); - final textPercentage = shrinkOffset.clamp(0, _maxExent - _minExent) / (_maxExent - _minExent); - - final iconSize = _iconMinSize + (_iconMaxSize - _iconMinSize) * (1 - iconPercentage); - final textSize = _textMinSize + (_textMaxSize - _textMinSize) * (1 - textPercentage); - final textTopPosition = _textMinTopPosition + (_textMaxTopPosition - _textMinTopPosition) * (1 - textPercentage); - final textLeftPosition = - _textMinLeftPosition + (_textMaxLeftPosition - _textMinLeftPosition) * (1 - textPercentage); - - return Container( - color: Theme.of(context).colorScheme.surface, - child: Stack( - children: [ - Positioned( - top: 0, - child: Icon( - Icons.dns_rounded, - size: iconSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - Positioned( - top: textTopPosition, - left: textLeftPosition, - child: Text( - t.onboarding.serverRequired, - style: TextStyle(fontSize: textSize), - ), - ), - ], - ), - ); - } - - @override - double get maxExtent => _maxExent; - - @override - double get minExtent => _minExent; - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { - return false; - } -} diff --git a/lib/screens/onboarding/ui/sliver_top_bar.dart b/lib/screens/onboarding/ui/sliver_top_bar.dart new file mode 100644 index 0000000..b4fa78d --- /dev/null +++ b/lib/screens/onboarding/ui/sliver_top_bar.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +const _minExent = 50.0; +const _maxExent = 130.0; + +const _iconMaxSize = 50; +const _iconMinSize = 30; + +const _textMaxSize = 28; +const _textMinSize = 22; + +const _textMaxTopPosition = 70; +const _textMinTopPosition = 0; + +const _textMaxLeftPosition = 0; +const _textMinLeftPosition = 44; + +class OnboardingSliverTopBar extends SliverPersistentHeaderDelegate { + final IconData icon; + final String title; + + const OnboardingSliverTopBar({ + required this.icon, + required this.title, + }); + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + final iconPercentage = shrinkOffset.clamp(0, _maxExent - _minExent) / (_maxExent - _minExent); + final textPercentage = shrinkOffset.clamp(0, _maxExent - _minExent) / (_maxExent - _minExent); + + final iconSize = _iconMinSize + (_iconMaxSize - _iconMinSize) * (1 - iconPercentage); + final textSize = _textMinSize + (_textMaxSize - _textMinSize) * (1 - textPercentage); + final textTopPosition = _textMinTopPosition + (_textMaxTopPosition - _textMinTopPosition) * (1 - textPercentage); + final textLeftPosition = + _textMinLeftPosition + (_textMaxLeftPosition - _textMinLeftPosition) * (1 - textPercentage); + + return Container( + color: Theme.of(context).colorScheme.surface, + child: Stack( + children: [ + Positioned( + top: 0, + child: Icon( + icon, + size: iconSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + Positioned( + top: textTopPosition, + left: textLeftPosition, + child: Text( + title, + style: TextStyle(fontSize: textSize), + ), + ), + ], + ), + ); + } + + @override + double get maxExtent => _maxExent; + + @override + double get minExtent => _minExent; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return false; + } +} diff --git a/lib/screens/onboarding/ui/welcome.dart b/lib/screens/onboarding/ui/welcome.dart index 74370fa..4a5ceb9 100644 --- a/lib/screens/onboarding/ui/welcome.dart +++ b/lib/screens/onboarding/ui/welcome.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:my_linkding/i18n/strings.g.dart'; + import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart'; class OnboardingWelcome extends ConsumerWidget { diff --git a/lib/widgets/section_label.dart b/lib/widgets/section_label.dart new file mode 100644 index 0000000..7c0f285 --- /dev/null +++ b/lib/widgets/section_label.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class SectionLabel extends StatelessWidget { + final String label; + final EdgeInsets? padding; + + const SectionLabel({ + Key? key, + required this.label, + this.padding, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: padding ?? const EdgeInsets.all(16), + child: Text( + label, + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16, color: Theme.of(context).colorScheme.primary), + ), + ), + ], + ); + } +}