Created project base structure, state management, router and login
This commit is contained in:
3
lib/config/shared_preferences_keys.dart
Normal file
3
lib/config/shared_preferences_keys.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class SharedPreferencesKeys {
|
||||
static const serversInstances = "serversInstances";
|
||||
}
|
||||
78
lib/config/theme.dart
Normal file
78
lib/config/theme.dart
Normal file
@@ -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,
|
||||
);
|
||||
18
lib/constants/colors.dart
Normal file
18
lib/constants/colors.dart
Normal file
@@ -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,
|
||||
];
|
||||
3
lib/constants/enums.dart
Normal file
3
lib/constants/enums.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
enum LoadStatus { loading, loaded, error }
|
||||
|
||||
enum ConnectionMethod { http, https }
|
||||
3
lib/constants/global_keys.dart
Normal file
3
lib/constants/global_keys.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final scaffoldMessengerGlobalKey = GlobalKey<ScaffoldMessengerState>();
|
||||
16
lib/constants/linkding_color.dart
Normal file
16
lib/constants/linkding_color.dart
Normal file
@@ -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);
|
||||
6
lib/constants/regexp.dart
Normal file
6
lib/constants/regexp.dart
Normal file
@@ -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]+)$');
|
||||
}
|
||||
4
lib/helpers/wrapped_class.dart
Normal file
4
lib/helpers/wrapped_class.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
class Wrapped<T> {
|
||||
final T value;
|
||||
const Wrapped.value(this.value);
|
||||
}
|
||||
50
lib/main.dart
Normal file
50
lib/main.dart
Normal file
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
lib/models/api_response.dart
Normal file
11
lib/models/api_response.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
class ApiResponse {
|
||||
final bool successful;
|
||||
final dynamic content;
|
||||
final int? statusCode;
|
||||
|
||||
const ApiResponse({
|
||||
required this.successful,
|
||||
this.content,
|
||||
this.statusCode,
|
||||
});
|
||||
}
|
||||
12
lib/models/app_status.dart
Normal file
12
lib/models/app_status.dart
Normal file
@@ -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();
|
||||
}
|
||||
35
lib/models/server_instance.dart
Normal file
35
lib/models/server_instance.dart
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
21
lib/providers/api_client_provider.dart
Normal file
21
lib/providers/api_client_provider.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
26
lib/providers/api_client_provider.g.dart
Normal file
26
lib/providers/api_client_provider.g.dart
Normal file
@@ -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
|
||||
18
lib/providers/app_status_provider.dart
Normal file
18
lib/providers/app_status_provider.dart
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
26
lib/providers/app_status_provider.g.dart
Normal file
26
lib/providers/app_status_provider.g.dart
Normal file
@@ -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
|
||||
23
lib/providers/router_provider.dart
Normal file
23
lib/providers/router_provider.dart
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
24
lib/providers/router_provider.g.dart
Normal file
24
lib/providers/router_provider.g.dart
Normal file
@@ -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
|
||||
48
lib/providers/server_instances_provider.dart
Normal file
48
lib/providers/server_instances_provider.dart
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
26
lib/providers/server_instances_provider.g.dart
Normal file
26
lib/providers/server_instances_provider.g.dart
Normal file
@@ -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
|
||||
6
lib/providers/shared_preferences_provider.dart
Normal file
6
lib/providers/shared_preferences_provider.dart
Normal file
@@ -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();
|
||||
});
|
||||
6
lib/router/paths.dart
Normal file
6
lib/router/paths.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
class RoutesPaths {
|
||||
static const connect = "/connect";
|
||||
static const links = "/links";
|
||||
static const search = "/search";
|
||||
static const settings = "/settings";
|
||||
}
|
||||
59
lib/router/routes.dart
Normal file
59
lib/router/routes.dart
Normal file
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
53
lib/screens/connect/model/connect_model.dart
Normal file
53
lib/screens/connect/model/connect_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
106
lib/screens/connect/provider/connect_provider.dart
Normal file
106
lib/screens/connect/provider/connect_provider.dart
Normal 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;
|
||||
}
|
||||
40
lib/screens/connect/provider/connect_provider.g.dart
Normal file
40
lib/screens/connect/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'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
|
||||
124
lib/screens/connect/ui/connect.dart
Normal file
124
lib/screens/connect/ui/connect.dart
Normal 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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
9
lib/screens/links/model/links_model.dart
Normal file
9
lib/screens/links/model/links_model.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'package:my_linkding/constants/enums.dart';
|
||||
|
||||
class LinksModel {
|
||||
final LoadStatus loadStatus;
|
||||
|
||||
LinksModel({
|
||||
required this.loadStatus,
|
||||
});
|
||||
}
|
||||
0
lib/screens/links/provider/links_provider.dart
Normal file
0
lib/screens/links/provider/links_provider.dart
Normal file
92
lib/screens/links/ui/links.dart
Normal file
92
lib/screens/links/ui/links.dart
Normal 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 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
lib/screens/search/ui/search.dart
Normal file
10
lib/screens/search/ui/search.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
39
lib/screens/settings/ui/settings.dart
Normal file
39
lib/screens/settings/ui/settings.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
19
lib/services/api_client.dart
Normal file
19
lib/services/api_client.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
9
lib/utils/http_overrides.dart
Normal file
9
lib/utils/http_overrides.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
210
lib/utils/http_request_client.dart
Normal file
210
lib/utils/http_request_client.dart
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
lib/utils/process_modal.dart
Normal file
29
lib/utils/process_modal.dart
Normal file
@@ -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!);
|
||||
}
|
||||
}
|
||||
22
lib/utils/snackbar.dart
Normal file
22
lib/utils/snackbar.dart
Normal file
@@ -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);
|
||||
}
|
||||
52
lib/widgets/layout.dart
Normal file
52
lib/widgets/layout.dart
Normal file
@@ -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",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/widgets/process_dialog.dart
Normal file
39
lib/widgets/process_dialog.dart
Normal file
@@ -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