Created project base structure, state management, router and login

This commit is contained in:
Juan Gilsanz Polo
2024-02-21 23:53:22 +01:00
commit 460e13b2e9
168 changed files with 6966 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
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;
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,
});
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,
}) =>
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,
);
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:my_linkding/providers/shared_preferences_provider.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/providers/server_instances_provider.dart';
import 'package:my_linkding/constants/enums.dart';
import 'package:my_linkding/utils/process_modal.dart';
import 'package:my_linkding/providers/router_provider.dart';
import 'package:my_linkding/router/paths.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,
);
}
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"));
}
}
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"));
}
}
}
void validateToken(String value) {
if (value != "") {
state = state.copyWidth(tokenError: const Wrapped.value(null));
} else {
state = state.copyWidth(tokenError: const Wrapped.value("Error"));
}
}
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("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: "Invalida auth", color: Colors.red);
}
return false;
}

View File

@@ -0,0 +1,40 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'connect_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$connectToServerHash() => r'af66fee9a4bf26aba08b27ff5a7fe4643215117a';
/// 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'bbdae015118381c538b7a3c15ca8345d30a28995';
/// 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

View File

@@ -0,0 +1,124 @@
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

@@ -0,0 +1,9 @@
import 'package:my_linkding/constants/enums.dart';
class LinksModel {
final LoadStatus loadStatus;
LinksModel({
required this.loadStatus,
});
}

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
class Links extends StatelessWidget {
const Links({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
title: Text("Links"),
),
),
],
body: SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverList.builder(
itemCount: 40,
itemBuilder: (context, index) => ListTile(
title: Text(
"This is the title",
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurface,
),
),
subtitle: Column(
children: [
Text(
"All the speed he took, all the turns hed taken and the dripping chassis of a skyscraper canyon. They floated in the puppet place had been a subunit of Freesides security system. The knives seemed to move of their own accord, gliding with a ritual lack of urgency through the center of his closed left eyelid. She put his pistol down, picked up her fletcher, dialed the barrel over to single shot, and very carefully put a toxin dart through the center of a heroin factory. Still it was a square of faint light. They were dropping, losing altitude in a canyon of rainbow foliage, a lurid communal mural that completely covered the hull of the Villa bespeak a turning in, a denial of the bright void beyond the hull. A narrow wedge of light from a half-open service hatch at the clinic, Molly took him to the simple Chinese hollow points Shin had sold him. Still it was a steady pulse of pain midway down his spine. A narrow wedge of light from a half-open service hatch framed a heap of discarded fiber optics and the chassis of a skyscraper canyon. The Tessier-Ashpool ice shattered, peeling away from the banks of every computer in the center of his closed left eyelid.",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Text(
"#flutter #apps",
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
Container(
width: 1,
height: 12,
margin: const EdgeInsets.symmetric(horizontal: 8),
color: Theme.of(context).dividerColor,
),
Text(
"1 week ago",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class Search extends StatelessWidget {
const Search({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class Settings extends StatelessWidget {
const Settings({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
title: Text("Settings"),
),
),
],
body: SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
],
),
),
),
),
);
}
}