Improved connection form

This commit is contained in:
Juan Gilsanz Polo
2024-02-22 11:54:44 +01:00
parent fb35943d12
commit be7a2bcf4b
13 changed files with 385 additions and 231 deletions

View File

@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 2 /// 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 // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
@@ -149,7 +149,6 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
// Translations // Translations
late final _StringsOnboardingEn onboarding = _StringsOnboardingEn._(_root); late final _StringsOnboardingEn onboarding = _StringsOnboardingEn._(_root);
late final _StringsConnectEn connect = _StringsConnectEn._(_root);
late final _StringsLinksEn links = _StringsLinksEn._(_root); late final _StringsLinksEn links = _StringsLinksEn._(_root);
late final _StringsSettingsEn settings = _StringsSettingsEn._(_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 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 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.'; String get serverRunningConfirmation => 'I confirm that I have an instance of the Linkding server already running.';
} String get createConnection => 'Create a connection';
String get createConnectionSubtitle => 'Enter all the required details to create a connection to your server.';
// Path: connect String get ipAddressOrDomain => 'IP address or domain';
class _StringsConnectEn { String get port => 'Port';
_StringsConnectEn._(this._root); String get token => 'Token';
String get required => 'Required';
final Translations _root; // ignore: unused_field String get serverDetails => 'Server details';
String get authentication => 'Authentication';
// Translations String get testConnectionUrl => 'Test connection url';
String get connect => 'Connect'; String get connect => 'Connect';
String get connecting => 'Connecting...'; String get connecting => 'Connecting...';
String get cannotConnectToServer => 'Cannot connect to the server.'; String get cannotConnectToServer => 'Cannot connect to the server.';
@@ -230,7 +229,6 @@ class _StringsEs implements Translations {
// Translations // Translations
@override late final _StringsOnboardingEs onboarding = _StringsOnboardingEs._(_root); @override late final _StringsOnboardingEs onboarding = _StringsOnboardingEs._(_root);
@override late final _StringsConnectEs connect = _StringsConnectEs._(_root);
@override late final _StringsLinksEs links = _StringsLinksEs._(_root); @override late final _StringsLinksEs links = _StringsLinksEs._(_root);
@override late final _StringsSettingsEs settings = _StringsSettingsEs._(_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 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 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.'; @override String get serverRunningConfirmation => 'Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.';
} @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';
// Path: connect @override String get ipAddressOrDomain => 'Dirección IP o dominio';
class _StringsConnectEs implements _StringsConnectEn { @override String get port => 'Puerto';
_StringsConnectEs._(this._root); @override String get token => 'Token';
@override String get required => 'Requerido';
@override final _StringsEs _root; // ignore: unused_field @override String get serverDetails => 'Detalles del servidor';
@override String get authentication => 'Autenticación';
// Translations @override String get testConnectionUrl => 'Probar URL de conexión';
@override String get connect => 'Conectar'; @override String get connect => 'Conectar';
@override String get connecting => 'Conectando...'; @override String get connecting => 'Conectando...';
@override String get cannotConnectToServer => 'No se puede conectar con el servidor.'; @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.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.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 'onboarding.serverRunningConfirmation': return 'I confirm that I have an instance of the Linkding server already running.';
case 'connect.connect': return 'Connect'; case 'onboarding.createConnection': return 'Create a connection';
case 'connect.connecting': return 'Connecting...'; case 'onboarding.createConnectionSubtitle': return 'Enter all the required details to create a connection to your server.';
case 'connect.cannotConnectToServer': return 'Cannot connect to the 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 'links.links': return 'Links';
case 'settings.settings': return 'Settings'; case 'settings.settings': return 'Settings';
case 'settings.disconnectFromServer': return 'Disconnect from server'; 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.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.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 'onboarding.serverRunningConfirmation': return 'Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.';
case 'connect.connect': return 'Conectar'; case 'onboarding.createConnection': return 'Crear una conexión';
case 'connect.connecting': return 'Conectando...'; case 'onboarding.createConnectionSubtitle': return 'Introduce todos los detalles requeridos para crear una conexión con el servidor';
case 'connect.cannotConnectToServer': return 'No se puede conectar 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 'links.links': return 'Enlaces';
case 'settings.settings': return 'Ajustes'; case 'settings.settings': return 'Ajustes';
case 'settings.disconnectFromServer': return 'Desconectar del servidor'; case 'settings.disconnectFromServer': return 'Desconectar del servidor';

View File

@@ -8,9 +8,16 @@
"serverRequired": "Servidor requerido", "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.", "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.", "installationInstructions": "Check the installation instructions on the official GitHub repository.",
"serverRunningConfirmation": "I confirm that I have an instance of the Linkding server already running." "serverRunningConfirmation": "I confirm that I have an instance of the Linkding server already running.",
}, "createConnection": "Create a connection",
"connect": { "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", "connect": "Connect",
"connecting": "Connecting...", "connecting": "Connecting...",
"cannotConnectToServer": "Cannot connect to the server." "cannotConnectToServer": "Cannot connect to the server."

View File

@@ -8,9 +8,16 @@
"serverRequired": "Servidor requerido", "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.", "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.", "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." "serverRunningConfirmation": "Confirmo que tengo una instancia del servidor Linkding ya en funcionamiento.",
}, "createConnection": "Crear una conexión",
"connect": { "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", "connect": "Conectar",
"connecting": "Conectando...", "connecting": "Conectando...",
"cannotConnectToServer": "No se puede conectar con el servidor." "cannotConnectToServer": "No se puede conectar con el servidor."

View File

@@ -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"),
),
],
),
],
),
),
),
);
}
}

