new start
This commit is contained in:
@@ -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()),
|
||||
],
|
||||
);
|
||||
@@ -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<ApiClient>((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<dynamic> call(String action, Map<String, dynamic> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<AuthState> {
|
||||
final ApiClient api;
|
||||
|
||||
AuthController(this.api) : super(const AuthState.loggedOut());
|
||||
|
||||
/// Beim App-Start/Hot Restart Session prüfen
|
||||
Future<void> 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<void> 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<void> 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<void> 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<AuthController, AuthState>((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<void>((ref) async {
|
||||
await ref.read(authProvider.notifier).checkSession();
|
||||
});
|
||||
@@ -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<void> 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<void> 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<String, dynamic>;
|
||||
await tokens.save(
|
||||
m['access_token'] as String, m['refresh_token'] as String);
|
||||
} else {
|
||||
throw Exception('Login failed: ${res.body}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<void> save(String access, String refresh) async {
|
||||
await _storage.write(key: 'access', value: access);
|
||||
await _storage.write(key: 'refresh', value: refresh);
|
||||
}
|
||||
|
||||
Future<String?> get access async => await _storage.read(key: 'access');
|
||||
Future<String?> get refresh async => await _storage.read(key: 'refresh');
|
||||
Future<void> clear() async {
|
||||
await _storage.deleteAll();
|
||||
}
|
||||
|
||||
/// Einfache Auto‑Refresh Logik
|
||||
Future<String?> 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<void> 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<String, dynamic>;
|
||||
await _storage.write(key: 'access', value: m['access_token'] as String);
|
||||
} else {
|
||||
await clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
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')),
|
||||
])),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<RegisterScreen> createState() => _RegisterScreenState();
|
||||
}
|
||||
|
||||
class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailCtrl = TextEditingController();
|
||||
final _passCtrl = TextEditingController();
|
||||
bool _obscure = true;
|
||||
bool _registerMode = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailCtrl.dispose();
|
||||
_passCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<HedgehogService>((ref) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return HedgehogService(api);
|
||||
});
|
||||
|
||||
/// Igel-Liste: lädt nur wenn eingeloggt (vermeidet 401 vor Login)
|
||||
final hedgehogsFutureProvider = FutureProvider<List<Hedgehog>>((ref) async {
|
||||
final auth = ref.watch(authProvider);
|
||||
if (!auth.loggedIn) return const <Hedgehog>[];
|
||||
|
||||
final svc = ref.read(hedgehogServiceProvider);
|
||||
final result = await svc.list();
|
||||
return List<Hedgehog>.from(result);
|
||||
});
|
||||
|
||||
/// Messungen & Fotos per Igel-ID (AutoDispose, damit Speicher frei wird)
|
||||
final measurementsFutureProvider =
|
||||
AutoDisposeFutureProvider.family<List<Measurement>, int>((ref, hedgehogId) async {
|
||||
final svc = ref.read(hedgehogServiceProvider);
|
||||
final list = await svc.listMeasurements(hedgehogId);
|
||||
return List<Measurement>.from(list);
|
||||
});
|
||||
|
||||
final photosFutureProvider =
|
||||
AutoDisposeFutureProvider.family<List<Photo>, int>((ref, hedgehogId) async {
|
||||
final svc = ref.read(hedgehogServiceProvider);
|
||||
final list = await svc.listPhotos(hedgehogId);
|
||||
return List<Photo>.from(list);
|
||||
});
|
||||
|
||||
class HedgehogService {
|
||||
final ApiClient api;
|
||||
HedgehogService(this.api);
|
||||
|
||||
/* ---------- Hedgehogs ---------- */
|
||||
|
||||
Future<List<Hedgehog>> 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<List<Hedgehog>>.value(<Hedgehog>[]);
|
||||
|
||||
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<String, dynamic>.from(e as Map);
|
||||
final id = _asInt(m['id']);
|
||||
if (id != null) m['id'] = id;
|
||||
return Hedgehog.fromJson(m);
|
||||
}).toList();
|
||||
|
||||
return Future<List<Hedgehog>>.value(list);
|
||||
}
|
||||
|
||||
Future<Hedgehog> 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<String, dynamic>.from(data));
|
||||
}
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
await api.call('hedgehog.delete', {'id': id});
|
||||
}
|
||||
|
||||
/* ---------- Measurements ---------- */
|
||||
|
||||
Future<List<Measurement>> 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<List<Measurement>>.value(<Measurement>[]);
|
||||
|
||||
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<String, dynamic>.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<List<Measurement>>.value(list);
|
||||
}
|
||||
|
||||
Future<Measurement> 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<String, dynamic>.from(data));
|
||||
}
|
||||
|
||||
Future<void> deleteMeasurement(int id) async {
|
||||
await api.call('meas.delete', {'id': id});
|
||||
}
|
||||
|
||||
/* ---------- Photos ---------- */
|
||||
|
||||
Future<List<Photo>> 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<List<Photo>>.value(<Photo>[]);
|
||||
|
||||
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<String, dynamic>.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<List<Photo>>.value(list);
|
||||
}
|
||||
|
||||
Future<Photo> uploadPhoto({
|
||||
required int hedgehogId,
|
||||
required String filename,
|
||||
required List<int> 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<String, dynamic>.from(data));
|
||||
}
|
||||
|
||||
Future<void> deletePhoto(int id) async {
|
||||
await api.call('photo.delete', {'id': id});
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> json) =>
|
||||
_$HedgehogFromJson(json);
|
||||
}
|
||||
@@ -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>(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<String, dynamic> 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<String, dynamic> 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<Hedgehog> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'hedgehog.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$HedgehogImpl _$$HedgehogImplFromJson(Map<String, dynamic> json) =>
|
||||
_$HedgehogImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
species: json['species'] as String?,
|
||||
notes: json['notes'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$HedgehogImplToJson(_$HedgehogImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'species': instance.species,
|
||||
'notes': instance.notes,
|
||||
};
|
||||
@@ -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 = <String, String>{'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<void> _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<void> _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<void> _createHedgehogDialog(BuildContext context, WidgetRef ref) async {
|
||||
final service = ref.read(hedgehogServiceProvider);
|
||||
final nameCtrl = TextEditingController();
|
||||
|
||||
final ok = await showDialog<bool>(
|
||||
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<void>.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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void> _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<void> _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<void> _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<bool>(
|
||||
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<void> _deleteMeasurement(BuildContext context, WidgetRef ref, Measurement m) async {
|
||||
final df = DateFormat('dd.MM.yyyy HH:mm');
|
||||
final ok = await showDialog<bool>(
|
||||
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<void> _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<void> _deletePhoto(BuildContext context, WidgetRef ref, Photo p) async {
|
||||
final ok = await showDialog<bool>(
|
||||
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<Measurement> 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<void>.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),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Igel>> list() async {
|
||||
final res = await api.get('/igel');
|
||||
final list = (res as List).cast<Map<String, dynamic>>();
|
||||
return list.map(Igel.fromMap).toList();
|
||||
}
|
||||
|
||||
Future<Igel> create(String name, {String? note}) async {
|
||||
final res = await api.post('/igel', {'name': name, 'note': note});
|
||||
return Igel.fromMap(res);
|
||||
}
|
||||
|
||||
Future<void> update(int id, String name, {String? note}) async {
|
||||
await api.put('/igel/$id', {'name': name, 'note': note});
|
||||
}
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
await api.delete('/igel/$id');
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> m) => Igel(
|
||||
id: m['id'] as int,
|
||||
name: m['name'] as String,
|
||||
note: m['note'] as String?);
|
||||
}
|
||||
@@ -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<IgelListScreen> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends ConsumerState<IgelListScreen> {
|
||||
late final IgelRepository repo;
|
||||
List<Igel> items = [];
|
||||
bool busy = true;
|
||||
String? err;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
repo = ref.read(igelRepoProvider);
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() => busy = true);
|
||||
try {
|
||||
items = await repo.list();
|
||||
err = null;
|
||||
} catch (e) {
|
||||
err = e.toString();
|
||||
} finally {
|
||||
if (mounted) setState(() => busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _create() async {
|
||||
final name = await showDialog<String>(
|
||||
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<String>(
|
||||
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')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> json) =>
|
||||
_$MeasurementFromJson(json);
|
||||
}
|
||||
@@ -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>(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<String, dynamic> 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<String, dynamic> 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<Measurement> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'measurement.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$MeasurementImpl _$$MeasurementImplFromJson(Map<String, dynamic> 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<String, dynamic> _$$MeasurementImplToJson(_$MeasurementImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'hedgehogId': instance.hedgehogId,
|
||||
'measuredAt': instance.measuredAt.toIso8601String(),
|
||||
'weightGrams': instance.weightGrams,
|
||||
'lengthMm': instance.lengthMm,
|
||||
'temperatureC': instance.temperatureC,
|
||||
'note': instance.note,
|
||||
};
|
||||
@@ -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<String, dynamic> json) => _$PhotoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PhotoToJson(this);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'photo.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Photo _$PhotoFromJson(Map<String, dynamic> 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<String, dynamic> _$PhotoToJson(Photo instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'filename': instance.filename,
|
||||
'mime': instance.mime,
|
||||
'size': instance.size,
|
||||
'createdAt': instance.createdAt,
|
||||
'url': instance.url,
|
||||
};
|
||||
+21
-69
@@ -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<TokenStorage>((ref) => TokenStorage(kApiBase));
|
||||
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient(kApiBase,
|
||||
getAccessToken: () =>
|
||||
ref.read(tokenStorageProvider).getValidAccessToken()));
|
||||
final igelRepoProvider = Provider<IgelRepository>(
|
||||
(ref) => IgelRepository(ref.read(apiClientProvider)));
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
runApp(const ProviderScope(child: IgelApp()));
|
||||
}
|
||||
|
||||
final routerProvider = Provider<GoRouter>((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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String?> Function()? getAccessToken;
|
||||
|
||||
Future<http.Response> _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<Map<String, dynamic>> get(String path) async =>
|
||||
_decode(await _send('GET', path));
|
||||
Future<Map<String, dynamic>> post(
|
||||
String path, Map<String, dynamic> body) async =>
|
||||
_decode(await _send('POST', path, body: body));
|
||||
Future<Map<String, dynamic>> put(
|
||||
String path, Map<String, dynamic> body) async =>
|
||||
_decode(await _send('PUT', path, body: body));
|
||||
Future<Map<String, dynamic>> delete(String path) async =>
|
||||
_decode(await _send('DELETE', path));
|
||||
Map<String, dynamic> _decode(http.Response r) {
|
||||
if (r.statusCode >= 200 && r.statusCode < 300) {
|
||||
if (r.body.isEmpty) return {};
|
||||
return jsonDecode(r.body) as Map<String, dynamic>;
|
||||
}
|
||||
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';
|
||||
}
|
||||
@@ -6,14 +6,10 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
+51
-507
@@ -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:
|
||||
|
||||
+3
-18
@@ -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
|
||||
@@ -6,12 +6,9 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user