Improved connection form
This commit is contained in:
57
lib/screens/onboarding/model/connect.model.dart
Normal file
57
lib/screens/onboarding/model/connect.model.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:my_linkding/helpers/wrapped_class.dart';
|
||||
|
||||
class ConnectModel {
|
||||
final bool isConnecting;
|
||||
final int method;
|
||||
final TextEditingController ipDomainController;
|
||||
final String? ipDomainError;
|
||||
final TextEditingController portController;
|
||||
final String? portError;
|
||||
final TextEditingController pathController;
|
||||
final String? pathError;
|
||||
final TextEditingController tokenController;
|
||||
final String? tokenError;
|
||||
final bool validValues;
|
||||
|
||||
ConnectModel({
|
||||
required this.isConnecting,
|
||||
required this.method,
|
||||
required this.ipDomainController,
|
||||
required this.ipDomainError,
|
||||
required this.portController,
|
||||
required this.portError,
|
||||
required this.pathController,
|
||||
required this.pathError,
|
||||
required this.tokenController,
|
||||
required this.tokenError,
|
||||
required this.validValues,
|
||||
});
|
||||
|
||||
ConnectModel copyWidth({
|
||||
bool? isConnecting,
|
||||
int? method,
|
||||
TextEditingController? ipDomainController,
|
||||
Wrapped<String?>? ipDomainError,
|
||||
TextEditingController? portController,
|
||||
Wrapped<String?>? portError,
|
||||
TextEditingController? pathController,
|
||||
Wrapped<String?>? pathError,
|
||||
TextEditingController? tokenController,
|
||||
Wrapped<String?>? tokenError,
|
||||
bool? validValues,
|
||||
}) =>
|
||||
ConnectModel(
|
||||
isConnecting: isConnecting ?? this.isConnecting,
|
||||
method: method ?? this.method,
|
||||
ipDomainController: ipDomainController ?? this.ipDomainController,
|
||||
ipDomainError: ipDomainError != null ? ipDomainError.value : this.ipDomainError,
|
||||
portController: portController ?? this.portController,
|
||||
portError: portError != null ? portError.value : this.portError,
|
||||
pathController: pathController ?? this.pathController,
|
||||
pathError: pathError != null ? pathError.value : this.pathError,
|
||||
tokenController: tokenController ?? this.tokenController,
|
||||
tokenError: tokenError != null ? tokenError.value : this.tokenError,
|
||||
validValues: validValues ?? this.validValues,
|
||||
);
|
||||
}
|
||||
108
lib/screens/onboarding/provider/connect.provider.dart
Normal file
108
lib/screens/onboarding/provider/connect.provider.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.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';
|
||||
import 'package:my_linkding/constants/enums.dart';
|
||||
import 'package:my_linkding/utils/process_modal.dart';
|
||||
import 'package:my_linkding/utils/snackbar.dart';
|
||||
import 'package:my_linkding/models/server_instance.dart';
|
||||
import 'package:my_linkding/providers/api_client_provider.dart';
|
||||
import 'package:my_linkding/services/api_client.dart';
|
||||
import 'package:my_linkding/constants/regexp.dart';
|
||||
import 'package:my_linkding/helpers/wrapped_class.dart';
|
||||
|
||||
part 'connect.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class Connect extends _$Connect {
|
||||
@override
|
||||
ConnectModel build() {
|
||||
return ConnectModel(
|
||||
isConnecting: false,
|
||||
method: 0,
|
||||
ipDomainController: TextEditingController(),
|
||||
ipDomainError: null,
|
||||
portController: TextEditingController(),
|
||||
portError: null,
|
||||
pathController: TextEditingController(),
|
||||
pathError: null,
|
||||
tokenController: TextEditingController(),
|
||||
tokenError: null,
|
||||
validValues: false,
|
||||
);
|
||||
}
|
||||
|
||||
void setConnectionMethod(int method) {
|
||||
state = state.copyWidth(method: method);
|
||||
}
|
||||
|
||||
void validateIpDomain(String value) {
|
||||
if (Regexps.ipAddress.hasMatch(value) || Regexps.domain.hasMatch(value)) {
|
||||
state = state.copyWidth(ipDomainError: const Wrapped.value(null));
|
||||
} else {
|
||||
state = state.copyWidth(ipDomainError: const Wrapped.value("Error"));
|
||||
}
|
||||
state = state.copyWidth(validValues: validValues());
|
||||
}
|
||||
|
||||
void validatePort(value) {
|
||||
if (value != null && value != '') {
|
||||
if (int.tryParse(value) != null && int.parse(value) <= 65535) {
|
||||
state = state.copyWidth(portError: const Wrapped.value(null));
|
||||
} else {
|
||||
state = state.copyWidth(portError: const Wrapped.value("Invalid port"));
|
||||
}
|
||||
}
|
||||
state = state.copyWidth(validValues: validValues());
|
||||
}
|
||||
|
||||
void validateToken(String value) {
|
||||
if (value != "") {
|
||||
state = state.copyWidth(tokenError: const Wrapped.value(null));
|
||||
} else {
|
||||
state = state.copyWidth(tokenError: const Wrapped.value("Error"));
|
||||
}
|
||||
state = state.copyWidth(validValues: validValues());
|
||||
}
|
||||
|
||||
bool validValues() {
|
||||
return state.tokenController.text != "" &&
|
||||
state.tokenError == null &&
|
||||
state.ipDomainController.text != "" &&
|
||||
state.ipDomainError == null;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<bool> connectToServer(ConnectToServerRef ref) async {
|
||||
const uuid = Uuid();
|
||||
|
||||
final serverInstance = ServerInstance(
|
||||
id: uuid.v4(),
|
||||
method: ConnectionMethod.values[ref.watch(connectProvider).method].name,
|
||||
ipDomain: ref.watch(connectProvider).ipDomainController.text,
|
||||
token: ref.watch(connectProvider).tokenController.text,
|
||||
);
|
||||
|
||||
final apiClient = ApiClient(serverInstance: serverInstance);
|
||||
|
||||
final processModal = ProcessModal();
|
||||
processModal.open(t.onboarding.connecting);
|
||||
|
||||
final result = await apiClient.checkConnectionInstance();
|
||||
|
||||
processModal.close();
|
||||
if (result == true) {
|
||||
ref.read(apiClientProviderProvider.notifier).setApiClient(apiClient);
|
||||
ref.read(serverInstancesProvider.notifier).saveNewInstance(serverInstance);
|
||||
// ref.watch(routerProvider).replace(RoutesPaths.links);
|
||||
} else {
|
||||
showSnacbkar(label: t.onboarding.cannotConnectToServer, color: Colors.red);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
40
lib/screens/onboarding/provider/connect.provider.g.dart
Normal file
40
lib/screens/onboarding/provider/connect.provider.g.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'connect.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$connectToServerHash() => r'85925cb547fccb708e657d263e39a46867ac2bca';
|
||||
|
||||
/// See also [connectToServer].
|
||||
@ProviderFor(connectToServer)
|
||||
final connectToServerProvider = AutoDisposeFutureProvider<bool>.internal(
|
||||
connectToServer,
|
||||
name: r'connectToServerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$connectToServerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ConnectToServerRef = AutoDisposeFutureProviderRef<bool>;
|
||||
String _$connectHash() => r'2c2e4fff717252259381056b4767b9a4511b5009';
|
||||
|
||||
/// See also [Connect].
|
||||
@ProviderFor(Connect)
|
||||
final connectProvider =
|
||||
AutoDisposeNotifierProvider<Connect, ConnectModel>.internal(
|
||||
Connect.new,
|
||||
name: r'connectProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$connectHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Connect = AutoDisposeNotifier<ConnectModel>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
198
lib/screens/onboarding/ui/connect.dart
Normal file
198
lib/screens/onboarding/ui/connect.dart
Normal 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
73
lib/screens/onboarding/ui/sliver_top_bar.dart
Normal file
73
lib/screens/onboarding/ui/sliver_top_bar.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user