Created project base structure, state management, router and login
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
class SharedPreferencesKeys {
|
||||
static const serversInstances = "serversInstances";
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ThemeData lightTheme(ColorScheme? dynamicColorScheme) => ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: dynamicColorScheme,
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
brightness: Brightness.light,
|
||||
listTileTheme: ListTileThemeData(
|
||||
tileColor: Colors.transparent,
|
||||
textColor: dynamicColorScheme != null
|
||||
? dynamicColorScheme.onSurfaceVariant
|
||||
: const Color.fromRGBO(117, 117, 117, 1),
|
||||
iconColor: dynamicColorScheme != null
|
||||
? dynamicColorScheme.onSurfaceVariant
|
||||
: const Color.fromRGBO(117, 117, 117, 1),
|
||||
),
|
||||
);
|
||||
|
||||
ThemeData darkTheme(ColorScheme? dynamicColorScheme) => ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: dynamicColorScheme,
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
brightness: Brightness.dark,
|
||||
listTileTheme: ListTileThemeData(
|
||||
tileColor: Colors.transparent,
|
||||
textColor: dynamicColorScheme != null
|
||||
? dynamicColorScheme.onSurfaceVariant
|
||||
: const Color.fromRGBO(187, 187, 187, 1),
|
||||
iconColor: dynamicColorScheme != null
|
||||
? dynamicColorScheme.onSurfaceVariant
|
||||
: const Color.fromRGBO(187, 187, 187, 1),
|
||||
),
|
||||
);
|
||||
|
||||
ThemeData lightThemeOldVersions(MaterialColor primaryColor) => ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: primaryColor,
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||
elevation: 4,
|
||||
),
|
||||
listTileTheme: const ListTileThemeData(
|
||||
tileColor: Colors.transparent,
|
||||
textColor: Color.fromRGBO(117, 117, 117, 1),
|
||||
iconColor: Color.fromRGBO(117, 117, 117, 1),
|
||||
),
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
|
||||
ThemeData darkThemeOldVersions(MaterialColor primaryColor) => ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: primaryColor,
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
contentTextStyle: const TextStyle(color: Colors.white),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||
elevation: 4,
|
||||
),
|
||||
listTileTheme: const ListTileThemeData(
|
||||
tileColor: Colors.transparent,
|
||||
textColor: Color.fromRGBO(187, 187, 187, 1),
|
||||
iconColor: Color.fromRGBO(187, 187, 187, 1),
|
||||
),
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:my_linkding/constants/linkding_color.dart';
|
||||
|
||||
const List<MaterialColor> colors = [
|
||||
linkdingColor,
|
||||
Colors.red,
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.yellow,
|
||||
Colors.orange,
|
||||
Colors.brown,
|
||||
Colors.cyan,
|
||||
Colors.purple,
|
||||
Colors.pink,
|
||||
Colors.deepOrange,
|
||||
Colors.indigo,
|
||||
];
|
||||
@@ -0,0 +1,3 @@
|
||||
enum LoadStatus { loading, loaded, error }
|
||||
|
||||
enum ConnectionMethod { http, https }
|
||||
@@ -0,0 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final scaffoldMessengerGlobalKey = GlobalKey<ScaffoldMessengerState>();
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const Map<int, Color> swatch = {
|
||||
50: Color.fromRGBO(87, 85, 217, .1),
|
||||
100: Color.fromRGBO(87, 85, 217, .2),
|
||||
200: Color.fromRGBO(87, 85, 217, .3),
|
||||
300: Color.fromRGBO(87, 85, 217, .4),
|
||||
400: Color.fromRGBO(87, 85, 217, .5),
|
||||
500: Color.fromRGBO(87, 85, 217, .6),
|
||||
600: Color.fromRGBO(87, 85, 217, .7),
|
||||
700: Color.fromRGBO(87, 85, 217, .8),
|
||||
800: Color.fromRGBO(87, 85, 217, .9),
|
||||
900: Color.fromRGBO(87, 85, 217, 1),
|
||||
};
|
||||
|
||||
const MaterialColor linkdingColor = MaterialColor(0xff5755d9, swatch);
|
||||
@@ -0,0 +1,6 @@
|
||||
class Regexps {
|
||||
static final url =
|
||||
RegExp(r'/https?:\/\/(?:www\.)?([-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b)*(\/[\/\d\w\.-]*)*(?:[\?])*(.+)*/gi');
|
||||
static final ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
|
||||
static final domain = RegExp(r'^(([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+)$');
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
class Wrapped<T> {
|
||||
final T value;
|
||||
const Wrapped.value(this.value);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:my_linkding/constants/global_keys.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
|
||||
import 'package:my_linkding/config/theme.dart';
|
||||
import 'package:my_linkding/constants/colors.dart';
|
||||
import 'package:my_linkding/providers/router_provider.dart';
|
||||
import 'package:my_linkding/providers/shared_preferences_provider.dart';
|
||||
import 'package:my_linkding/utils/http_overrides.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) => MaterialApp.router(
|
||||
title: 'My Linkding',
|
||||
theme: lightDynamic != null ? lightTheme(lightDynamic) : lightThemeOldVersions(colors[0]),
|
||||
darkTheme: darkDynamic != null ? darkTheme(darkDynamic) : darkThemeOldVersions(colors[0]),
|
||||
debugShowCheckedModeBanner: false,
|
||||
scaffoldMessengerKey: scaffoldMessengerGlobalKey,
|
||||
routerConfig: ref.watch(routerProvider),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
class ApiResponse {
|
||||
final bool successful;
|
||||
final dynamic content;
|
||||
final int? statusCode;
|
||||
|
||||
const ApiResponse({
|
||||
required this.successful,
|
||||
this.content,
|
||||
this.statusCode,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:my_linkding/models/server_instance.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppStatus {
|
||||
AppStatus();
|
||||
|
||||
AppStatus copyWith({
|
||||
SharedPreferences? sharedPreferences,
|
||||
ServerInstance? serverInstance,
|
||||
}) =>
|
||||
AppStatus();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
class ServerInstance {
|
||||
final String id;
|
||||
final String method;
|
||||
final String ipDomain;
|
||||
final int? port;
|
||||
final String? path;
|
||||
final String token;
|
||||
|
||||
const ServerInstance({
|
||||
required this.id,
|
||||
required this.method,
|
||||
required this.ipDomain,
|
||||
this.port,
|
||||
this.path,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
factory ServerInstance.fromJson(Map<String, dynamic> data) => ServerInstance(
|
||||
id: data['id'],
|
||||
method: data['method'],
|
||||
ipDomain: data['ipDomain'],
|
||||
port: data['port'],
|
||||
path: data['path'],
|
||||
token: data['token'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"method": method,
|
||||
"ipDomain": ipDomain,
|
||||
"port": port,
|
||||
"path": path,
|
||||
"token": token,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:my_linkding/providers/server_instances_provider.dart';
|
||||
import 'package:my_linkding/services/api_client.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'api_client_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ApiClientProvider extends _$ApiClientProvider {
|
||||
@override
|
||||
ApiClient? build() {
|
||||
final savedInstances = ref.watch(serverInstancesProvider);
|
||||
if (savedInstances.isNotEmpty) {
|
||||
return ApiClient(serverInstance: savedInstances[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void setApiClient(ApiClient client) {
|
||||
state = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'api_client_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$apiClientProviderHash() => r'eff6aeacbfe369f2204d77f4884cc9d78ecf1e27';
|
||||
|
||||
/// See also [ApiClientProvider].
|
||||
@ProviderFor(ApiClientProvider)
|
||||
final apiClientProviderProvider =
|
||||
NotifierProvider<ApiClientProvider, ApiClient?>.internal(
|
||||
ApiClientProvider.new,
|
||||
name: r'apiClientProviderProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$apiClientProviderHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ApiClientProvider = Notifier<ApiClient?>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:my_linkding/models/app_status.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
part 'app_status_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppStatusProvider extends _$AppStatusProvider {
|
||||
@override
|
||||
AppStatus build() {
|
||||
return AppStatus();
|
||||
}
|
||||
|
||||
void setSharedPreferencesInstance(SharedPreferences instance) {
|
||||
state = state.copyWith(sharedPreferences: instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_status_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$appStatusProviderHash() => r'ba585a0cf3cf46f88f5895eacec3e992bf0cd42f';
|
||||
|
||||
/// See also [AppStatusProvider].
|
||||
@ProviderFor(AppStatusProvider)
|
||||
final appStatusProviderProvider =
|
||||
NotifierProvider<AppStatusProvider, AppStatus>.internal(
|
||||
AppStatusProvider.new,
|
||||
name: r'appStatusProviderProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$appStatusProviderHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AppStatusProvider = Notifier<AppStatus>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:my_linkding/providers/api_client_provider.dart';
|
||||
import 'package:my_linkding/router/paths.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:my_linkding/router/routes.dart';
|
||||
|
||||
part 'router_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
GoRouter router(RouterRef ref) {
|
||||
return GoRouter(
|
||||
redirect: (context, state) {
|
||||
if (ref.watch(apiClientProviderProvider) == null) {
|
||||
return RoutesPaths.connect;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
initialLocation: RoutesPaths.links,
|
||||
routes: appRoutes,
|
||||
navigatorKey: rootNavigatorKey,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'router_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$routerHash() => r'47e3f3d747dcb7b2543c73209f8a2f5561eb6ff7';
|
||||
|
||||
/// See also [router].
|
||||
@ProviderFor(router)
|
||||
final routerProvider = Provider<GoRouter>.internal(
|
||||
router,
|
||||
name: r'routerProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$routerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef RouterRef = ProviderRef<GoRouter>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:my_linkding/config/shared_preferences_keys.dart';
|
||||
import 'package:my_linkding/providers/shared_preferences_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:my_linkding/services/api_client.dart';
|
||||
import 'package:my_linkding/providers/api_client_provider.dart';
|
||||
import 'package:my_linkding/models/server_instance.dart';
|
||||
|
||||
part 'server_instances_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ServerInstances extends _$ServerInstances {
|
||||
@override
|
||||
List<ServerInstance> build() {
|
||||
final instances = ref.watch(sharedPreferencesProvider).getStringList(SharedPreferencesKeys.serversInstances);
|
||||
|
||||
if (instances == null) return [];
|
||||
|
||||
List<ServerInstance> generatedInstances = [];
|
||||
for (final instance in instances) {
|
||||
try {
|
||||
generatedInstances.add(ServerInstance.fromJson(jsonDecode(instance)));
|
||||
} catch (_) {/* IF ERROR OMIT */}
|
||||
}
|
||||
|
||||
return generatedInstances;
|
||||
}
|
||||
|
||||
ServerInstance? getInstance(String id) {
|
||||
try {
|
||||
return state.firstWhere((instance) => instance.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void saveNewInstance(ServerInstance instance) {
|
||||
state = [instance];
|
||||
saveServerInstances([instance]);
|
||||
}
|
||||
|
||||
void saveServerInstances(List<ServerInstance> instances) async {
|
||||
final stringList = instances.map((i) => jsonEncode(i.toJson())).toList();
|
||||
ref.read(sharedPreferencesProvider).setStringList(SharedPreferencesKeys.serversInstances, stringList);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server_instances_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$serverInstancesHash() => r'82b7d3367afbe5c19ad0238977e5df35fac3499b';
|
||||
|
||||
/// See also [ServerInstances].
|
||||
@ProviderFor(ServerInstances)
|
||||
final serverInstancesProvider =
|
||||
NotifierProvider<ServerInstances, List<ServerInstance>>.internal(
|
||||
ServerInstances.new,
|
||||
name: r'serverInstancesProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$serverInstancesHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ServerInstances = Notifier<List<ServerInstance>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
||||
throw UnimplementedError();
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
class RoutesPaths {
|
||||
static const connect = "/connect";
|
||||
static const links = "/links";
|
||||
static const search = "/search";
|
||||
static const settings = "/settings";
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:my_linkding/screens/connect/ui/connect.dart';
|
||||
import 'package:my_linkding/screens/links/ui/links.dart';
|
||||
import 'package:my_linkding/screens/search/ui/search.dart';
|
||||
import 'package:my_linkding/screens/settings/ui/settings.dart';
|
||||
import 'package:my_linkding/widgets/layout.dart';
|
||||
|
||||
import 'package:my_linkding/router/paths.dart';
|
||||
|
||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final linksNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final searchNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final settingsNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final List<RouteBase> appRoutes = [
|
||||
GoRoute(
|
||||
path: RoutesPaths.connect,
|
||||
builder: (context, state) => const Connect(),
|
||||
),
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (context, state, navigationShell) => Layout(
|
||||
navigationShell: navigationShell,
|
||||
),
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
navigatorKey: linksNavigatorKey,
|
||||
initialLocation: RoutesPaths.links,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutesPaths.links,
|
||||
builder: (context, state) => const Links(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: searchNavigatorKey,
|
||||
initialLocation: RoutesPaths.search,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutesPaths.search,
|
||||
builder: (context, state) => const Search(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: settingsNavigatorKey,
|
||||
initialLocation: RoutesPaths.settings,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutesPaths.settings,
|
||||
builder: (context, state) => const Settings(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:my_linkding/constants/enums.dart';
|
||||
|
||||
class LinksModel {
|
||||
final LoadStatus loadStatus;
|
||||
|
||||
LinksModel({
|
||||
required this.loadStatus,
|
||||
});
|
||||
}
|
||||
@@ -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 he’d taken and the dripping chassis of a skyscraper canyon. They floated in the puppet place had been a subunit of Freeside’s 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:my_linkding/models/server_instance.dart';
|
||||
import 'package:my_linkding/utils/http_request_client.dart';
|
||||
|
||||
final dio = Dio();
|
||||
|
||||
class ApiClient {
|
||||
final ServerInstance serverInstance;
|
||||
|
||||
const ApiClient({
|
||||
required this.serverInstance,
|
||||
});
|
||||
|
||||
Future<bool> checkConnectionInstance() async {
|
||||
final result = await HttpRequestClient.get(urlPath: "/bookmarks/?limit=1", server: serverInstance);
|
||||
return result.successful;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
class MyHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
return super.createHttpClient(context)
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:my_linkding/models/server_instance.dart';
|
||||
|
||||
enum ExceptionType { socket, timeout, handshake, http, unknown }
|
||||
|
||||
class HttpResponse {
|
||||
final bool successful;
|
||||
final String? body;
|
||||
final int? statusCode;
|
||||
final ExceptionType? exception;
|
||||
|
||||
const HttpResponse({
|
||||
required this.successful,
|
||||
required this.body,
|
||||
required this.statusCode,
|
||||
this.exception,
|
||||
});
|
||||
}
|
||||
|
||||
String getConnectionString({
|
||||
required ServerInstance server,
|
||||
required String urlPath,
|
||||
}) {
|
||||
return "${server.method}://${server.ipDomain}${server.port != null ? ':${server.port}' : ""}${server.path ?? ""}/api$urlPath";
|
||||
}
|
||||
|
||||
class HttpRequestClient {
|
||||
static Future<HttpResponse> get({
|
||||
required String urlPath,
|
||||
required ServerInstance server,
|
||||
int timeout = 10,
|
||||
}) async {
|
||||
final String connectionString = getConnectionString(server: server, urlPath: urlPath);
|
||||
try {
|
||||
HttpClient httpClient = HttpClient();
|
||||
HttpClientRequest request = await httpClient.getUrl(Uri.parse(connectionString));
|
||||
request.headers.set('Authorization', 'Token ${server.token}');
|
||||
HttpClientResponse response = await request.close().timeout(
|
||||
Duration(seconds: timeout),
|
||||
);
|
||||
String reply = await response.transform(utf8.decoder).join();
|
||||
httpClient.close();
|
||||
return HttpResponse(
|
||||
successful: response.statusCode >= 400 ? false : true,
|
||||
body: reply,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
} on SocketException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.socket,
|
||||
);
|
||||
} on TimeoutException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.timeout,
|
||||
);
|
||||
} on HandshakeException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.handshake,
|
||||
);
|
||||
} on HttpException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.http,
|
||||
);
|
||||
} catch (e) {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<HttpResponse> post({
|
||||
required String urlPath,
|
||||
required ServerInstance server,
|
||||
dynamic body,
|
||||
int timeout = 10,
|
||||
}) async {
|
||||
final String connectionString = getConnectionString(server: server, urlPath: urlPath);
|
||||
try {
|
||||
HttpClient httpClient = HttpClient();
|
||||
HttpClientRequest request = await httpClient.postUrl(Uri.parse(connectionString));
|
||||
request.headers.set('Authorization', 'Token ${server.token}');
|
||||
request.headers.set('content-type', 'application/json');
|
||||
request.add(utf8.encode(json.encode(body)));
|
||||
HttpClientResponse response = await request.close().timeout(
|
||||
Duration(seconds: timeout),
|
||||
);
|
||||
String reply = await response.transform(utf8.decoder).join();
|
||||
httpClient.close();
|
||||
return HttpResponse(
|
||||
successful: response.statusCode >= 400 ? false : true,
|
||||
body: reply,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
} on SocketException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.socket,
|
||||
);
|
||||
} on TimeoutException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.timeout,
|
||||
);
|
||||
} on HttpException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.http,
|
||||
);
|
||||
} on HandshakeException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.handshake,
|
||||
);
|
||||
} catch (e) {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<HttpResponse> put({
|
||||
required String urlPath,
|
||||
required ServerInstance server,
|
||||
dynamic body,
|
||||
int timeout = 10,
|
||||
}) async {
|
||||
final String connectionString = getConnectionString(server: server, urlPath: urlPath);
|
||||
try {
|
||||
HttpClient httpClient = HttpClient();
|
||||
HttpClientRequest request = await httpClient.putUrl(Uri.parse(connectionString));
|
||||
request.headers.set('Authorization', 'Token ${server.token}');
|
||||
request.headers.set('content-type', 'application/json');
|
||||
request.add(utf8.encode(json.encode(body)));
|
||||
HttpClientResponse response = await request.close().timeout(
|
||||
Duration(seconds: timeout),
|
||||
);
|
||||
String reply = await response.transform(utf8.decoder).join();
|
||||
httpClient.close();
|
||||
return HttpResponse(
|
||||
successful: response.statusCode >= 400 ? false : true,
|
||||
body: reply,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
} on SocketException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.socket,
|
||||
);
|
||||
} on TimeoutException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.timeout,
|
||||
);
|
||||
} on HttpException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.http,
|
||||
);
|
||||
} on HandshakeException {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.handshake,
|
||||
);
|
||||
} catch (e) {
|
||||
return const HttpResponse(
|
||||
successful: false,
|
||||
body: null,
|
||||
statusCode: null,
|
||||
exception: ExceptionType.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:my_linkding/widgets/process_dialog.dart';
|
||||
|
||||
import 'package:my_linkding/router/routes.dart';
|
||||
|
||||
class ProcessModal {
|
||||
void open(String message) async {
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 0),
|
||||
() => {
|
||||
showDialog(
|
||||
context: rootNavigatorKey.currentContext!,
|
||||
builder: (ctx) {
|
||||
return ProcessDialog(
|
||||
message: message,
|
||||
);
|
||||
},
|
||||
barrierDismissible: false,
|
||||
useSafeArea: true,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void close() {
|
||||
Navigator.pop(rootNavigatorKey.currentContext!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:my_linkding/constants/global_keys.dart';
|
||||
|
||||
void showSnacbkar({
|
||||
required String label,
|
||||
required Color color,
|
||||
Color? labelColor,
|
||||
GlobalKey<ScaffoldMessengerState>? key,
|
||||
}) async {
|
||||
final GlobalKey<ScaffoldMessengerState> scaffoldKey = key ?? scaffoldMessengerGlobalKey;
|
||||
|
||||
final snackBar = SnackBar(
|
||||
content: Text(
|
||||
label,
|
||||
style: TextStyle(color: labelColor ?? Colors.white),
|
||||
),
|
||||
backgroundColor: color,
|
||||
);
|
||||
scaffoldKey.currentState?.showSnackBar(snackBar);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class Layout extends StatelessWidget {
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
const Layout({
|
||||
Key? key,
|
||||
required this.navigationShell,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void goBranch(int index) {
|
||||
navigationShell.goBranch(
|
||||
index,
|
||||
initialLocation: index == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: PageTransitionSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
transitionBuilder: (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
child: child,
|
||||
),
|
||||
child: navigationShell,
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: (s) => goBranch(s),
|
||||
selectedIndex: navigationShell.currentIndex,
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.link_rounded),
|
||||
label: "Links",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.search_rounded),
|
||||
label: "Search",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.settings_rounded),
|
||||
label: "Settings",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProcessDialog extends StatelessWidget {
|
||||
final String message;
|
||||
|
||||
const ProcessDialog({
|
||||
Key? key,
|
||||
required this.message,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Dialog(
|
||||
backgroundColor: Theme.of(context).dialogBackgroundColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 30,
|
||||
horizontal: 30,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(width: 40),
|
||||
Flexible(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user