View File

@@ -12,6 +12,7 @@ class ConnectModel {
final String? pathError; final String? pathError;
final TextEditingController tokenController; final TextEditingController tokenController;
final String? tokenError; final String? tokenError;
final bool validValues;
ConnectModel({ ConnectModel({
required this.isConnecting, required this.isConnecting,
@@ -24,6 +25,7 @@ class ConnectModel {
required this.pathError, required this.pathError,
required this.tokenController, required this.tokenController,
required this.tokenError, required this.tokenError,
required this.validValues,
}); });
ConnectModel copyWidth({ ConnectModel copyWidth({
@@ -37,6 +39,7 @@ class ConnectModel {
Wrapped<String?>? pathError, Wrapped<String?>? pathError,
TextEditingController? tokenController, TextEditingController? tokenController,
Wrapped<String?>? tokenError, Wrapped<String?>? tokenError,
bool? validValues,
}) => }) =>
ConnectModel( ConnectModel(
isConnecting: isConnecting ?? this.isConnecting, isConnecting: isConnecting ?? this.isConnecting,
@@ -49,5 +52,6 @@ class ConnectModel {
pathError: pathError != null ? pathError.value : this.pathError, pathError: pathError != null ? pathError.value : this.pathError,
tokenController: tokenController ?? this.tokenController, tokenController: tokenController ?? this.tokenController,
tokenError: tokenError != null ? tokenError.value : this.tokenError, tokenError: tokenError != null ? tokenError.value : this.tokenError,
validValues: validValues ?? this.validValues,
); );
} }

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:riverpod_annotation/riverpod_annotation.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/providers/server_instances_provider.dart';
import 'package:my_linkding/i18n/strings.g.dart'; import 'package:my_linkding/i18n/strings.g.dart';
@@ -32,6 +32,7 @@ class Connect extends _$Connect {
pathError: null, pathError: null,
tokenController: TextEditingController(), tokenController: TextEditingController(),
tokenError: null, tokenError: null,
validValues: false,
); );
} }
@@ -45,6 +46,7 @@ class Connect extends _$Connect {
} else { } else {
state = state.copyWidth(ipDomainError: const Wrapped.value("Error")); state = state.copyWidth(ipDomainError: const Wrapped.value("Error"));
} }
state = state.copyWidth(validValues: validValues());
} }
void validatePort(value) { void validatePort(value) {
@@ -55,6 +57,7 @@ class Connect extends _$Connect {
state = state.copyWidth(portError: const Wrapped.value("Invalid port")); state = state.copyWidth(portError: const Wrapped.value("Invalid port"));
} }
} }
state = state.copyWidth(validValues: validValues());
} }
void validateToken(String value) { void validateToken(String value) {
@@ -63,6 +66,7 @@ class Connect extends _$Connect {
} else { } else {
state = state.copyWidth(tokenError: const Wrapped.value("Error")); state = state.copyWidth(tokenError: const Wrapped.value("Error"));
} }
state = state.copyWidth(validValues: validValues());
} }
bool validValues() { bool validValues() {
@@ -87,7 +91,7 @@ FutureOr<bool> connectToServer(ConnectToServerRef ref) async {
final apiClient = ApiClient(serverInstance: serverInstance); final apiClient = ApiClient(serverInstance: serverInstance);
final processModal = ProcessModal(); final processModal = ProcessModal();
processModal.open(t.connect.connecting); processModal.open(t.onboarding.connecting);
final result = await apiClient.checkConnectionInstance(); final result = await apiClient.checkConnectionInstance();
@@ -97,7 +101,7 @@ FutureOr<bool> connectToServer(ConnectToServerRef ref) async {
ref.read(serverInstancesProvider.notifier).saveNewInstance(serverInstance); ref.read(serverInstancesProvider.notifier).saveNewInstance(serverInstance);
// ref.watch(routerProvider).replace(RoutesPaths.links); // ref.watch(routerProvider).replace(RoutesPaths.links);
} else { } else {
showSnacbkar(label: t.connect.cannotConnectToServer, color: Colors.red); showSnacbkar(label: t.onboarding.cannotConnectToServer, color: Colors.red);
} }
return false; return false;

View File

@@ -6,7 +6,7 @@ part of 'connect.provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$connectToServerHash() => r'c40898fd5dd6dd7507140c7a69a6f336a56e30c0'; String _$connectToServerHash() => r'85925cb547fccb708e657d263e39a46867ac2bca';
/// See also [connectToServer]. /// See also [connectToServer].
@ProviderFor(connectToServer) @ProviderFor(connectToServer)
@@ -21,7 +21,7 @@ final connectToServerProvider = AutoDisposeFutureProvider<bool>.internal(
); );
typedef ConnectToServerRef = AutoDisposeFutureProviderRef<bool>; typedef ConnectToServerRef = AutoDisposeFutureProviderRef<bool>;
String _$connectHash() => r'bbdae015118381c538b7a3c15ca8345d30a28995'; String _$connectHash() => r'2c2e4fff717252259381056b4767b9a4511b5009';
/// See also [Connect]. /// See also [Connect].
@ProviderFor(Connect) @ProviderFor(Connect)

View File

@@ -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),
],
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.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/server_required.dart';
import 'package:my_linkding/screens/onboarding/ui/welcome.dart'; import 'package:my_linkding/screens/onboarding/ui/welcome.dart';
import 'package:my_linkding/widgets/system_overlay_style.dart'; import 'package:my_linkding/widgets/system_overlay_style.dart';
@@ -20,6 +21,7 @@ class Onboarding extends ConsumerWidget {
children: const [ children: const [
OnboardingWelcome(), OnboardingWelcome(),
OnboardingServerRequired(), OnboardingServerRequired(),
OnboardingConnect(),
], ],
), ),
), ),

View File

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.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/constants/urls.dart';
import 'package:my_linkding/i18n/strings.g.dart'; import 'package:my_linkding/i18n/strings.g.dart';
@@ -22,9 +23,12 @@ class OnboardingServerRequired extends ConsumerWidget {
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
const SliverPersistentHeader( SliverPersistentHeader(
pinned: true, pinned: true,
delegate: _Header(), delegate: OnboardingSliverTopBar(
icon: Icons.dns_rounded,
title: t.onboarding.serverRequired,
),
), ),
SliverList.list( SliverList.list(
children: [ 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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:my_linkding/i18n/strings.g.dart'; import 'package:my_linkding/i18n/strings.g.dart';
import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart'; import 'package:my_linkding/screens/onboarding/provider/onboarding.provider.dart';
class OnboardingWelcome extends ConsumerWidget { class OnboardingWelcome extends ConsumerWidget {

View File

@@ -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),
),
),
],
);
}
}