From 9654072b1df676ef989f6316ad1e0eb9bdf87c69 Mon Sep 17 00:00:00 2001 From: Herwig Birke Date: Tue, 21 Oct 2025 21:27:23 +0200 Subject: [PATCH] new start --- lib/app_router.dart | 14 + lib/core/api_client.dart | 100 --- lib/features/auth/auth_controller.dart | 130 ---- lib/features/auth/data/auth_repository.dart | 41 ++ lib/features/auth/data/token_storage.dart | 42 ++ .../auth/presentation/login_screen.dart | 57 ++ .../auth/presentation/register_screen.dart | 85 +++ lib/features/auth/ui/login_screen.dart | 222 ------- .../hedgehogs/data/hedgehog_service.dart | 205 ------- lib/features/hedgehogs/models/hedgehog.dart | 16 - .../hedgehogs/models/hedgehog.freezed.dart | 216 ------- lib/features/hedgehogs/models/hedgehog.g.dart | 23 - .../hedgehogs/ui/dashboard_screen.dart | 215 ------- lib/features/hedgehogs/ui/detail_screen.dart | 567 ------------------ lib/features/igel/data/igel_repository.dart | 26 + lib/features/igel/domain/igel.dart | 10 + .../igel/presentation/igel_list_screen.dart | 125 ++++ .../measurements/models/measurement.dart | 19 - .../models/measurement.freezed.dart | 294 --------- .../measurements/models/measurement.g.dart | 29 - lib/features/photos/models/photo.dart | 25 - lib/features/photos/models/photo.g.dart | 25 - lib/main.dart | 90 +-- lib/shared/api_client.dart | 54 ++ linux/flutter/generated_plugin_registrant.cc | 12 +- linux/flutter/generated_plugins.cmake | 3 +- macos/Flutter/GeneratedPluginRegistrant.swift | 12 +- pubspec.lock | 558 ++--------------- pubspec.yaml | 21 +- .../flutter/generated_plugin_registrant.cc | 9 +- windows/flutter/generated_plugins.cmake | 3 +- 31 files changed, 540 insertions(+), 2708 deletions(-) create mode 100644 lib/app_router.dart delete mode 100644 lib/core/api_client.dart delete mode 100644 lib/features/auth/auth_controller.dart create mode 100644 lib/features/auth/data/auth_repository.dart create mode 100644 lib/features/auth/data/token_storage.dart create mode 100644 lib/features/auth/presentation/login_screen.dart create mode 100644 lib/features/auth/presentation/register_screen.dart delete mode 100644 lib/features/auth/ui/login_screen.dart delete mode 100644 lib/features/hedgehogs/data/hedgehog_service.dart delete mode 100644 lib/features/hedgehogs/models/hedgehog.dart delete mode 100644 lib/features/hedgehogs/models/hedgehog.freezed.dart delete mode 100644 lib/features/hedgehogs/models/hedgehog.g.dart delete mode 100644 lib/features/hedgehogs/ui/dashboard_screen.dart delete mode 100644 lib/features/hedgehogs/ui/detail_screen.dart create mode 100644 lib/features/igel/data/igel_repository.dart create mode 100644 lib/features/igel/domain/igel.dart create mode 100644 lib/features/igel/presentation/igel_list_screen.dart delete mode 100644 lib/features/measurements/models/measurement.dart delete mode 100644 lib/features/measurements/models/measurement.freezed.dart delete mode 100644 lib/features/measurements/models/measurement.g.dart delete mode 100644 lib/features/photos/models/photo.dart delete mode 100644 lib/features/photos/models/photo.g.dart create mode 100644 lib/shared/api_client.dart diff --git a/lib/app_router.dart b/lib/app_router.dart new file mode 100644 index 0000000..775f62a --- /dev/null +++ b/lib/app_router.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'features/auth/presentation/login_screen.dart'; +import 'features/auth/presentation/register_screen.dart'; +import 'features/igel/presentation/igel_list_screen.dart'; + +GoRouter buildRouter({required bool isAuthed}) => GoRouter( + initialLocation: isAuthed ? '/igel' : '/login', + routes: [ + GoRoute(path: '/login', builder: (_, __) => const LoginScreen()), + GoRoute(path: '/register', builder: (_, __) => const RegisterScreen()), + GoRoute(path: '/igel', builder: (_, __) => const IgelListScreen()), + ], + ); diff --git a/lib/core/api_client.dart b/lib/core/api_client.dart deleted file mode 100644 index da0e4ca..0000000 --- a/lib/core/api_client.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:dio/dio.dart'; -import 'package:dio/browser.dart' as dio_web; - -final apiClientProvider = Provider((ref) => ApiClient()); - -class ApiClient { - final Dio dio; - String? _sid; // Session-ID aus dem Server (Header-Token) - - ApiClient() - : dio = Dio( - BaseOptions( - baseUrl: 'https://api.windesign.at/hedgehog.php', - connectTimeout: const Duration(seconds: 20), - receiveTimeout: const Duration(seconds: 30), - headers: const { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - validateStatus: (_) => true, // wir werten selbst aus - ), - ) { - if (kIsWeb) { - final adapter = dio_web.BrowserHttpClientAdapter(); - adapter.withCredentials = true; // falls Cookies doch erlaubt sind - dio.httpClientAdapter = adapter; - } - - dio.interceptors.add( - InterceptorsWrapper( - onRequest: (options, handler) { - // Header-SID immer mitsenden (unabhängig von Cookies) - if (_sid != null && _sid!.isNotEmpty) { - options.headers['X-Session'] = _sid!; - options.headers['Authorization'] = 'Bearer $_sid'; - } - options.extra['withCredentials'] = true; // Web: fetch credentials - handler.next(options); - }, - ), - ); - } - - void setSessionId(String? sid) { - _sid = sid; - } - - Future call(String action, Map payload) async { - try { - final req = {'action': action, ...payload}; - final resp = await dio.post('', data: req); - - // Debug - // ignore: avoid_print - print('[API] $action status=${resp.statusCode} type=${resp.data.runtimeType}'); - - dynamic data = resp.data; - if (data is String) { - try { data = jsonDecode(data); } catch (_) { - throw StateError('API "$action": Antwort ist kein JSON.'); - } - } - - // Statuscodes selbst prüfen - final code = resp.statusCode ?? 0; - if (code >= 400) { - final msg = (data is Map && data['error'] != null) - ? data['error'].toString() - : 'HTTP $code'; - throw StateError(msg); - } - - // Wrapper {ok,data} respektieren - if (data is Map && data.containsKey('ok')) { - // 👉 SID aus login/session mitnehmen - if (action == 'login' || action == 'session') { - final sid = (data['data']?['sid'] ?? data['sid'])?.toString(); - if (sid != null && sid.isNotEmpty) setSessionId(sid); - } - if (data['ok'] == true) return data; - throw StateError(data['error']?.toString() ?? 'Unbekannter API-Fehler'); - } - - return data; - } on DioException catch (e) { - final msg = e.response?.data?.toString() ?? e.message ?? 'Netzwerkfehler'; - // ignore: avoid_print - print('[API] $action dioError: $msg'); - throw StateError(msg); - } catch (e) { - // ignore: avoid_print - print('[API] $action error: $e'); - rethrow; - } - } - -} diff --git a/lib/features/auth/auth_controller.dart b/lib/features/auth/auth_controller.dart deleted file mode 100644 index 0c541af..0000000 --- a/lib/features/auth/auth_controller.dart +++ /dev/null @@ -1,130 +0,0 @@ -// lib/features/auth/auth_controller.dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../core/api_client.dart'; - -class AuthState { - final bool loggedIn; - final String? email; - final bool loading; - final String? error; - - const AuthState({ - required this.loggedIn, - this.email, - this.loading = false, - this.error, - }); - - const AuthState.loggedOut() - : this(loggedIn: false, email: null, loading: false, error: null); - - AuthState copyWith({ - bool? loggedIn, - String? email, - bool? loading, - String? error, - }) { - return AuthState( - loggedIn: loggedIn ?? this.loggedIn, - email: email ?? this.email, - loading: loading ?? this.loading, - error: error, - ); - } - - @override - String toString() => - 'AuthState(loggedIn=$loggedIn, email=$email, loading=$loading, error=$error)'; -} - -class AuthController extends StateNotifier { - final ApiClient api; - - AuthController(this.api) : super(const AuthState.loggedOut()); - - /// Beim App-Start/Hot Restart Session prüfen - Future checkSession() async { - try { - state = state.copyWith(loading: true, error: null); - final res = await api.call('session', {}); - final data = (res is Map && res['data'] != null) - ? res['data'] as Map - : (res as Map); - final logged = data['loggedIn'] == true; - final email = data['email']?.toString(); - final sid = data['sid']?.toString(); - if (sid != null && sid.isNotEmpty) { - api.setSessionId(sid); // 👉 wichtig - } - state = AuthState(loggedIn: logged, email: email, loading: false); - } catch (e) { - state = const AuthState.loggedOut(); - } - } - - Future login(String email, String password) async { - state = state.copyWith(loading: true, error: null); - try { - final res = - await api.call('login', {'email': email, 'password': password}); - final data = (res is Map && res['data'] != null) - ? res['data'] as Map - : (res as Map); - if (data['loggedIn'] == true) { - final sid = data['sid']?.toString(); - if (sid != null && sid.isNotEmpty) { - api.setSessionId(sid); // 👉 wichtig - } - state = AuthState( - loggedIn: true, email: data['email']?.toString(), loading: false); - } else { - state = state.copyWith(loading: false, error: 'Login fehlgeschlagen'); - } - } catch (e) { - state = state.copyWith(loading: false, error: e.toString()); - } - } - - Future register(String email, String password) async { - state = state.copyWith(loading: true, error: null); - try { - final res = - await api.call('register', {'email': email, 'password': password}); - final data = (res is Map && res['data'] != null) - ? res['data'] as Map - : (res as Map); - if (data['registered'] == true) { - // Optional: Direkt einloggen oder Erfolg nur anzeigen - await login(email, password); - } else { - state = state.copyWith( - loading: false, error: 'Registrierung fehlgeschlagen'); - } - } catch (e) { - state = state.copyWith(loading: false, error: e.toString()); - } - } - - Future logout() async { - try { - await api.call('logout', {}); - } catch (_) { - // ignore: Fehlermeldung egal – wir setzen lokalen Zustand zurück - } - state = const AuthState.loggedOut(); - } -} - -/// StateNotifierProvider: globaler Auth-Zustand -final authProvider = StateNotifierProvider((ref) { - final api = ref.read(apiClientProvider); - final controller = AuthController(api); - // Beim ersten Read direkt Session prüfen - controller.checkSession(); - return controller; -}); - -/// Praktischer „Trigger“-Provider, falls du irgendwo explizit neu prüfen willst -final authSessionCheckProvider = FutureProvider((ref) async { - await ref.read(authProvider.notifier).checkSession(); -}); diff --git a/lib/features/auth/data/auth_repository.dart b/lib/features/auth/data/auth_repository.dart new file mode 100644 index 0000000..d3040eb --- /dev/null +++ b/lib/features/auth/data/auth_repository.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'token_storage.dart'; + +class AuthRepository { + final String baseUrl; + final TokenStorage tokens; + AuthRepository(this.baseUrl, this.tokens); + + Future register(String email, String password) async { + final res = await http.post(Uri.parse('$baseUrl/auth/register'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'email': email, 'password': password})); + if (res.statusCode != 201) { + throw Exception('Register failed: ${res.body}'); + } + } + + Future login(String email, String password) async { + final res = await http.post(Uri.parse('$baseUrl/auth/login'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'email': email, 'password': password})); + if (res.statusCode == 200) { + final m = jsonDecode(res.body) as Map; + await tokens.save( + m['access_token'] as String, m['refresh_token'] as String); + } else { + throw Exception('Login failed: ${res.body}'); + } + } + + Future logout() async { + final r = await tokens.refresh; + if (r != null) { + await http.post(Uri.parse('$baseUrl/auth/logout'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'refresh_token': r})); + } + await tokens.clear(); + } +} diff --git a/lib/features/auth/data/token_storage.dart b/lib/features/auth/data/token_storage.dart new file mode 100644 index 0000000..3ed20fa --- /dev/null +++ b/lib/features/auth/data/token_storage.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:http/http.dart' as http; + +class TokenStorage { + final _storage = const FlutterSecureStorage(); + final String baseUrl; + TokenStorage(this.baseUrl); + + Future save(String access, String refresh) async { + await _storage.write(key: 'access', value: access); + await _storage.write(key: 'refresh', value: refresh); + } + + Future get access async => await _storage.read(key: 'access'); + Future get refresh async => await _storage.read(key: 'refresh'); + Future clear() async { + await _storage.deleteAll(); + } + + /// Einfache Auto‑Refresh Logik + Future getValidAccessToken() async { + final token = await access; +// (Optional) hier exp prüfen. Für Kürze direkt refresh call beim 401 außerhalb. + return token; + } + + Future refreshAccess() async { + final r = await refresh; + if (r == null) return; + final res = await http.post(Uri.parse('$baseUrl/auth/refresh'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'refresh_token': r})); + if (res.statusCode == 200) { + final m = jsonDecode(res.body) as Map; + await _storage.write(key: 'access', value: m['access_token'] as String); + } else { + await clear(); + } + } +} diff --git a/lib/features/auth/presentation/login_screen.dart b/lib/features/auth/presentation/login_screen.dart new file mode 100644 index 0000000..4bb65e3 --- /dev/null +++ b/lib/features/auth/presentation/login_screen.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hedgehog/features/auth/data/auth_repository.dart'; +import 'package:hedgehog/main.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final emailC = TextEditingController(); + final passC = TextEditingController(); + bool busy = false; + String? err; + @override + Widget build(BuildContext context) { + final auth = AuthRepository(kApiBase, ref.read(tokenStorageProvider)); + return Scaffold( + appBar: AppBar(title: const Text('Login')), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column(children: [ + TextField( + controller: emailC, + decoration: const InputDecoration(labelText: 'E-Mail')), + TextField( + controller: passC, + decoration: const InputDecoration(labelText: 'Passwort'), + obscureText: true), + if (err != null) + Text(err!, style: const TextStyle(color: Colors.red)), + const SizedBox(height: 12), + FilledButton( + onPressed: busy + ? null + : () async { + setState(() => busy = true); + try { + await auth.login(emailC.text, passC.text); + if (mounted) context.go('/igel'); + } catch (e) { + setState(() => err = e.toString()); + } finally { + if (mounted) setState(() => busy = false); + } + }, + child: const Text('Einloggen')), + TextButton( + onPressed: () => context.go('/register'), + child: const Text('Registrieren')), + ])), + ); + } +} diff --git a/lib/features/auth/presentation/register_screen.dart b/lib/features/auth/presentation/register_screen.dart new file mode 100644 index 0000000..828ab0f --- /dev/null +++ b/lib/features/auth/presentation/register_screen.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hedgehog/features/auth/data/auth_repository.dart'; +import 'package:hedgehog/main.dart'; + +class RegisterScreen extends ConsumerStatefulWidget { + const RegisterScreen({super.key}); + @override + ConsumerState createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends ConsumerState { + final emailC = TextEditingController(); + final passC = TextEditingController(); + final confirmC = TextEditingController(); + bool busy = false; + String? err; + + @override + Widget build(BuildContext context) { + final auth = AuthRepository(kApiBase, ref.read(tokenStorageProvider)); + return Scaffold( + appBar: AppBar(title: const Text('Registrieren')), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: emailC, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(labelText: 'E-Mail'), + ), + const SizedBox(height: 12), + TextField( + controller: passC, + decoration: const InputDecoration(labelText: 'Passwort'), + obscureText: true, + ), + const SizedBox(height: 12), + TextField( + controller: confirmC, + decoration: const InputDecoration(labelText: 'Passwort bestätigen'), + obscureText: true, + ), + if (err != null) ...[ + const SizedBox(height: 8), + Text(err!, style: const TextStyle(color: Colors.red)), + ], + const SizedBox(height: 20), + FilledButton( + onPressed: busy + ? null + : () async { + final email = emailC.text.trim(); + final pass = passC.text; + final confirm = confirmC.text; + if (pass != confirm) { + setState(() => err = 'Passwörter stimmen nicht überein'); + return; + } + setState(() => busy = true); + try { + await auth.register(email, pass); + if (mounted) context.go('/login'); + } catch (e) { + setState(() => err = e.toString()); + } finally { + if (mounted) setState(() => busy = false); + } + }, + child: const Text('Registrieren'), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () => context.go('/login'), + child: const Text('Schon registriert? Zum Login'), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/auth/ui/login_screen.dart b/lib/features/auth/ui/login_screen.dart deleted file mode 100644 index 19e4ee3..0000000 --- a/lib/features/auth/ui/login_screen.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../auth_controller.dart'; - -class LoginScreen extends ConsumerStatefulWidget { - const LoginScreen({super.key}); - - @override - ConsumerState createState() => _LoginScreenState(); -} - -class _LoginScreenState extends ConsumerState { - final _formKey = GlobalKey(); - final _emailCtrl = TextEditingController(); - final _passCtrl = TextEditingController(); - bool _obscure = true; - bool _registerMode = false; - - @override - void dispose() { - _emailCtrl.dispose(); - _passCtrl.dispose(); - super.dispose(); - } - - Future _submit() async { - if (!_formKey.currentState!.validate()) return; - - final email = _emailCtrl.text.trim(); - final pass = _passCtrl.text; - - // Referenz VOR await holen - final authNotifier = ref.read(authProvider.notifier); - - if (_registerMode) { - await authNotifier.register(email, pass); - } else { - await authNotifier.login(email, pass); - } - - // Nach await: zuerst prüfen, ob der Screen noch lebt - if (!mounted) return; - - final authState = ref.read(authProvider); - if (authState.error != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(authState.error!)), - ); - } - // Navigation macht das GoRouter-redirect automatisch, nichts weiter nötig. - } - - @override - Widget build(BuildContext context) { - final auth = ref.watch(authProvider); - final loading = auth.loading; - - return Scaffold( - body: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 460), - child: Padding( - padding: const EdgeInsets.all(24), - child: Material( - elevation: 2, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(24), - child: Form( - key: _formKey, - child: FocusTraversalGroup( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - Icon(Icons.pets, - size: 48, - color: Theme.of(context).colorScheme.primary), - const SizedBox(height: 12), - Text( - _registerMode ? 'Neu registrieren' : 'Anmelden', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 16), - - // Fehlermeldung aus dem State (optional) - if (auth.error != null && auth.error!.isNotEmpty) ...[ - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: Colors.red.withOpacity(0.08), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.red.withOpacity(0.25)), - ), - child: Text(auth.error!, - style: const TextStyle(color: Colors.red)), - ), - const SizedBox(height: 12), - ], - - TextFormField( - controller: _emailCtrl, - autofocus: true, - enabled: !loading, - decoration: const InputDecoration( - labelText: 'E-Mail', - hintText: 'name@beispiel.at', - prefixIcon: Icon(Icons.mail), - ), - keyboardType: TextInputType.emailAddress, - autofillHints: const [ - AutofillHints.username, - AutofillHints.email - ], - validator: (v) { - final t = (v ?? '').trim(); - if (t.isEmpty) return 'Bitte E-Mail eingeben'; - if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(t)) - return 'Ungültige E-Mail'; - return null; - }, - onFieldSubmitted: (_) => _submit(), - ), - const SizedBox(height: 12), - - TextFormField( - controller: _passCtrl, - enabled: !loading, - obscureText: _obscure, - decoration: InputDecoration( - labelText: 'Passwort', - prefixIcon: const Icon(Icons.lock), - suffixIcon: IconButton( - tooltip: _obscure - ? 'Passwort anzeigen' - : 'Passwort verbergen', - icon: Icon(_obscure - ? Icons.visibility - : Icons.visibility_off), - onPressed: () => - setState(() => _obscure = !_obscure), - ), - ), - autofillHints: const [AutofillHints.password], - inputFormatters: [ - FilteringTextInputFormatter.singleLineFormatter - ], - onFieldSubmitted: (_) => _submit(), - validator: (v) { - if ((v ?? '').isEmpty) - return 'Bitte Passwort eingeben'; - if (_registerMode && (v!.length < 6)) - return 'Mindestens 6 Zeichen'; - return null; - }, - ), - const SizedBox(height: 20), - - SizedBox( - width: double.infinity, - height: 48, - child: FilledButton( - onPressed: loading ? null : _submit, - child: loading - ? const SizedBox( - width: 22, - height: 22, - child: CircularProgressIndicator( - strokeWidth: 2)) - : Text(_registerMode - ? 'Registrieren' - : 'Anmelden'), - ), - ), - const SizedBox(height: 10), - - TextButton.icon( - onPressed: loading - ? null - : () { - setState( - () => _registerMode = !_registerMode); - }, - icon: const Icon(Icons.person_add_alt), - label: Text(_registerMode - ? 'Schon ein Konto? Jetzt anmelden' - : 'Neu registrieren'), - ), - - const SizedBox(height: 6), - if (!loading) - TextButton( - onPressed: () { - // Demo/Testzugang automatisch füllen (optional) - _emailCtrl.text = 'test@example.com'; - _passCtrl.text = 'test1234'; - _submit(); - }, - child: const Text('Mit Testkonto fortfahren'), - ), - - const SizedBox(height: 8), - ], - ), - ), - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/features/hedgehogs/data/hedgehog_service.dart b/lib/features/hedgehogs/data/hedgehog_service.dart deleted file mode 100644 index 4386d09..0000000 --- a/lib/features/hedgehogs/data/hedgehog_service.dart +++ /dev/null @@ -1,205 +0,0 @@ -// lib/features/hedgehogs/data/hedgehog_service.dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:dio/dio.dart'; -import 'package:http_parser/http_parser.dart'; - -import '../../../core/api_client.dart'; -import '../../auth/auth_controller.dart'; -import '../../hedgehogs/models/hedgehog.dart'; -import '../../measurements/models/measurement.dart'; -import '../../photos/models/photo.dart'; - -/// Low-level Service -final hedgehogServiceProvider = Provider((ref) { - final api = ref.read(apiClientProvider); - return HedgehogService(api); -}); - -/// Igel-Liste: lädt nur wenn eingeloggt (vermeidet 401 vor Login) -final hedgehogsFutureProvider = FutureProvider>((ref) async { - final auth = ref.watch(authProvider); - if (!auth.loggedIn) return const []; - - final svc = ref.read(hedgehogServiceProvider); - final result = await svc.list(); - return List.from(result); -}); - -/// Messungen & Fotos per Igel-ID (AutoDispose, damit Speicher frei wird) -final measurementsFutureProvider = - AutoDisposeFutureProvider.family, int>((ref, hedgehogId) async { - final svc = ref.read(hedgehogServiceProvider); - final list = await svc.listMeasurements(hedgehogId); - return List.from(list); -}); - -final photosFutureProvider = - AutoDisposeFutureProvider.family, int>((ref, hedgehogId) async { - final svc = ref.read(hedgehogServiceProvider); - final list = await svc.listPhotos(hedgehogId); - return List.from(list); -}); - -class HedgehogService { - final ApiClient api; - HedgehogService(this.api); - - /* ---------- Hedgehogs ---------- */ - - Future> list() async { - final res = await api.call('hedgehog.list', {}); - final raw = (res is Map && res['data'] is List) ? res['data'] : (res as List?); - if (raw == null) return Future>.value([]); - - int? _asInt(dynamic v) { - if (v == null) return null; - if (v is int) return v; - if (v is num) return v.toInt(); - if (v is String) return int.tryParse(v); - return null; - } - - final iterable = List.from(raw); - final list = iterable.map((e) { - final m = Map.from(e as Map); - final id = _asInt(m['id']); - if (id != null) m['id'] = id; - return Hedgehog.fromJson(m); - }).toList(); - - return Future>.value(list); - } - - Future create(String name, {String? species, String? notes}) async { - final res = await api.call('hedgehog.create', { - 'name': name, - if (species != null) 'species': species, - if (notes != null) 'notes': notes, - }); - final data = res is Map && res['data'] != null ? res['data'] : res; - return Hedgehog.fromJson(Map.from(data)); - } - - Future delete(int id) async { - await api.call('hedgehog.delete', {'id': id}); - } - - /* ---------- Measurements ---------- */ - - Future> listMeasurements(int hedgehogId) async { - final res = await api.call('meas.list', {'hedgehogId': hedgehogId}); - final raw = (res is Map && res['data'] is List) ? res['data'] : (res as List?); - if (raw == null) return Future>.value([]); - - int? _asInt(dynamic v) { - if (v == null) return null; - if (v is int) return v; - if (v is num) return v.toInt(); - if (v is String) return int.tryParse(v); - return null; - } - - double? _asDouble(dynamic v) { - if (v == null) return null; - if (v is double) return v; - if (v is int) return v.toDouble(); - if (v is num) return v.toDouble(); - if (v is String) return double.tryParse(v); - return null; - } - - final iterable = List.from(raw); - final list = iterable.map((e) { - final m = Map.from(e as Map); - final id = _asInt(m['id']); - if (id != null) m['id'] = id; - final hid = _asInt(m['hedgehogId']); - if (hid != null) m['hedgehogId'] = hid; - final wg = _asInt(m['weightGrams']); - if (wg != null) m['weightGrams'] = wg; - final lm = _asInt(m['lengthMm']); - if (lm != null) m['lengthMm'] = lm; - final tc = _asDouble(m['temperatureC']); - if (tc != null) m['temperatureC'] = tc; - return Measurement.fromJson(m); - }).toList(); - - return Future>.value(list); - } - - Future addMeasurement({ - required int hedgehogId, - required DateTime measuredAt, - int? weightGrams, - int? lengthMm, - double? temperatureC, - String? note, - }) async { - final res = await api.call('meas.create', { - 'hedgehogId': hedgehogId, - 'measuredAt': measuredAt.toIso8601String(), - if (weightGrams != null) 'weightGrams': weightGrams, - if (lengthMm != null) 'lengthMm': lengthMm, - if (temperatureC != null) 'temperatureC': temperatureC, - if (note != null) 'note': note, - }); - final data = res is Map && res['data'] != null ? res['data'] : res; - return Measurement.fromJson(Map.from(data)); - } - - Future deleteMeasurement(int id) async { - await api.call('meas.delete', {'id': id}); - } - - /* ---------- Photos ---------- */ - - Future> listPhotos(int hedgehogId) async { - final res = await api.call('photo.list', {'hedgehogId': hedgehogId}); - final raw = (res is Map && res['data'] is List) ? res['data'] : (res as List?); - if (raw == null) return Future>.value([]); - - int _asInt(dynamic v) { - if (v is int) return v; - if (v is num) return v.toInt(); - if (v is String) return int.tryParse(v) ?? 0; - return 0; - } - - final iterable = List.from(raw); - final list = iterable.map((e) { - final m = Map.from(e as Map); - m['id'] = _asInt(m['id']); - m['size'] = _asInt(m['size']); - if (m.containsKey('created_at') && !m.containsKey('createdAt')) { - m['createdAt'] = m['created_at']; - } - return Photo.fromJson(m); - }).toList(); - - return Future>.value(list); - } - - Future uploadPhoto({ - required int hedgehogId, - required String filename, - required List bytes, - String? contentType, // z. B. image/jpeg - }) async { - final form = FormData.fromMap({ - 'action': 'photo.upload', - 'hedgehogId': hedgehogId, - 'file': MultipartFile.fromBytes( - bytes, - filename: filename, - contentType: contentType != null ? MediaType.parse(contentType) : null, - ), - }); - final resp = await api.dio.post('', data: form); - final data = resp.data['data'] ?? resp.data; - return Photo.fromJson(Map.from(data)); - } - - Future deletePhoto(int id) async { - await api.call('photo.delete', {'id': id}); - } -} diff --git a/lib/features/hedgehogs/models/hedgehog.dart b/lib/features/hedgehogs/models/hedgehog.dart deleted file mode 100644 index 3401f71..0000000 --- a/lib/features/hedgehogs/models/hedgehog.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -part 'hedgehog.freezed.dart'; -part 'hedgehog.g.dart'; - -@freezed -class Hedgehog with _$Hedgehog { - const factory Hedgehog({ - required int id, - required String name, - String? species, - String? notes, - }) = _Hedgehog; - - factory Hedgehog.fromJson(Map json) => - _$HedgehogFromJson(json); -} diff --git a/lib/features/hedgehogs/models/hedgehog.freezed.dart b/lib/features/hedgehogs/models/hedgehog.freezed.dart deleted file mode 100644 index fe66e1a..0000000 --- a/lib/features/hedgehogs/models/hedgehog.freezed.dart +++ /dev/null @@ -1,216 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'hedgehog.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -Hedgehog _$HedgehogFromJson(Map json) { - return _Hedgehog.fromJson(json); -} - -/// @nodoc -mixin _$Hedgehog { - int get id => throw _privateConstructorUsedError; - String get name => throw _privateConstructorUsedError; - String? get species => throw _privateConstructorUsedError; - String? get notes => throw _privateConstructorUsedError; - - /// Serializes this Hedgehog to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Hedgehog - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $HedgehogCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $HedgehogCopyWith<$Res> { - factory $HedgehogCopyWith(Hedgehog value, $Res Function(Hedgehog) then) = - _$HedgehogCopyWithImpl<$Res, Hedgehog>; - @useResult - $Res call({int id, String name, String? species, String? notes}); -} - -/// @nodoc -class _$HedgehogCopyWithImpl<$Res, $Val extends Hedgehog> - implements $HedgehogCopyWith<$Res> { - _$HedgehogCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of Hedgehog - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? name = null, - Object? species = freezed, - Object? notes = freezed, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as int, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - species: freezed == species - ? _value.species - : species // ignore: cast_nullable_to_non_nullable - as String?, - notes: freezed == notes - ? _value.notes - : notes // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$HedgehogImplCopyWith<$Res> - implements $HedgehogCopyWith<$Res> { - factory _$$HedgehogImplCopyWith( - _$HedgehogImpl value, $Res Function(_$HedgehogImpl) then) = - __$$HedgehogImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({int id, String name, String? species, String? notes}); -} - -/// @nodoc -class __$$HedgehogImplCopyWithImpl<$Res> - extends _$HedgehogCopyWithImpl<$Res, _$HedgehogImpl> - implements _$$HedgehogImplCopyWith<$Res> { - __$$HedgehogImplCopyWithImpl( - _$HedgehogImpl _value, $Res Function(_$HedgehogImpl) _then) - : super(_value, _then); - - /// Create a copy of Hedgehog - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? name = null, - Object? species = freezed, - Object? notes = freezed, - }) { - return _then(_$HedgehogImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as int, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - species: freezed == species - ? _value.species - : species // ignore: cast_nullable_to_non_nullable - as String?, - notes: freezed == notes - ? _value.notes - : notes // ignore: cast_nullable_to_non_nullable - as String?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$HedgehogImpl implements _Hedgehog { - const _$HedgehogImpl( - {required this.id, required this.name, this.species, this.notes}); - - factory _$HedgehogImpl.fromJson(Map json) => - _$$HedgehogImplFromJson(json); - - @override - final int id; - @override - final String name; - @override - final String? species; - @override - final String? notes; - - @override - String toString() { - return 'Hedgehog(id: $id, name: $name, species: $species, notes: $notes)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$HedgehogImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.name, name) || other.name == name) && - (identical(other.species, species) || other.species == species) && - (identical(other.notes, notes) || other.notes == notes)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, id, name, species, notes); - - /// Create a copy of Hedgehog - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$HedgehogImplCopyWith<_$HedgehogImpl> get copyWith => - __$$HedgehogImplCopyWithImpl<_$HedgehogImpl>(this, _$identity); - - @override - Map toJson() { - return _$$HedgehogImplToJson( - this, - ); - } -} - -abstract class _Hedgehog implements Hedgehog { - const factory _Hedgehog( - {required final int id, - required final String name, - final String? species, - final String? notes}) = _$HedgehogImpl; - - factory _Hedgehog.fromJson(Map json) = - _$HedgehogImpl.fromJson; - - @override - int get id; - @override - String get name; - @override - String? get species; - @override - String? get notes; - - /// Create a copy of Hedgehog - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$HedgehogImplCopyWith<_$HedgehogImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/features/hedgehogs/models/hedgehog.g.dart b/lib/features/hedgehogs/models/hedgehog.g.dart deleted file mode 100644 index 1209dbe..0000000 --- a/lib/features/hedgehogs/models/hedgehog.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'hedgehog.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$HedgehogImpl _$$HedgehogImplFromJson(Map json) => - _$HedgehogImpl( - id: (json['id'] as num).toInt(), - name: json['name'] as String, - species: json['species'] as String?, - notes: json['notes'] as String?, - ); - -Map _$$HedgehogImplToJson(_$HedgehogImpl instance) => - { - 'id': instance.id, - 'name': instance.name, - 'species': instance.species, - 'notes': instance.notes, - }; diff --git a/lib/features/hedgehogs/ui/dashboard_screen.dart b/lib/features/hedgehogs/ui/dashboard_screen.dart deleted file mode 100644 index 193f101..0000000 --- a/lib/features/hedgehogs/ui/dashboard_screen.dart +++ /dev/null @@ -1,215 +0,0 @@ -// lib/features/hedgehogs/ui/dashboard_screen.dart -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../auth/auth_controller.dart'; -import '../data/hedgehog_service.dart'; -import '../models/hedgehog.dart'; - -class DashboardScreen extends ConsumerWidget { - const DashboardScreen({super.key}); - - String _fmtMySql(DateTime dt, {bool endOfDay = false}) { - final d = DateTime( - dt.year, - dt.month, - dt.day, - endOfDay ? 23 : 0, - endOfDay ? 59 : 0, - endOfDay ? 59 : 0, - ); - String two(int v) => v.toString().padLeft(2, '0'); - return '${d.year}-${two(d.month)}-${two(d.day)} ${two(d.hour)}:${two(d.minute)}:${two(d.second)}'; - } - - Uri _exportAllUrl({DateTime? from, DateTime? to}) { - final params = {'action': 'meas.export_all_csv'}; - if (from != null) params['from'] = _fmtMySql(from, endOfDay: false); - if (to != null) params['to'] = _fmtMySql(to, endOfDay: true); - return Uri.https('api.windesign.at', '/hedgehog.php', params); - } - - Future _exportAll(BuildContext context) async { - final ok = await launchUrl(_exportAllUrl(), mode: LaunchMode.externalApplication); - if (!ok && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Konnte Export-URL nicht öffnen')), - ); - } - } - - Future _exportAllFiltered(BuildContext context) async { - DateTime? from; - DateTime? to; - - from = await showDatePicker( - context: context, - firstDate: DateTime(2000), - lastDate: DateTime.now().add(const Duration(days: 365)), - initialDate: DateTime.now().subtract(const Duration(days: 7)), - helpText: 'Von-Datum wählen', - ); - if (!context.mounted) return; - - to = await showDatePicker( - context: context, - firstDate: DateTime(2000), - lastDate: DateTime.now().add(const Duration(days: 365)), - initialDate: DateTime.now(), - helpText: 'Bis-Datum wählen', - ); - if (!context.mounted) return; - - if (from == null && to == null) return; - - final ok = await launchUrl(_exportAllUrl(from: from, to: to), mode: LaunchMode.externalApplication); - if (!ok && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Konnte Export-URL nicht öffnen')), - ); - } - } - - Future _createHedgehogDialog(BuildContext context, WidgetRef ref) async { - final service = ref.read(hedgehogServiceProvider); - final nameCtrl = TextEditingController(); - - final ok = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Neuer Igel'), - content: TextField( - controller: nameCtrl, - decoration: const InputDecoration(labelText: 'Name', hintText: 'z. B. Frida'), - textInputAction: TextInputAction.done, - onSubmitted: (_) => Navigator.of(ctx).pop(true), - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Abbrechen')), - FilledButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Speichern')), - ], - ), - ); - - if (ok == true && nameCtrl.text.trim().isNotEmpty) { - try { - await service.create(nameCtrl.text.trim()); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Igel angelegt.'))); - ref.invalidate(hedgehogsFutureProvider); // Liste neu laden - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Fehler beim Anlegen: $e'))); - } - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final auth = ref.watch(authProvider); - final asyncHogs = ref.watch(hedgehogsFutureProvider); - - return Scaffold( - appBar: AppBar( - title: Text('Meine Igel${auth.email != null ? " (${auth.email})" : ""}'), - actions: [ - IconButton( - tooltip: 'Alle CSV exportieren', - onPressed: () => _exportAll(context), - icon: const Icon(Icons.file_download), - ), - IconButton( - tooltip: 'Export (Datum filtern)', - onPressed: () => _exportAllFiltered(context), - icon: const Icon(Icons.calendar_today), - ), - IconButton( - tooltip: 'Abmelden', - onPressed: () async { - await ref.read(authProvider.notifier).logout(); - if (!context.mounted) return; - context.go('/login'); - }, - icon: const Icon(Icons.logout), - ), - ], - ), - body: RefreshIndicator( - onRefresh: () async { - ref.invalidate(hedgehogsFutureProvider); - await Future.delayed(const Duration(milliseconds: 150)); - }, - child: asyncHogs.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (err, _) => ListView( - children: [ - const SizedBox(height: 48), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text('Fehler beim Laden:\n$err', textAlign: TextAlign.center), - ), - ), - const SizedBox(height: 12), - Center( - child: FilledButton.icon( - onPressed: () => ref.invalidate(hedgehogsFutureProvider), - icon: const Icon(Icons.refresh), - label: const Text('Erneut versuchen'), - ), - ), - ], - ), - data: (list) { - if (list.isEmpty) { - return ListView( - children: [ - const SizedBox(height: 80), - const Icon(Icons.pets, size: 72), - const SizedBox(height: 12), - const Center( - child: Text( - 'Noch keine Igel angelegt.', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), - ), - ), - const SizedBox(height: 8), - Center( - child: TextButton.icon( - onPressed: () => _createHedgehogDialog(context, ref), - icon: const Icon(Icons.add), - label: const Text('Jetzt ersten Igel anlegen'), - ), - ), - ], - ); - } - - return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: list.length, - separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (context, i) { - final h = list[i]; - return ListTile( - leading: const Icon(Icons.pets), - title: Text(h.name), - subtitle: Text(h.species ?? '—'), - trailing: const Icon(Icons.chevron_right), - onTap: () => context.push('/hedgehogs/${h.id}'), - ); - }, - ); - }, - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _createHedgehogDialog(context, ref), - tooltip: 'Neuer Igel', - child: const Icon(Icons.add), - ), - ); - } -} diff --git a/lib/features/hedgehogs/ui/detail_screen.dart b/lib/features/hedgehogs/ui/detail_screen.dart deleted file mode 100644 index fee2e18..0000000 --- a/lib/features/hedgehogs/ui/detail_screen.dart +++ /dev/null @@ -1,567 +0,0 @@ -// lib/features/hedgehogs/ui/detail_screen.dart -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:dio/dio.dart'; - -import '../data/hedgehog_service.dart'; -import '../../../core/api_client.dart'; -import '../../measurements/models/measurement.dart'; -import '../../photos/models/photo.dart'; - -class HedgehogDetailScreen extends ConsumerWidget { - final int id; - const HedgehogDetailScreen({super.key, required this.id}); - - /* ---------------- CSV Export/Import ---------------- */ - - Uri _exportCsvUrl(int hedgehogId) => - Uri.parse('https://api.windesign.at/hedgehog.php?action=meas.export_csv&hedgehogId=$hedgehogId'); - - Future _doExport(BuildContext context) async { - final ok = await launchUrl(_exportCsvUrl(id), mode: LaunchMode.externalApplication); - if (!ok && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Konnte Export-URL nicht öffnen')), - ); - } - } - - Future _doImport(BuildContext context, WidgetRef ref) async { - final picked = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['csv'], - withData: true, - ); - if (picked == null || picked.files.isEmpty) return; - - final file = picked.files.single; - if (file.bytes == null) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Datei konnte nicht gelesen werden')), - ); - return; - } - - try { - final dio = ref.read(apiClientProvider).dio; - final form = FormData.fromMap({ - 'action': 'meas.import_csv', - 'hedgehogId': id, - 'file': MultipartFile.fromBytes(file.bytes!, filename: file.name), - }); - - final resp = await dio.post('', data: form); - if (!context.mounted) return; - final data = resp.data['data'] ?? {}; - final imported = data['imported'] ?? 0; - final updated = data['updated'] ?? 0; - final skipped = data['skipped'] ?? 0; - final errors = data['errors'] ?? 0; - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Import: $imported importiert, $updated aktualisiert, $skipped überspr., $errors Fehler')), - ); - - // Nach Import: Messungen neu laden - ref.invalidate(measurementsFutureProvider(id)); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Import-Fehler: $e')), - ); - } - } - - /* ---------------- Messungen anlegen/löschen ---------------- */ - - Future _addMeasurementDialog(BuildContext context, WidgetRef ref) async { - final svc = ref.read(hedgehogServiceProvider); - final df = DateFormat('dd.MM.yyyy HH:mm'); - - DateTime when = DateTime.now(); - final weightCtrl = TextEditingController(); - final lengthCtrl = TextEditingController(); - final tempCtrl = TextEditingController(); - final noteCtrl = TextEditingController(); - - final ok = await showDialog( - context: context, - builder: (ctx) => StatefulBuilder( - builder: (ctx, setDialog) => AlertDialog( - title: const Text('Messung hinzufügen'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.event), - title: Text(df.format(when)), - trailing: TextButton( - onPressed: () async { - final d = await showDatePicker( - context: context, - firstDate: DateTime(2000), - lastDate: DateTime.now().add(const Duration(days: 1)), - initialDate: when, - ); - if (d == null) return; - final t = await showTimePicker( - context: context, - initialTime: TimeOfDay.fromDateTime(when), - ); - if (t == null) return; - when = DateTime(d.year, d.month, d.day, t.hour, t.minute); - setDialog(() {}); - }, - child: const Text('ändern'), - ), - ), - const SizedBox(height: 8), - TextField( - controller: weightCtrl, - decoration: const InputDecoration(labelText: 'Gewicht (g)', hintText: 'z. B. 450'), - keyboardType: TextInputType.number, - ), - const SizedBox(height: 8), - TextField( - controller: lengthCtrl, - decoration: const InputDecoration(labelText: 'Länge (mm)', hintText: 'z. B. 180'), - keyboardType: TextInputType.number, - ), - const SizedBox(height: 8), - TextField( - controller: tempCtrl, - decoration: const InputDecoration(labelText: 'Temperatur (°C)', hintText: 'z. B. 36.5'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 8), - TextField( - controller: noteCtrl, - decoration: const InputDecoration(labelText: 'Notiz'), - maxLines: 3, - ), - ], - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Abbrechen')), - FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Speichern')), - ], - ), - ), - ); - - if (ok == true) { - try { - int? w = int.tryParse(weightCtrl.text.trim()); - int? l = int.tryParse(lengthCtrl.text.trim()); - double? t = double.tryParse(tempCtrl.text.trim()); - await svc.addMeasurement( - hedgehogId: id, - measuredAt: when, - weightGrams: w, - lengthMm: l, - temperatureC: t, - note: noteCtrl.text.trim().isEmpty ? null : noteCtrl.text.trim(), - ); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Messung gespeichert.'))); - ref.invalidate(measurementsFutureProvider(id)); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Fehler: $e'))); - } - } - } - - Future _deleteMeasurement(BuildContext context, WidgetRef ref, Measurement m) async { - final df = DateFormat('dd.MM.yyyy HH:mm'); - final ok = await showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Messung löschen?'), - content: Text('${df.format(m.measuredAt)}\nGewicht: ${m.weightGrams ?? '-'} g'), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Abbrechen')), - FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Löschen')), - ], - ), - ); - if (ok == true) { - try { - await ref.read(hedgehogServiceProvider).deleteMeasurement(m.id); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Gelöscht.'))); - ref.invalidate(measurementsFutureProvider(id)); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Fehler: $e'))); - } - } - } - - /* ---------------- Fotos Upload/Löschen ---------------- */ - - Future _uploadPhoto(BuildContext context, WidgetRef ref) async { - final picked = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['jpg', 'jpeg', 'png', 'webp'], - withData: true, - ); - if (picked == null || picked.files.isEmpty) return; - - final f = picked.files.single; - final bytes = f.bytes; - if (bytes == null) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Datei konnte nicht gelesen werden'))); - return; - } - - try { - await ref.read(hedgehogServiceProvider).uploadPhoto( - hedgehogId: id, - filename: f.name, - bytes: bytes, - ); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Foto hochgeladen.'))); - ref.invalidate(photosFutureProvider(id)); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Upload-Fehler: $e'))); - } - } - - Future _deletePhoto(BuildContext context, WidgetRef ref, Photo p) async { - final ok = await showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Foto löschen?'), - content: Text('Datei: ${p.filename}'), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Abbrechen')), - FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Löschen')), - ], - ), - ); - if (ok == true) { - try { - await ref.read(hedgehogServiceProvider).deletePhoto(p.id); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Foto gelöscht.'))); - ref.invalidate(photosFutureProvider(id)); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Fehler: $e'))); - } - } - } - - /* ---------------- Chart ---------------- */ - - Widget _buildChart(List data) { - final df = DateFormat('dd.MM.'); - final raw = data.where((m) => m.weightGrams != null).toList(); - if (raw.isEmpty) return const Center(child: Text('Keine Gewichts-Daten für den Chart.')); - - final points = raw - .map((m) => FlSpot( - m.measuredAt.millisecondsSinceEpoch.toDouble(), - m.weightGrams!.toDouble(), - )) - .toList() - ..sort((a, b) => a.x.compareTo(b.x)); - - final minX = points.first.x; - final maxX = points.last.x; - final rangeX = (maxX - minX).abs(); - final desiredXTicks = 5; - final intervalX = rangeX == 0 ? 1.0 : rangeX / desiredXTicks; - - double minY = points.map((e) => e.y).reduce((a, b) => a < b ? a : b); - double maxY = points.map((e) => e.y).reduce((a, b) => a > b ? a : b); - if (minY == maxY) { - minY -= 10; - maxY += 10; - } else { - final pad = (maxY - minY) * 0.1; - minY -= pad; - maxY += pad; - } - - String _fmtDate(double x) => df.format(DateTime.fromMillisecondsSinceEpoch(x.toInt())); - - return InteractiveViewer( - minScale: 0.9, - maxScale: 4, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), - child: LineChart( - LineChartData( - minX: minX, maxX: maxX, minY: minY, maxY: maxY, - clipData: const FlClipData.all(), - lineTouchData: const LineTouchData(enabled: false), - titlesData: FlTitlesData( - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, reservedSize: 36, interval: intervalX, - getTitlesWidget: (value, meta) => Padding( - padding: const EdgeInsets.only(top: 4), - child: Text(_fmtDate(value), style: const TextStyle(fontSize: 10)), - ), - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, reservedSize: 44, - interval: ((maxY - minY) / 4).clamp(1, 1000).toDouble(), - getTitlesWidget: (value, meta) => Text('${value.toInt()} g', style: const TextStyle(fontSize: 10)), - ), - ), - rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - ), - gridData: const FlGridData(show: true, drawVerticalLine: true, drawHorizontalLine: true), - borderData: FlBorderData(show: true), - lineBarsData: [ - LineChartBarData(isCurved: true, spots: points, dotData: const FlDotData(show: false), barWidth: 3), - ], - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final df = DateFormat('dd.MM.yyyy HH:mm'); - final measAsync = ref.watch(measurementsFutureProvider(id)); - final photosAsync = ref.watch(photosFutureProvider(id)); - - return Scaffold( - appBar: AppBar( - title: Text('Igel #$id – Messungen & Fotos'), - actions: [ - IconButton( - tooltip: 'CSV exportieren', - onPressed: () => _doExport(context), - icon: const Icon(Icons.download), - ), - IconButton( - tooltip: 'CSV importieren', - onPressed: () => _doImport(context, ref), - icon: const Icon(Icons.upload), - ), - IconButton( - tooltip: 'Foto hochladen', - onPressed: () => _uploadPhoto(context, ref), - icon: const Icon(Icons.photo_camera), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _addMeasurementDialog(context, ref), - child: const Icon(Icons.add), - ), - body: RefreshIndicator( - onRefresh: () async { - ref.invalidate(measurementsFutureProvider(id)); - ref.invalidate(photosFutureProvider(id)); - await Future.delayed(const Duration(milliseconds: 200)); - }, - child: ListView( - children: [ - // Messungen - measAsync.when( - loading: () => const SizedBox( - height: 260, - child: Center(child: CircularProgressIndicator()), - ), - error: (e, _) => Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Text('Fehler (Messungen): $e'), - const SizedBox(height: 8), - FilledButton.icon( - onPressed: () => ref.invalidate(measurementsFutureProvider(id)), - icon: const Icon(Icons.refresh), - label: const Text('Erneut laden'), - ), - ], - ), - ), - data: (meas) { - final sorted = meas.toList()..sort((a, b) => a.measuredAt.compareTo(b.measuredAt)); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 260, child: _buildChart(sorted)), - const Divider(height: 1), - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), - child: Text('Messungen (${sorted.length})', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), - ), - ...sorted.reversed.map((m) => Dismissible( - key: ValueKey(m.id), - background: Container( - color: Colors.red.withOpacity(0.1), - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 16), - child: const Icon(Icons.delete, color: Colors.red), - ), - direction: DismissDirection.endToStart, - confirmDismiss: (_) async { - await _deleteMeasurement(context, ref, m); - return false; // wir invalidieren selbst - }, - child: ListTile( - leading: const Icon(Icons.stacked_line_chart), - title: Text(df.format(m.measuredAt)), - subtitle: Text([ - if (m.weightGrams != null) 'Gewicht: ${m.weightGrams} g', - if (m.lengthMm != null) 'Länge: ${m.lengthMm} mm', - if (m.temperatureC != null) 'Temp: ${m.temperatureC!.toStringAsFixed(1)} °C', - if (m.note != null && m.note!.isNotEmpty) 'Notiz: ${m.note}', - ].join(' ')), - ), - )), - const SizedBox(height: 16), - ], - ); - }, - ), - - const Divider(height: 1), - - // Fotos - photosAsync.when( - loading: () => const Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ), - error: (e, _) => Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Text('Fehler (Fotos): $e'), - const SizedBox(height: 8), - FilledButton.icon( - onPressed: () => ref.invalidate(photosFutureProvider(id)), - icon: const Icon(Icons.refresh), - label: const Text('Erneut laden'), - ), - ], - ), - ), - data: (photos) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - child: Row( - children: [ - const Icon(Icons.photo_library), - const SizedBox(width: 8), - Text('Fotos (${photos.length})', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), - const Spacer(), - TextButton.icon( - onPressed: () => _uploadPhoto(context, ref), - icon: const Icon(Icons.add_a_photo), - label: const Text('Foto hinzufügen'), - ), - ], - ), - ), - if (photos.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text('Noch keine Fotos hochgeladen.'), - ) - else - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: LayoutBuilder( - builder: (ctx, c) { - final w = c.maxWidth; - final cross = w > 900 ? 5 : w > 700 ? 4 : w > 500 ? 3 : 2; - return GridView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: photos.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: cross, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), - itemBuilder: (ctx, i) { - final p = photos[i]; - return InkWell( - onTap: () async { - final uri = Uri.parse(p.url); - await launchUrl(uri, mode: LaunchMode.externalApplication); - }, - onLongPress: () => _deletePhoto(context, ref, p), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Stack( - fit: StackFit.expand, - children: [ - Image.network( - p.url, - fit: BoxFit.cover, - loadingBuilder: (context, child, progress) { - if (progress == null) return child; - return const Center(child: CircularProgressIndicator()); - }, - errorBuilder: (_, __, ___) => Container( - color: Colors.grey.shade200, - alignment: Alignment.center, - child: const Icon(Icons.broken_image), - ), - ), - Positioned( - right: 4, - top: 4, - child: Container( - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(6), - ), - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - child: Text( - (p.mime.split('/').last).toUpperCase(), - style: const TextStyle(color: Colors.white, fontSize: 10), - ), - ), - ), - ], - ), - ), - ); - }, - ); - }, - ), - ), - const SizedBox(height: 24), - ], - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/igel/data/igel_repository.dart b/lib/features/igel/data/igel_repository.dart new file mode 100644 index 0000000..c78a340 --- /dev/null +++ b/lib/features/igel/data/igel_repository.dart @@ -0,0 +1,26 @@ +import 'package:hedgehog/shared/api_client.dart'; +import 'package:hedgehog/features/igel/domain/igel.dart'; + +class IgelRepository { + final ApiClient api; + IgelRepository(this.api); + + Future> list() async { + final res = await api.get('/igel'); + final list = (res as List).cast>(); + return list.map(Igel.fromMap).toList(); + } + + Future create(String name, {String? note}) async { + final res = await api.post('/igel', {'name': name, 'note': note}); + return Igel.fromMap(res); + } + + Future update(int id, String name, {String? note}) async { + await api.put('/igel/$id', {'name': name, 'note': note}); + } + + Future delete(int id) async { + await api.delete('/igel/$id'); + } +} diff --git a/lib/features/igel/domain/igel.dart b/lib/features/igel/domain/igel.dart new file mode 100644 index 0000000..7457d23 --- /dev/null +++ b/lib/features/igel/domain/igel.dart @@ -0,0 +1,10 @@ +class Igel { + final int id; + final String name; + final String? note; + Igel({required this.id, required this.name, this.note}); + factory Igel.fromMap(Map m) => Igel( + id: m['id'] as int, + name: m['name'] as String, + note: m['note'] as String?); +} diff --git a/lib/features/igel/presentation/igel_list_screen.dart b/lib/features/igel/presentation/igel_list_screen.dart new file mode 100644 index 0000000..01786ab --- /dev/null +++ b/lib/features/igel/presentation/igel_list_screen.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hedgehog/main.dart'; +import 'package:hedgehog/features/igel/data/igel_repository.dart'; +import 'package:hedgehog/features/igel/domain/igel.dart'; + +class IgelListScreen extends ConsumerStatefulWidget { + const IgelListScreen({super.key}); + @override + ConsumerState createState() => _State(); +} + +class _State extends ConsumerState { + late final IgelRepository repo; + List items = []; + bool busy = true; + String? err; + @override + void initState() { + super.initState(); + repo = ref.read(igelRepoProvider); + _load(); + } + + Future _load() async { + setState(() => busy = true); + try { + items = await repo.list(); + err = null; + } catch (e) { + err = e.toString(); + } finally { + if (mounted) setState(() => busy = false); + } + } + + Future _create() async { + final name = await showDialog( + context: context, builder: (_) => const _NewIgelDialog()); + if (name != null && name.isNotEmpty) { + await repo.create(name); + await _load(); + } + } + + @override + Widget build(BuildContext ctx) { + return Scaffold( + appBar: AppBar(title: const Text('Meine Igel')), + floatingActionButton: FloatingActionButton( + onPressed: _create, child: const Icon(Icons.add)), + body: busy + ? const Center(child: CircularProgressIndicator()) + : err != null + ? Center(child: Text(err!)) + : ListView.separated( + itemCount: items.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (_, i) { + final x = items[i]; + return ListTile( + title: Text(x.name), + subtitle: x.note != null ? Text(x.note!) : null, + trailing: PopupMenuButton( + itemBuilder: (_) => [ + const PopupMenuItem( + value: 'edit', child: Text('Bearbeiten')), + const PopupMenuItem( + value: 'del', child: Text('Löschen')), + ], + onSelected: (v) async { + if (v == 'del') { + await repo.delete(x.id); + await _load(); + } + if (v == 'edit') { + final name = await showDialog( + context: context, + builder: (_) => + _NewIgelDialog(initial: x.name)); + if (name != null) { + await repo.update(x.id, name); + await _load(); + } + } + }), + ); + }, + ), + ); + } +} + +class _NewIgelDialog extends StatefulWidget { + final String? initial; + const _NewIgelDialog({this.initial}); + @override + State<_NewIgelDialog> createState() => _NewIgelDialogState(); +} + +class _NewIgelDialogState extends State<_NewIgelDialog> { + late final TextEditingController c; + @override + void initState() { + super.initState(); + c = TextEditingController(text: widget.initial ?? ''); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(widget.initial == null ? 'Neuer Igel' : 'Igel bearbeiten'), + content: TextField( + controller: c, + decoration: const InputDecoration(labelText: 'Name')), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Abbrechen')), + FilledButton( + onPressed: () => Navigator.pop(context, c.text.trim()), + child: const Text('Speichern')), + ]); + } +} diff --git a/lib/features/measurements/models/measurement.dart b/lib/features/measurements/models/measurement.dart deleted file mode 100644 index c74c68b..0000000 --- a/lib/features/measurements/models/measurement.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -part 'measurement.freezed.dart'; -part 'measurement.g.dart'; - -@freezed -class Measurement with _$Measurement { - const factory Measurement({ - required int id, - required int hedgehogId, - required DateTime measuredAt, - int? weightGrams, - int? lengthMm, - double? temperatureC, - String? note, - }) = _Measurement; - - factory Measurement.fromJson(Map json) => - _$MeasurementFromJson(json); -} diff --git a/lib/features/measurements/models/measurement.freezed.dart b/lib/features/measurements/models/measurement.freezed.dart deleted file mode 100644 index 1264125..0000000 --- a/lib/features/measurements/models/measurement.freezed.dart +++ /dev/null @@ -1,294 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'measurement.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -Measurement _$MeasurementFromJson(Map json) { - return _Measurement.fromJson(json); -} - -/// @nodoc -mixin _$Measurement { - int get id => throw _privateConstructorUsedError; - int get hedgehogId => throw _privateConstructorUsedError; - DateTime get measuredAt => throw _privateConstructorUsedError; - int? get weightGrams => throw _privateConstructorUsedError; - int? get lengthMm => throw _privateConstructorUsedError; - double? get temperatureC => throw _privateConstructorUsedError; - String? get note => throw _privateConstructorUsedError; - - /// Serializes this Measurement to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Measurement - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $MeasurementCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $MeasurementCopyWith<$Res> { - factory $MeasurementCopyWith( - Measurement value, $Res Function(Measurement) then) = - _$MeasurementCopyWithImpl<$Res, Measurement>; - @useResult - $Res call( - {int id, - int hedgehogId, - DateTime measuredAt, - int? weightGrams, - int? lengthMm, - double? temperatureC, - String? note}); -} - -/// @nodoc -class _$MeasurementCopyWithImpl<$Res, $Val extends Measurement> - implements $MeasurementCopyWith<$Res> { - _$MeasurementCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of Measurement - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? hedgehogId = null, - Object? measuredAt = null, - Object? weightGrams = freezed, - Object? lengthMm = freezed, - Object? temperatureC = freezed, - Object? note = freezed, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as int, - hedgehogId: null == hedgehogId - ? _value.hedgehogId - : hedgehogId // ignore: cast_nullable_to_non_nullable - as int, - measuredAt: null == measuredAt - ? _value.measuredAt - : measuredAt // ignore: cast_nullable_to_non_nullable - as DateTime, - weightGrams: freezed == weightGrams - ? _value.weightGrams - : weightGrams // ignore: cast_nullable_to_non_nullable - as int?, - lengthMm: freezed == lengthMm - ? _value.lengthMm - : lengthMm // ignore: cast_nullable_to_non_nullable - as int?, - temperatureC: freezed == temperatureC - ? _value.temperatureC - : temperatureC // ignore: cast_nullable_to_non_nullable - as double?, - note: freezed == note - ? _value.note - : note // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$MeasurementImplCopyWith<$Res> - implements $MeasurementCopyWith<$Res> { - factory _$$MeasurementImplCopyWith( - _$MeasurementImpl value, $Res Function(_$MeasurementImpl) then) = - __$$MeasurementImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {int id, - int hedgehogId, - DateTime measuredAt, - int? weightGrams, - int? lengthMm, - double? temperatureC, - String? note}); -} - -/// @nodoc -class __$$MeasurementImplCopyWithImpl<$Res> - extends _$MeasurementCopyWithImpl<$Res, _$MeasurementImpl> - implements _$$MeasurementImplCopyWith<$Res> { - __$$MeasurementImplCopyWithImpl( - _$MeasurementImpl _value, $Res Function(_$MeasurementImpl) _then) - : super(_value, _then); - - /// Create a copy of Measurement - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? hedgehogId = null, - Object? measuredAt = null, - Object? weightGrams = freezed, - Object? lengthMm = freezed, - Object? temperatureC = freezed, - Object? note = freezed, - }) { - return _then(_$MeasurementImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as int, - hedgehogId: null == hedgehogId - ? _value.hedgehogId - : hedgehogId // ignore: cast_nullable_to_non_nullable - as int, - measuredAt: null == measuredAt - ? _value.measuredAt - : measuredAt // ignore: cast_nullable_to_non_nullable - as DateTime, - weightGrams: freezed == weightGrams - ? _value.weightGrams - : weightGrams // ignore: cast_nullable_to_non_nullable - as int?, - lengthMm: freezed == lengthMm - ? _value.lengthMm - : lengthMm // ignore: cast_nullable_to_non_nullable - as int?, - temperatureC: freezed == temperatureC - ? _value.temperatureC - : temperatureC // ignore: cast_nullable_to_non_nullable - as double?, - note: freezed == note - ? _value.note - : note // ignore: cast_nullable_to_non_nullable - as String?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$MeasurementImpl implements _Measurement { - const _$MeasurementImpl( - {required this.id, - required this.hedgehogId, - required this.measuredAt, - this.weightGrams, - this.lengthMm, - this.temperatureC, - this.note}); - - factory _$MeasurementImpl.fromJson(Map json) => - _$$MeasurementImplFromJson(json); - - @override - final int id; - @override - final int hedgehogId; - @override - final DateTime measuredAt; - @override - final int? weightGrams; - @override - final int? lengthMm; - @override - final double? temperatureC; - @override - final String? note; - - @override - String toString() { - return 'Measurement(id: $id, hedgehogId: $hedgehogId, measuredAt: $measuredAt, weightGrams: $weightGrams, lengthMm: $lengthMm, temperatureC: $temperatureC, note: $note)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$MeasurementImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.hedgehogId, hedgehogId) || - other.hedgehogId == hedgehogId) && - (identical(other.measuredAt, measuredAt) || - other.measuredAt == measuredAt) && - (identical(other.weightGrams, weightGrams) || - other.weightGrams == weightGrams) && - (identical(other.lengthMm, lengthMm) || - other.lengthMm == lengthMm) && - (identical(other.temperatureC, temperatureC) || - other.temperatureC == temperatureC) && - (identical(other.note, note) || other.note == note)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, id, hedgehogId, measuredAt, - weightGrams, lengthMm, temperatureC, note); - - /// Create a copy of Measurement - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$MeasurementImplCopyWith<_$MeasurementImpl> get copyWith => - __$$MeasurementImplCopyWithImpl<_$MeasurementImpl>(this, _$identity); - - @override - Map toJson() { - return _$$MeasurementImplToJson( - this, - ); - } -} - -abstract class _Measurement implements Measurement { - const factory _Measurement( - {required final int id, - required final int hedgehogId, - required final DateTime measuredAt, - final int? weightGrams, - final int? lengthMm, - final double? temperatureC, - final String? note}) = _$MeasurementImpl; - - factory _Measurement.fromJson(Map json) = - _$MeasurementImpl.fromJson; - - @override - int get id; - @override - int get hedgehogId; - @override - DateTime get measuredAt; - @override - int? get weightGrams; - @override - int? get lengthMm; - @override - double? get temperatureC; - @override - String? get note; - - /// Create a copy of Measurement - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$MeasurementImplCopyWith<_$MeasurementImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/features/measurements/models/measurement.g.dart b/lib/features/measurements/models/measurement.g.dart deleted file mode 100644 index 4953ee2..0000000 --- a/lib/features/measurements/models/measurement.g.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'measurement.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$MeasurementImpl _$$MeasurementImplFromJson(Map json) => - _$MeasurementImpl( - id: (json['id'] as num).toInt(), - hedgehogId: (json['hedgehogId'] as num).toInt(), - measuredAt: DateTime.parse(json['measuredAt'] as String), - weightGrams: (json['weightGrams'] as num?)?.toInt(), - lengthMm: (json['lengthMm'] as num?)?.toInt(), - temperatureC: (json['temperatureC'] as num?)?.toDouble(), - note: json['note'] as String?, - ); - -Map _$$MeasurementImplToJson(_$MeasurementImpl instance) => - { - 'id': instance.id, - 'hedgehogId': instance.hedgehogId, - 'measuredAt': instance.measuredAt.toIso8601String(), - 'weightGrams': instance.weightGrams, - 'lengthMm': instance.lengthMm, - 'temperatureC': instance.temperatureC, - 'note': instance.note, - }; diff --git a/lib/features/photos/models/photo.dart b/lib/features/photos/models/photo.dart deleted file mode 100644 index 5db4a9d..0000000 --- a/lib/features/photos/models/photo.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'photo.g.dart'; - -@JsonSerializable() -class Photo { - final int id; - final String filename; - final String mime; - final int size; - final String? createdAt; - final String url; - - Photo({ - required this.id, - required this.filename, - required this.mime, - required this.size, - required this.url, - this.createdAt, - }); - - factory Photo.fromJson(Map json) => _$PhotoFromJson(json); - Map toJson() => _$PhotoToJson(this); -} diff --git a/lib/features/photos/models/photo.g.dart b/lib/features/photos/models/photo.g.dart deleted file mode 100644 index 7c633f6..0000000 --- a/lib/features/photos/models/photo.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'photo.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Photo _$PhotoFromJson(Map json) => Photo( - id: (json['id'] as num).toInt(), - filename: json['filename'] as String, - mime: json['mime'] as String, - size: (json['size'] as num).toInt(), - url: json['url'] as String, - createdAt: json['createdAt'] as String?, - ); - -Map _$PhotoToJson(Photo instance) => { - 'id': instance.id, - 'filename': instance.filename, - 'mime': instance.mime, - 'size': instance.size, - 'createdAt': instance.createdAt, - 'url': instance.url, - }; diff --git a/lib/main.dart b/lib/main.dart index 0331c99..8578abf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,82 +1,34 @@ -// lib/main.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; +import 'app_router.dart'; +import 'features/auth/data/token_storage.dart'; +import 'shared/api_client.dart'; +import 'features/igel/data/igel_repository.dart'; -import 'features/auth/auth_controller.dart'; -import 'features/auth/ui/login_screen.dart'; -import 'features/hedgehogs/ui/dashboard_screen.dart'; -import 'features/hedgehogs/ui/detail_screen.dart'; +const kApiBase = + String.fromEnvironment('API_BASE', defaultValue: 'https://api.windesign.at/hedgehogs.php?r='); +// String.fromEnvironment('API_BASE', defaultValue: 'https://api.windesign.at/hedgehogs.php'); + +final tokenStorageProvider = + Provider((ref) => TokenStorage(kApiBase)); +final apiClientProvider = Provider((ref) => ApiClient(kApiBase, + getAccessToken: () => + ref.read(tokenStorageProvider).getValidAccessToken())); +final igelRepoProvider = Provider( + (ref) => IgelRepository(ref.read(apiClientProvider))); void main() { - WidgetsFlutterBinding.ensureInitialized(); - runApp(const ProviderScope(child: MyApp())); + runApp(const ProviderScope(child: IgelApp())); } -final routerProvider = Provider((ref) { - final auth = ref.watch(authProvider); - - return GoRouter( - // Wenn du Deep Links möchtest, nimm hier einfach eine feste initialLocation - // initialLocation: '/', - routes: [ - GoRoute( - path: '/login', - name: 'login', - builder: (context, state) => const LoginScreen(), - ), - GoRoute( - path: '/', - name: 'dashboard', - builder: (context, state) => const DashboardScreen(), - routes: [ - GoRoute( - path: 'hedgehogs/:id', - name: 'hedgehogDetail', - builder: (context, state) { - final idStr = state.pathParameters['id'] ?? '0'; - final id = int.tryParse(idStr) ?? 0; - return HedgehogDetailScreen(id: id); - }, - ), - ], - ), - ], - redirect: (context, state) { - final loggingIn = state.matchedLocation == '/login'; - final loggedIn = auth.loggedIn; - - // Während Session-Check: nicht springen - if (auth.loading) return null; - - if (!loggedIn) { - // Nicht eingeloggt → außer Login immer auf /login - return loggingIn ? null : '/login'; - } - - // Eingeloggt → weg von /login auf Dashboard - if (loggingIn) return '/'; - - // sonst keine Umleitung - return null; - }, - ); -}); - -class MyApp extends ConsumerWidget { - const MyApp({super.key}); - +class IgelApp extends ConsumerWidget { + const IgelApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final router = ref.watch(routerProvider); - return MaterialApp.router( - title: 'Hedgehog', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), - useMaterial3: true, - ), - routerConfig: router, + title: 'Igel', + routerConfig: buildRouter(isAuthed: false), // TODO: Token prüfen + theme: ThemeData(useMaterial3: true), ); } } diff --git a/lib/shared/api_client.dart b/lib/shared/api_client.dart new file mode 100644 index 0000000..1641f76 --- /dev/null +++ b/lib/shared/api_client.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class ApiClient { + ApiClient(this.baseUrl, {this.getAccessToken}); + final String baseUrl; // z.B. https://api.example.com + final Future Function()? getAccessToken; + + Future _send(String method, String path, + {Object? body}) async { + final uri = Uri.parse('$baseUrl$path'); + final headers = {'Content-Type': 'application/json'}; + final token = await getAccessToken?.call(); + if (token != null) headers['Authorization'] = 'Bearer $token'; + switch (method) { + case 'GET': + return http.get(uri, headers: headers); + case 'POST': + return http.post(uri, headers: headers, body: jsonEncode(body)); + case 'PUT': + return http.put(uri, headers: headers, body: jsonEncode(body)); + case 'DELETE': + return http.delete(uri, headers: headers); + default: + throw UnimplementedError(method); + } + } + + Future> get(String path) async => + _decode(await _send('GET', path)); + Future> post( + String path, Map body) async => + _decode(await _send('POST', path, body: body)); + Future> put( + String path, Map body) async => + _decode(await _send('PUT', path, body: body)); + Future> delete(String path) async => + _decode(await _send('DELETE', path)); + Map _decode(http.Response r) { + if (r.statusCode >= 200 && r.statusCode < 300) { + if (r.body.isEmpty) return {}; + return jsonDecode(r.body) as Map; + } + throw ApiException(r.statusCode, r.body); + } +} + +class ApiException implements Exception { + final int status; + final String body; + ApiException(this.status, this.body); + @override + String toString() => 'ApiException($status): $body'; +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 7299b5c..d0e7f79 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,10 @@ #include "generated_plugin_registrant.h" -#include -#include +#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); - file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 786ff5c..b29e9ba 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,8 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - file_selector_linux - url_launcher_linux + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4397c25..15a1671 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,18 +5,10 @@ import FlutterMacOS import Foundation -import file_picker -import file_selector_macos +import flutter_secure_storage_macos import path_provider_foundation -import shared_preferences_foundation -import sqflite_darwin -import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) - FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 9da0e8d..987522a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,14 +22,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.0" - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" args: dependency: transitive description: @@ -118,30 +110,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.0" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" characters: dependency: transitive description: @@ -158,14 +126,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" code_builder: dependency: transitive description: @@ -190,22 +150,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - cookie_jar: - dependency: "direct main" - description: - name: cookie_jar - sha256: a6ac027d3ed6ed756bfce8f3ff60cb479e266f3b0fdabd6242b804b6765e52de - url: "https://pub.dev" - source: hosted - version: "4.0.8" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.dev" - source: hosted - version: "0.3.4+2" crypto: dependency: transitive description: @@ -222,38 +166,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" - dio: - dependency: "direct main" - description: - name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 - url: "https://pub.dev" - source: hosted - version: "5.9.0" - dio_cookie_manager: - dependency: "direct main" - description: - name: dio_cookie_manager - sha256: d39c16abcc711c871b7b29bd51c6b5f3059ef39503916c6a9df7e22c4fc595e0 - url: "https://pub.dev" - source: hosted - version: "3.3.0" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - equatable: - dependency: transitive - description: - name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - url: "https://pub.dev" - source: hosted - version: "2.0.7" ffi: dependency: transitive description: @@ -270,46 +182,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - file_picker: - dependency: "direct main" - description: - name: file_picker - sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 - url: "https://pub.dev" - source: hosted - version: "8.3.7" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.dev" - source: hosted - version: "0.9.3+2" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" - url: "https://pub.dev" - source: hosted - version: "0.9.4+2" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.dev" - source: hosted - version: "2.6.2" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" - url: "https://pub.dev" - source: hosted - version: "0.9.3+4" fixnum: dependency: transitive description: @@ -318,43 +190,11 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - fl_chart: - dependency: "direct main" - description: - name: fl_chart - sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb - url: "https://pub.dev" - source: hosted - version: "0.68.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - flutter_lints: - dependency: "direct main" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf" - url: "https://pub.dev" - source: hosted - version: "2.0.26" flutter_riverpod: dependency: "direct main" description: @@ -363,27 +203,59 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" - freezed: - dependency: "direct dev" - description: - name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" - url: "https://pub.dev" - source: hosted - version: "2.5.7" - freezed_annotation: - dependency: "direct main" - description: - name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 - url: "https://pub.dev" - source: hosted - version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -433,93 +305,13 @@ packages: source: hosted version: "3.2.2" http_parser: - dependency: "direct main" + dependency: transitive description: name: http_parser sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted version: "4.0.2" - image: - dependency: "direct main" - description: - name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" - url: "https://pub.dev" - source: hosted - version: "4.5.4" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a" - url: "https://pub.dev" - source: hosted - version: "0.8.12+21" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" - url: "https://pub.dev" - source: hosted - version: "0.8.12+2" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" - url: "https://pub.dev" - source: hosted - version: "0.2.1+2" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" - url: "https://pub.dev" - source: hosted - version: "0.2.1+2" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" - url: "https://pub.dev" - source: hosted - version: "2.10.1" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - intl: - dependency: "direct main" - description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" - source: hosted - version: "0.19.0" io: dependency: transitive description: @@ -532,10 +324,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -552,14 +344,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.9.0" - lints: - dependency: transitive - description: - name: lints - sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" - url: "https://pub.dev" - source: hosted - version: "5.0.0" logging: dependency: transitive description: @@ -608,14 +392,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" package_config: dependency: transitive description: @@ -680,22 +456,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - photo_view: - dependency: "direct main" - description: - name: photo_view - sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.dev" - source: hosted - version: "0.15.0" platform: dependency: transitive description: @@ -720,14 +480,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" - posix: - dependency: transitive - description: - name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.dev" - source: hosted - version: "6.0.3" pub_semver: dependency: transitive description: @@ -752,70 +504,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1" - url: "https://pub.dev" - source: hosted - version: "2.4.7" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" shelf: dependency: transitive description: @@ -861,54 +549,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" - url: "https://pub.dev" - source: hosted - version: "2.5.4+6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" - url: "https://pub.dev" - source: hosted - version: "2.4.1+1" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" stack_trace: dependency: transitive description: @@ -949,14 +589,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" - url: "https://pub.dev" - source: hosted - version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -989,86 +621,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" - url: "https://pub.dev" - source: hosted - version: "6.3.1" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" - url: "https://pub.dev" - source: hosted - version: "6.3.14" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" - url: "https://pub.dev" - source: hosted - version: "6.3.3" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" - url: "https://pub.dev" - source: hosted - version: "2.3.3" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - url: "https://pub.dev" - source: hosted - version: "3.1.4" - uuid: - dependency: transitive - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" vector_math: dependency: transitive description: @@ -1125,14 +677,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4c1299d..873d8ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,28 +7,13 @@ dependencies: flutter: { sdk: flutter } flutter_riverpod: ^2.5.1 go_router: ^14.2.0 - dio: ^5.5.0 - dio_cookie_manager: ^3.1.1 - cookie_jar: ^4.0.8 - freezed_annotation: ^2.4.4 - json_annotation: ^4.9.0 - fl_chart: ^0.68.0 - image_picker: ^1.1.2 - file_picker: ^8.0.3 - shared_preferences: ^2.3.2 - flutter_lints: ^5.0.0 - intl: ^0.19.0 - url_launcher: ^6.3.0 - http_parser: ^4.0.2 - image: ^4.2.0 http: ^1.2.2 - cached_network_image: ^3.4.0 - photo_view: ^0.15.0 + flutter_secure_storage: ^9.2.2 + json_annotation: ^4.9.0 dev_dependencies: build_runner: ^2.4.11 - freezed: ^2.5.7 - json_serializable: ^6.8.0 + json_serializable: ^6.9.0 flutter: uses-material-design: true \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 043a96f..0c50753 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,9 @@ #include "generated_plugin_registrant.h" -#include -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - FileSelectorWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FileSelectorWindows")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a95e267..4fc759c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - file_selector_windows - url_launcher_windows + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST