added frontend

This commit is contained in:
verboomp
2026-01-21 16:08:09 +01:00
parent d2e6f5164a
commit b3de3eec8c
74 changed files with 4938 additions and 26 deletions

View File

@@ -0,0 +1,17 @@
final class DateTimeUtils {
static DateTime? toDateTime(dynamic element) {
if (element == null) {
return null;
}
String text = element.toString();
int? time = int.tryParse(text);
if (time == null) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(time);
}
static int? fromDateTime(DateTime? dt) {
return dt?.millisecondsSinceEpoch;
}
}

View File

@@ -0,0 +1,38 @@
import 'package:fotodocumentation/controller/login_controller.dart';
import 'package:fotodocumentation/pages/ui_utils/dialog/snackbar_utils.dart';
import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/pages/ui_utils/header_utils.dart';
import 'package:fotodocumentation/utils/http_client_utils.dart';
import 'package:fotodocumentation/utils/jwt_token_storage.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:fotodocumentation/utils/url_utils.dart';
class DiContainer {
static final DiContainer instance = DiContainer._privateConstructor();
DiContainer._privateConstructor();
final _container = {};
static T get<T>() {
return instance._container[T] as T;
}
void initState() {
DiContainer.instance.put(LoginCredentials, LoginCredentialsImpl());
DiContainer.instance.put(GeneralStyle, GeneralStyleImpl());
DiContainer.instance.put(JwtTokenStorage, JwtTokenStorageImpl());
DiContainer.instance.put(HttpClientUtils, HttpCLientUtilsImpl());
DiContainer.instance.put(HeaderUtils, HeaderUtilsImpl());
DiContainer.instance.put(UrlUtils, UrlUtilsImpl());
DiContainer.instance.put(SnackbarUtils, SnackbarUtilsImpl());
DiContainer.instance.put(LoginController, LoginControllerImpl());
}
void put<T>(Type key, T object) {
_container[key] = object;
}
T get2<T>() {
return _container[T] as T;
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart' show Colors, Color;
extension HexColor on Color {
/// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#".
static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
}
extension RiskColor on Color {
static const Color noRisk = Colors.transparent;
static final Color lowRisk = HexColor.fromHex("#FFFF00");
static final Color mediumRisk = HexColor.fromHex("#FF9000");
static final Color highRisk = HexColor.fromHex("#FF4000");
static Color colorForRisk(int value) {
if (value == 1) {
return lowRisk;
} else if (value == 2) {
return mediumRisk;
} else if (value == 3) {
return highRisk;
}
return noRisk;
}
}

View File

@@ -0,0 +1,51 @@
// needed for web horizontal scroll behavior
import 'package:flutter/material.dart';
import 'package:fotodocumentation/main.dart';
import 'package:fotodocumentation/pages/customer/customer_widget.dart';
import 'package:fotodocumentation/pages/login/login_widget.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:go_router/go_router.dart';
class GlobalRouter {
static final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
static final GlobalKey<NavigatorState> bottomBarNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'bottombar');
static final GlobalKey<NavigatorState> adminNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'admin');
static final GlobalKey<NavigatorState> skillEditorNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'skillEditor');
static final String pathHome = "/home";
static final String pathLogin = "/login";
static final GoRouter router = createRouter(pathHome);
static GoRouter createRouter(String initialLocation) {
return GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: initialLocation,
routes: <RouteBase>[
GoRoute(
path: "/",
redirect: (_, __) => pathHome,
),
GoRoute(
path: pathLogin,
builder: (BuildContext context, GoRouterState state) => const LoginWidget(),
),
GoRoute(
path: pathHome,
builder: (context, state) => CustomerWidget(),
),
],
redirect: (context, state) {
var uriStr = state.uri.toString();
logger.t("uri $uriStr");
LoginCredentials loginCredentials = DiContainer.get();
if (!loginCredentials.isLoggedIn) {
return pathLogin;
}
return null;
},
);
}
}

View File

@@ -0,0 +1,16 @@
class GlobalStack<T> {
final _list = <T>[];
void push(T value) => _list.add(value);
T pop() => _list.removeLast();
T peek() => _list.last;
bool get isEmpty => _list.isEmpty;
bool get isNotEmpty => _list.isNotEmpty;
@override
String toString() => _list.toString();
}

View File

@@ -0,0 +1,11 @@
import 'package:http/http.dart' as http;
import 'package:fotodocumentation/utils/http_client_factory_stub.dart';
HttpCLientFactory getHttpClientFactory() => HttpClientFactoryApp();
class HttpClientFactoryApp extends HttpCLientFactory {
@override
http.Client createHttpClient() {
return http.Client();
}
}

View File

@@ -0,0 +1,21 @@
import 'package:http/http.dart' as http;
HttpCLientFactory getHttpClientFactory() => throw UnsupportedError('Cannot create http client');
class HttpCLientFactory {
http.Client createHttpClient() {
// Check if running on the Web
/*if (kIsWeb) {
var client = http.Client();
(client as BrowserClient).withCredentials = true;
return client;
} else if (universal_io.Platform.isAndroid || universal_io.Platform.isIOS) {
// Platform-specific logic for Android and iOS
return http.Client();
} else {
throw UnsupportedError('Unsupported platform');
}*/
throw UnsupportedError('Cannot create http client');
}
}

View File

@@ -0,0 +1,14 @@
import 'package:http/browser_client.dart';
import 'package:http/http.dart' as http;
import 'package:fotodocumentation/utils/http_client_factory_stub.dart';
HttpCLientFactory getHttpClientFactory() => HttpClientFactoryWeb();
class HttpClientFactoryWeb extends HttpCLientFactory{
@override
http.Client createHttpClient() {
var client = http.Client();
(client as BrowserClient).withCredentials = true;
return client;
}
}

View File

@@ -0,0 +1,37 @@
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:fotodocumentation/utils/global_router.dart';
/// HTTP client that intercepts all responses and handles 401 status codes
/// by logging out the user and redirecting to the login page.
class HttpClientInterceptor extends http.BaseClient {
final http.Client _inner;
HttpClientInterceptor(this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final response = await _inner.send(request);
// Check for 401 Unauthorized
if (response.statusCode == 401) {
_handle401Unauthorized();
}
return response;
}
void _handle401Unauthorized() {
// Clear login credentials
final loginCredentials = DiContainer.get<LoginCredentials>();
loginCredentials.logout();
// Navigate to login page using GoRouter
final context = GlobalRouter.rootNavigatorKey.currentContext;
if (context != null) {
context.go(GlobalRouter.pathLogin);
}
}
}

View File

@@ -0,0 +1,20 @@
import 'package:http/http.dart' as http;
import 'http_client_factory_stub.dart' if (dart.library.io) 'http_client_factory_app.dart' if (dart.library.js) 'http_client_factory_web.dart';
import 'http_client_interceptor.dart';
abstract class HttpClientUtils {
http.Client get client;
}
class HttpCLientUtilsImpl extends HttpClientUtils {
http.Client? _client;
@override
http.Client get client => _getClient();
http.Client _getClient() {
_client ??= HttpClientInterceptor(getHttpClientFactory().createHttpClient());
return _client!;
}
}

View File

@@ -0,0 +1,69 @@
abstract class JwtTokenStorage {
/// Save both access and refresh tokens
///
/// @param accessToken The short-lived access token
/// @param refreshToken The long-lived refresh token
Future<void> saveTokens(String accessToken, String refreshToken);
/// Get the stored access token
///
/// @return Access token or null if not found
Future<String?> getAccessToken();
/// Get the stored refresh token
///
/// @return Refresh token or null if not found
Future<String?> getRefreshToken();
/// Clear all stored tokens (on logout)
Future<void> clearTokens();
/// Check if tokens are stored
///
/// @return true if access token exists
Future<bool> hasTokens();
/// Update only the access token (used after refresh)
///
/// @param accessToken New access token
Future<void> updateAccessToken(String accessToken);
}
class JwtTokenStorageImpl extends JwtTokenStorage {
// Storage keys
String? _keyAccessToken;
String? _keyRefreshToken;
@override
Future<void> saveTokens(String accessToken, String refreshToken) async {
_keyAccessToken = accessToken;
_keyRefreshToken = refreshToken;
}
@override
Future<String?> getAccessToken() async {
return _keyAccessToken;
}
@override
Future<String?> getRefreshToken() async {
return _keyRefreshToken;
}
@override
Future<void> clearTokens() async {
_keyAccessToken = null;
_keyRefreshToken = null;
}
@override
Future<bool> hasTokens() async {
return _keyAccessToken != null && _keyAccessToken!.isNotEmpty;
}
@override
Future<void> updateAccessToken(String accessToken) async {
_keyAccessToken == accessToken;
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
abstract class LoginCredentials extends ChangeNotifier {
String get fullname;
bool get isLoggedIn;
void setLoggedIn(bool loggedIn);
void logout();
}
class LoginCredentialsImpl extends LoginCredentials {
bool loggedIn = false;
@override
bool get isLoggedIn => loggedIn;
@override
String get fullname => "";
@override
void setLoggedIn(bool loggedIn) {
this.loggedIn = loggedIn;
}
@override
void logout() {
loggedIn = false;
notifyListeners();
}
}

View File

@@ -0,0 +1,33 @@
// needed for web horizontal scroll behavior
import 'dart:convert' show jsonDecode;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:json_theme/json_theme.dart';
import 'package:fotodocumentation/main.dart' show logger;
class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like dragDevices
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}
class ThemeLoader {
static Future<ThemeData> loadTheme() async {
try {
String prefix = kDebugMode && kIsWeb ? "" : "assets/";
String url = "${prefix}theme/appainter_theme.json";
final themeStr = await rootBundle.loadString(url);
final themeJson = jsonDecode(themeStr);
return ThemeDecoder.decodeThemeData(themeJson)!;
} catch (e) {
logger.e("Failed to load theme $e", error: e);
return ThemeData.light();
}
}
}

View File

@@ -0,0 +1,12 @@
import 'package:basic_utils/basic_utils.dart' show StringUtils;
abstract interface class PasswordUtils {
String create();
}
class PasswordUtilsImpl implements PasswordUtils {
@override
String create() {
return StringUtils.generateRandomString(8, special: false);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/foundation.dart' show kReleaseMode;
abstract interface class UrlUtils {
String getBaseUrl();
}
class UrlUtilsImpl extends UrlUtils {
@override
String getBaseUrl() {
if (kReleaseMode){
return "${Uri.base.origin}/api/";
}
return "http://localhost:8080/api/";
}
}