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

@@ -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 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<String?>? pathError,
TextEditingController? tokenController,
Wrapped<String?>? 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,
);
}

View File

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

View File

@@ -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<bool>.internal(
);
typedef ConnectToServerRef = AutoDisposeFutureProviderRef<bool>;
String _$connectHash() => r'bbdae015118381c538b7a3c15ca8345d30a28995';
String _$connectHash() => r'2c2e4fff717252259381056b4767b9a4511b5009';
/// See also [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: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(),
],
),
),

View File

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

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_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 {